高并发下的redis锁笔记

314 阅读17分钟

描述介绍

  分布式高并发下,jvm下的锁是没有办法保证线程安全的。因为分布式下,他们是两个不同的进程,无法保证他们之间的数据可见性下操作。所以需要在分布式中间找一个提供互斥性的工具,进行协调多个进程间的数据安全。

redis实现分布式锁

redis实现加锁的最基本方式

@RestController
public class testController{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping(“deductStock”)
    public String deductStock(){
        //获取到的商品id
        String lockKey = "lock:product_001";
        //此处省略实例化redis客户端连接

        //用redis加锁
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, “duay”);

        if(!result){
            return "数据已经存在,不能被操作";
        }

        //当前库存数
        int stockNum = Integer.parselInt(stringRedisTemplate.opsForValue().get("stockNum"));
        if(stockNum > 0){
            //库存数减一
            int residueNum = stockNum - 1;
            //更新当前库存数
            stringRedisTemplate.opsForValue().set(“stockNum”, residueNum + “”);
            System.out.println(“扣减成功,剩余库存:”+realStock);
        }else{
            System.out.println(“扣减失败,库存不足”);
        }
        return "end";
    }
    
}

  当库存处理处的代码抛异常时候,后面的流程无法执行,而导致锁一直释放不掉。其他的线程就无法进行操作,就会导致死锁。下面对抛异常导致死锁进行优化

对抛异常导致死锁的优化

@RestController
public class testController{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @RequestMapping(“deductStock”)
    public String deductStock(){
        //获取到的商品id
        String lockKey = "lock:product_001";
        //此处省略实例化redis客户端连接

        //用redis加锁
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, “duay”);

        if(!result){
            return "数据已经存在,不能被操作";
        }

        try{
            //当前库存数
            int stockNum = Integer.parselInt(stringRedisTemplate.opsForValue().get("stockNum"));
            if(stockNum > 0){
                //库存数减一
                int residueNum = stockNum - 1;
                //更新当前库存数
                stringRedisTemplate.opsForValue().set(“stockNum”, residueNum + “”);
                System.out.println(“扣减成功,剩余库存:”+realStock);
            }else{
                System.out.println(“扣减失败,库存不足”);
            }
        }finally{
            //防止try中代码抛异常造成死锁
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }
    
}

  当服务器宕机时候,finally就无法解决,这种情况下,程序无法运行而造成的死锁

服务器宕机造成的死锁优化一

@RestController
public class testController{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping(“deductStock”)
    public String deductStock(){
        //获取到的商品id
        String lockKey = "lock:product_001";
        //此处省略实例化redis客户端连接

        //1 用redis加锁
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, “duay”);

        //最直接想到的方式就是,设置超时时间。
        // 2 stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

        if(!result){
            return "数据已经存在,不能被操作";
        }

        try{
            //当前库存数
            int stockNum = Integer.parselInt(stringRedisTemplate.opsForValue().get("stockNum"));
            if(stockNum > 0){
                //库存数减一
                int residueNum = stockNum - 1;
                //更新当前库存数
                stringRedisTemplate.opsForValue().set(“stockNum”, residueNum + “”);
                System.out.println(“扣减成功,剩余库存:”+realStock);
            }else{
                System.out.println(“扣减失败,库存不足”);
            }
        }finally{
            //防止try中代码抛异常造成死锁
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

}

  这里存在的问题是,当服务器宕机发生在设置锁成功,但是设置定时的代码起效之前宕机,依然会出现死锁可能性。
  这时候需要我们把上面的一二行代码做原子操作处理,要么一起成功,要么一起失败。
  这时候,顺序访问,当执行完他两时候,给个回复。然后后面的等待的任务再去执行。

原子性设置锁和设置定时失效

优化分开设置锁和设置定时失效时间,因为宕机在他们之前导致的死锁

@RestController
public class testController{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping(“deductStock”)
    public String deductStock(){
        //获取到的商品id
        String lockKey = "lock:product_001";
        //此处省略实例化redis客户端连接

        /**
         * 所以上面两行命令,用下面一行命令代替解决上面的问题
         * 因为这个指令是原子性设置锁和定时失效
         */
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, “zhuge”, 10, TimeUnit.SECONDS);

        if(!result){
            return "数据已经存在,不能被操作";
        }

