Redis分布式锁的正确使用姿势

本篇文章将详细介绍实现redis分布式锁的几种方式已经存在的问题,并提供最终的解决方案。还介绍如何实现 等待锁

setnx 命令

当且仅当key不存在时,将key的值设置为value。若给定的key已经存在( 返回0 ),则setnx不做任何操作( 返回1 )。加锁解锁伪代码如下:

if (setnx(key, 1) == 1){
    expire(key, 30)  // 防止执行业务宕机造成死锁
    try {
        //TODO 业务逻辑
    } finally {
        del(key)  // 直接删除redis的key
    }
}

上述代码是有问题的。比如有如下的场景:

如果 SETNX 成功,在执行expire时,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时时间变成死锁。

于是我们应该保证 setnx 和 expire的绝对原子性(同时成功或者失败), 于是我们可以使用lua脚本。

Lua脚本

---------------------------加锁脚本--------------------------
-- redis lock impl for lua  作者hadluo

if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then
		return redis.call('expire',KEYS[1],ARGV[2])
else
	return 0
end


---------------------------解锁脚本--------------------------
return redis.call('del', KEYS[1]) 

上面lua脚本步骤:

  • 执行setnx命令。
  • 返回1拿锁成功,执行expire。
  • 解锁直接执行 del 命令,用不到脚本。

这样我们通过 redis执行lua脚本时以原子方式执行 的特性来解决SETNX 和expire的非原子性。

但是上面也存在问题, 比如锁误删除问题。

锁误删除

有这样一个场景:

  • 线程A setnx拿锁成功,并且设置过期时间20秒成功,执行业务逻辑超过了20秒,锁过期释放,但是业务逻辑还在执行。
  • 此时,线程B执行拿锁成功,线程B开始执行业务逻辑。
  • 线程A业务逻辑终于执行完成,于是执行finally释放锁,于是把线程B的锁也给删除了(此此时线程B还在执行业务逻辑)。

这个问题也可以解决。删除时加上一个标识。于是lua脚本可以这样写:

---------------------------加锁脚本--------------------------
-- redis lock impl for lua  作者hadluo

if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then
	if redis.call('get',KEYS[1])==ARGV[1] then
		return redis.call('expire',KEYS[1],ARGV[2])
	else
		return 0
	end
else
	return 0
end

---------------------------解锁脚本--------------------------
if redis.call('get', KEYS[1]) == ARGV[1] then
	return redis.call('del', KEYS[1]) 
else
	return 0
end



------------------------- 结合 RedisTemplate 使用lua脚本加锁代码-------------------
    public boolean lock(String lockKey, String releaseFlag, int expireSecond) {
        if (StringUtils.isEmpty(lockKey)) {
            return true;
        }
        // 脚本字符串
        String lockScript ="if redis.call('setNx',KEYS[。。。。。。"; 
        Long ret = getRedisTemplate().execLua(Long.class, lockScript, Collections.singletonList(lockKey),
                releaseFlag, expireSecond + "");
        if (ret == 1) {
            // 拿锁成功
            return true;
        }
        return false;
    }
  • KEYS[1] : 对应方法参数lockKey。
  • ARGV[1] : 对应方法参数releaseFlag。
  • ARGV[2] : 对应方法参数expireSecond。

releaseFlag 就是我们直接生成的uuid。加锁时生成一个uuid。解锁时判断这个uuid是不是加锁时候的,是的话就解锁成功,否则就解锁失败,这样就避免了 锁误删除

但是这样还是会有一个问题, 超时误解锁

超时误解锁

线程A 加锁成功并且设置20秒过期时间。线程A执行业务逻辑超过20秒,此时锁过期释放。线程B就可以获取锁,结果导致 线程A和线程B并发执行了

两种解决方式:

  • 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
  • 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。

第二种是啥意思呢? 开启一个守护线程,循环判断锁将要过期了(此时还没过期),将锁的过期时间延长,也就是续命操作。


Redission 工具


获取redisson 客户端(集群模式):

Config config = new Config();
config.useClusterServers()
    .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
    //可以用"rediss://"来启用SSL连接
    .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
    .addNodeAddress("redis://127.0.0.1:7002");

RedissonClient redisson = Redisson.create(config);

加锁代码:

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

信号量代码:

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

信号量封装成等待锁

public class RedissionWaitLock{

    RSemaphore semaphore;

    public RedissionWaitLock(RedissonClient redisson, String key) {
        semaphore = redisson.getSemaphore(key);
    }

    @Override
    public void lock() {
        if (semaphore == null) {
            return;
        }
        semaphore.trySetPermits(1);
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
            unLock();
        }
    }

    @Override
    public void unLock() {
        if (semaphore != null) {
            semaphore.release();
        }
    }
}

redission的更多高级功能请见:

罗政:redission 详细教程文档

推荐一个Java架构师博客,带你一起写架构:

Java架构师修炼

支付宝打赏 微信打赏

如果文章对您有帮助,您可以鼓励一下作者