当前位置:网站首页>分布式前修课:MySQL实现分布式锁
分布式前修课:MySQL实现分布式锁
2022-07-30 14:26:00 【肥肥技术宅】
前言
前面已经介绍了原理性的内容,如果原理看的不爽,想看点实际展示效果的话,那么它来了它来了,它带着代码走来啦:blush:
基于MySQL分布式锁实现原理及代码
工欲善其事必先利其器,在基于MySQL实现分布式锁之前,我们要先了解一点MySQL锁自身的相关内容
MySQL锁
我们知道: 锁是计算机协调多个进程或者线程并发访问同一资源的机制 ,而在数据库中,除了传统的机器资源的争用之外,存储下来的数据也属于供用户共享的资源,所以如何保证数据并发的一致性,有效性是每个数据库必须解决的问题。
除此之外,锁冲突也是影响数据库并发性能的主要因素,所以锁对于数据库而言就显得非常重要,也非常复杂。
而 存储引擎 是MySQL中非常重要的底层组件,主要用来处理不同类型的SQL操作,其中包括创建,读取,删除和修改操作。在MySQL中提供了不同类型的存储引擎,根据其不同的特性提供了不同的存储机制,索引和锁功能。
根据 show engines; 能够列出MySQL下支持的存储引擎

如果没有特殊指定,那么在 MySQL8.0 中会设置 InnoDB 为默认的存储引擎
在实际工作中,根据需求选择最多的两种存储引擎分别为:
- InnoDB
- MyISAM
所以我们主要针对这两种类型来介绍MySQL的锁
InnoDB
InnoDB 支持 多粒度锁定 ,可以支持 行锁 ,也可以支持 表锁 。如果没有升级锁粒度,那么默认情况下是以行锁来设计的。
关于行锁和表锁的介绍
- 行锁对指定数据进行加锁,锁定粒度最小,开销大,加锁慢,容易出现死锁问题,出现锁冲突的概率最小,并发性最高
- 表锁对整个表进行加锁,锁定粒度大,开销小,加锁快,不会出现死锁,出现锁冲突的概率最大,并发性最低
这里没法说明那种锁最好,只有合适不合适
在行级锁中,可以分为两种类型
- 共享锁
- 排他锁
共享锁
共享锁又称为 读锁 ,允许其他 事务 读取被锁定的对象,也可以在其上获取其他共享锁,但不能写入。
举个例子:
- 事务T在数据A拥有共享锁,那么当前事务T对数据A可以读,但是不能修改。而且事务T2同样可以对数据A拥有共享锁,这样相当于在数据A上分别存在不同事务的共享锁
- 数据A拥有了事务T的共享锁,那么就不能再拥有其他事务的排他锁
下面是关于共享锁的具体实现,关键代码: select .. from table lock in share mode
-- 创建实例表
create table tb_lock(
id bigint primary key auto_increment,
t_name varchar(20)
) engine=InnoDB;开启两个窗口来测试
| session1 | session2 |
|---|---|
| set autocommit=0; | set autocommit=0; |
| select * from tb_lock where t_name = 'zs' lock in share mode; | |
| select * from tb_lock where t_name = 'zs' lock in share mode; | |
| select * from tb_lock where t_name = 'lsp' lock in share mode; | |
| update tb_lock set t_name = 'lzs' where t_name = 'zs'; | |
| update tb_lock set t_name = 'lsp111' where t_name = 'lsp'; | |
| select * from tb_lock where t_name = 'zs'; | |
| commit; |
自动提交全部关闭,可以通过 select @@autocommit; 来查看
通过以上实验,我们总结:
- 共享锁基于行锁处理,不同事务可以在同一条数据上获取共享锁
- 如果多个事务在同一条数据上获取共享锁,当想要修改该条数据的时候,会出现阻塞状态。直到其他事务将锁释放,该能够继续修改
修改,删除,插入会默认对涉及到的数据加上排他锁
- 单纯的
select操作不会有任何影响,select不会加任何锁 - 执行
commit;自动释放锁
排它锁
又叫 写锁 。只允许获取锁的事务对数据进行操作【更新,删除】,其他事务对相同数据集只能进行读取,不能有跟新或者删除操作。而且也不能在相同数据集获取到共享锁。
没错,就是这么霸道
在MySQL中,想要基于排它锁实现行级锁,就需要对表中索引列加锁,否则的话,排它锁就属于表级锁
下面一一来展示,关键代码: select .. from XX for update
首先是有索引列状态
| session1 | session2 |
|---|---|
| set autocommit=0; | set autocommit=0; |
| select * from tb_lock; | select * from tb_lock; |
| select * from tb_lock where id = 1 for update; | |
| select * from tb_lock where id = 1 for update; | |
| select * from tb_lock where id = 2 for update; | |
| commit; |
通过以上实验,得到结论:
- 对索引列进行加锁的锁定级别为行级锁,如上所示,当其他事务想要对相同的数据再次加锁的时候,就会进行到阻塞状态。并且如果等待时间过长,会出现如下异常:
Lock wait timeout exceeded; try restarting transaction
- 对不同行数据再次加排它锁,是没有任何问题的。
- 对已经上锁的相同数据做修改和删除操作不需要多说,因为InnoDB默认会对其加入排它锁
下面是无索引列状态
| session1 | session2 |
|---|---|
| set autocommit=0; | set autocommit=0; |
| select * from tb_lock; | select * from tb_lock; |
| select * from tb_lock where t_name = 'ls' for update; | |
| select * from tb_lock where t_name = 'ls' for update; | |
| commit |
通过以上实验,得到结论:
- 对非索引列其中一条数据加入了排它锁后,在其他事务中对不同数据再次加入排它锁,进入了阻塞状态
- 说明当加锁列属于非索引时,InnoDB会对整个表进行上锁,进入到表级锁
接下来我们来看看 MyISAM 的方式
MyISAM
MyISAM属于表级锁,被用来防止任何其他 事务 访问表的锁。
其中表锁又分为两种形式
- 表共享读锁: READ
- 表独占写锁: WRITE
这里我们要注意:表级锁只能防止其他会话进行不适当的读取或写入。
- 持有
WRITE锁的会话可以执行表级操作,比如DELETE或者TRUNCATE - 持有会话
READ锁,不能够执行DELETE或者TRUNCATE操作
表共享读锁
不管是 READ 还是 WRITE ,都是通过 lock table 来获取表锁的,而 READ 锁拥有如下特性:
- 持有锁的会话可以读取表,但是不能进行写入操作
- 多个会话可以同时获取
READ表的锁,而其他会话可以在不显式获取READ锁的情况下读取该表:也就是说直接通过select来操作
那么,接下来我们来看实际操作,关键代码: lock tables table_name read
create table tb_lock_isam(
id bigint primary key auto_increment,
t_name varchar(20)
) engine=MyISAM;开启两个窗口来进行操作
| session1 | session2 |
|---|---|
| set autocommit=0; | set autocommit=0; |
| LOCK TABLES tb_lock_isam READ; | |
| select * from tb_lock_isam; | |
| select * from tb_lock; | |
| select * from tb_lock_isam; | |
| LOCK TABLES tb_lock_isam READ; | |
| select * from tb_lock_isam; | |
| select * from tb_lock; | |
| unlock tables; | insert into tb_lock_isam(t_name) values('ll'); |
通过以上实战,验证以下结论:
- 在当前事务下,获取到读锁,直接查询锁定表是没有问题的,但是如果想要读取其他表下的数据,那么就会出现以下异常:因为其他表并没有LOCK在其中
Table 'tb_lock' was not locked with LOCK TABLES
- 事务A获取到读锁之后,在其他事务中是可以正常读取的,并且也可以再次获取读锁。
- 在读锁中如果想要进行插入操作是不会成功的,出现以下异常:
Table 'tb_lock_isam' was locked with a READ lock and can't be updated
- 当前表获取到读锁之后,在当前表没有释放读锁之前,再获取写锁会一直进入到阻塞状态。
- 可以通过非加锁方式来读取数据,但是要注意: 一定是在不同的事务下
表独占写锁
WRITE锁 的特性和 排它锁 的特性非常相似,都特别霸道:
WRITE
还是通过具体实战来进行演示效果,关键代码: lock tables table_name write
| session1 | session2 |
|---|---|
| select * from tb_lock_isam; | select * from tb_lock_isam; |
| lock table tb_lock_isam write; | |
| select * from tb_lock_isam; | |
| insert into tb_lock_isam(t_name) values('66'); | |
| select * from tb_lock_isam; | |
| unlock tables; |
通过以上实战,验证以下结论:
WRITE锁 WRITE锁
Table 'tb_index' was not locked with LOCK TABLES'
【注意】
MyISAM 在执行查询语句之前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要使用命令来显式加锁
分布式锁实现
既然已经了解到了MySQL锁相关内容,那么我们就来看看如何实现,首先我们需要创建一张数据表
当然,只需要初始化创建一次
create table if not exists fud_distribute_lock(
id bigint unsigned primary key auto_increment,
biz varchar(50) comment '业务Key'
unique(biz)
) engine=innodb;在其中, biz 是为了区分不同的业务,也可以理解为 资源隔离 ,并且对 biz 设置唯一索引,也能够防止其锁级别变为表级锁
既然 for udpate 就是加锁成功,事务提交就自动释放锁,那么这个事情就非常好办了:
// 省略了构造方法,需要传入DataSource和biz
private static final String SELECT_SQL =
"SELECT * FROM fud_distribute_lock WHERE `biz` = ? for update";
private static final String INSERT_SQL =
"INSERT INTO fud_distribute_lock(`biz`) values(?)";
// 从构造方法中传入
private final DataSource source;
private Connection connection;
public void lock() {
PreparedStatement psmt = null;
ResultSet rs = null;
try {
// while(true);
for (; ; ) {
connection = this.source.getConnection();
// 关闭自动提交事务
connection.setAutoCommit(false);
psmt = connection.prepareStatement(SELECT_SQL);
psmt.setString(1, biz);
rs = psmt.executeQuery();
if (rs.next()) {
return;
}
connection.commit();
close(connection, psmt, rs);
// 如果没有相关查询,需要插入
Connection updConnection = this.source.getConnection();
PreparedStatement insertStatement = null;
try {
insertStatement = updConnection.prepareStatement(INSERT_SQL);
insertStatement.setString(1, biz);
if (insertStatement.executeUpdate() == 1) {
LOGGER.info("创建锁记录成功");
}
} catch (Exception e) {
LOGGER.error("创建锁记录异常:{}", e.getMessage());
} finally {
close(insertStatement, updConnection);
}
}
} catch (Exception e) {
LOGGER.error("lock异常信息:{}", e.getMessage());
throw new BusException(e);
} finally {
close(psmt, rs);
}
}
public void unlock() {
try {
// 事务提交之后自动解锁
connection.commit();
close(connection);
} catch (Exception e) {
LOGGER.error("unlock异常信息:{}", e.getMessage());
throw new BusException(e);
}
}
public void close(AutoCloseable... closeables) {
Arrays.stream(closeables).forEach(closeable -> {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
LOGGER.error("close关闭异常:{}", e.getMessage());
}
}
});
}难点:为什么需要for(;;)
如果一个请求是第一次进来的,比如 biz=order ,在这个表中是不会存储 order 这条记录,那么 select ...for update 就不会生效,所以就需要先将 order 插入到表记录中,也就是执行 insert 操作。
insert 执行成功之后,记录 select...for update ,这样获取锁才能生效
最后
基于MySQL的分布式锁在实际开发过程中很少使用,但是我们还是要有一个思路在。那么本节针对MySQL的分布式锁实现到这里就结束了,掌握了MySQL的基础锁,那么就会非常简单了。

