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

当使用 MySQL 数据库及 InnoDB 存储引擎时,我们可以利用唯一索引来保障同一个列的值具有唯一性。显然,在 t_account 这张表中,我们最开始是没有为 open_id 列创建唯一索引的。如果我们想要此时加上唯一索引的话,可以利用下列的 ALTER TABLE 语句。

ALTER TABLE t_account ADD UNIQUE uk_open_id( open_id );

一旦为 open_id 列加上唯一索引后,当上述并发情况发生时,请求 A 和请求 B 中必然有一者会优先完成数据的插入操作,而另一者则会得到类似错误。因此,最终保证 t_account 表中只有一条 openid=xxx 的记录存在。

Error Code: 1062. Duplicate entry ‘xxx’ for key ‘uk_open_id’

3.2 应用程序层面处理——分布式锁


另一种解决的思路是我们不依赖底层的数据库来为我们提供唯一性的保障,而是靠应用程序自身的代码逻辑来避免并发冲突。应用层的保障其实是一种更具通用性的方案,毕竟我们不能假设所有系统使用的数据持久化组件都具备数据唯一性检测的能力。

那具体怎么做呢?简单来说,就是化并行为串行。之所以我们会遇到重复插入数据的问题,是因为“检测数据是否已经存在”和“插入数据”两个动作被分割开来。由于这两个步骤不具备原子性,才导致两个不同的请求可以同时通过第一步的检测。如果我们能够把这两个动作合并为一个原子操作,就可以避免数据冲突了。这时候我们就需要通过加锁,来实现这个代码块的原子性。

对于 Java 语言,大家最熟悉的锁机制就是 synchronized 关键字了。

public synchronized void submit(String openId, String localIdentifier){ Account account = accountDao.find(openId); if (account == null) { // insert } else { // update }``}

但是,事情可没这么简单。要知道,我们的程序可不是只部署在一台服务器上,而是部署了多个节点。也就是说这里的并发不仅仅是线程间的并发,而是进程间的并发。因此,我们无法通过 java 语言层面的锁机制来解决这个同步问题,我们这里需要的应该是分布式锁。

3.3 两种解决方案的权衡


基于以上的分析,看上去两种方案都是可行的,但最终我们选择了分布式锁的方案。为什么明明第一种方案只需要简单地加个索引,我们却不采用呢?

因为现有的线上数据已然在 open_id 列上存在重复数据,如果此时直接去加唯一索引是无法成功的。为了加上唯一索引,我们必须首先将已有的重复数据先进行清理。但是问题又来了,线上的程序一直持续运行着,重复数据可能会源源不断地产生。那我们能不能找一个用户请求不活跃的时间段去进行清理,并在新的重复数据插入之前完成唯一索引的建立?答案当然是肯定的,只不过这种方案需要运维、DBA、开发多方协同处理,而且由于业务特性,最合适的处理时间段应该是凌晨这种夜深人静的时候。即便是采取这么苛刻的修复措施,也不能百分之百完全保证数据清理完成到索引建立之间不会有新的重复数据插入。因此,基于唯一索引的修复方案乍看之下非常合适,但是具体操作起来还是略为麻烦。

事实上,建立唯一索引最合适的契机应该是在系统最初的设计阶段,这样就能有效避免重复数据的问题。然而木已成舟,在当前这个情景下,我们还是选择了可操作性更强的分布式锁方案。因为选择这个方案的话,我们可以先上线加入了分布式锁修复的新代码,阻断新的重复数据插入,然后再对原有的重复数据执行清理操作,这样一来只需要修改代码并一次上线即可。当然,待问题彻底解决之后,我们可以重新再考虑为数据表加上唯一索引。

那么接下来,我们就来看看基于分布式锁的方案如何实现。首先我们先来回顾一下分布式锁的相关知识。

四、分布式锁概述

========

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

那么如何才能正确的掌握Redis呢?

为了让大家能够在Redis上能够加深,所以这次给大家准备了一些Redis的学习资料,还有一些大厂的面试题,包括以下这些面试题

  • 并发编程面试题汇总

  • JVM面试题汇总

  • Netty常被问到的那些面试题汇总

  • Tomcat面试题整理汇总

  • Mysql面试题汇总

  • Spring源码深度解析

  • Mybatis常见面试题汇总

  • Nginx那些面试题汇总

  • Zookeeper面试题汇总

  • RabbitMQ常见面试题汇总

JVM常频面试:

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(一)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(二)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Redis常见面试题汇总(300+题)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

is常见面试题汇总

  • Nginx那些面试题汇总

  • Zookeeper面试题汇总

  • RabbitMQ常见面试题汇总

JVM常频面试:

[外链图片转存中…(img-PTymfBfn-1714754268634)]

Mysql面试题汇总(一)

[外链图片转存中…(img-2QQSd1nz-1714754268635)]

Mysql面试题汇总(二)

[外链图片转存中…(img-3gGL3r6x-1714754268636)]

Redis常见面试题汇总(300+题)

[外链图片转存中…(img-rJ5hq1uQ-1714754268636)]

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

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值