Preface {#preface}
以前还在上学的时候,每每听到分布式锁等相关名词的时候总觉得高大上,复杂。虽说工作后用到的机会也不多,但随着工作经验、代码能力、~~年龄的~~增长,最近初学完分布式锁的原理和使用 Redission 实现,揭开分布式锁的神秘面纱之后发现也就那样,这个名词听起来唬人,实际上要实现的目标很明确,实现的方法也很简单。
什么是分布式锁? {#什么是分布式锁}
我个人通俗的理解是:同一个应用在不同的机器上部署了好几份,当它们操作同一个共享数据(如数据库中的数据)的时候需要保证顺序执行,避免同一时刻被多个应用修改出现数据错乱的问题,它发挥的作用和 Java 中的锁是相同的,至于为什么叫分布式锁那是因为应用在不同机器上部署了好几份,成为了分布式应用,因为分布式应用中使用到的这把锁就叫分布式锁。
什么是 Redission {#什么是-redission}
Redisson 是一个基于 Java 的开源框架,专门用于简化在分布式环境中使用 Redis 的过程。它提供了丰富的分布式 Java 对象和集合的实现,如分布式 Map、Set、List、Queue 等,使开发者可以通过简单的 API 轻松地在分布式系统中操作这些数据结构。Redisson 不仅提供了高级的分布式功能,还解决了在并发和数据一致性方面的常见问题,使得使用 Redis 在 Java 应用中进行分布式数据存储和处理变得更加便捷和高效。
Redisson 提供了丰富的功能和 API,使得开发者可以更方便地在 Java 应用中利用 Redis 的强大功能,特别是在分布式环境下管理和操作数据结构。
使用 Redission 可以很方便的通过 Redis 实现分布式锁的功能,这也是如今主流的使用方式之一。
Redission 实现分布式锁原理 {#redission-实现分布式锁原理}
Redission 是基于 Redis 协议的 Java 客户端,它只是简化了我们的操作,实现分布式锁的功能还是要依赖底层的 Redis 数据库。
在 Redis 中有个setnx
命令,它的作用是:在指定的 key 不存在时,为 key 设置指定的值;设置成功,返回 1 。 设置失败,返回 0 。
setnx
命令的用法如下:
所以当部署的多个应用要操作同一个共享数据时,需要使用setnx
向 Redis 中设置同样的一个键,如果可以设置成功表明拿到分布式锁了可以对共享数据进行相应的操作,否则设置失败未拿到锁时禁止操作共享数据,可以一直循环尝试获取分布式锁或者执行获取锁失败的逻辑。
另外还需要使用expire
,del
命令与setnx
搭配使用,当获取到锁的应用操作共享数据完毕时需要释放分布式锁(删除键)给其它的应用获取;另外还需要给键设置一个过期时间,避免应用执行异常退出时,未执行到释放分布式锁(删除键)而导致其余应用一直获取不到锁的情况发生。
expire
命令是用来设置键的过期时间。
del
命令用来删除键。
但是如何才能设置一个合理的过期时间呢?如果设置的时间过短,当前获取到分布式锁的应用还未操作完共享数据,键就过期了,这回导致别的应用获取到锁开始操作同一个共享数据最终造成数据错乱的问题;如果设置的时间过程当获取到锁的应用某次执行失败退出时导致锁迟迟得不到释放,别的应用只能干巴巴的等待,也不合理。
别着急,在 Redission 中就为此种情况提供了解决方法,它实现了一个看门口的逻辑,当使用 Redission 获取分布式锁时,它会为当前操作共享数据的线程创建一个守护线程用来更新键的过期时间,当键的过期时间小于某个阈值时会重新设置一遍键的过期时间,一直循环往复,直到释放掉分布式锁,如果在此之间应用异常退出,守护线程也会随之销毁,在一段不长的时间过后键就会过期,其它应用依旧可以获取到分布式锁操作共享数据。
Redission 实现分布式锁案例 {#redission-实现分布式锁案例}
就拿我前一阵子写的社区帖子点赞功能为例。
它的流程如下:
它的伪代码如下:
public class Demo {
private RedissonClient redissonClient;
/**
* 更新帖子点赞
*
* @param memberId 会员id
* @param targetId 帖子id
* @param action 0 取消点赞 1点赞
*/
public void updateLike(String memberId, String targetId, Short action) {
// 构造一个 key,包含会员id和帖子id,不要影响别的用户的更新操作
String lockKey = "UPDATE_LIKE_" + memberId + "_" + targetId;
RLock lock = null;
try {
// 使用 redission,自带看门狗
lock = redissonClient.getLock(lockKey);
// 尝试获取分布式锁
/*
waitTime -- the maximum time to acquire the lock 等待获取锁时间(最大尝试获得锁的时间),超时返回false
time unit -- time unit 时间单位
*/
boolean tryLock = lock.tryLock(1, TimeUnit.SECONDS);
if (tryLock) {
LOG.info("获取分布式锁成功");
} else {
LOG.info("获取分布式锁失败,继续尝试获取分布式锁");
updateLike(memberId, targetId, action);
}
if (action == 0) {
// 取消点赞逻辑...
} else if (action == 1) {
// 点赞逻辑...
}
} catch (InterruptedException e) {
LOG.error("点赞异常", e);
} finally {
LOG.info("更新点赞流程结束,释放分布式锁!");
// 锁不为空且要释放的分布式锁由当前线程持有(避免释放掉其它线程加的锁)
// isHeldByCurrentThread 方法很重要,不能少
if (null != lock && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}