简历项目知识,八股文

124 阅读17分钟

关于简历项目

基本网络知识

TCP协议

传输控制协议

三次握手

  • 客户机发送连接请求报文段到服务器,并进入SYN_SENT状态,等待服务器确认(SYN = 1,seq = x)
  • 服务器收到连接请求报文,如果同意建立连接,向客户机发回确认报文段,并为该TCP连接分配TCP缓存和变量(SYN = 1,ACK = 1,seq = y,ack = x+1)
  • 客户机收到服务器的确认报文段后,向服务器给出确认报文段,并且也要给该连接分配缓存和变量,此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手

四次挥手

  • TCP客户端发送一个FIN,用来关闭客户端到服务器的数据传输
  • 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号+1。和SYN一样,一个FIN将占用一个序号
  • 服务器关闭客户端的连接,发送一个FIN给客户端
  • 客户端发回ACK报文确认,并将确认序号设置为收到序号+1

TCP握手为什么不能是两次

防止两次握手的情况下已经失效的连接请求报文段突然又传送到服务端而产生错误

HTTP请求中的GET和POST方法的区别

主要区别在于GET方法是请求读取由URL所标志的信息,POST是给服务器添加信息。

报文:get请求放在url,post请求放在报文体中,部分浏览器对URL长度有限制

Cookie和Session的区别

  • cookie数据保存在客户端,session数据保存在服务器端
  • 都可以存放敏感信息
  • cookie和session都是用来跟踪浏览器用户身份的会话方式
  • cookie保存在客户机硬盘上,session默认被保存在服务器的一个文件夹里(不是内存)
  • session的运行依赖sessionId,而sessionId是存放在cookie中的,也就是说,如果浏览器禁用了cookie,同时session也会失效,但可以通过URL传递sessionId
  • session可以存放在文件、数据库、内存中都可以
  • 用户验证这种场合一般会用session

博客项目

前后端的技术栈

前端是vue,在最近的一次修改中使用echarts线性表格来展示某一时间段的文章浏览量。

后端使用springboot、springsecurity、swagger、mybatisplus、Redis等。

SpringSecurity核心功能

  • 认证(Authentication) : 验证用户身份的过程
  • 授权(Authorization):确认用户权限
  • 防护攻击:如跨域请求伪造CSRF和跨站脚本攻击XSS
  • Servlet API集成:与Java Servlet API无缝集成,提供web安全功能
  • 可扩展性:通过自定义组件和扩展点可轻松扩展springSecurity

springSecurity的架构

由以下组件组成:

  • securityContextHolder:存储与当前线程关联的安全上下文。
  • Authentication:表示用户的认证信息。
  • UserDetails:表示用户的详细信息。
  • AuthenticationManager:负责处理认证请求。
  • AccessDecisionManager:负责授权决策
  • FilterChainProxy:负责处理Http请求的过滤器链。
  • SecurityFilterChain:由一系列安全过滤器组成的链。

什么是SecurityContextHolder

securityContextHolder是一个用于存储与当前线程相关的安全上下文的类,它使用ThreadLocal机制来存储当前用户的认证信息,如Authentication对象。

springsecurity登录如何实现

@Service
public class LoginServiceImpl implements LoginServcie {
​
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;
​
    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisCache.setCacheObject("login:"+userId,loginUser);
        //把token响应给前端
        HashMap<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return new ResponseResult(200,"登陆成功",map);
    }
}
HttpServletRequest request =new HttpSerletRequest();
String token = request.getHeader("token");

获取Http请求头中的token,解析获取用户ID

Clamis clamis = null;
userId = claims.getSubject();

token超时或非法就重新登录。

去redis中找用户,存入securityContextHolder中

放行,doFilter(自定义过滤器)

获取userId生成token

JwtUtil.creatJwt(userId);
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
​
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
​
/**
 * JWT工具类
 */
public class JwtUtil {
​
    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "Haluki";
​
    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }
​
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }
​
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("haluki")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }
​
    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }
​
    public static void main(String[] args) throws Exception {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
        Claims claims = parseJWT(token);
        System.out.println(claims);
    }
​
    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
​
​
}

jwt是什么

JSON Web Token,目前最流行的跨域身份验证解决方案

为什么使用jwt

jwt的精髓在于去中心化,数据保存在客户端里面。

jwt工作原理

在服务器身份验证后,生成一个JSON对象,并将其发回给用户,之后,用户与服务器通信时,用户在请求中发回JSON对象,服务器将在生成对象时添加签名。