        try{
            //当前库存数
            int stockNum = Integer.parselInt(stringRedisTemplate.opsForValue().get("stockNum"));
            if(stockNum > 0){
                //库存数减一
                int residueNum = stockNum - 1;
                //更新当前库存数
                stringRedisTemplate.opsForValue().set(“stockNum”, residueNum + “”);
                System.out.println(“扣减成功,剩余库存:”+realStock);
            }else{
                System.out.println(“扣减失败,库存不足”);
            }
        }finally{
            //防止try中代码抛异常造成死锁
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }
    
}

  当设置的失效时间已经到了,但是程序还没有执行完,这时候上面的代码也会出现数据不安全。他会出现线程1结束时候释放的是正在执行的线程二的锁。
  这时候我们需要给订单设置一个身份,uuid,假设uuid在高并发下不产生重复数据。

@RestController
public class testController{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping(“deductStock”)
    public String deductStock(){
        //获取到的商品id
        String lockKey = "lock:product_001";
        //此处省略实例化redis客户端连接

        /**
         * 设置uuid作为身份验证,
         * 不可以使用用户账号或者用于区分用户的唯一性的字段作为身份验证,因为这样同一个人多
         * 次下单会出现问题
         */
        String clientId = UUID.randomUUID().toString();

        /**
         * 所以上面两行命令,用下面一行命令代替解决上面的问题
         * 因为这个指令是原子性设置锁和定时失效
         */
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, “zhuge”, 10, TimeUnit.SECONDS);

        if(!result){
            return "数据已经存在,不能被操作";
        }

        try{
            //当前库存数
            int stockNum = Integer.parselInt(stringRedisTemplate.opsForValue().get("stockNum"));
            if(stockNum > 0){
                //库存数减一
                int residueNum = stockNum - 1;
                //更新当前库存数
                stringRedisTemplate.opsForValue().set(“stockNum”, residueNum + “”);
                System.out.println(“扣减成功,剩余库存:”+realStock);
            }else{
                System.out.println(“扣减失败,库存不足”);
            }
        }finally{
            //判断加锁id和产生的唯一标识id是否是同一个,如果是,就释放,否则不释放
            if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                //防止try中代码抛异常造成死锁
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }

}

  上面代码的最后两行代码,还是会出现如下问题。

if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
    
    
    /** 当线程在判断身份验证到释放锁的过程中,网络延迟卡顿了一下,造成了锁的失效时间到期
     * 了,还会出现问题
     */
    stringRedisTemplate.delete(lockKey);
}

  解决上面的问题就要用到经典的解决方式,锁续命。
  主线程执行业务,再搞一个分线程,去做定时任务,定时任务的时间一定要小于主线程的失效时间。然后判断主线程有没有结束,就是判断主线程加的锁是否还在。锁还在就续命,不在就释放锁。
  redisson和jedis一样都是java客户端操作redis。redisson的使用场景更多是在分布式中。它实现的就是类似于锁续命的逻辑。

使用redisson

引入jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.6.5</version>
</dependency>

初始化一个redisson客户端

public class RdeissonClient{
    @Bean
    public Redisson redisson(){
        //这是单机模式
        Config config = new Config();
       
        //配置操作哪个redis,哪个redis下的数据库
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        //设置分布式锁watch dog超时时间
        return (Redisson) Redisson.create(config);
    }
}

redisson实现分布式锁

@RestController
public class userController{
    @Autowired
    private Redisson redisson;
    
    @RequestMapping(“deductStock”)
    public String deductStock(){
        //获取到的商品id
        String lockKey = "lock:product_001";
        //此处省略实例化redis客户端连接
        
        //获得一把锁对象
        RLock redissonLock = redisson.getLock(lockKey);
        //加分布式锁
        redissonLock.lock();

        try{
            //当前库存数
            int stockNum = Integer.parselInt(stringRedisTemplate.opsForValue().get("stockNum"));
            if(stockNum > 0){
                //库存数减一
                int residueNum = stockNum - 1;
                //更新当前库存数
                stringRedisTemplate.opsForValue().set(“stockNum”, residueNum + “”);
                System.out.println(“扣减成功,剩余库存:”+realStock);
            }else{
                System.out.println(“扣减失败,库存不足”);
            }
        }finally{
            //防止try中代码抛异常造成死锁
            stringRedisTemplate.delete(lockKey);
        }
        //释放分布式锁
        redissonLock.unlock();
        return "end";
    }
}

