在使用redis加锁的函数:setifAbsent(key,value)时,如果加锁成功,则对当前锁加一个过期时间:expire(key,timeout),而这时会出现一个问题,二者的原子性问题,如果在加上锁之后,服务器宕机了,这时还没有进行加过期时间的操作,这样锁就会永久存在,所以要解决这个问题;
使用redis2.1.6以上版本里面的函数 setifAbsent(key,value,timeout),这个函数将加锁和过期时间封装在一起使用:
redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue,20,TimeUnit.SECONDS);
注意事项:
当更新redis版本到2.1.6以上版本的时候,如果使用了jedis,在启动的时候可能会报错,提示找不到一个类,原因是因为jedis的版本过低,所以要升级jedis的版本;
jedis、spring-data-redis和Spring的版本匹配表:
脚本编写:(首先调用setnx,检测加锁成功之后,则调用expire加过期时间)
private static final String LOCK_LUA = "if redis.call('setnx', KEYS[1], ARGV[1]) then
redis.call('expire', KEYS[1], ARGV[2], ARGV[3]) return 'true' else return 'false' end";
脚本使用:(在redisTemplate传参的时候的参数为:redisScirpt,key,obj,obj....,其中key这个数组在lua脚本里可以用key[1]来使用,而key后面的参数可以为多个,在脚本里用ARGV[1]、ARGV[2]来使用)
public boolean lock(String key,String value,long expireTime) {
DefaultRedisScript redisScript = new DefaultRedisScript<>(LOCK_LUA, String.class);
String result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, expireTime, TimeUnit.SECONDS);
if (Boolean.parseBoolean(result)) {
return true;
}
return false;
}
当然也可以直接使用脚本文件:
// 执行 lua 脚本
DefaultRedisScript redisScript = new DefaultRedisScript<>();
// 指定 lua 脚本
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/SetKey.lua")));
// 指定返回类型
redisScript.setResultType(Long.class);
// 参数一:redisScript,参数二:key列表,参数三:arg(可多个)
Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey));
使用multi()和exec(),其中multi表示事务的开始,这时程序会将后面的代码加到一个事务里,当碰到exec函数的时候,释放执行;
注意:但是redis并没有完全意义上的事务,事务中的方法在执行的时候,如果执行到一半,也就是加锁成功了之后,服务器宕机了,这样后面的加过期时间并不会执行,并且redis并不支持回滚,所以并不能算原子性的操作。
private Boolean setLock(RecordEventModel event) {
String lockKey = event.getModel() + ":" + event.getAction() + ":" + event.getId() + ":" + event.getMessage_id();
log.info("lockKey : {}" , lockKey);
SessionCallback sessionCallback = new SessionCallback() {
List