程序员:redis分布式锁,你真的用对了吗?

wumingxiaozei 2019-11-30

随着业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。说到分布式,不得不提的就是分布式锁和分布式事物。今天我们就来谈谈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;

}

程序员:redis分布式锁,你真的用对了吗?

相关推荐

DiamondTao / 0评论 2020-08-30