Redis实现简单分布式锁

311 阅读3分钟

Redis实现分布式锁

1、什么是分布式锁

提到锁,我们最熟悉的就是线程锁和进程锁

线程锁主要是给方法、代码块加锁。在同一时刻只有一个线程能够获取锁。线程锁只在同一个JVM内起作用,线程锁的实现在根本上是依靠线程之间共享内存来实现的,比如Synchronized、Lock等

进程锁是控制同一操作系统中多个进程访问某个共享资源

在单机环境下,针对多线程并发问题可以通过线程锁来进行互斥控制,但是分布式环境下,系统分别部署在不同的机器上,不是同一个JVM,因此线程锁不起作用,为了解决这种问题,因此需要一种跨JVM的互斥机制来控制资源的访问,因此分布式锁出现了

2、分布式锁的实现方式

①数据库乐观锁

②基于Zookeeper实现分布式锁

③基于Redis实现分时锁(主要介绍)

3、Redis如何实现分布式锁

主要是基于Redis命令:SET [key] [value] NX EX [time]

下面是一个模拟分布式锁(模拟售票问题)

1、代码

//加锁
    public void test(){
​
        try {
            //setIfAbsent就是 SETNX在Java里面的封装
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock-key", "lock-value");
            if(result == null || !result){
                //加锁失败,返回错误信息
                System.out.println("加锁失败");
                return;
            }
            
            //获取库存
            int ticket = Integer.parseInt(stringRedisTemplate.opsForValue().get("ticket"));
​
            if(ticket > 0){
                int  nowTicket = ticket - 1;
                stringRedisTemplate.opsForValue().set("ticket",nowTicket+"");
                System.out.println("扣减成功,当前剩余票数"+nowTicket);
            }else {
                System.out.println("票数不足!");
            }
            
        } finally {
            //如果中间发生异常,最终都会释放锁
            stringRedisTemplate.delete(key);
        }
​
    }

可能存在问题:

1、当中途突然宕机或者运维kill -9 直接杀死进程,finally无法执行,锁最终无法释放,造成堵塞问题 (解决方式:加锁时增加过期时间)
2、当AB两个线程同时进来后,A线程加锁成功,然后B线程加锁失败,此时B线程结束前执行finally方法,将A线程加的锁删除了,此时C线程进来,又获得了锁,AC两个线程仍然存在并发问题 (解决方式:给线程增加唯一标识,只能由加锁的线程来解锁)

优化后的代码:

 public void test(){
​
        String key = "lock-key";
        //给予当前线程的唯一标识
        String value = UUID.randomUUID().toString();
        try {
            //setIfAbsent就是 SETNX在Java里面的封装,优化:在此处设定30s的过期时间,就算是中途进程被杀掉,30S后锁会自动过期删除
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, value,30,TimeUnit.SECONDS);
            if(result == null || !result){
                //加锁失败,返回错误信息
                System.out.println("加锁失败");
                return;
            }
            
            //获取库存
            int ticket = Integer.parseInt(stringRedisTemplate.opsForValue().get("ticket"));
​
            if(ticket > 0){
                int  nowTicket = ticket - 1;
                stringRedisTemplate.opsForValue().set("ticket",nowTicket+"");
                System.out.println("扣减成功,当前剩余票数"+nowTicket);
            }else {
                System.out.println("票数不足!");
            }
            
        } finally {
            
            //当前线程的唯一标识和锁存储的唯一标识匹配时才删除锁
            if(value.equals(stringRedisTemplate.opsForValue().get(key))){
                stringRedisTemplate.delete(key);
            }
        }
​
    }

此时就完成了一个分布式锁,对于一般并发量不高的场景是足够的,但这就没有问题了吗?

仍可能存在问题:

1、当中间业务逻辑执行时间较长,超过30s,锁仍然会自动释放,仍然会造成当前线程未完成,然后另一线程又获得了锁的情况
解决方式:可以另外开启一个线程,定时扫描,如每次定时10s扫描一次分布式锁是否超时,如果分布式锁超时,而且当前线程没有完成,那就增加分布式锁的时间(续命)

redission框架集成了这种操作:

//引入redission依赖后
 public void test(){
​
        String key = "lock-key";
        //初始化
        RLock redissionLock = redission.getLock(key);
     
        try {
            //加锁
            redissionLock.lock();
            
            //获取库存
            int ticket = Integer.parseInt(stringRedisTemplate.opsForValue().get("ticket"));
​
            if(ticket > 0){
                int  nowTicket = ticket - 1;
                stringRedisTemplate.opsForValue().set("ticket",nowTicket+"");
                System.out.println("扣减成功,当前剩余票数"+nowTicket);
            }else {
                System.out.println("票数不足!");
            }
            
        } finally {
            //解锁
            redissionLock.unlock();
        }
​
    }