Redis 分布式锁
Redis Spring Boot 锁 About 6,723 words单点 Redis 分布式锁
上锁
SET resource_name my_random_value NX PX 30000
解锁
此处为Lua
脚本,需通过eval
命令调用,脚本后的1
表示只有一个key
:
eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 resource_name my_random_value
不能使用 delete 方法
假设A
线程抢占了锁并设置3
秒自动过期,在A
线程将要释放锁的前一刻,刚好过期时间到了 key 被自动删除了,并且在此时线程B
又抢占了同名的这把锁,接着线程A
开始执行delete
语句,把线程B
上的锁给删了,导致其他线程C
、线程D
来抢锁,破坏同步逻辑。
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123", 3, TimeUnit.SECONDS);
if (lock != null && lock) {
try {
TimeUnit.SECONDS.sleep(2);
} finally {
// 在 delete 前,刚好这个 key 自动过期了,并且 其他线程又设置了这个 lock 锁了,接着此程序接着执行 delete 把其他线程设置的锁给删除了。
redisTemplate.delete("lock");
}
}
Redis 分布式锁 Spring Boot 版本
基于Spring Boot 2.4.3
,默认Redis
底层驱动为Lettuce
。
@Component
public class RedisLock {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String UNLOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private static final Long UNLOCK_SUCCESS_RESULT = 1L;
public boolean tryLock(String key, String value, long timeout, TimeUnit timeUnit) {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeout, timeUnit);
return lock != null && lock;
}
public boolean unlock(String key, String value) {
Long result = stringRedisTemplate.execute(RedisScript.of(UNLOCK_LUA_SCRIPT, Long.class), Collections.singletonList(key), value);
return UNLOCK_SUCCESS_RESULT.equals(result);
}
}
若使用的是Jedis
驱动,则不能直接使用execute
。
Jedis
驱动的集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行。
Long result = stringRedisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA_SCRIPT, key, value);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA_SCRIPT, key, value);
}
return 0L;
}
});
应用示例
@SpringBootApplication
public class LockApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(LockApplication.class, args);
}
@Resource
private RedisLock redisLock;
@Override
public void run(String... args) throws Exception {
String key = "myLock";
// 注意 锁同一个对象时,切记设置不同的 value 值,因为 Lua 脚本是根据传入的值与 Redis 中保存的值做比较,相同才执行 delete
String value = DigestUtils.md5DigestAsHex(String.valueOf(System.currentTimeMillis()).getBytes());
boolean lock = redisLock.tryLock(key, value, 10, TimeUnit.SECONDS);
if (lock) {
try {
TimeUnit.SECONDS.sleep(5);
} finally {
boolean unlock = redisLock.unlock(key, value);
System.out.println(unlock);
}
}
}
}
注意
锁的value
切记设置不同的值,因为Lua
脚本是根据传入的值与Redis
中保存的值做比较,相同才执行delete
,而不同线程都是设置固定的值可能会导致A
线程删除了B
线程设置的值。
value
可采用雪花算法、数据库自增(序列)、MongoId
、Redis INCR key
等方式。
推荐使用StringRedisTemplate
,若使用RedisTemplate
则key
会出现\xac\xed\x00\x05t\x00\x04
前缀,因为RedisTemplate
默认使用JdkSerializationRedisSerializer
。
集群 Redis 分布式锁
单节点Redis
容易故障,生产环境一般是集群。
但是使用主从或集群存在问题:master
拿到锁,但是加锁的key
还没有同步到slave
节点,master
就故障了,发生故障转移,slave
节点升级为master
节点,导致锁丢失。
可使用Redlock
实现。
Redlock
摘自:https://redis.io/topics/distlock#the-redlock-algorithm
The Redlock algorithm In the distributed version of the algorithm we assume we have N Redis masters. Those nodes are totally independent, so we don’t use replication or any other implicit coordination system. We already described how to acquire and release the lock safely in a single instance. We take for granted that the algorithm will use this method to acquire and release the lock in a single instance. In our examples we set N=5, which is a reasonable value, so we need to run 5 Redis masters on different computers or virtual machines in order to ensure that they’ll fail in a mostly independent way. In order to acquire the lock, the client performs the following operations:
- It gets the current time in milliseconds.
- It tries to acquire the lock in all the N instances sequentially, using the same key name and random value in all the instances. During step 2, when setting the lock in each instance, the client uses a timeout which is small compared to the total lock auto-release time in order to acquire it. For example if the auto-release time is 10 seconds, the timeout could be in the ~ 5-50 milliseconds range. This prevents the client from remaining blocked for a long time trying to talk with a Redis node which is down: if an instance is not available, we should try to talk with the next instance ASAP.
- The client computes how much time elapsed in order to acquire the lock, by subtracting from the current time the timestamp obtained in step 1. If and only if the client was able to acquire the lock in the majority of the instances (at least 3), and the total time elapsed to acquire the lock is less than lock validity time, the lock is considered to be acquired.
- If the lock was acquired, its validity time is considered to be the initial validity time minus the time elapsed, as computed in step 3.
- If the client failed to acquire the lock for some reason (either it was not able to lock N/2+1 instances or the validity time is negative), it will try to unlock all the instances (even the instances it believed it was not able to lock).
谷歌翻译:
在算法的分布式版本中,我们假设我们有 N 个 Redis master 节点。这些节点是完全互相独立的,因此我们不使用主从复制或任何其他隐式协调系统(集群等)。我们已经描述了如何在单个实例中安全地获取和释放锁。我们认为该算法将使用此方法在单个实例中获取和释放锁,这是理所当然的。在我们的示例中,我们将 N = 5 设置为一个合理的值,因此我们需要在不同的计算机或虚拟机上运行 5 个 Redis master 节点,以确保它们不会同时都宕机。
为了获取锁,客户端执行以下操作:
- 以毫秒为单位获取当前时间。
- 尝试在所有 N 个实例中顺序使用所有实例中相同的键名和随机值来获取锁定。当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间小于锁的过期时间,以便获取该超时时间。例如,如果锁的过期时间为 10 秒,则超时时间可能在 5 到 50 毫秒之间。这样可以防止客户端长时间与处于故障状态的 Redis 节点进行通信:如果某个实例不可用,我们应该尝试与下一个实例尽快进行通信。
- 客户端通过从当前时间中减去在步骤1中获得的时间戳,来计算获取锁所花费的时间。当且仅当客户端能够在大多数实例(至少 3 个)中获取锁时,并且获取锁所花费的总时间小于锁的过期时间,则认为已获取锁。
- 如果获取了锁,则将其有效时间视为初始有效时间减去经过的时间,如步骤3中所计算。
- 如果客户端由于某种原因(没有在至少 N/2+1 个实例取到锁或取锁时间已经超过了有效时间)而未能获得该锁,客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功)。
redisson实现了Redlock。
Redlock 可能失效的原因
- 时钟发生跳跃;
- 长时间的GC pause;
- 长时间的网络延迟。
选择
- 一般生产环境是
Redis Cluster
,Redlock
的实现比较浪费资源,至少3
个互相独立的主节点部署在不同的服务器; Zookeeper
分布式锁也不是100%
可靠;- 在能够接受一定可靠性的情况下,可选择
set nx px
或set nx ex
;
参考
https://redis.io/topics/distlock
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
————        END        ————
Give me a Star, Thanks:)
https://github.com/fendoudebb/LiteNote扫描下方二维码关注公众号和小程序↓↓↓