分布式事物【悲观锁、乐观锁、读锁、写锁、间隙锁、临键锁 、 表锁、行锁、页面锁、 如何避免死锁】(二)-全面详解(学习总结---从入门到深化)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
日志服务 SLS,月写入数据量 50GB 1个月
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介: 分布式事物【悲观锁、乐观锁、读锁、写锁、间隙锁、临键锁 、 表锁、行锁、页面锁、 如何避免死锁】(二)-全面详解(学习总结---从入门到深化)

MySQL中锁的分类



从本质上讲,锁是一种协调多个进程或多个线程对某一资源的访问 的机制,MySQL使用锁和MVCC机制实现了事务隔离级别。


锁的分类



悲观锁和乐观锁


悲观锁


顾名思义,悲观锁对于数据库中数据的读写持悲观态度,即在整个数据处理的过程中,它会将相应的数据锁定。在数据库中,悲观锁的实现需要依赖数据库提供的锁机制,以保证对数据库加锁后,其他应用系统无法修改数据库中的数据。


注意:

在悲观锁机制下,读取数据库中的数据时需要加锁,此时不能对这些数据进行修改操作。修改数据库中的数据时也需要加锁,此时不能对这些数据进行读取操作。


乐观锁


悲观锁会极大地降低数据库的性能,特别是对长事务而言,性能的损耗往往是无法承受的。乐观锁则在一定程度上解决了这个问题。


注意:


实现乐观锁的一种常用做法是为数据增加一个版本标识,如果是通过数据库实现,往往会在数据表中增加一个类似version的版本号字段。


读锁和写锁


读锁


读锁又称为共享锁,共享锁就是多个事务对于同一数据可以共享一 把锁,都能访问到数据,但是只能读不能修改。


写锁


写锁又称为排他锁,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁, 包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。


注意:


需要注意的是,对同一份数据,如果加了读锁,则可以继续为 其加读锁,且多个读锁之间互不影响,但此时不能为数据增加 写锁。一旦加了写锁,则不能再增加写锁和读锁。因为读锁具有共享性,而写锁具有排他性。


表锁、行锁和页面锁


表锁


表锁也称为表级锁,就是在整个数据表上对数据进行加锁和释放锁。典型特点是开销比较小,加锁速度快,一般不会出现死锁,锁定的粒度比较大,发生锁冲突的概率最高,并发度最低。


手动增加表锁

mysql> lock table userinfo read;
Query OK, 0 rows affected (0.00 sec)
mysql> lock table userinfo write;
Query OK, 0 rows affected (0.00 sec)


查看数据表上增加的锁

show open tables;


删除表锁

mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)


行锁

行锁也称为行级锁,就是在数据行上对数据进行加锁和释放锁。典 型特点是开销比较大,加锁速度慢,可能会出现死锁,锁定的粒度最小,发生锁冲突的概率最小,并发度最高。


页面锁


页面锁也称为页级锁,就是在页面级别对数据进行加锁和释放锁。 对数据的加锁开销介于表锁和行锁之间,可能会出现死锁,锁定的粒度大小介于表锁和行锁之间,并发度一般。


间隙锁和临键锁


间隙锁


在MySQL中使用范围查询时,如果请求共享锁或排他锁,InnoDB 会给符合条件的已有数据的索引项加锁。如果键值在条件范围内, 而这个范围内并不存在记录,则认为此时出现了“间隙(也就是 GAP)”。InnoDB存储引擎会对这个“间隙”加锁,而这种加锁机制就是间隙锁(GAP Lock)。


例如,userinfo数据表中存在如下数据。


解释:


此时,userinfo数据表中的间隙包括id为(3,15]、(15,20]、 (20,正无穷]的三个区间。如果执行如下命令,将符合条件的用 户的账户余额增加100元。 update userinfo set balance = balance + 100 where id > 5 and id <16; 则其他事务无法在(3,20]这个区间内插入或者修改任何数据。 这里需要注意的是,间隙锁只有在可重复读事务隔离级别下才 会生效。


临键锁


临键锁(Next-Key Lock)是行锁和间隙锁的组合,例如上面例子中 的区间(3,20]就可以称为临键锁。


MySQL中的死锁问题



什么是死锁