redisson加锁的核心流程

image.png
  redisson是如何实现在redis中多个指令完成的操作,在redisson中实现的数据安全性的,因为redisson的底层执行的redis指令用的lua脚本进行执行的。

redis主从架构锁失效问题

  当主节点刚写完数据,要同步给从节点时,主节点挂了。从节点会变为新的主节点,而新的主节点里面是没有数据的。
  再来一个线程对同一个商品加锁的时候,新的线程就会加锁成功,这时候,因为以前的主节点数据没有同步过来的原因,导致新的线程拿到锁进行数据再次更新,导致超卖问题。

zookeeper主从架构解决防止锁失效的问题

  当主节点挂掉的时候,zk会把数据多的节点作为主节点,就会解决节点切换过程中锁丢失的问题。

redlock存在的问题

  redlock可以解决redis主从架构锁失效的问题,它只是增加防止锁失效的概率,不能完全解决。
  超过半数的redis节点加锁成功,才算加锁成功。用redlock加锁的话,系统会有延迟。
  用红锁时候,假设三个主节点,每个主节点都有一个从节点,两个主节点加锁成功,一个没有加锁。每个主节点都会把数据同步给从节点,其中两个有锁的主节点同步给从节点的时候,其中一个主节点挂了,没有同步成功,这时候从节点变成了主节点。这时候有一个线程访问,一看一个主节点没锁,一个从节点变为的主节点没锁,这时候大于半数,这个线程就可以拿到锁。但实际是不能拿到的(因为一个主节点有锁,另一个挂掉的主节点也有锁,只是没有同步到它的从节点)。
  如果不搞从节点就不会有这个问题,但是如果有三个主节点,都有锁了,但是挂掉了两个,这时候锁就会加不成功。
  解决redis主从架构锁失效问题,不推荐使用redlock。

redis分布式锁

  在秒杀的时候,会把库存提前放到缓存中。redis分布式锁实现的语义,就是让所有资源进行顺序访问。这样当足够高的并发,这种分布式锁,也会影响性能。这时候我们可以用分段锁进行性能优化。

使用redis缓存

redis缓存的普通方式

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            product = JSON.parseObject(productStr, Product.class);
            return product;
        }
        
        product = productDao.get(productId);
        if(product != null){
            redisUtil.set(productCacheKey, JSON.toJSONString(productResult));
        }
        return product;
    }       
}

  商品不多的时候,上面代码没有问题,当商品很多,上亿的时候,这种就不行了。我们可以把访问量大的商品放redis缓存,数据量小的不放redis缓存。可以在商品放缓存的时候,加失效时间。

为redis缓存加失效时间

  为redis缓存加失效时间,解决缓存中堆积大量不经常使用的数据

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), PRODUCT_CHCHE_TIMEOUT, TimeUnit.SECDNDS);
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            product = JSON.parseObject(productStr, Product.class);
            return product;
        }
        
        product = productDao.get(productId);
        if(product != null){
            redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CHCHE_TIMEOUT, TimeUnit.SECDNDS);
        }
        return product;
    }       
}

redis缓存加失效时间优化

  解决常用信息失效,每查询一次就延时一次。

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), PRODUCT_CHCHE_TIMEOUT, TimeUnit.SECDNDS);
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, PRODUCT_CHCHE_TIMEOUT, TimeUnit.SECDNDS);
            return product;
        }
        
        product = productDao.get(productId);
        if(product != null){
            redisUtil.set(productCacheKey, JSON.toJSONString(product), PRODUCT_CHCHE_TIMEOUT, TimeUnit.SECDNDS);
        }
        return product;
    }       
}

缓存击穿

  当大批量的商品进行更新,它的缓存过期时间是一样的,这就会有大批量的商品过期。大量的客户端访问的时候,都没有访问到,就会大量数据访问到数据库。这就叫缓存击穿(缓存失效)。
解决缓存击穿
  将失效时间设置为随机失效时间

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), genProductCacheTimeout(), TimeUnit.SECDNDS);
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);
            return product;
        }
        
        product = productDao.get(productId);
        if(product != null){
            redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECDNDS);
        }
        return product;
    }   
    
    //生成随机失效时间
    private Integer genProductCacheTimeout(){
        return PRODUCT_CHCHE_TIMEOUT + new Random().nextInt(5)*60*60;
    }
}

