程序员:redis分布式锁,你真的用对了吗?
随着业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。说到分布式,不得不提的就是分布式锁和分布式事物。今天我们就来谈谈redis实现的分布式锁的问题!
实现要求:
- 1.互斥性,在同一时刻,只能有一个客户端持有锁
- 2.防止死锁,如果持有锁的客户端崩溃而且没有主动释放锁,怎样保证锁可以正常释放,使得客户端可以正常加锁
- 3.加锁和释放锁必须是同一个客户端。
- 4.容错性,只有redis还有节点存活,就可以正常的加锁解锁操作。
错误使用方式一:
保证互斥和防止死锁,首先想到的使用redis的setnx命令保证互斥,为了防止死锁,需要设置一个超时时间。
public Object getAndSet(String key, Object value, long timeout) {
Object object = redisTemplate.opsForValue().getAndSet(key, value);
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
return object;
}
在多线程并发环境下,任何非原子性的操作,都可能导致问题。在这段代码中,如果设置过期时间,redis实例崩溃,就无法设置过期时间。如果客户端没有正确释放锁,那么该锁永远不会过期,就永远不会被释放。
错误方式二
比较容易想到的就是设置值和超时时间为原子操作不就可以了吗。那么使用方法就是这样了
public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
long expireTs = System.currentTimeMillis() + expireTime;
// 锁不存在,当前线程加锁成果
if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
return true;
}
String value = jedis.get(key);
//如果当前锁存在,且锁已过期
if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
//锁过期,设置新的过期时间
String oldValue = jedis.getSet(key, String.valueOf(expireTs));
if (oldValue != null && oldValue.equals(value)) {
// 多线程并发下,只有一个线程会设置成功
// 设置成功的这个线程,key的旧值一定和设置之前的key的值一致
return true;
}
}
// 其他情况,加锁失败
return true;
}
这段代码,乍一眼看没啥问题,你仔细看就会发现:
1.value 设置为过期时间,就要要求各个客户端严格的时钟同步,这需要使用到同步时钟。即使有同步时钟,分布式的服务器一般也会有少许误差,这不重要
2. 锁过期时,使用jedis.getSet虽然可以保证一个线程设置成功,但不能保证加锁和解锁为同一个客户端,因为没有标志时那个客户端设置的
解锁错误方式一:
直接删除key
public static void wrongReleaseLock(Jedis jedis, String key) {
//不是自己加锁的key,也会被释放
jedis.del(key);
}
简单粗暴,但这样做的话,不是自己的锁也会被删除掉。不够严谨
解锁错误方式二:
判断自己是不是锁的持有者,只有持有者才可以释放锁
public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
if (uniqueId.equals(jedis.get(key))) {
// 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁
jedis.del(key);
}
}
完美!
真的完美?
看起来很完美,但是如果你判断的时候锁是自己持有的,这时候超时自动释放了,然后又被其他客户端重新上锁了,然后你删除的不就是其他客户端的锁,一样不就乱套了?
基于以上信息探索,给出以下示例,仅供学习交流!
- 1.命令必须保证是互斥的
- 2. 设置的key必须要有过期时间
- 3. value使用唯一id,标志每个客户端。只有锁的持有者才能释放锁。
加锁直接使用set命令同时设置唯一id和过期时间;其中解锁些微复杂些,加锁后可以返回唯一ID,标志此锁是该客户端锁拥有;释放锁时要先判断是否是自己,只有自己才有删除操作,代码示例如下:
@Component
@Slf4j
public class RedisLockUtil {
// 超时时间
private static int EXPIRE_TIME = 5 * 1000;
@Autowired
private RedisTemplate redisTemplate;
private static Map<String, Thread> threadMap = new ConcurrentHashMap();
public Object lock(String key, Long timeOut) {
log.info("加锁开始");
try {
// 超时等待时间
Long waitEnd = System.currentTimeMillis() + EXPIRE_TIME;
// 生成一个uuid,使得分布式调用有一个拥有者
String uuid = UUID.randomUUID().toString();
String value = key + uuid;
// 在等待时间内,尝试获取锁
while (System.currentTimeMillis() < waitEnd) {
log.info("尝试获取锁");
// 同步代码,使得操作原子性
synchronized (this) {
if (Objects.nonNull(redisTemplate.opsForValue().get(key))) {
continue;
}
Object result = redisTemplate.opsForValue().getAndSet(key, value);
if (Objects.isNull(result)) {
log.info("成功获取锁");
}
// 设置过期时间,以防死锁
redisTemplate.expire(key, timeOut, TimeUnit.MILLISECONDS);
// 开启一个守护进程,给当前锁动态添加时间
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
if(System.currentTimeMillis() > waitEnd) {
System.out.println(Thread.currentThread().getName() + "-->" + " 更新redis时间2s ");
redisTemplate.expire(key, 1 * 60000, TimeUnit.MILLISECONDS);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.setDaemon(true); // 守护进程
threadMap.put(value, thread);
thread.setName(key+"-"+value);
thread.start();
return value;
}
}
}catch (Exception e) {
log.error("lock error:", e);
throw new RuntimeException("未能获取分布式锁");
}
log.info("获取锁失败");
throw new RuntimeException("获取分布式锁超时");
}
public boolean unLock(String key, Object value) {
log.info("释放锁:{}--{}", key, value);
if (Objects.isNull(key) ) {
return false;
}
DefaultRedisScript script = new DefaultRedisScript();
script.setResultType(List.class);
script.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end");
Object o = redisTemplate.execute(script, Collections.singletonList(key), value);
if (Objects.nonNull(o) && ((ArrayList)o).size() !=0) {
threadMap.remove(value).stop();
}
log.info("释放锁{}", o);
return true;
}
}
模拟调用代码
@GetMapping("/hello")
public Object hello(String hello) {
log.info("设置key值开始!");
Object object = redisLockUtil.lock(REDIS_KEY, 1*60000L);
try {
log.info("设置key值{}", object);
// 这里是模拟业务处理场景
try {
Thread.sleep(1 * 60000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (Exception e) {
}finally {
redisLockUtil.unLock(REDIS_KEY, object);
}
return object;
}