常用分布式锁的情况:
例如一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读取用户状态,然后在内存中修改,修改完成后,在存入数据库。单线程环境下没有问题,但多线程环境下,可能会出现数据紊乱等问题。
对于这种问题,使用分布式锁可以限制程序的并发执行。
分布式锁实现思路: 一个线程先占位,当别的线程进入时,发现已经有线程占位,就会放弃,或者稍后再来。
利用Redis中的setnx指令(创建一个key\value,如果没有该key,就会创建新的key,如果已经有了该key,就不会创建也不会修改value),线程操作完成后,在调用del指令释放位置。
根据思路初步构建代码:
public static void main(String[] args) {
Redis redis = new Redis();
redis.excute(jedis -> {
long setnx = jedis.setnx("k1", "v1");
if (setnx ==1){
//添加过期时间,防止业务抛出异常,导致锁不释放
jedis.expire("k1",5);
//没有占位,执行业务模块
jedis.set("name","neko");
System.out.println(jedis.get("name"));
//释放锁
jedis.del("k1");
}else{
//有占位,停止/暂缓操作
}
});
}
由于expire和setnx不是一起执行,上述代码还是存在死锁可能。通过Redis的 set 方法可以改进:
public static void main(String[] args) {
Redis redis = new Redis();
redis.excute(jedis -> {
String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
if (set !=null && "OK".equals(set)){
//添加过期时间,防止业务抛出异常,导致锁不释放
jedis.expire("k1",5);
//没有占位,执行业务模块
jedis.set("name","neko");
System.out.println(jedis.get("name"));
//释放锁
jedis.del("k1");
}else{
//有占位,停止/暂缓操作
}
});
}
}
解决超时问题
为了防止锁不释放,给锁添加了超时时间,超时后会自动释放锁。但如果业务耗时,在超时之前未执行完成,锁就被释放,导致第二个线程占用锁。此时第一个线程执行完毕,释放了第二个线程的锁。锁被释放,第三个线程又占用锁。
两个角度解决问题:
- 尽量避免获取锁之后进行耗时操作
- 优化锁的结构:将锁的value值设置为随机字符串,每次释放锁的时候,先去比较字符串是否一致,确认锁的归属,一致再去释放锁,否则不释放。
第二种方法:分三个步骤,1. 获取锁的value值 2. 比较value的值 3. 释放锁。三步不具备原子性,通过引入Lua脚本解决。
Lua脚本:
- Redis中内置了对Lua的支持
- Lua脚本可以在Redis服务端执行多个Redis命令,并且具备原子性。
- 网络会影响Redis性能,使用Lua脚本让多个命令一次执行,减少网络通信次数,提高Redis性能的影响
Redis中使用Lua脚本大致分为两种:
- 提前在Redis服务端上写好脚本,然后在Java客户端去调用脚本(推荐)
- 在Java客户端写Lua脚本,需要执行时,发送到Redis上执行
创建Lua脚本
if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
计算脚本的SHA1校验和,并且缓存到redis中
cat h:\getidthengetentity.lua | redis-cli script load --pipe
测试代码
public class LuaTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.excute(jedis -> {
String string = UUID.randomUUID().toString();
String k1 = jedis.set("k1", string, new SetParams().nx().ex(5));
if (k1 != null && "OK".equals(k1)){
jedis.set("name","neko");
System.out.println(jedis.get("name"));
//执行lua脚本,判断是否是自己的锁
jedis.evalsha("adkjagfjhalgjaldad", Arrays.asList(k1),Arrays.asList(string));
}else {
System.out.println("没获取到锁");
}
});
}
}