灵活运用分布式锁解决数据重复插入问题

4.1 分布式锁需要具备哪些特性?


  • 在分布式系统环境下,同一时间只有一台机器的一个线程可以获取到锁;

  • 高可用的获取锁与释放锁;

  • 高性能的获取锁与释放锁;

  • 具备可重入特性;

  • 具备锁失效机制,防止死锁;

  • 具备阻塞/非阻塞锁特性。

4.2 分布式锁有哪些实现方式?


分布式锁实现主要有如下三种:

  • 基于数据库实现分布式锁;

  • 基于 Zookeeper 实现分布式锁;

  • 基于 Redis 实现分布式锁;

4.2.1 基于数据库的实现方式

基于数据库的实现方式就是直接创建一张锁表,通过操作表数据来实现加锁、解锁。以 MySQL 数据库为例,我们可以创建这样一张表,并且对 method_name 进行加上唯一索引的约束:

然后,我们就可以通过插入数据和删除数据的方式来实现加锁和解锁:

#加锁insert into myLock(method_name, value) values ('m1', '1');` `#解锁delete from myLock where method_name =‘m1’;

基于数据库实现的方式虽然简单,但是存在一些明显的问题:

  • 没有锁失效时间,如果解锁失败,就会导致锁记录永远留在数据库中,造成死锁。

  • 该锁不可重入,因为它不认识请求方是不是当前占用锁的线程。

  • 当前数据库是单点,一旦宕机,锁机制就会完全崩坏。

4.2.2 基于 Zookeeper 的实现方式

ZooKeeper 是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下的节点名称都是唯一的。

ZooKeeper 的节点(Znode)有 4 种类型:

  • 持久化节点(会话断开后节点还存在)

  • 持久化顺序节点

  • 临时节点(会话断开后节点就删除了)

  • 临时顺序节点

当一个新的 Znode 被创建为一个顺序节点时,ZooKeeper 通过将 10 位的序列号附加到原始名称来设置 Znode 的路径。例如,如果将具有路径/mynode 的 Znode 创建为顺序节点,则 ZooKeeper 会将路径更改为/mynode0000000001,并将下一个序列号设置为 0000000002,这个序列号由父节点维护。如果两个顺序节点是同时创建的,那么 ZooKeeper 不会对每个 Znode 使用相同的数字。

基于 ZooKeeper 的特性,可以按照如下方式来实现分布式锁:

  • 创建一个目录 mylock;

  • 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点;

  • 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;

  • 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;

  • 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

由于创建的是临时节点,当持有锁的线程意外宕机时,锁依然可以得到释放,因此可以避免死锁的问题。另外,我们也可以通过节点排队监听机制实现阻塞特性,也可以通过在 Znode 中携带线程标识来实现可重入锁。同时,由于 ZooKeeper 集群的高可用特性,分布式锁的可用性也能够得到保障。不过,因为需要频繁的创建和删除节点,Zookeeper 方式在性能上不如 Redis 方式。

4.2.3 基于 Redis 的实现方式

Redis 是一个开源的键值对(Key-Value)存储数据库,其基于内存实现,性能非常高,常常被用作缓存。

基于 Redis 实现分布式锁的核心原理是:尝试对特定 key 进行 set 操作,如果设置成功(key 之前不存在)了,则相当于获取到锁,同时对该 key 设置一个过期时间,避免线程在释放锁之前退出造成死锁。线程执行完同步任务后主动释放锁则通过 delete 命令来完成。

这里需要特别注意的一点是如何加锁并设置过期时间。有的人会使用 setnx + expire 这两个命令来实现,但这是有问题的。假设当前线程执行 setnx 获得了锁,但是在执行 expire 之前宕机了,就会造成锁无法被释放。当然,我们可以将两个命令合并在一段 lua 脚本里,实现两条命令的原子提交。

其实,我们简单利用 set 命令可以直接在一条命令中实现 setnx 和设置过期时间,从而完成加锁操作:

SET key value [EX seconds] [PX milliseconds] NX

解锁操作只需要:

DEL key

五、基于 Redis 分布式锁的解决方案

====================

在本案例中,我们采用了基于 Redis 实现分布式锁的方式。

5.1 分布式锁的 Java 实现


