当前位置:网站首页>MySQL:Update高并发下变慢的案例及其涉及的特性
MySQL:Update高并发下变慢的案例及其涉及的特性
2022-08-08 14:31:00 【InfoQ】
一、问题说明

二、分析方式
- MDL LOCK堵塞不可能,因为state状态不对,MDL LOCK堵塞的现象是waitting for开头的。
- 可能是row lock堵塞,因为在update语句的情况下row lock堵塞也是updating状态。




create table testsemi(a int auto_increment primary key,b int,c int,d int,key(b,c));
修改语句大概如下:
update testsemi set d=20 where c=20;
数据量大约百万左右。
- Innodb层 semi update
- MySQL层unlock row
三、RC隔离级别下的semi update和unlock row优化
mysql> show variables like '%transaction_isolation%';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
mysql> show create table testsemi30 \G;
*************************** 1. row ***************************
Table: testsemi30
Create Table: CREATE TABLE `testsemi30` (
`a` int(11) NOT NULL AUTO_INCREMENT,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) NOT NULL,
PRIMARY KEY (`a`),
KEY `b` (`b`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)
ERROR:
No query specified
mysql> select * from testsemi30;
+----+------+------+---+
| a | b | c | d |
+----+------+------+---+
| 2 | 2 | 2 | 0 |
| 4 | 4 | 4 | 0 |
| 6 | 6 | 6 | 0 |
| 8 | 8 | 8 | 0 |
| 12 | 12 | 12 | 0 |
+----+------+------+---+
5 rows in set (0.00 sec)
- 3.1.2 例子1:
session1:
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> update testsemi30 set d=6 where c=6;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> desc update testsemi30 set d=6 where c=6;
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | UPDATE | testsemi30 | NULL | index | NULL | PRIMARY | 4 | NULL | 5 | 100.00 | Using where |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
1 row in set (0.01 sec)
---TRANSACTION 808623, ACTIVE 19 sec
2 lock struct(s), heap size 1160, 1 row lock(s), undo log entries 1
MySQL thread id 16, OS thread handle 140735862056704, query id 349 localhost root
TABLE LOCK table `test`.`testsemi30` trx id 808623 lock mode IX
RECORD LOCKS space id 9694 page no 3 n bits 72 index PRIMARY of table `test`.`testsemi30` trx id 808623 lock_mode X(LOCK_X) locks rec but not gap(LOCK_REC_NOT_GAP)
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 6; hex 0000000c56af; asc V ;;
2: len 7; hex 7b000001ea0fdc; asc { ;;
3: len 4; hex 80000006; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000006; asc ;;
session2:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from testsemi30 where c=4 for update;
此处堵塞,row lock如下:
TABLE LOCK table `test`.`testsemi30` trx id 808624 lock mode IX
RECORD LOCKS space id 9694 page no 3 n bits 72 index PRIMARY of table `test`.`testsemi30` trx id 808624 lock_mode X(LOCK_X) locks rec but not gap(LOCK_REC_NOT_GAP)
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000c5687; asc V ;;
2: len 7; hex e200000089011d; asc ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000004; asc ;;
5: len 4; hex 80000004; asc ;;
RECORD LOCKS space id 9694 page no 3 n bits 72 index PRIMARY of table `test`.`testsemi30` trx id 808624 lock_mode X(LOCK_X) locks rec but not gap(LOCK_REC_NOT_GAP) waiting(LOCK_WAIT)
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 6; hex 0000000c56af; asc V ;;
2: len 7; hex 7b000001ea0fdc; asc { ;;
3: len 4; hex 80000006; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000006; asc ;;
- 例子2:如果将上面session 2的select for update语句换为update语句就不同了如下:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update testsemi30 set d=4 where c=4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
这个语句是可以完成。事务上锁如下:
---TRANSACTION 808627, ACTIVE 4 sec
2 lock struct(s), heap size 1160, 2 row lock(s), undo log entries 1
MySQL thread id 18, OS thread handle 140735862867712, query id 363 localhost root
TABLE LOCK table `test`.`testsemi30` trx id 808627 lock mode IX
RECORD LOCKS space id 9694 page no 3 n bits 72 index PRIMARY of table `test`.`testsemi30` trx id 808627 lock_mode X(LOCK_X) locks rec but not gap(LOCK_REC_NOT_GAP)
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000c56b3; asc V ;;
2: len 7; hex 7e000001da1d79; asc ~ y;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000004; asc ;;
5: len 4; hex 80000004; asc ;;
mysql_update:
if ((!qep_tab.skip_record(thd, &skip_record) && !skip_record)) //跳过操作 是否符合查询条件
table->file->unlock_row(); //如果是where条件过滤的直接跳到解锁这步
对比比较我们可以直接debug整数的比较函数如下:
#0 Item_func_eq::val_int (this=0x7fff2800ad28) at /opt/percona-server-locks-detail-5.7.22/sql/item_cmpfunc.cc:2506
#1 0x0000000000f4a17b in QEP_TAB::skip_record (this=0x7fff9f1cdf78, thd=0x7fff28012cc0, skip_record_arg=0x7fff9f1ce0fe) at /opt/percona-server-locks-detail-5.7.22/sql/sql_executor.h:457
#2 0x0000000001626efa in mysql_update (thd=0x7fff28012cc0, fields=..., values=..., limit=18446744073709551615, handle_duplicates=DUP_ERROR, found_return=0x7fff9f1ce268,
updated_return=0x7fff9f1ce260) at /opt/percona-server-locks-detail-5.7.22/sql/sql_update.cc:816
这个地方可以看到两个比较的值
(gdb) p val1
$12 = 2
(gdb) p val2
$13 = 2
switch (m_prebuilt->row_read_type) {
case ROW_READ_WITH_LOCKS: //如果是加锁了
if (!srv_locks_unsafe_for_binlog //判定隔离级别为RC才做解锁
&& m_prebuilt->trx->isolation_level
> TRX_ISO_READ_COMMITTED) {
break;
}
/* fall through */
case ROW_READ_TRY_SEMI_CONSISTENT://如果semi update,TRY_SEMI才进行解锁
row_unlock_for_mysql(m_prebuilt, FALSE); mysql_update
break;
case ROW_READ_DID_SEMI_CONSISTENT://如果semi update,为DID_SEMI那么就不做了,因为没有锁可以解了,semi update 已经在引擎层解掉了
m_prebuilt->row_read_type = ROW_READ_TRY_SEMI_CONSISTENT;
break;
}
- 每行row lock加锁是不可避免的,但是会在MySQL层判定后解锁,那么最终这个事务加锁的记录就会很少,这会提高业务的并发,这一点是非常重要的,这种情况下show engine 最终看到的row lock 锁信息就很少了。
- 但是频繁的lock/unlock rec导致LOCK_SYS这个mutex很容易成为热点mutex。
- 不能为唯一性扫描(unique_search)
- 必须为主键(index != clust_index)
- 不能产生死锁(Check whether it was a deadlock or not)
- RC隔离级别或者innodb_locks_unsafe_for_binlog参数设置了(8.0移除了本参数)
- update语句才可以
lock_cancel_waiting_and_release
->lock_rec_dequeue_from_page //lock_sys_t中的hash结构会清除,trx_lock中移除
->lock_reset_lock_and_trx_wait //wait_lock设置为NULL

- 对于这个rec_hash这个hash查找表的 hash值来自于space_id和page_no 。
- lock_t是所谓的lock struct,相关的属性比如LOCK_X/LOCK_S,还有LOCK_REC_NOT_GAP/LOCK_GAP/LOCK_WAIT/LOCK_ORDINARY/LOCK_INSERT_INTENTION 等都是它的属性,而不是某行记录的属性。言外之意如果获取一个row lock,如果正常获取就可以合并到现有page的lock_t中,如果堵塞了必须要新建lock_t,因为这个lock_t带有属性LOCK_WAIT。
- 一个lock_t的bit map最多能够容纳下一个page的所有行的加锁情况。
- bit map才是实际的加锁的体现,它附着在每一个lock_t结构上,innodb通过lock_t[1]快速的找到了他的位置,然后进行设置,在函数lock_rec_reset_nth_bit可以看到这种操作如下:
reinterpret_cast<byte*>(&lock[1])
- row locks:trx->lock->n_rec_locks 这个值是trx_lock_t上的一个统计值而已,在每个调用函数lock_rec_reset_nth_bit和lock_rec_set_nth_bit的末尾减少和增加,对应是解锁和加锁某一行操作。
- lock struct: UT_LIST_GET_LEN(trx->lock.trx_locks) 这个值实际上就是上面我们看到的链表的长度,应该来说是比较准确的。
表结构和数据:
mysql> show create table testsemi40 \G
*************************** 1. row ***************************
Table: testsemi40
Create Table: CREATE TABLE `testsemi40` (
`a` int(11) NOT NULL AUTO_INCREMENT,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) NOT NULL,
PRIMARY KEY (`a`),
KEY `b` (`b`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
mysql> select *from testsemi40;
+---+------+------+----+
| a | b | c | d |
+---+------+------+----+
| 2 | 2 | 2 | 0 |
| 4 | 4 | 4 | 0 |
| 6 | 6 | 6 | 0 |
+---+------+------+----+
3 rows in set (0.00 sec)
session 1:
mysql> begin;
Query OK, 0 rows affected (0.10 sec)
mysql> update testsemi40 set d=6 where c=6;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
session2:
mysql> begin;
Query OK, 0 rows affected (0.10 sec)
mysql> update testsemi40 set d=2 where c=2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
show engine信息,session2上锁的信息如下:
---TRANSACTION 808633, ACTIVE 4 sec
2 lock struct(s), heap size 1160, 2 row lock(s), undo log entries 1 (这里有2 row locks)
MySQL thread id 18, OS thread handle 140735862867712, query id 381 localhost root
TABLE LOCK table `test`.`testsemi40` trx id 808633 lock mode IX
RECORD LOCKS space id 9695 page no 3 n bits 72 index PRIMARY of table `test`.`testsemi40` trx id 808633 lock_mode X(LOCK_X) locks rec but not gap(LOCK_REC_NOT_GAP)
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 0000000c56b9; asc V ;;
2: len 7; hex 21000001ec2701; asc ! ' ;;
3: len 4; hex 80000002; asc ;;
4: len 4; hex 80000002; asc ;;
5: len 4; hex 80000002; asc ;;
断点:lock_rec_print
大体输出流程如下:
lock_print_info_all_transactions
循环输出所有的事务的信息
->lock_trx_print_locks
循环输出当前事务的所有lock_t 行锁信息
->lock_rec_print
循环lock_t的位图信息,打印出详细的加锁行
我们只需要在lock_rec_print 函数中通过如下输出
(gdb) p (&lock[1])
$21 = (const ib_lock_t *) 0x2fd79c0
(gdb) x/8bx 0x2fd79c0
0x2fd79c0: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
打印所有的lock_t结构就可以了
(1) INFIMUM record offset:99 heapno:0 n_owned 1,delflag:N minflag:0 rectype:2
(2) normal record offset:126 heapno:2 n_owned 0,delflag:N minflag:0 rectype:0
(3) SUPREMUM record offset:112 heapno:1 n_owned 5,delflag:N minflag:0 rectype:3
void lock_reset_lock_and_trx_wait(lock_t *lock) /*!< in/out: record lock */
{
...
@see trx_lock_t::wait_lock_type for more detailed explanation. */
lock->type_mode &= ~LOCK_WAIT;
ut_ad(lock->trx->lock.n_rec_locks.load() > 0); //增加
lock->trx->lock.n_rec_locks.fetch_sub(1, std::memory_order_relaxed); //增加
---TRANSACTION 2740515, ACTIVE 6 sec
2 lock struct(s), heap size 1200, 1 row lock(s), undo log entries 1 (这里显示正确了)
MySQL thread id 9, OS thread handle 140736352634624, query id 36 localhost root starting
show engine innodb status
---TRANSACTION 2740513, ACTIVE 54 sec
2 lock struct(s), heap size 1200, 1 row lock(s), undo log entries 1
MySQL thread id 8, OS thread handle 140736353167104, query id 21 localhost root
最后
边栏推荐
猜你喜欢
什么是发饰hair accessories?
token系统讲解及过期处理
Review: What is the pre-approval of autumn recruitment?What is an ordinary autumn move?It's all recruitment, why do you need to set these two recruitment time periods?
TCP补充
See how three years of CRUD programmers solve database deadlocks
如何成为团队核心?从写出不可维护的代码开始
KMP Media Group South Africa implemented a DMS (Document Management System) to digitize the process, employees can again focus on their actual tasks, providing efficiency
JS-BOM-阶乘计算
剑指 Offer 66. 构建乘积数组
shell------常用小工具,sort,uniq,tr,cut
随机推荐
浏览器跨域方案,适用于本地调试接口(超简单)
基于ModelArts的StyleGAN3生成高清图丨【华为云至简致远】
【控制】动力学建模举例 --> 拉格朗日法
2022-08-07 第五小组 顾祥全 学习笔记 day31-集合-Map集合
华为云会议初体验【华为云至简致远】
Ubuntu下使用sudo dpkg --configure -a后数据库出现问题
如何成为团队核心?从写出不可维护的代码开始
a += 1 += 1为什么是错的?
mysql 查询一个字段为特定值,并且另一个字段的值出现两次的记录?
Tensorflow and Keras for machine learning, deep learning
shell三剑客-----awk命令
H5不同屏幕大小显示不同的文字大小图片大小
pip install xxx 出现 AttributeError: ‘tuple’ object has no attribute ‘read’ 错误
shell正则表达式,三剑客grep命令
bzoj 3624 [Apio2008]免费道路
Fast DDS 共享内存测试例修改栈空间大小的方式-如改为30M
医学图像数据增强-归一化
Time to update your tech arsenal in 2020: Asgi vs Wsgi (FastAPI vs Flask)
俄驻美大使馆:扎波罗热核电站遭炮击威胁欧洲核安全
超详细的最新版 2022.2 kali 安装步骤及拍摄快照的方法