缓存穿透

  缓存和数据库都没有数据,这时候大量请求进行访问没有的数据,就会造成缓存穿透。
解决缓存穿透

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    public static final String EMPTY_CHCHE = {};
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), genProductCacheTimeout(), TimeUnit.SECDNDS);
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){  
            if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                //防止一直一个空缓存访问
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                return null;
            }
            
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);
            return product;
        }
        
        product = productDao.get(productId);
        if(product != null){
            redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECDNDS);
        }else{  //防止缓存穿透
            redisUtil.set(productCacheKey, EMPTY_CHCHE, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
        }
        return product;
    }   
    
    //生成随机失效时间
    private Integer genProductCacheTimeout(){
        return PRODUCT_CHCHE_TIMEOUT + new Random().nextInt(5)*60*60;
    }
    
    //防止大量的空缓存占用redis空间
    private Integer genEmptyCacheTimeout(){
        return new Random().nextInt(5)*60*60;
    }
}

解决完冷门数据突然变热点数据

  以上代码当几万的冷门数据来访问,就会造成这大量的数据跳过缓存,直接打在数据库上。
解决方案
加锁,加双重检测锁

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    public static final String EMPTY_CHCHE = {};
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), genProductCacheTimeout(), TimeUnit.SECDNDS);
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){  
            if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                //防止一直一个空缓存访问
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                return null;
            }
            
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);
            return product;
        }
        
        //双重检测锁,将上面的查询再进行一次查询
        synchronized(this){
            productStr = redisUtil.get(productCacheKey);
            if(!StringUtils.isEmpty(productStr)){  
                if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                    //防止一直一个空缓存访问
                    redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                    return null;
                }
                product = JSON.parseObject(productStr, Product.class);
                //每读取一次,对数据进行一次延期 实现冷热分离
                redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);
                return product;
            }
            
            product = productDao.get(productId);
            if(product != null){
                redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECDNDS);
            }else{  //防止缓存穿透
                redisUtil.set(productCacheKey, EMPTY_CHCHE, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
            }
        }
        return product;
    }   
    
    //生成随机失效时间
    private Integer genProductCacheTimeout(){
        return PRODUCT_CHCHE_TIMEOUT + new Random().nextInt(5)*60*60;
    }
    
    //防止大量的空缓存占用redis空间
    private Integer genEmptyCacheTimeout(){
        return new Random().nextInt(5)*60*60;
    }
}

  上面synchronized给this加的锁,表示,所有不同类型商品都会被阻塞等待,但是我们只想让同一种商品阻塞,来提高效率。而且分布式下,每一个分支都要去加锁。这时候我们可以用分布式锁替换synchronized。

优化双重检测锁

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    public static final String EMPTY_CHCHE = {};
    public static final String PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX = "lock:product:hot_cache_create:";
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), genProductCacheTimeout(), TimeUnit.SECDNDS);
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){  
            if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                //防止一直一个空缓存访问
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                return null;
            }
            
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);
            return product;
        }
        
        /**
         * 双重检测分布式锁,将上面的查询再进行一次查询
         * 用于解决热点缓存并发重建问题
         */
        RLock coolChangeHotProductCreateCacheLock =  redisson.getLock(PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX + productId);
        coolChangeHotProductCreateCacheLock.lock();
        
        try{
            productStr = redisUtil.get(productCacheKey);
            if(!StringUtils.isEmpty(productStr)){  
                if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                    //防止一直一个空缓存访问
                    redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                    return null;
                }
                product = JSON.parseObject(productStr, Product.class);
                //每读取一次,对数据进行一次延期 实现冷热分离
                redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);
                return product;
            }

            product = productDao.get(productId);
            if(product != null){
                redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECDNDS);
            }else{  //防止缓存穿透
                redisUtil.set(productCacheKey, EMPTY_CHCHE, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
            }
        }finally{
            //解锁
            coolChangeHotProductCreateCacheLock.unlock();
        }
        
        return product;
    }   
    
    //生成随机失效时间
    private Integer genProductCacheTimeout(){
        return PRODUCT_CHCHE_TIMEOUT + new Random().nextInt(5)*60*60;
    }
    
    //防止大量的空缓存占用redis空间
    private Integer genEmptyCacheTimeout(){
        return new Random().nextInt(5)*60*60;
    }
}