jwt组成

头部header、载荷payload、签名signature。

文章访问量没必要用redis

博客主页中的文章访问量采用了redis技术,为了更新的更加快速,减少查询数据库的次数,减小数据库压力。但后来发现没有必要,于是在博客后台中图表显示浏览量就直接查询数据库了。

我使用了@Scheduled(cron = "0/5 * * * * ?")这个注解,每五秒种在redis中查询一次浏览量数据,使用stream流收集数据,然后更新到数据库中。

后台中,文章某个时间段的浏览量

在设计方面,我使用文章id+每隔5分钟的时间戳作为组合主键,用户访问文章时,以当前时间上下5分钟取整记录到数据库中。如果访问量大就设计成时间戳%5min+文章Id+Count的方式记录访问量,计数的颗粒度是5分钟,减小数据库压力,后台可以查看任意时间段内的文章浏览量。(颗粒度:数据的细化程度)

密码加密算法

这个项目中,我使用了BCrypt加密算法对用户密码进行加密处理。

  • 为什么使用这个BC算法呢:这个算法对于攻击人员来说需要消耗的计算成本高,安全性好。该算法会自动生成随机盐值。可扩展性高。适用于长密码。简单易用。
  • 为什么不用最新的Argon2加密算法呢:Argon2加密算法很新,可能会导致不兼容的情况出现,并且Argon2算法的计算成本高,高负载下可能产生性能问题。

密码加密流程

  • pom
groupId:org.springframework.security
artifactId:spring-security-crypto
version:5.6.1
  • 在配置文件中注入BCryptPasswordEncoder:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Bean
  public PasswordEncoder passwordEncoder(){
      return new BCryptPasswordEncoder();
  }
}
  • 在需要使用密码的地方调用passwordEncoder.encode()方法对密码进行加密
@Autowired
private PasswordEncoder passwordEncoder;
@Override public User register(User user){
    String encodedPassword = passwordEncoder.encode(user.getPassword());
    user.setPassword(encodedPassword);
  //...
    return user;
}

图片上传

使用七牛云OSS存储上传的图片,前端发起post请求,后端接收后处理上传。

先添加七牛云的依赖,在配置文件里面输入密钥和私钥以及bucket。后端响应是否上传成功,以及图片的访问链接

@SpringBootTest
@ConfigurationProperties(prefix = "oss")
public class OSSTest {
​
    private String accessKey;
    private String secretKey;
    private String bucket;
​
    public void setAccessKey(String accessKey) {
        this.accessKey = accessKey;
    }
​
    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }
​
    public void setBucket(String bucket) {
        this.bucket = bucket;
    }
​
    @Test
    public void testOss(){
        //构造一个带指定 Region 对象的配置类
        Configuration cfg = new Configuration(Region.autoRegion());
        //...其他参数参考类注释
​
        UploadManager uploadManager = new UploadManager(cfg);
        //...生成上传凭证,然后准备上传
//        String accessKey = "your access key";
//        String secretKey = "your secret key";
//        String bucket = "haluki";
​
        //默认不指定key的情况下,以文件内容的hash值作为文件名
        String key = "2022/xxx.png";
​
        try {
//            byte[] uploadBytes = "hello qiniu cloud".getBytes("utf-8");
//            ByteArrayInputStream byteInputStream=new ByteArrayInputStream(uploadBytes);
​
​
            InputStream inputStream = new FileInputStream("C:\Users\root\Desktop\Snipaste_2022-02-28_22-48-37.png");
            Auth auth = Auth.create(accessKey, secretKey);
            String upToken = auth.uploadToken(bucket);
​
            try {
                Response response = uploadManager.put(inputStream,key,upToken,null, null);
                //解析上传成功的结果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                System.out.println(putRet.key);
                System.out.println(putRet.hash);
            } catch (QiniuException ex) {
                Response r = ex.response;
                System.err.println(r.toString());
                try {
                    System.err.println(r.bodyString());
                } catch (QiniuException ex2) {
                    //ignore
                }
            }
        } catch (Exception ex) {
            //ignore
        }
​
    }
}

JPA

Java持久层API,将实体对象持久化到数据库中。

博客

前后端分离。单账户,一个人发布博客,其他人可以登录评论,可改头像和个人信息,可评论。可以上传友链,但这些用户上传的友链和发表的评论需要在后台审核通过后才可以正常的上传成功,后台管理员可以对其修改或者删除。

