【0】README outlines are as follows :
- 行锁;
- 事务;
- 隔离级别;
- 行锁变表锁;
- 间隙锁;
- 如何锁定一行;
- 行锁总结;
【1】行锁+事务+存储引擎基础
1、行锁: 偏向于 innodb 存储引擎,开销大,加锁慢,会出现死锁; 锁定粒度最小,发送锁冲突的概率最低, 并发度最高;
2、innodb 与 myisam 的两点区别:
- 区别1:innodb 支持事务;
- 区别2: innodb 采用了 行级锁;
补充0、事务定义:
- 事务就是一组原子性的sql查询,或者说一个独立的工作单元。即事务内的sql语句,要么全部执行成功,要么全部执行失败;
补充1:事务及其acid属性;事务的ACID概念:原子性automicity,一致性consistency,隔离性isolation,持久性durability;
- 原子性:一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚;
- 一致性;数据库总是从一个一致性状态转移到另一个一致性状态;
- 隔离性:通常来说,一个事务所做的修改在最终提交前,对其他事务都是不可见的;
- 持久性:一旦事务提交,则其所做的修改就会永久保存到数据库中;此时即使数据库崩溃,数据也不会丢失;
补充2:并发事务带来的问题:
- 更新丢失, lost update; 最后的事务更新覆盖了其他事务所做的更新;
- 脏读, dirty read;事务A读取到了事务B已修改但尚未提交的数据;
- 不可重复读, non-repeatable read;事务A读取到了事务B已经提交的数据,不符合隔离性;
- 幻读, phantom read;事务A读取到了事务B提交的新增数据;
幻读和脏读的区别:
- 脏读是事务A读取到了事务B里面修改了数据;
- 幻读是事务A读取到了事务B里面新增了数据;
补充3:事务隔离级别; 脏读,不可重复读,幻读,其实都是数据一致性问题,必须由数据库提供一定的事务隔离机制来了解决;
mysql的4种隔离级别
- 级别1)READ UNCOMMITED 未提交读:事务的修改,即使没有提交,对其他事务也是可见的;
- 级别2)READ COMMITTED 提交读:一个事务从开始直到提交之前,所做的任何修改对其他事务不可见的;
- 也叫不可重复读,因为两次执行相同的查询,可能得到不同的查询结果;
- 级别3)REPEATABLE READ 可重复读:RR 解决了脏读问题。该级别保证在同一事务中多次读取同样记录的结果是一致的 ;(mysql的默认事务隔离级别) 但 RR 无法解决幻读问题,幻读指的是当某个事物在读取某个范围内的记录时,另外一个事务又在该范围内插入了一条新的记录,当之前的事务再次读取到该范围的记录时,会产生幻行; 不过 mysql中的 innodb 和 XtraDB 存储引擎通过多版本并发控制 MVVC(multiversion concurrency control) 解决了幻读问题;
- 级别4)SERIALIZABLE 可串行化:最高隔离级别。通过强制事务串行执行,避免了前面说的幻读问题。简单说,SERIALIZABLE 会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁征用的问题;实际应用中很少考虑这种隔离级别;
补充说明:数据库的事务隔离级别越严格,并发副作用越小,但付出的代价也越大。因为事务隔离级别实质上就是使事务在一定程度上串行化进行,这显然与并发是矛盾的。 同时,不同的应用对读一致性和事务隔离程度的要求也是不同的。如许多应用对不可重复读和幻读不care, 但关心数据并发访问的能力;
查看当前数据库的事务隔离级别: show variables like 'tx_isolation' ;
=======================================================================================
【2】行锁荔枝
2.0)造数
-- 造数据:
use mybatis;
-- 行锁荔枝
drop table if exists test_innodb_lock_tbl;
create table test_innodb_lock_tbl (`rcrd_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '记录编号',`a` int NOT NULL default 0 COMMENT '列a',`b` varchar(20) NOT NULL default '' COMMENT '列b',PRIMARY KEY (`rcrd_id`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8 COMMENT='innodb行锁测试表';-- 插入数据
-- 新建存储过程-调用函数批量插入数据
drop procedure if exists insert_test_innodb_lock_tbl;
delimiter ##
create procedure insert_test_innodb_lock_tbl(in start_num int, in max_num int)
begin declare i int default 0;set autocommit=0;repeat set i=i+1;INSERT INTO mybatis.test_innodb_lock_tbl (a, b)VALUES(rand_num(), rand_str(10));until i = max_numend repeat;commit;
end ##
delimiter ;call insert_test_innodb_lock_tbl(0, 10)
;-- 建立索引
alter table test_innodb_lock_tbl
add key `idx_a` (`a`) comment '索引a'
;
alter table test_innodb_lock_tbl
add key `idx_b` (`b`) comment '索引b'
;
-- 查看索引
show index from test_innodb_lock_tbl;
-- 查询数据
select * from test_innodb_lock_tbl;
2.1)行锁基本演示:
补充: 一旦会话1更新数据后且提交事务后, 会话2等待锁释放的线程结束阻塞状态,立即执行更新操作并更新成功;
2.2)会话1操作具体步骤:
2.3)会话2操作具体步骤:
============================================================================
【补充荔枝】 会话1和会话2 各自更新不同的行,则两个会话间没有任何影响; 不存在说 会话1在没有提交事务前阻塞会话2的情况;
【注意】索引失效行锁变表锁, 当索引使用不当时, 行锁会升级为表锁;
当varchar 类型 作为where子句的查询条件时, 如果没有写单引号,则行锁升级为表锁;
但是在mysql8中,如果没有写单引号,则sql 执行报错。
============================================================================
【3】间隙锁
1)什么是间隙锁?
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时, innodb 会给符合条件的已有数据记录的索引项加锁, 对于键值在条件范围内但并不存在的记录,叫做间隙-GAP;
innodb 也会对这个间隙加锁,这种锁机制就是所谓的间隙锁;(next-key锁);
会话1:
会话2:
2)间隙锁的危害:
- 危害1)因为query执行过程中通过范围查找的话, 它会锁定整个范围内所有的索引键值,即使这个键值不存在;
- 危害2)间隙锁有一个比较致命的弱点,就是当锁定一个范围键值后,即使某些不存在的键值也会被无辜的锁定, 而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大危害;
============================================================================
【4】如何锁定一行?
0)执行如下语句,就可以锁定行(在查询语句最后加上 for update 即可);
select * from test_innodb_lock_tbl where a=101 for update;
1)会话1:
mysql> select * from test_innodb_lock_tbl order by a;
+---------+-----+------------+
| rcrd_id | a | b |
+---------+-----+------------+
| 107 | 101 | pkdOHXNXzD |
| 111 | 102 | 2000 |
| 103 | 103 | 181125 |
| 108 | 104 | 181125 |
| 106 | 105 | 181125 |
| 109 | 106 | IsNODBWgNx |
| 105 | 107 | xaMIfDzMnd |
| 102 | 108 | zhou222 |
| 104 | 109 | tZPdVseqrN |
| 110 | 109 | vkNjJSlDhd |
+---------+-----+------------+
10 rows in set (0.00 sec)mysql>
mysql>
mysql> begin;
Query OK, 0 rows affected (0.00 sec)mysql> select * from test_innodb_lock where a=8 fro update;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'fro update' at line 1
mysql>
mysql> select * from test_innodb_lock where a=101 for update;
ERROR 1146 (42S02): Table 'mybatis.test_innodb_lock' doesn't exist
mysql>
mysql> select * from test_innodb_lock_tbl where a=101 for update;
+---------+-----+------------+
| rcrd_id | a | b |
+---------+-----+------------+
| 107 | 101 | pkdOHXNXzD |
+---------+-----+------------+
1 row in set (0.00 sec)mysql> commit;
Query OK, 0 rows affected (0.00 sec)
2)会话2:
mysql> update test_innodb_lock_tbl set b='xxx' where a=101;
Query OK, 1 row affected (22.25 sec)
Rows matched: 1 Changed: 1 Warnings: 0
============================================================================
【5】行锁总结
1)innodb存储引擎:由于实现了行级锁定,虽然在锁机制的实现方面所带来的性能损耗高于表级锁, 但整体上的并发处理能力要远远高于 myisam的表级锁定;当系统并发量较高时,innodb的整体性能和myisam相比就会有比较明显的优势了;
2)但是, innodb的行级锁同样也有其脆弱的一面,当我们使用不当的时候,可能会让 innodb的整体性能表现不仅不能比 myisam高,甚至会更差;
3)如何分析行锁定
3.1)通过检查 innodb_row_lock 状态变量来分析系统上的行锁的争夺情况;
show status like 'innodb_row_lock%';
状态说明如下:
- Innodb_row_lock_current_waits:当前正在等待锁定的数量;
- Innodb_row_lock_time:从系统启动到现在锁定总时间长度;(等待总时长-比较重要)
- Innodb_row_lock_time_avg:每次等待锁花费的平均时间; (等待时长均值-比较重要)
- Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花费的时间;
- Innodb_row_lock_waits:系统启动后到现在总共等待的次数;(等待总次数-比较重要)
【注意】 当等待次数很高,其每次等待时长不小的时候,我们就需要分析系统中为什么会有如此多的等待;然后根据分析结果着手指定优化计划(show profile);
4)优化建议:
- 建议1)尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁;(如varchar类型在where子句中不加单引号‘’)
- 建议2)合理设计索引,尽量缩小锁的范围;
- 建议3)尽可能少的检索条件,避免间隙锁;
- 建议4)尽量控制事务大小,减少锁定资源量和时间长度;
- 建议5)尽可能低级别事务隔离;