对上面的代码进行重构

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    public static final String EMPTY_CHCHE = {};
    public static final String PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX = "lock:product:hot_cache_create:";
    
    @Transactional
    public Product create(Product product){
        Product productResult = productDao.create(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = productDao.update(product);
        //也可以delete缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), genProductCacheTimeout(), TimeUnit.SECDNDS);
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        product = getProductFromCache(productCacheKey);
        if(product != null){
            return product;
        }
        
        /**
         * 双重检测分布式锁,将上面的查询再进行一次查询
         * 用于解决热点缓存并发重建问题
         */
        RLock coolChangeHotProductCreateCacheLock =  redisson.getLock(PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX + productId);
        coolChangeHotProductCreateCacheLock.lock();
        
        try{
            product = getProductFromCache(productCacheKey);
            if(product != null){
                return product;
            }

            product = productDao.get(productId);
            if(product != null){
                redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECDNDS);
            }else{  //防止缓存穿透
                redisUtil.set(productCacheKey, EMPTY_CHCHE, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
            }
        }finally{
            //解锁
            coolChangeHotProductCreateCacheLock.unlock();
        }
        
        return product;
    }   
    
    //生成随机失效时间
    private Integer genProductCacheTimeout(){
        return PRODUCT_CHCHE_TIMEOUT + new Random().nextInt(5)*60*60;
    }
    
    //防止大量的空缓存占用redis空间
    private Integer genEmptyCacheTimeout(){
        return new Random().nextInt(5)*60*60;
    }
    
    //从缓存中获取商品
    private Product getProductFromCache(String productCacheKey){
        Product product = null;
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){  
            if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                //防止一直一个空缓存访问
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                return new Product();
            }
            
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);            
        }
        return product;
    }
     
}

缓存与数据库双写不一致问题

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    public static final String EMPTY_CHCHE = {};
    public static final String PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX = "lock:product:hot_cache_create:";
    public static final String PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX = "lock:product:update_data_cache_different:";
    
    @Transactional
    public Product create(Product product){
        Product productResult = null;
        RLock productUpdateLock = redisson.getLock(PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX + product.getId());
        productUpdateLock.lock();
        try{
            productResult = productDao.create(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        }finally{
            productUpdateLock.unlock();
        }
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = null;
        RLock productUpdateLock = redisson.getLock(PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX + product.getId());
        productUpdateLock.lock();
        try{
            productResult = productDao.update(product);
            //也可以delete缓存
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), genProductCacheTimeout(), TimeUnit.SECDNDS);
        }finally{
            productUpdateLock.unlock();
        }
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        product = getProductFromCache(productCacheKey);
        if(product != null){
            return product;
        }
        
        /**
         * 双重检测分布式锁,将上面的查询再进行一次查询
         * 用于解决热点缓存并发重建问题
         */
        RLock coolChangeHotProductCreateCacheLock =  redisson.getLock(PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX + productId);
        coolChangeHotProductCreateCacheLock.lock();
        
        try{
            product = getProductFromCache(productCacheKey);
            if(product != null){
                return product;
            }
            
            //缓存数据库双写不一致问题出现地方
            RLock productUpdateLock = redisson.getLock(PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX + productId);
            productUpdateLock.lock();
            try{
                product = productDao.get(productId);
                if(product != null){
                    redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECDNDS);
                }else{  //防止缓存穿透
                    redisUtil.set(productCacheKey, EMPTY_CHCHE, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                }
            }finally{
                productUpdateLock.unlock();
            }
        }finally{
            //解锁
            coolChangeHotProductCreateCacheLock.unlock();
        }
        
        return product;
    }   
    
    //生成随机失效时间
    private Integer genProductCacheTimeout(){
        return PRODUCT_CHCHE_TIMEOUT + new Random().nextInt(5)*60*60;
    }
    
    //防止大量的空缓存占用redis空间
    private Integer genEmptyCacheTimeout(){
        return new Random().nextInt(5)*60*60;
    }
    
    //从缓存中获取商品
    private Product getProductFromCache(String productCacheKey){
        Product product = null;
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){  
            if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                //防止一直一个空缓存访问
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                return new Product();
            }
            
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);            
        }
        return product;
    }
     
}

