为什么使用Redission解决高并发场景分布式锁问题

业务场景:高并发场景下的减库存代码实现

方案一:使用JVM或JDK级别的锁【synchronized】

问题:使用synchronized的加锁,如果是单机环境的话没有问题,但是对于集群/分布式环境则会出问题,对于跨tomcat就会锁不住。

@RestController
public class IndexControlelr {

    @Autowired
    private Redisson redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/deduct_stock")
    public String deductStock() {
  
        //以下的代码高并发场景下有问题
        synchronized(this) {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));   //获取redis值  jedis.setnx(key.value)
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("", realStock + "");   //设置redis值   jedis.set(key.value)
            Sysytem.out.printLn("扣减成功,剩余库存:" + realStock);
        } else {
            Sysytem.out.printLn("扣减失败,库存不足");
        }
      }
        return "end";
    }
}

方案二:为了解决方案一的问题,使用redis分布式锁的SETNX命令可以解决刚刚方案一的问题。

使用格式:setnx key value   将key的值设为value,当且仅当key不存在。若给定的key已存在,则SETNX不做任何操作。

问题:会出现死锁,就是当程序执行一般,中间的代码出现异常导致无法释放这把锁,此时就会出现死锁的现象。

     //使用redis分布式锁   
    @RequestMapping("/deduct_stock")
    public String deductStock() {
    
        String lockKey = "lockKey";
        boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl");
        if(!result){
            return "error_code";
        }
//如果执行到这里下面的代码抛异常则无法完成释放锁,死锁产生
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //获取redis值 jedis.setnx(key.value) if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("", realStock + ""); //设置redis值 jedis.set(key.value) Sysytem.out.printLn("扣减成功,剩余库存:" + realStock); } else { Sysytem.out.printLn("扣减失败,库存不足"); } //释放锁 stringRedisTemplate.delete(lockKey); return "end"; }

方案三:为了解决方案二的问题,设置key和操作时间+try ...catch...finally释放锁

问题:时间问题,高并发场景下此代码将就可以用了,但是会出现自己加的锁会被别人释放掉。

    @RequestMapping("/deduct_stock")
    public String deductStock() {
        
        String lockKey = "lockKey";
        
        try {
        //解决:给锁加一个超时时间,
        //boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl");
        //stringRedisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
        //设置key和操作时间==保证原子性 将上面的两行合并为下面的一行
        boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl",10,TimeUnit.SECONDS);
        
        if(!result){
            return "error_code";
        }
        
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));   //获取redis值  jedis.setnx(key.value)
        if (stock > 0) {
            int realStock = stock - 1; 
            stringRedisTemplate.opsForValue().set("", realStock + "");   //设置redis值  jedis.set(key.value)
            Sysytem.out.printLn("扣减成功,剩余库存:" + realStock);
        } else {
            Sysytem.out.printLn("扣减失败,库存不足");
        }
        //解决释放锁的问题使用finally释放锁
        }funally {
        //释放锁
        stringRedisTemplate.delete(lockKey);
      }
        return "end";
    }

方案四:为了解决方案三的问题,生成UUID,将这个UUID设置到锁对应的value里面,自己加的锁只能自己释放,并在后台启动一个分线程每个一段时间(一般这个时间是你设置的超时时间的1/3,超时时间一般默认为30s)去检查一下主线程是否还持有这把锁,如果还持有这把锁的话就把设置的超时时间顺延30s

问题:代码量太大了

    @RequestMapping("/deduct_stock")
    public String deductStock() {
        
        String lockKey = "lockKey";
        //生成一个UUID
        String clientId = UUID.randomUUID().toString();
        
        try {
        //boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl");
        //stringRedisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
        //将UUID设置到锁对应的value里面
        //这里的时间还是会存在问题==解决,在后台起一个分线程:起一个定时任务每10s(不超过30s,设置值的1/3时间)检查一下主线程是否还持有这把锁,
        //如果还持有把这个超时时间延长(重新设置30s),====>问题代码量太大了
        
        //解决方案:reddison框架底层的原理就是我们现在写的这些逻辑。
        //使用:pom直接引入依赖包即可,可以支持很多redis架构模式(主从,哨兵,高并发等)
        boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS);
        
        if(!result){
            return "error_code";
        }
        
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));   //获取redis值  jedis.setnx(key.value)
        if (stock > 0) {
            int realStock = stock - 1; 
            stringRedisTemplate.opsForValue().set("", realStock + "");   //设置redis值  jedis.set(key.value)
            Sysytem.out.printLn("扣减成功,剩余库存:" + realStock);
        } else {
            Sysytem.out.printLn("扣减失败,库存不足");
        }
        //解决释放锁的问题使用finally释放锁
        }funally {
        //判断一下这把锁是不是自己加的锁(线程id)
        if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
            stringRedisTemplate.delete(lockKey);
        }
      }
        return "end";
    }
    
}

方案五:为了解决方案四的问题。使用Redission框架

@SpringBootApplication
public class Application {

     public static void main(String[] args) {
     SpringApplication.run(Application.class,args);
     }
     
     @Bean
     public Redisson redisson() {
         Config config = new Config();
         config.useSingleServer().setAddress("redis://localhost:8769").setDatabase(0);
         return (Redisson) Redisson.create(config);
     }
}

-----------------------------------------------------------------------------------------------------
@RequestMapping("/deduct_stock")
    public String deductStock() {
        
        String lockKey = "lockKey";

        RLock redissonLock = redisson.getLock(lockKey);
        
        try {
            //加锁默认的超时时间30s
            redissonLock.lock();   //setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS);
            
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));   //获取redis值  jedis.setnx(key.value)
            if (stock > 0) {
                int realStock = stock - 1; 
                stringRedisTemplate.opsForValue().set("", realStock + "");   //设置redis值  jedis.set(key.value)
                Sysytem.out.printLn("扣减成功,剩余库存:" + realStock);
            } else {
            Sysytem.out.printLn("扣减失败,库存不足");
        }
    }funally {
         
        //释放锁
        redissonLock.unlock();
        }
    }
    return "end";
}

Redission 的源码剖析

 Lua脚本具有原子性,所以redis把上面的一整串字符串当作一条命令来执行,要么成功,要么失败(解决了分布式一致性的问题)。

Redission分布式锁实现原理

Redis集群架构一般是满足AP,redis主节点获取到锁之后会立马返回给客户端。QPS理论上是10万,但是一般达不到10万,才几万。

zookeeper是CP:一致性,主节点获取到锁之后先同步给从节点,半数以上的从节点获取到锁之后再返回给客户端。

****一般会使用zookeeper来做分布式锁,redis做缓存,但是还是有很多的公司选择使用redis来做分布式锁,因为redis的性能高(对并发要求比较高的情况)。

原文地址:https://www.cnblogs.com/nastu/p/15645645.html