边栏推荐
- 71页全域旅游综合整体解决方案2021 ppt
- A simple change for problem, knapsack problem sets of shell
- [深入研究4G/5G/6G专题-46]: 5G Link Adaption链路自适应-2-常见缩略语
- 有关收集箱的改进建议
- 地形分析的主要内容(流浪地球的特效水平)
- 那些破釜沉舟入局Web3.0的互联网精英都怎么样了?
- 深入浅出零钱兑换问题——背包问题的套壳
- Web3创始人和建设者必备指南:如何构建适合的社区?
- MPSK抗噪声性能对比(即MPSK标准误码率曲线)
- The main content of terrain analysis (the special effect level of the wandering earth)
猜你喜欢

算力顶天地,存力纳乾坤:国家超级计算济南中心的一体两面

时序数据库在船舶风险管理领域的应用

一文读懂网络效应对Web3的重要意义

Flink real-time data warehouse completed

Web3创始人和建设者必备指南:如何构建适合的社区?

3年软件测试经验面试要求月薪22K,明显感觉他背了很多面试题...

MaxWell scraped data

为什么做软件测试一定要学自动化?谈谈我眼中自动化测试的价值

浅析显卡市场的未来走向:现在可以抄底了吗?

Teach you how to write an eye-catching software testing resume, if you don't receive an interview invitation, I will lose
随机推荐
容器排序案例
吃透Chisel语言.28.Chisel进阶之有限状态机(二)——Mealy状态机及与Moore状态机的对比
3 years of software testing experience, the interview requires a monthly salary of 22K, obviously he has memorized a lot of interview questions...
自动化办公|办公软件和亿图脑图MindMaster快捷键
canal抓取数据
JSON common annotations
Web3创始人和建设者必备指南:如何构建适合的社区?
Hello,World
【Vue.js 3.0源码】KeepAlive 组件:如何让组件在内存中缓存和调度?
Start learning C language
sql中ddl和dml(sql与access的区别)
怎么判断两个字符串是否相等?
canal scrape data
惊艳!京东T8纯手码的Redis核心原理手册,基础与源码齐下
Flink real-time data warehouse completed
The truth of the industry: I will only test those that have no future, and I panic...
机房布线的至高境界,美到窒息
元宇宙邮局AI航天主题系列数字藏品 将于7月30日10:00点上线“元邮数藏”
CS内网横向移动 模拟渗透实操 超详细
MongoDB starts an error Process: 29784 ExecStart=/usr/bin/mongod $OPTIONS (code=exited, status=14)