分布式读写锁,优化分布式锁

public class ProductService{
    @Autowired
    private ProductDao productDao;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private Redisson redisson;
    
    public static final Integer PRODUCT_CHCHE_TIMEOUT = 60*60*24;
    public static final String EMPTY_CHCHE = {};
    public static final String PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX = "lock:product:hot_cache_create:";
    public static final String PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX = "lock:product:update_data_cache_different:";
    
    @Transactional
    public Product create(Product product){
        Product productResult = null;
        RLock productUpdateLock = redisson.getLock(PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX + product.getId());
        productUpdateLock.lock();
        try{
            productResult = productDao.create(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        }finally{
            productUpdateLock.unlock();
        }
        return productResult;
    }
    
    @Transactional
    public Product update(Product product){
        Product productResult = null;
        RReadWriteLock productUpdateLock = redisson.getReadWriteLock(PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX + product.getId());
        RLock writeLock = productUpdateLock.writeLock();
        writeLock.lock();
        try{
            productResult = productDao.update(product);
            //也可以delete缓存
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult), genProductCacheTimeout(), TimeUnit.SECDNDS);
        }finally{
            writeLock.unlock();
        }
        return productResult;
    }
    
    public Product get(Long productId){
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
        
        product = getProductFromCache(productCacheKey);
        if(product != null){
            return product;
        }
        
        /**
         * 优化双重检测分布式锁,使用读写分离锁
         * 用于解决热点缓存并发重建问题
         */
        RLock coolChangeHotProductCreateCacheLock = redisson.getLock(PRODUCT_HOT_CACHE_CREATE_LOCK_PREFIX + productId);
        coolChangeHotProductCreateCacheLock.lock();
        
        try{
            product = getProductFromCache(productCacheKey);
            if(product != null){
                return product;
            }
            
            //缓存数据库双写不一致问题出现地方
            RReadWriteLock productUpdateLock = redisson.getReadWriteLock(PRODUCT_UPDATE_DATA_CACHE_DIFFERENT_PREFIX + productId);
            RLock readLock = productUpdateLock.readLock();
            readLock.lock();
            try{
                product = productDao.get(productId);
                if(product != null){
                    redisUtil.set(productCacheKey, JSON.toJSONString(product), genProductCacheTimeout(), TimeUnit.SECDNDS);
                }else{  //防止缓存穿透
                    redisUtil.set(productCacheKey, EMPTY_CHCHE, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                }
            }finally{
                readLock.unlock();
            }
        }finally{
            //解锁
            coolChangeHotProductCreateCacheLock.unlock();
        }
        
        return product;
    }   
    
    //生成随机失效时间
    private Integer genProductCacheTimeout(){
        return PRODUCT_CHCHE_TIMEOUT + new Random().nextInt(5)*60*60;
    }
    
    //防止大量的空缓存占用redis空间
    private Integer genEmptyCacheTimeout(){
        return new Random().nextInt(5)*60*60;
    }
    
    //从缓存中获取商品
    private Product getProductFromCache(String productCacheKey){
        Product product = null;
        String productStr = redisUtil.get(productCacheKey);
        if(!StringUtils.isEmpty(productStr)){  
            if(EMPTY_CHCHE.equals(productStr)){  //防止缓存穿透
                //防止一直一个空缓存访问
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECDNDS);
                return new Product();
            }
            
            product = JSON.parseObject(productStr, Product.class);
            //每读取一次,对数据进行一次延期 实现冷热分离
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECDNDS);            
        }
        return product;
    }
     
}

缓存雪崩

缓存雪崩指的是缓存支撑不住或宕机,大量数据打到后端存储层。
预防和解决雪崩,可以从三方面入手
1、保证缓存层服务高可用,比如可以使用redis sentinel或redis cluster
2、依赖隔离组件为后端限流熔断并降级,如sentinel或hystrix限流降级组件
3、提前演练,在项目上线前,演练缓存层宕机后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定

多级缓存架构解决雪崩
用了多次缓存架构,用mq来同步各缓存之间数据同步
用了多层缓存架构,就不要要求数据强行一致了。做架构是需要有取舍的
加微信群一块讨论学习Day9884125