文章目录
前言
之前一篇文章spring的传播机制和事务控制(https://blog.csdn.net/weixin_43582896/article/details/138500891),文章更多的是在代码层面的应用,抛开代码层面,最终还是在db中执行按照一定的逻辑顺序执行相应的sql语句来完成相应的业务开发。
先了解什么是 锁机制 和 死锁 ?
1.在MySQL的InnoDB 引擎中,锁机制是指MySQL数据库管理系统在处理并发访问时使用的一种机制,用于控制对数据库中数据的访问和修改。锁机制可以确保在多个用户同时访问数据库时,数据的一致性和完整性。
2.在MySQL的InnoDB 引擎中,死锁是指两个或多个事务相互等待对方释放资源而无法继续执行的情况,当多个事务同时请求对同一组资源进行操作时,如果每个事务都持有一部分资源并且等待其他事务释放资源,就可能发生死锁(常见的一个事务更新一条数据,事务还没有提交,另一个事务又过来更新相同的着一条数据,当后来事务等待时间超过设定的阈值时就会进行回滚,最开始的能继续执行提交,一般表现在接口curd逻辑不规范,或者接口并发操作下造成的
)。
一、数据库事务属性(ACID)
其实就是事务的特性或行为
属性 | 解释 |
---|---|
原子性(Atomicity) | 事务是一个不可分割的工作单位,要么全部执行成功,要么全部执行失败,不会出现部分执行的情况(老掉牙的场景:A向B转账100元,如果转账失败了,A的钱没有退回,B也没有收到100元 )。 |
一致性(Consistency) | 事务执行前后,数据库的完整性约束没有被破坏,保持数据的一致性。(转账场景:A向B转账100元,转账成功了,不能A账户没有扣100元,B账户却增加了100元,这样两边数据就不一致了 ) |
隔离性(Isolation) | 多个事务并发执行时,每个事务的操作应该相互隔离,互不干扰,避免数据的混乱和不一致。(转账场景:比如A账户有100元,A正在从账户提现100元,在这个过程中B却不能给他的账户进行转账操作,或者B正在给A账户转账100元,A提现完后,发现账户是200元,这样都会导致数据混乱 ) |
持久性(Durability) | 一旦事务提交成功,对数据的修改将会永久保存在数据库中,即使系统发生故障也不会丢失。(转账场景:B给A账户转账100元,B账户收和A账户都收到成功的消息,之后A没有使用也没有提现这100元,银行一次服务器维护通知,A再次打开账户这100元就从A账户消失了 ) |
二、数据库的隔离级别
指多个事务之间的隔离程度,MYSQL默认的隔离级别是:可重复读(REPEATABLE READ)。这意味着在一个事务中,查询将看到一个一致的快照,即使其他事务正在对数据库进行更改。这可以防止脏读和不可重复读,但仍然允许幻读。
隔离级别 | 解释 |
---|---|
读未提交(Read Uncommitted) | 事务可以读取其他事务未提交的数据,可能会导致脏读、不可重复读和幻读等问题。 |
读已提交(Read Committed) | 事务只能读取其他事务已提交的数据,可以避免脏读,但可能会出现不可重复读和幻读。 |
可重复读(Repeatable Read) | 事务在执行期间看到的数据保持一致,可以避免脏读和不可重复读,但仍可能出现幻读。 |
可串行化(Serializable) | 事务之间完全隔离,可以避免脏读、不可重复读和幻读,但性能较差,通常不建议使用。会自动将所有普通select转化为select … lock in share mode执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低. |
三、什么是脏读,不可重复读,幻读?
这三者都是数据库并发操作系下发生的,可以当技术储备了解下场景
项目 | Value |
---|---|
脏读(Dirty Read) | 一个事务读取了另一个事务尚未提交的数据,如果另一个事务在之后回滚,则读取的数据就是脏数据。脏读可能导致不一致的数据。 |
不可重复读(Non-Repeatable Read) | 一个事务在读取某个数据后,另一个事务修改了该数据并提交,导致第一个事务再次读取该数据时,发现数据已经发生了变化,这就是不可重复读。 |
幻读(Phantom Read) | 一个事务在读取某个范围的数据后,另一个事务插入了新的数据,导致第一个事务再次读取该范围的数据时,发现数据量增加了(侧重于数据量的增减),就像出现了幻觉一样,这就是幻读。 |
四、数据库的中锁
从大的方面来看有两种锁,悲观锁和乐观锁
1.乐观锁:是一种乐观地认为数据不会发生冲突的锁机制。在使用乐观锁时,系统会认为在对数据进行操作时不会发生并发冲突,因此不会立即加锁,而是在提交数据时检查数据版本号或时间戳等信息,以确保数据的一致性。
常见就是,在表中加一个version 字段,更新数据这条数据的时候,先查出来当前version 的值,后面写更新语句的时候,将where 条件补充这个version 的条件 ---------------------sql---------------------- 1.select name ,version from tableA where id = '1234' ; 2.update tableA set name = '张三' ,version = version +1 where id = '1234' and version = (之前select 语句 version 的结果) ; ------------------------------sql------------------------------------------代码会返回一个影响行数,如果影响行数不符合预期,可以抛一个异常回滚事务,或者提示等等
2.悲观锁:是一种悲观地认为数据会发生冲突的锁机制。在使用悲观锁时,系统会认为在对数据进行操作时会发生并发冲突,因此会在操作数据之前先加锁,确保在操作过程中其他用户无法修改数据。
悲观锁从对数据操作来分
发生死锁,分析数据库死锁日志用到的多
1.读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响
2.写锁(排它锁,X锁(eXclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁
排它锁(X锁)从更细的粒度来分 :为表锁,行锁,间隙锁(Gap Lock),临键锁(Next-key Locks行锁与间隙锁的组合
)
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。
但是update、insert、delete操作会加行锁或者表锁。
五、死锁场景(在RR级别下)
基础sql准备
sql | 解释 |
---|---|
show engine innodb status; | 查看近期死锁日志信息(有一个status字段可以直接cv到文件展示查看,或者在mysql中开启打印日志,去log文件中看 ): |
select * from information_schema.innodb_trx; | 查询锁事务状态信息 |
select * from information_schema.innodb_lock_waits; | 锁等待的信息,可以看到堵塞和被堵塞者 |
通过information_schema.innodb_trx结果分析
information_schema.innodb_trx 表 | 字段解释 |
---|---|
trx_state字段 | 表示事务的状态,如果事务状态为LOCK WAIT,则表示该事务正在等待锁,可能导致表被锁或者发生死锁。 |
rx_query字段 | 表示当前事务正在执行的SQL语句,可以通过该字段来判断哪个SQL语句导致了表被锁或者发生死锁。 |
trx_mysql_thread_id字段 | 表示当前事务的MySQL线程ID,可以通过该字段来定位哪个线程导致了表被锁或者发生死锁。 |
行锁或表锁造成的死锁(大多数这种情况造成的)
第一种:分析这个日志一般看下面几处,主要是update 语句
执行 show engine innodb status;抓取日志
// 1.TRANSACTION 6294 6294 代表事务id
---TRANSACTION 6294, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
// 2.LOCK WAIT 代表有死锁发生 1 row lock(s) 有1行被锁了
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 43941, OS thread handle 139804610352896, query id 190166 36.7.122.54 root updating
//3.死锁发生的sql语句(后期该事务等待时间超过设定的阈值时就会进行回滚)
update gpt_user_question_record set to_user_name = '测试事务3' where id = 1
------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:
// 4.lock_mode X 代表排他锁 index PRIMARY 代表 锁对应的索引 下面对应的日志对于分析死锁产生的条件挺重要的
RECORD LOCKS space id 45 page no 4 n bits 96 index PRIMARY of table `chatgpt`.`gpt_user_question_record` trx id 6294 lock_mode X locks rec but not gap waiting
Record lock, heap no 29 PHYSICAL RECORD: n_fields 17; compact format; info bits 0
0: len 8; hex 8000000000000001; asc ;;
1: len 6; hex 000000001895; asc ;;
2: len 7; hex 79000001b30a5b; asc y [;;
3: len 28; hex 6f7056676d3656637750524468346b3468665053686f703133414263; asc opVgm6VcwPRDh4k4hfPShop13ABc;;
4: len 17; hex 3234333934393134393537343031323633; asc 24394914957401263;;
5: len 13; hex e6b58be8af95e4ba8be58aa131; asc 1;;
6: len 28; hex 6f7056676d3656637750524468346b3468665053686f703133414263; asc opVgm6VcwPRDh4k4hfPShop13ABc;;
7: len 0; hex ; asc ;;
8: len 6; hex e4bda0e698af; asc ;;
9: len 30; hex e68891e698afe4b880e4b8aae5908de4b8ba2043686174474c4d322d3642; asc ChatGLM2-6B; (total 238 bytes);
10: len 0; hex ; asc ;;
11: len 0; hex ; asc ;;
12: len 5; hex 99b1fe39c1; asc 9 ;;
13: len 5; hex 99b356a10a; asc V ;;
14: len 1; hex 80; asc ;;
15: len 5; hex 99b356a10a; asc V ;;
16: SQL NULL;
在sql客户端模拟相应的执行语句,我们模拟两个客户端
session1(客户端1) | session2(客户端2) |
---|---|
这里会发现session1(客户端2) 一直处于堵塞状态,最终该事务执行失败被回滚,在并发操作下,则会回滚最新执行这条语句的sql
第二种:分析这个日志一般看下面几处,主要是update 和insert 语句,且使用了普通索引
执行 show engine innodb status;抓取日志
//这个事务是session2
---TRANSACTION 6302, ACTIVE 14 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 43968, OS thread handle 139804478662400, query id 190400 36.7.122.54 root update
//sql语句
INSERT INTO `chatgpt`.`gpt_user_question_record`(`id`, `open_id`, `msg_id`, `to_user_name`, `from_user_name`, `msg_type`, `question_content`, `question_result`, `event`, `event_key`, `record_tim
`, `create_time`, `is_delete`, `update_time`, `model_num`) VALUES (262, 'opVgm6VcwPRDh4k4hfPShop13ABc', '24398552206719911', '测试事务', 'opVgm6VcwPRDh4k4hfPShop13ABc', 'text', '你是?', ' chatgp
', '', '', '2024-01-03 02:13:43', '2024-05-11 06:21:09', 0, '2024-05-11 06:21:09', NULL)
------- TRX HAS BEEN WAITING 14 SEC FOR THIS LOCK TO BE GRANTED:
// index msg_id_index 这是一个普通索引
RECORD LOCKS space id 45 page no 6 n bits 360 index msg_id_index of table `chatgpt`.`gpt_user_question_record` trx id 6302 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 9 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 17; hex 3234343031303134333837363331333839; asc 24401014387631389;;
1: len 8; hex 8000000000000008; asc ;;
------------------
//这个事务是session1
---TRANSACTION 6301, ACTIVE 30 sec
4 lock struct(s), heap size 1136, 7 row lock(s), undo log entries 3
MySQL thread id 43966, OS thread handle 139804481095424, query id 190398 36.7.122.54 root
--------
session1(客户端1) | session2(客户端2) |
---|---|
上面两个sql运行时事务都没有提交,因为msg_id上面有索引只会给 msg_id 所在的行创建一个排他锁(X锁),两个sql都在相互等待锁资源的释放,最终造成死锁
平常排查问题的方向:一般先看sql中对应的表,后看哪个字段造成的死锁,看哪个字段就可以通过死锁日志中的索引信息来确定,最后找到对应的代码块是在定时任务中(定时任务频繁根据某个索引字段更新表),还是接口之间的并发造成的
**这里的更新语句需要注意一点,如果id 上面没有索引,只是普通字段 执行update 或者delete 语句行锁会升级为表锁,因为 锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁,这样如果有别的sql操作这张表会严重影响接口性能,甚至造成死锁(RR级别会升级为表锁,RC级别不会升级为表锁)
关于锁优化的一些总结
有点空洞,当了解理论吧,为解决问题奠定方向,大部分的都是在实际业务中遇到问题,优化方案,解决问题
-
尽量使用较小的范围进行锁定,避免对整个表或整个数据库进行锁定,可以使用行级锁或者表级锁。
-
尽量减少事务的持有时间,尽快释放锁资源,避免长时间占用锁资源导致其他事务阻塞。
-
尽量避免在事务中进行大量的数据操作,可以将数据操作拆分成多个小事务,减少锁的竞争。
-
使用合适的索引来优化查询,减少锁的竞争,提高查询效率。
-
尽量避免在事务中进行大量的数据修改操作,可以使用批量操作或者定时任务来处理大量数据,减少锁的竞争。
-
使用事务隔离级别来控制锁的粒度,根据业务需求选择合适的隔离级别。
-
定期监控数据库性能,及时发现并解决锁竞争问题,避免影响系统性能。