死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并 发读写请求场景中。当两个及以上的事务,双方都在等待对方释放 已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出 现“死锁”。


Deadlock found when trying to get lock...

举例来说 A 事务持有 X1 锁 ,申请 X2 锁,B事务持有 X2 锁,申请 X1 锁。A 和 B 事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。


第一步


打开终端A,登录MySQL,将事务隔离级别设置为可重复读,开启 事务后为userinfo数据表中id为1的数据添加排他锁,如下所示。

mysql> set session transaction isolation level
repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from userinfo;
+----+-------+---------+
| id | name  | balance |
+----+-------+---------+
|  1 | Java  |  100.00 |
|  2 | MySQL |  200.00 |
+----+-------+---------+
2 rows in set (0.00 sec)


第二步


打开终端B,登录MySQL,将事务隔离级别设置为可重复读,开启事务后为userfinfo数据表中id为2的数据添加排他锁,如下所示。

mysql> set session transaction isolation level
repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from userinfo where id = 2;
+----+-------+---------+
| id | name  | balance |
+----+-------+---------+
|  2 | MySQL |  200.00 |
+----+-------+---------+
1 row in set (0.00 sec)


第三步


在终端A为userinfo数据表中id为2的数据添加排他锁,如下所示。

mysql> select * from userinfo where id =2 for
update;


注意: 此时,线程会一直卡住,因为在等待终端B中id为2的数据释放排他锁。


第四步


在终端B中为userinfo数据表中id为1的数据添加排他锁,如下所示。

mysql> select * from userinfo where id =1 for
update;
ERROR 1213 (40001): Deadlock found when trying
to get lock; try restarting transaction


通过如下命令可以查看死锁的日志信息。


show engine innodb status\G


注意:

通过命令行查看LATEST DETECTED DEADLOCK选项相关的信 息,可以发现死锁的相关信息,或者通过配置 innodb_print_all_deadlocks(MySQL 5.6.2版本开始提供)参数为ON,将死锁相关信息打印到MySQL错误日志中。


如何避免死锁



MySQL事务的实现原理_什么是redo log



MySQL的事务实现离不开Redo Log和Undo Log。从某种程度上 说,事务的隔离性是由锁和MVCC机制实现的,原子性和持久性是 由Redo Log实现的,一致性是由Undo Log实现的。


什么是redo log


redo log叫做重做日志,是用来实现事务的持久性。该日志文件由 两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件 (redo log),前者是在内存中,后者在磁盘中。当事务提交之后会 把所有修改信息都会存到该日志中。


注意:

先写日志,再写磁盘的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging) 技术。


Redo Log刷盘规则


在计算机操作系统中,用户空间( user space )下的缓冲区数据一般情况 下是无法直接写入磁盘的,中间必须经过操作系统内核空间( kernel space )缓冲区( OS Buffer )。因此, redo log buffer 写入 redo log file 实际上是先 写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file 中。


mysql 支持三种将 redo log buffer 写入 redo log file 的时机。 innodb_flush_log_at_trx_commit。



Redo Log刷盘最佳实践


不同的Redo Log刷盘规则,对MySQL数据库性能的影响也不同。


创建测试数据库

create database if not exists test;
create table flush_disk_test(
 id int not null auto_increment,
 name varchar(20),
primary key(id)
)engine=InnoDB;


编写存储过程


为了测试方便,这里创建一个名为insert_data的存储过程,接收一 个int类型的参数。这个参数表示向flush_disk_test数据表中插入的记录行数。

drop procedure if exists insert_data;
-- 该段命令是否已经结束了,mysql是否可以执行了。
delimiter $$
create procedure insert_data(i int)
begin
-- 声明变量 s
declare s int default 1;
-- 声明变量 c
declare c varchar(50) default 'binghe';
-- while循环
while s<=i do
-- 开启事务
start transaction;
-- 添加数据
insert into flush_disk_test (name) values(c);
-- 提交事务
commit;
-- s变量累加
set s=s+1;
-- 循环结束
end while;
end$$
-- 该段命令是否已经结束了,mysql是否可以执行了。
delimiter ;


查看刷盘规则

show variables like
'innodb_flush_log_at_trx_commit';


第一步


将innodb_flush_log_at_trx_commit变量的值设置为0。

