Redis怎样实现分布式锁?
前言
我们知道,在Java单进程中,多线程的环境下,如果我们要操作一个共享变量,需要使用synchronized或者是JUC同步工具类才能保证线程安全。那么,多进程环境下,我们要怎样保证线程安全?
为什么需要分布式锁?
我们知道,synchronized或者是JUC同步工具类只能在同一进程中保证线程安全,他们的影响范围没办法超出本Java进程。但是随着分布式成为主流,多进程共享数据的情况越来越常见。

如上图,两个进程同时对存储在MySQL、Redis或者是zookeeper中的共享数据进行读写,即有可能出现线程安全问题,这种情况下,我们就需要一个可以在分布式环境下也可以使用的锁,来保证线程安全,这即是分布式锁。
分布式锁可以使用什么组件实现?
因为要在分布式环境下生效,因此实现分布式锁使用的组件也必须是每个进程都可以连接到的,目前比较常见的是使用Redis和Zookeeper来实现。
Redis分布式锁实现逻辑
Redis中,我们使用String数据结构来实现分布式锁。
加锁
Redis中,set的语法如下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
- PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
- NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
- XX :只在键已经存在时,才对键进行设置操作。
上述参数中,我们可以将key作为锁标识,然后设置NX参数。如果key写入成功,表示当前Redis中不存在这个key,可以加锁;如果key写入失败,表示当前Redis中已存在这个key,已经有其它线程获取到锁了。命令如下:
SET lockKey requestId NX
lockKey为锁标识,最好带上使用共享变量唯一标识,可以使用订单编号、用户编号等。 requestId为本次锁请求编号,释放锁时使用。
上述命令中,可以实现加锁,但是如果在加锁后,应用挂了,或者出现了其它问题,导致没有及时解锁,就有可能出现死锁,因此,需要再给加上一个过期时间,让锁可以自动消失。命令如下:
SET lockKey requestId EX seconds NX
释放锁
释放锁的时候,我们不能直接使用del命令去删除Redis键值,否则会出现A获取的锁,B也可以释放的情况。因此,我们在释放锁的时候,需要判断当前锁的请求ID是否是加锁时是否一致,如果一致,才能释放锁。
由于没有现成的命令可以实现上述的释放锁的逻辑,所以我们需要使用Redis的script来实现,script脚本如下:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
KEYS[1]为锁标识,即加锁时的lockKey
ARGV[1]为锁请求编号,即加锁时的requestId
Redis分布式锁代码
public class RedisDistributedLock implements Lock { // 锁键值 private String lockKey; // 锁请求编号 private String requestId; // redis集群客户端 private JedisCluster jedisCluster; public RedisDistributedLock(String lockKey, JedisCluster jedisCluster){ this.lockKey = lockKey; // 使用UUID作为锁唯一标识 requestId = UUID.randomUUID().toString(); this.jedisCluster = jedisCluster; } /** * 尝试获取锁 */ @Override public boolean tryLock() { // 获取锁 String result = jedisCluster.set(lockKey, requestId, "NX", "EX", 2); return StringUtils.isNotBlank(result) && LOCK_SUCCESS.equals(result); } /** * 释放锁 */ @Override public void unlock() { // 若redis中存在lockKey,则删除lockKey,否则返回0 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedisCluster.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); // 成功 if (result != null && "OK".equals(result.toString())) { logger.debug("释放锁成功,lockKey:{}, requestId:{}", lockKey, requestId); } else {// 失败 logger.debug("释放锁失败,lockKey:{}, requestId:{}", lockKey, requestId); } } }#Java##程序员#