由于项目采用了 Jedis 框架,而且线上 Redis 部署为集群模式,因此我们基于 redis.clients.jedis.JedisCluster 封装了一个 RedisLock 类,提供加锁与解锁接口。

public class RedisLock { private static final String LOCK_SUCCESS = “OK”; private static final String LOCK_VALUE = “lock”; private static final int EXPIRE_SECONDS = 3; @Autowired protected JedisCluster jedisCluster; public boolean lock(String openId) { String redisKey = this.formatRedisKey(openId); String ok = jedisCluster.set(redisKey, LOCK_VALUE, “NX”, “EX”, EXPIRE_SECONDS); return LOCK_SUCCESS.equals(ok); } public void unlock(String openId) { String redisKey = this.formatRedisKey(openId); jedisCluster.del(redisKey); } private String formatRedisKey(String openId){ return “keyPrefix:” + openId; }``}

在具体实现上,我们设置了 3 秒钟的过期时间,因为被加锁的任务是简单的数据库查询和插入,而且服务器与数据库部署在同个机房,正常情况下 3 秒钟已经完全能够足够满足代码的执行。

事实上,以上的实现是一个简陋版本的 Redis 分布式锁,我们在实现中并没有考虑线程的可重入性,也没有考虑锁被其他进程误释放的问题,但是它在这个业务场景下已经能够满足我们的需求了。假设推广到更为通用的业务场景,我们可以考虑在 value 中加入当前进程的特定标识,并在上锁和释放锁的阶段做相对应的匹配检测,就可以得到一个更为安全可靠的 Redis 分布式锁的实现了。

当然,像 Redission 之类的框架也提供了相当完备的 Redis 分布式锁的封装实现,在一些要求相对严苛的业务场景下,我建议直接使用这类框架。由于本文侧重于介绍排查及解决问题的思路,因此没有对 Redisson 分布式的具体实现原理做更多介绍,感兴趣的小伙伴可以在网上找到非常丰富的资料。

5.2 改进后的代码逻辑


现在,我们可以利用封装好的 RedisLock 来改进原来的代码了。

public class AccountService { @Autowired private RedisLock redisLock; public void submit(String openId, String localIdentifier) { if (!redisLock.lock(openId)) { // 如果相同openId并发情况下,线程没有抢到锁,则直接丢弃请求 return; } // 获取到锁,开始执行用户数据同步逻辑 try { Account account = accountDao.find(openId); if (account == null) { // insert } else { // update } } finally { // 释放锁 redisLock.unlock(openId); } }``}

5.3 数据清理


最后再简单说一下收尾工作。由于重复数据的数据量较大,不太可能手工去慢慢处理。于是我们编写了一个定时任务类,每隔一分钟执行一次清理操作,每次清理 1000 个重复的 OpenID,避免短时间内大量查询和删除操作对数据库性能造成影响。当确认重复数据已经完全清理完毕后就停掉定时任务的调度,并在下一次版本迭代中将此代码移除。

六、总结

在日常开发过程中难免会各种各样的问题,我们要学会顺藤摸瓜逐步分析,找到问题的根因;然后在自己的认知范围内尽量去寻找可行的解决方案,并且仔细权衡各种方案的利弊,才能最终高效地解决问题。

作者:vivo 快应用服务器研发团队-Lin Yupan

总结

本文从基础到高级再到实战,由浅入深,把MySQL讲的清清楚楚,明明白白,这应该是我目前为止看到过最好的有关MySQL的学习笔记了,我相信如果你把这份笔记认真看完后,无论是工作中碰到的问题还是被面试官问到的问题都能迎刃而解!

MySQL50道高频面试题整理:

找到问题的根因;然后在自己的认知范围内尽量去寻找可行的解决方案,并且仔细权衡各种方案的利弊,才能最终高效地解决问题。

作者:vivo 快应用服务器研发团队-Lin Yupan

总结

本文从基础到高级再到实战,由浅入深,把MySQL讲的清清楚楚,明明白白,这应该是我目前为止看到过最好的有关MySQL的学习笔记了,我相信如果你把这份笔记认真看完后,无论是工作中碰到的问题还是被面试官问到的问题都能迎刃而解!

MySQL50道高频面试题整理:

[外链图片转存中…(img-EfJULtJx-1714754295314)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值