Redis分布式锁

92 阅读3分钟

介绍

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。

使用分布式锁方案

针对同一个key操作

@RequestMapping("/deduct_stock")
public String deductStock() {
    String lockKey = "lock:product_101";
    //放到value中,校验uuid,防止别的线程释放锁
    String clientId = UUID.randomUUID().toString();
    //方式一、加锁
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
    if (!result) {
        return "error_code";
    }
    
    //方式二、加锁 获取锁对象
    RLock redissonLock = redisson.getLock(lockKey);
    //加分布式锁
    redissonLock.lock();  //  .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
    
    try {
        //业务逻辑
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    } finally {
        //方式一、解锁
        if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
            stringRedisTemplate.delete(lockKey);
        }
        //方式二、解锁
        redissonLock.unlock();
        }

    return "end";
}

方式一存在问题:

  1. 高并发情况下,如果业务逻辑代码执行的时间大于超时时间,key失效,线程1还没执行完,线程2就会加锁成功;线程1执行完后删除key,删除的是线程2的锁,接着线程3又会加锁成功;(可以把uuid放到value中,谁加的锁才能谁释放)
  2. 释放锁时存在原子性问题,如果线程1校验成功后,出现卡顿,线程2加锁成功,线程1还是会删掉线程2的锁,那线程3又会加锁成功

解决办法:锁续命

分布式锁源码分析

Redis加锁流程 image.png

流程1

image.png

加锁逻辑(设置key,并设置过期时间)主要由上面这段lua脚本完成,由于redis单线程的特性,这段lua脚本具有原子性,不会被打断;

lua脚本

  • 减少网络开销,可以一次执行多条命令,类似管道
  • 原子操作,整个脚本作为一个整体执行,中间不会被其他命令插入;(管道不是原子的)
  • 替代redis事务功能

参数介绍

KEYS[1] = Collections.<Object>singletonList(getName()) //传入的key的名字

ARGV[1] = internalLockLeaseTime = lockWatchdogTimeout = 30 * 1000 //看门狗超时时间

ARGV[2] = getLockName(threadId) = uuid:threadId  //hash的field

流程2

加锁成功后,返回0,回调执行scheduleExpirationRenewal方法,进行锁续命

image.png

锁续命逻辑

image.png 延迟10s执行run方法,也是一段lua脚本,如果存在当前主线程存在的key,重新设置超时时间为30s 如果设置成功,返回1,再隔10s调用自己

流程5自旋

流程1加锁逻辑中,如果加锁成功返回nil,不成功,返回剩余ttl,线程2阻塞对应时间后,再去尝试重新加锁

image.png

流程6

线程1执行完释放锁后,发布消息到一个队列,当前正在阻塞的线程订阅这个队列,阻塞的线程2收到消息后重新执行加锁逻辑

线程1在释放锁时发布消息,同样是lua脚本实现

image.png

1、如果锁不存在,发布unlockMessage

2、删除锁,发布unlockMessage

image.png 收到到消息后,释放信号量

主从架构锁失效问题

master接收客户端的写入操作后,还没同步消息到slave就挂了,slave升级为master时,数据就会丢失 解决办法:zookeeper(强一致性),RedLock(未完全解决,不推荐使用)

RedLock

image.png

客户端向3个节点设置锁,有2个加锁成功,才算加锁成功(所以节点最好是奇数)

主从锁失效问题

  1. 假设每个master都有一个slave,保证高可用。客户端写入master后未同步slave就挂了,slave升级为master后还是没有key,依然可以加锁成功
  2. 假设没有slave,主节点挂了半数以上,RedLock就无法加锁成功
  3. 假设没有slave,master加锁成功后通过客户端,这是还未持久化到aof文件(持久化策略不是every),redis这个时候重启,也会造成锁失效