安全部分

设置允许跨域的路径,请求方式,允许时间,cookie是否允许等。

安全部分主要是避免XSS和CSRF攻击

XSS跨站脚本攻击即用户在输入框内输入JavaScript代码,此项目使用secure-only方式,只允许Https请求读取发送请求时自动发送cookie

CSRF:登录生成token为JSON对象,再返回给前端,之后每次客户端与服务器交互的时候都会带着token,服务器获取用户token解析获取userId,在redis中找,再把这两个token对比。

如何防止跨站请求伪造CSRF攻击

SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。SpringSecurity默认启用了CSRF防护,要禁用CSRF防护,可以在configure()方法中配置HttpSecurity:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
  
  @Override
  protected void configure(HttpSecurity http)throws Exception{
    http.csrf().disable()
      .authorizeRequests()
      .antMatchers("/admin/**").hasRole("ADMIN")
      .antMatchers("/user/**").hasRole("USER")
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .and()
      .httpBasic();
}
}

如何在SpringSecurity中自定义认证逻辑

需要实现AuthenticationProvider接口,并在configure() 方法中将自定义的AuthenticationProvider添加到AuthenticationManagerBuilder。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Autowired
  private CutomAuthenticationProvider provider;
  
  @Override
  protected void configure(AuthenticationManagerBuilder auth){
    auth.authenticationProvider(provider);
}
}

如何在SpringSecurity中使用基于角色的控制访问

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。

​ 然后设置我们的资源所需要的权限即可。

SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。

​ 但是要使用它我们需要先开启相关配置。

@EnableGlobalMethodSecurity(prePostEnabled = true)

​ 然后就可以使用对应的注解。@PreAuthorize

@RestController
public class HelloController {
​
    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
        return "hello";
    }
}

可以在configure()中配置HttpSecurity

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
  
  @Override
  protected void configure(HttpSecurity http)throws Exception{
    http.authorizeRequests()
      .antMatchers("/admin/**").hasRole("ADMIN")
      .antMatchers("/user/**").hasRole("USER")
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .and()
      .httpBasic();
}
}

自定义jwt认证过滤器

这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。

​ 使用userid去redis中获取对应的LoginUser对象。

​ 然后封装Authentication对象存入SecurityContextHolder

把jwtAuthenticationTokenFilter添加到SpringSecurity的过滤器链中

    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    //允许跨域
    http.cors();
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
​
    @Autowired
    private RedisCache redisCache;
​
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

自定义认证成功处理器

自定义认证失败处理

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

​ 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以,如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可

自定义实现类

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
​
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}

配置给SpringSecurity

先注入对应的处理器

@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
​
@Autowired
private AccessDeniedHandler accessDeniedHandler;

然后我们可以使用HttpSecurity对象的方法去配置

http.exceptionHandling().authenticationEntryPoint(autheticationEntryPoint).accessDeniedHandler(accessDeniedHandler);

如何在springSecurity中实现jwt认证

  • 首先引入依赖:jjwt

  • 实现一个用于生成和解析jwt的工具类

  • 创建自定义的AuthenticationFilter,用于从请求头中提取jwt并进行认证。

  • 在securityConfig类中配置HttpSecurity,将自定义的AuthenticationFilter添加到过滤链

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
      
      @Autowired
      private JwtAuthenticationFilter jwtAuFilter;
      
      @Override
      protected void configure(HttpSecurity http)throws Exception{
        http.csrf().disable()
          .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
          .and()
          .addFilterBefore(jwtAuFilter,usernamePasswordAuthentication.class)
          .anyRequest().authenticated();
        //把token校验过滤器添加到过滤器链中
            http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
    }
    

在上述代码中,禁用了CSRF防护和会话管理,然后将自定义的JwtAuthenticationFilter添加到过滤器链。

认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key,最后把token响应给前端。

@Service
public class LoginServiceImpl implements LoginServcie {
​
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;
​
    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisCache.setCacheObject("login:"+userId,loginUser);
        //把token响应给前端
        HashMap<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return new ResponseResult(200,"登陆成功",map);
    }
}

得物项目

Redis给键值加过期时间

减少redis空间占用以防止用户恶意查询,在得物项目中使用的是定时删除

Redis三大删除策略

  • 定时删除:设置过期时间
  • 惰性删除:每次从数据库中取键值时判断是否过期
  • 定期删除:每隔一段时间查一次数据库,可以在redis.config文件中配置,随机删除过期键

