Distributed lock routine summary

I am participating in the "Nuggets · Sailing Program"

What is a distributed lock?

Let's first review what a lock is. Lock is a solution to data inconsistency caused by multiple threads operating the same resource at the same time. Usually the locks that people use are stand-alone locks, such as synchronizedkeywords and ReentrantLocklocks, which are only locks within the same process. Now many services will have more than one instance, so the single-machine lock is useless at all. So the concept of distributed lock was introduced.

The function of distributed lock is exactly the same as that of stand-alone lock, except that it usually needs to be implemented by a third-party service. Mainstream third-party services have redisand mysql.

Let's explain the two implementation methods and differences one by one, as well as the best method in my opinion.

Redis implements distributed locks

Redis usually supports higher concurrency, and redis provides an atomic command, so it is suitable as a distributed lock,:

SET key value NX PX 30000
复制代码

This command means to set a key-value of string type to redis, which will expire automatically after 30 seconds , and will be set successfully only when the key does not exist. This is quite in line with our requirements for distributed locks. Since redis is single-threaded, we can perform this operation on the client side, and it will ensure that only one instance is successfully set up. Does it mean that the lock is successful soon? Part of the code is as follows:

// 仅当key不存在才会设置成功,通常是将key设置为需要操作的资源唯一id,
// 例如,我们需要秒杀商品,key就设置为商品id
// 而 value一般设置为随机数,来保证释放锁的时候是当前线程持有。我这里使用【hutool】工具生成了16位随机字符串
// 过期时间也需要设置,因为如果该线程出现异常,就会导致资源无法释放,造成其他线程永远拿不到锁了
String randomString = RandomUtil.randomString(16);
//此方法会返回一个结果来表示是否操作成功
Boolean result = redisTemplate.opsForValue().setIfAbsent("productId", randomString);
//加锁成功
if (Boolean.TRUE.equals(result)) {
    try {
        // 业务处理
    } catch (Exception ignored) {
        // 回滚事务
    } finally {
        // 判断该锁是否当前线程持有,是才会释放
        if (redisTemplate.opsForValue().get("productId").equals(randomString)) {
            //提交事务
            //释放锁
            redisTemplate.delete("productId");
        } else {
            //业务执行时间过长导致锁自动失效,此时需要释放资源,如回滚事物等
        } 
    }
}
//加锁失败
else {
    // 可以返回服务器繁忙,请稍后再试之类的友好提示
}
复制代码

Some advice on transactions, generally speaking, if the current method involves 2 or more operations that modify data, you need to use transactions

Generally speaking, for general concurrency, the above scheme is completely sufficient, however, it still has some flaws:

  1. Judging whether the lock is held by the current thread and releasing the lock is not an atomic operation. If the lock is just judged to be held by the current thread, it will expire in the next second, and it is held by other threads at this time, then other threads will be released soon. Hold the lock?
  2. There are similar problems when submitting things

To solve this problem, we can use the lua script to perform the atomic operation of releasing the lock:

//释放锁的时候判断了锁是否当前线程持有
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
复制代码

The lua script can ensure that we judge whether the value is equal to our expectation, and only when it is equal will the resource be released. And it's an atomic operation.

However, this looks beautiful and solves problem 1 above, but problem 2 still cannot be solved because transactions and redis operations are not atomic operations. The lua script obviously cannot help us submit mysql transactions, so what should we do?

下面就推荐大名鼎鼎的redisson,它是一个redis客户端,主要支持一些分布式相关的工具,其中就有分布式锁。

说白了,上述两个问题,我们已经解决了其中一个了,采用了lua脚本解决,而另外一个问题,根本原因是因为自动过期时间设置多大问题。

那么,我们应该设置多长时间呢?

首先我们应该尽可能的设置一个保证在业务能够正常执行结束的范围。

但是,其实不管我们设置多少,理论上来说都不合适,因为你无法保证业务代码执行的具体时间。倘若设置小了,导致业务执行结束后锁过期,还要额外进行回滚操作,设置大了,可能导致其他线程阻塞时间过长。所以,这个时间怎么设置都不好使,总会有瑕疵。

redisson采用了看门狗设置,也就是会起一个守护线程,来监测这个线程是否释放锁,如果此线程一直在活动,且过期时间快要结束,看门狗机制就会自动续期。

所以,看门狗机制保证了,线程持有锁后,只要线程还在活跃,且锁未释放,锁会永不过期,有人看到这里可能会怀疑了,永不过期? ,那么岂不是跟没设置过期时间没啥两样,哈哈,并不是,我说的只是理论上永不过期,实际上我们的代码终究会执行结束(除非写了死循环)。

redisson释放锁的时候同样采用了lua脚本的方式判断是否当前锁持有。

最佳实践代码如下:

适合并发一般的情况:

RLock lock = redissonClient.getLock("productId");
//会一直阻塞去获取锁,直到成功,默认30秒过期,会自动续期
lock.lock();
try {
    //执行业务代码
    //正常执行,然后提交事务,这里锁会一直续期,所以不用担心锁会自动过期
} catch (Exception ignored) {
    //异常,回滚事务
} finally {
    //释放锁,需要判断一下是否当前线程持有
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
复制代码

适合并发较高的情况:

RLock lock = redissonClient.getLock("productId");
//会一直阻塞去获取锁,直到成功,但5秒还未获取锁,会返回结果,锁默认30秒过期,会自动续期
// ture代表获取锁成功,否则失败
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        //执行业务代码
        //正常执行,然后提交事务,这里锁会一直续期,所以不用担心锁会自动过期导致事务提交后才过期
    } catch (Exception ignored) {
        //异常,回滚事务
    } finally {
        //释放锁,需要判断一下是否当前线程持有
        //注意:这里如果不判断也是可以的,只不过会抛出异常
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
} else {
    // 可以返回服务器繁忙,请稍后再试之类的友好提示
}
复制代码

关于redisson的看门狗失效情况:

// 没有Watch Dog ,10s后锁释放
lock.lock(10, TimeUnit.SECONDS);
// 没有Watch Dog ,10s后锁释放,尝试获取100s
lock.tryLock(100, 10, TimeUnit.SECONDS);
复制代码

redisson默认锁过期时间为30s,只要设置了过期时间,看门狗机制就会失效

MYSQL实现分布式锁

mysql实现分布式锁的方式最为简单,我们可以利用mysql主键唯一的性质,将新增数据这一动作的成功与否作为获取锁的结果。对于实现自动过期。我们可以增加字段来实现,增加一个过期时间字段和创建时间字段。

释放锁就是删除数据即可,如果锁支持自动失效,需要在释放锁时添加相应条件以防止释放锁时刚好自动失效。

Guess you like

Origin juejin.im/post/7145637351765573662