几种常用的redis做分布式锁
分布式锁需要注意的几点
1,需要锁的内容,即key的值(订单不可以并发就key就设置成订单号,也可以把key设置成某一常量;
2,value的选择:通常保存的value都用于解锁使用,用于来保证加锁和解锁为同一角色;
3,保证加锁和加过期时间为原子操作;
public class StringRedisUtil {
@Autowired
StringRedisTemplate stringRedisTemplate;
public Boolean redisLock(String key, Long currentTime, Long timeOut, Long timeOffset) {
//currentTime当前时间,timeOffset补偿时间 timeOut 超时时间
Long currentTimeOffset = currentTime + timeOffset + timeOut;
//使用redisSETNX 命令实现分布式锁
try {
if (stringRedisTemplate.opsForValue().setIfAbsent(key, currentTimeOffset.toString())) {
stringRedisTemplate.expire(key, timeOut, TimeUnit.MILLISECONDS);
return true;
}
//因为redisSETNX 过期时间无法保证原子性,增加一个补偿。
String value = stringRedisTemplate.opsForValue().get(key);
if (value != null && Long.parseLong(value) < currentTime) {
stringRedisTemplate.opsForValue().set(key, currentTimeOffset.toString(), timeOut, TimeUnit.MILLISECONDS);
return true;
}
} catch (Exception e) {
log.error("redisLock exception redisException", e);
return true;
}
return false;
}
public void redisUnlock(String key, Long currentTime, Long timeOut, Long timeOffset) {
Long currentTimeOffset = currentTime + timeOffset + timeOut;
try {
String value = stringRedisTemplate.opsForValue().get(key);
//属于你的锁,才可以删除锁
if (Long.parseLong(value) == currentTimeOffset) {
stringRedisTemplate.delete(key);
}
} catch (Exception e) {
log.error("RedisLock Exception redisException", e);
}
}
}
这一方案用,时间做value,用当前时间+过期时间+补偿时间
每次获取锁没有成功的话,比较当前时间和value,如果当前时间大于value说明,该锁本应该被解锁,或者过期,直接用创建新锁覆盖掉原来的value;
解锁也是根据value值是否相等,相等说明是分毫不差是同一个角色的锁,不相等不允许解锁;
这是第一种方案,在无法保证加锁和加过期时间两个操作的原子性的情况,添加了一个兜底的策略,同时用时间戳来解锁;
方案2:
通常我们在调用Redis的时候使用的都是RedisTemplate的相关方法,它无法保证加锁和过期时间为原子性,但是jedis中有这样的方法
此为加锁方法;
完成了保证原子性操作,剩下的就是解锁的正确性;
我们此时使用的value为随机数,解锁我们可以使用lua脚本,根据value找key,value可以放在threadlocal中
@Component
public class RedisLock {
/* add by jiangjunjie Task 1502理财分布式锁优化 start */
private RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
private String UNLOCK_LUA;
private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
@Autowired
public RedisLock(RedisTemplate<Object, Object> redisTemplate)
{
// 通过Lua脚本来达到释放锁的原子性
if("".equals(this.UNLOCK_LUA) || this.UNLOCK_LUA==null )
{
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
this.UNLOCK_LUA = sb.toString();
}
this.redisTemplate=redisTemplate;
}
public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
boolean result = setRedis(key, expire);
// 如果获取锁失败,按照传入的重试次数进行重试
while((!result) && retryTimes--> 0){
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
return false;
}
result = setRedis(key, expire);
}
return result;
}
private boolean setRedis(String key, long expire) {
//为了保证设置锁和过期时间的两个操作原子性 spring data 的 RedisTemplate当中没有这样的方法,但是jedis当中有这样的原子操作的方法
//需要通过RedisTemplate的execute方法获取jedis里操作命令对象
// NX:表示只有当锁定资源不存在的时候才能set成功。利用Redis的原子性,保证了只有第一个请求的线程才能获得锁,而后其他线程在锁定资源释放前都不能获取锁
// PX:expire表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定。
//通过set NX,PX的命令设置保证了Redis值和自动过期时间的原子性,避免在调用setIfAbsent方法的时候线程挂掉,没有设置过期时间而导致死锁,使得锁不能释放
try {
String result = redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
String uuid = UUID.randomUUID().toString();
lockFlag.set(uuid); // 锁定的资源
return commands.set(key, uuid, "NX", "PX", expire);
}
});
return !StringUtils.isEmpty(result);
} catch (Exception e) {
System.out.println(e.getMessage());
}
return false;
}
/*上面的方法通过设置set的NX,PX命令保证了Redis值和自动过期时间的原子性,但是还有一个问题是如果线程T1获取锁,但是在处理T1的业务时候,
由于某些原因阻塞了较长时间,这个时候设定的过期时间到了,线程T2获取了锁,线程T1操作完后释放了锁(释放了T2的锁)
所以也就是说T2的线程上面没有提供锁的保护机制。因此需要给锁定一个拥有者的标识,即每次在获取锁的时候,生成一个随机不唯一的串放入当前线程,
释放锁的时候先去判断对应的值是否和线程中的值相同。*/
public boolean releaseLock(String key) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
List<String> keys = new ArrayList<String>();
keys.add(key);
List<String> args = new ArrayList<String>();
args.add(lockFlag.get());
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
Long result = redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
}
});
return result != null && result > 0;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}