当服务采用集群方式部署的时候,本地锁无法发挥作用,所以需要分布式锁来实现加锁。
Redis主要运用setnx命令进行锁操作
eval "return {KEYS[1], KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
-- 第一个参数是lua程序脚本
-- 第二个参数是lua脚本后面的那个参数,表示KEYS参数的个数
-- 第三个参数是Redis键名。lua脚本可以访问有KEYS全局变量组成的一维数据参数
-- 第四个参数是相应KEYS所对应的值,并且lua脚本可以通过ARGV访问其值
可以使用redis.call(),redis.pcall()从lua脚本调用Redis命令。
redis.call()与redis.pcall()唯一的区别在于Redis命令调用错误时,redis.call抛出Lua类型的错误,再强制EVAL将错误返回给命令的调用者,而redis.pcall将捕获错误并返回表示错误的Lua表类型。
在设置锁成功后,而设置超时时间时因为服务器挂掉、重启、网络等问题而没有执行成功
解决方法:
public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {
String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
"redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
List<String> keys = new ArrayList<>();
List<String> values = new ArrayList<>();
keys.add(key);
values.add(UniqueId);
values.add(String.valueOf(seconds));
Object result = jedis.eval(lua_scripts, keys, values);
//判断是否成功
return result.equals(1L);
}
如果线程A获取到锁并设置过期时间为30s,然后过期时间到了线程B获取到了锁,随后线程A执行完毕,就会使用DEL误释放B的锁。
解决方法:
通过在value中设置当前锁的标识,并在删除之前判断是否为当前线程所持有。
// 设置锁
public boolean tryLock_with_set(String key, String uniqueId, int seconds){
// u niqueId具有唯一性
// N X
return "Ok".equals(jedis.set(key,uniqueId,"NX","EX",seconds));
}
// 释放锁
public boolean releaseLock_with_lua(String key, String value){
// 使用lua脚本尽量保持原子性
String luaScript= "if redis.call('get', KEYS[1])==ARGV[1] then "+
"return redis.call('del',KEYS[1]) else return 0 end";
return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
}
如果线程A获取到了锁并设置了过期时间30s,但线程A执行时间超过了30s,这个时候线程B获取到了锁,就会导致A,B并发执行。
为了解决这个问题,我们可以
通过本地记录重入次数在分布式锁保证可重入性,由于考虑到过期时间、本地以及Redis一致性的问题,会增加代码的复杂性。
解决方法:
// 如果lock_key不存在
if (redis.call('exists', KEYS[1]==0))
then
// 设置lock_key线程标识1进行加锁
redis.call('hset',KEYS[1],ARGV[2],1);
// 设置过期时间
redis.call('pexpire',KEYS[1],ARGV[1]);
// 如果lock_key存在且线程标识是当前欲加锁线程的标识
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
// 重入次数自增
then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 重置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 如果加锁失败,返回锁剩余时间
return redis.call('pttl', KEYS[1]);
我们可以通过两种方式来等待锁的释放,第一种是比较传统的客户端轮询的方式,当未获取到锁的时候会等待一段时间后重新去获取锁,直到成功获取锁或等待超时。可是这种方式比较消耗服务器资源
另一种是使用redis的发布订阅功能,当获取锁失败的时候,订阅锁释放消息
https://www.jianshu.com/p/366d1b4f0d13
https://www.cnblogs.com/PatrickLiu/p/8656675.html
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
https://juejin.cn/post/6844903830442737671