分布式锁

为了确保分布式锁可用,至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁解开,不能误解锁。
  4. 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

如何解决商品超卖

我使用了Redisson的分布式锁组件

RLock rlock = redisson.getLock(key);

获取到锁->开启事务->释放锁

// 1.构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);
// 2.构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 3.获取锁对象实例(无法保证是按线程的顺序获取到)
RLock rLock = redissonClient.getLock(lockKey);
try {
    /**
     * 4.尝试获取锁
     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
     * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
     */
    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
    if (res) {
        //成功获得锁,在这里处理业务
    }
} catch (Exception e) {
    throw new RuntimeException("aquire lock fail");
}finally{
    //无论如何, 最后都要解锁
    rLock.unlock();
}

Redis针对缓存穿透、击穿、雪崩的解决方案

  • 缓存穿透:存在则缓存返回,不存在则查询db,恶意查询不存在的key对后端压力增大就叫穿透

    解决方案:查询为空的时候缓存下(缓存空值,并设置过期时间),查询以后更新缓存,过滤key

  • 缓存击穿:缓存中没有但数据库中也可能没有,大量查询穿过缓存直击数据库

    解决方案:使用互斥锁,当1号线程查询缓存未命中时,去获取互斥锁,然后查询数据库获取结果并将结果写入到缓存中,最后释放锁,在1号线程释放锁之前,其他线程都不能获取到锁,只能睡眠一段时间后重试,如果能命中缓存,则返回数据,否则继续尝试获取互斥锁

  • 缓存雪崩:缓存服务器重启或者大量缓存失效,都去访问数据库,引起数据库压力过大

    解决方案:将key的过期时间分布均匀,避免短时间内大量key过期,加锁避免过大并发量,控制访问流量。

订单支付模块

  1. 用户下单付款
  2. 客户端请求服务器获取签名后的订单信息。
  3. 返回签名后的订单信息
  4. 调用支付接口,发送支付请求
  5. 返回支付结果
  6. 返回给服务器验证签名,解析支付结果
  7. 返回最终支付结果
  8. 显示支付结果

调用支付宝SDK,实例化客户端

new DefaultAliPayClient("网关地址",APPID,应用公钥,参数格式JSON,编码格式UTF-8,支付宝公钥,签名方式,RSA2)

调用接口生成表单,把信息填进去,orderID、金额等。下单成功后更新支付记录,更新商品付款人数,更新订单状态。

配置回调地址->调用支付接口->支付成功->跳转到回调地址->判断订单状态

状态为已支付:更新订单状态->更新支付流水->更新付款人数

状态为未支付:更新订单状态为未支付->更新支付流水为支付失败

豆瓣项目

Redis支持

String:此类型是二进制安全的,可包含任何数据,如jpg图片或序列化的对象

Hash:是一个键值对集合,适合用于存储对象

List:字符串列表,可在头部(左边)或尾部(右边)插入数据

Set:是String类型的无序集合

ZSet:同Set,并且不允许重复成员

Mongodb与Mysql的区别

Mongodb本质上还是一个数据库产品,与MySQL的区别在于它不会遵循一些约束,比如sql标准、表结构等

Mongodb特性

  • 面向集合文档的存储,适合存储Bson(JSON的扩展)形式的数据
  • 格式自由
  • 强大的查询语句
  • 完整的索引支持
  • 支持二进制数据以及大型对象(文件)的高效存储

爬虫是如何实现的

构建请求头,不同的爬取目标有不同的值。

String getContent(String url,Map<String,String>headers){
    //定义request
    Builder reqBuilder = new Request.Builder().url(url);
    //如果传入httpheader,则放入request中
    if(headers!=null && !headers.isEmpty()){
        for(String key:headers.Keyset()){
          reqBuilder.addHeader(key,headers.get(key));
    }
    }
  Request request  = reqBuilder.build();
  //调用client去请求
  Call call = okHttpClient.newCall(reqeust);
  //返回结果字符串
  String result = null;
  try{
      //获得返回结果
        logger.info("Requst" + url+ "Begin.");
        result = call.execute().body().string();
  }catch(IOException e){
      logger.error("request"+url+"exception"+e);
  }
  return result;
}

获取数据后,转换成Map格式

Map dataObj = JSON.parseObject(content,Map.class);

解析获取到的数据

Map songListData = (Map)dataObj.get("songlist");