set global innodb_flush_log_at_trx_commit=0;


调用insert_data向flush_disk_test数据表中插入10万条数据,如下 所示。

mysql> call insert_data (100000);
Query OK, 0 rows affected (2.18 sec)


注意: 可以看到,当innodb_flush_log_at_trx_commit变量的值设置为0时,向表中插入10万条数据耗时2.18s。


第二步


将innodb_flush_log_at_trx_commit变量的值设置为1。

set global innodb_flush_log_at_trx_commit=1;


调用insert_data向flush_disk_test数据表中插入10万条数据,如下所示。

mysql> call insert_data (100000);
Query OK, 0 rows affected (16.18 sec)


注意:

可以看到,当innodb_flush_log_at_trx_commit变量的值设置为1时,向表中插入10万条数据耗时16.18s。


第三步


将innodb_flush_log_at_trx_commit变量的值设置为2。

set global innodb_flush_log_at_trx_commit=2;


调用insert_data向flush_disk_test数据表中插入10万条数据,如下所示。

mysql> call insert_data (100000);
Query OK, 0 rows affected (3.05 sec)


注意:

可以看到,当innodb_flush_log_at_trx_commit变量的值设置为2时,向表中插入10万条数据耗时3.05s。


结论

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
5月前
|
NoSQL 调度 Redis
分布式锁—3.Redisson的公平锁
Redisson公平锁(RedissonFairLock)是一种基于Redis实现的分布式锁,确保多个线程按申请顺序获取锁,从而实现公平性。其核心机制是通过队列和有序集合管理线程的排队顺序。加锁时,线程会进入队列并等待,锁释放后,队列中的第一个线程优先获取锁。RedissonFairLock支持可重入加锁,即同一线程多次加锁不会阻塞。新旧版本在排队机制上有所不同,新版本在5分钟后才会重排队列,而旧版本在5秒后就会重排。释放锁时,Redisson会移除队列中等待超时的线程,并通知下一个排队的线程获取锁。通过这种机制,RedissonFairLock确保了锁的公平性和顺序性。
|
25天前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
2月前
|
NoSQL Redis
分布式锁设计吗,你是如何实现锁类型切换、锁策略切换基于限流的?
本方案基于自定义注解与AOP实现分布式锁,支持锁类型(如可重入锁、公平锁等)与加锁策略(如重试、抛异常等)的灵活切换,并结合Redisson实现可重入、自动续期等功能,通过LUA脚本保障原子性,兼顾扩展性与实用性。
48 0
|
3月前
|
缓存 NoSQL Java
【📕分布式锁通关指南 11】源码剖析redisson之读写锁的实现
Redisson 的 `RedissonReadWriteLock` 提供了高效的分布式读写锁实现,适用于读多写少的场景。通过 Redis 与 Lua 脚本结合,确保读锁并行、写锁互斥,以及读写之间的互斥,保障了分布式环境下的数据一致性。它支持可重入、自动过期和锁释放机制,提升了系统并发性能与资源控制能力。
69 0
|
5月前
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
297 17
|
5月前
|
监控 NoSQL Java
分布式锁—2.Redisson的可重入锁
本文主要介绍了Redisson可重入锁RedissonLock概述、可重入锁源码之创建RedissonClient实例、可重入锁源码之lua脚本加锁逻辑、可重入锁源码之WatchDog维持加锁逻辑、可重入锁源码之可重入加锁逻辑、可重入锁源码之锁的互斥阻塞逻辑、可重入锁源码之释放锁逻辑、可重入锁源码之获取锁超时与锁超时自动释放逻辑、可重入锁源码总结。
|
7月前
|
安全
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
399 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
7月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
255 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
6月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
186 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
|
7月前
|
NoSQL Java Redis
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
本文详细解析了Redisson可重入锁的加锁流程。首先从`RLock.lock()`方法入手,通过获取当前线程ID并调用`tryAcquire`尝试加锁。若加锁失败,则订阅锁释放通知并循环重试。核心逻辑由Lua脚本实现:检查锁是否存在,若不存在则创建并设置重入次数为1;若存在且为当前线程持有,则重入次数+1。否则返回锁的剩余过期时间。此过程展示了Redisson高效、可靠的分布式锁机制。
228 0
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁

热门文章

最新文章