Redis分布式锁

一、简介

单机器环境下,可以通过锁来解决共享资源的竞争问题;而在分布式集群环境下,机器与机器之间的资源竞争则需要依赖Redis、ZooKeeper等中间件去协调。

简单总结一下自己对Redis分布式锁的一些理解

二、代码实现

第一步先是获取锁,通过setnx操作,设置指定key及其过期时间。较新的版本支持setnx和过期时间的原子性操作,如果是较老的版本,只能通过Lua脚本来完成这一步。

若setnx返回true,则代表成功获取到锁,否则没有获取到锁。

boolean setIfAbsent = false;
try {
		setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, lockTimeout, TimeUnit.MILLISECONDS);
} catch (Exception e) {
		log.error("redis lock occur error", e);
}
if (!setIfAbsent) {
		return;
}

获取到锁以后开始执行task任务

执行完成后需要释放锁

try {
		redisTemplate.delete(lockKey);
} catch (Exception e) {
		logger.error("release lock occur error", e);
}

乍一看没什么问题,但是在某种情况下是有问题的,假如A获取到了锁开始执行task,但是task执行时间很长,超过了lockTimeout时间,key过期了,此时B尝试获取锁,成功获取到了,开始执行task,A完成了task执行了redisTemplate.delete操作,把B的锁给删除了,那么锁的功能就失效了。

在这种情况下就需要保证获取锁的那一方的锁不会被其他方释放,锁只能被获取方释放或过期自动释放,不能有其他被释放的情况发生。因此可以通过锁key时设置指定的value,只有相同的value才能释放锁。而这个value的比较与锁的释放必须保证原子性,需要通过Lua脚本来实现。

try {
		redisTemplate.execute(
				RedisScript.of(
						"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
      			Long.class
    		),
    		Collections.singletonList(lockKey),
    		clientId);
} catch (Exception e) {
  	log.error("release lock occur error", e);
}

完整的代码示例如下:

public class RedisLock {

    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * @param lockKey     锁key
     * @param clientId    锁value
     * @param lockTimeout 锁超时时间(毫秒)
     * @param task        执行类
     */
    public void lock(String lockKey, String clientId, long lockTimeout, Task task) {
        boolean setIfAbsent = false;
        try {
            setIfAbsent = redisTemplate.opsForValue().
                    setIfAbsent(lockKey, clientId, lockTimeout, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.error("redis lock occur error", e);
        }
        if (!setIfAbsent) {
            return;
        }
        try {
            task.action();
        } catch (Exception e) {
            log.error("task action occur error", e);
        }
        try {
            redisTemplate.execute(
                    RedisScript.of(
                            "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
                            Long.class
                    ),
                    Collections.singletonList(lockKey),
                    clientId);
        } catch (Exception e) {
            log.error("release lock occur error", e);
        }
    }

}

三、不足

  • Redis锁并没有极高的可靠性,Redis一般采用主从方式部署,如果在加锁过程中,Redis主节点挂掉,加锁的指令只在主节点执行完成,但是还未同步到从节点,此时从节点变更为主节点,但是锁信息并没有得到同步,锁的功能就失效了。

  • 上述的方案,如果task执行出现耗时超过锁的超时时间,仍然会出现task并发执行的情况,这种情况需要根据实际业务场景决定,是否允许此类情况发生,如果不允许是否需要增加超时时间,或者提供锁延长超时时间的机制。

posted @ 2023-07-02 23:00  mini的爸爸  阅读(41)  评论(0)    收藏  举报