redis实现分布式锁

如何设计安全的分布式锁

在生产环境下一般不会使用单实例redis, 然而集群下的redis使用如下方式获取分布式锁存在安全性问题, 不管是 sentinel 模式或者 cluster 模式, 主要受限于 redis 数据同步模型. 

从 CAP 理论上讲, redis 实现了 AP 即放弃了一致性, 然而分布式锁应该实现 CP 才对.

不过, 在项目中如果可以容忍重复获取分布式锁, 可以考虑如下方案或者redlock.

https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

概要

一个分布式锁需要满足以下三个条件:

  1. 分布式锁的加锁和释放锁要保证操作的原子性.
  2. 锁的拥有者因为异常退出, 锁在合理的时间范围内可以自动释放.
  3. 只有锁的拥有者可以释放锁.

使用 setnx key value 实现分布式锁的缺点:

  1. 没有设置超时时间, 获取锁的服务器一旦发生OOM挂掉, 其他服务器将不再有机会获取到锁.
  2. 如果在value中设置时间, 对锁的存在和超时判断将不是原子性操作.

加锁

获取锁使用如下命令:

set key value [expiration EX seconds|PX milliseconds] [NX|XX]

此处的 value 存储了 requestId, 可以理解为锁的钥匙, 只有锁的拥有者知道. 可以是一个随机生成的字符串. 例如:

# NX: SET_IF_NOT_EXIST
# PX: SET_WITH_EXPIRE_TIME
127.0.0.1:6379> set key value NX PX 3000
OK
127.0.0.1:6379> get key
"value"

加锁成功会返回 "OK"

解锁

解锁时要验证锁的拥有者, 验证 requestId 是否正确, 正确则可以删除锁. 这包含了两步操作, 验证和删除. 我们可以通过 lua 脚本把这两个操作合并成一个操作.
redis 执行 lua 脚本具有原子性(注意: SharedJedis 客户端不支持 lua 脚本).

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

核心代码

/**
 * redis 实现分布式锁
 *
 * @author xxx
 * @date 2021-01-15 15:50
 **/
public class RedisDistributedLock implements DistributedLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    @Override
    public boolean lock(String lockKey, String requestId, int expireMillis) {
        String result = RedisUtil.opsForValue()
            .set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireMillis);
        return LOCK_SUCCESS.equals(result);
    }

    @Override
    public boolean unLock(String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = RedisUtil
            .eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        return RELEASE_SUCCESS.equals(result);
    }
}

使用

 @Test
    public void DistributedLockTest(){
        DistributedLock distributedLock = new RedisDistributedLock();
        String lockKey = "xxx";
        try {
            if (distributedLock.lock(lockKey, "111", 3000)){
                System.out.println("获取成功!");
            }
        }finally {
            if (distributedLock.unLock(lockKey, "222")){
                System.out.println("222--释放成功!");
            } else {
                System.out.println("222--释放失败!");
            }
            if (distributedLock.unLock(lockKey, "111")){
                System.out.println("111--释放成功!");
            }else {
                System.out.println("111--释放失败!");
            }
        }
    }

输出结果:

获取成功!
222--释放失败!
111--释放成功!

 

 
posted @ 2021-02-23 14:13  景岳  阅读(201)  评论(0编辑  收藏  举报