本文介绍锁和事物
锁分类
从性能上分为乐观锁(用版本对比来实现)和悲观锁
从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响 写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁
从对数据操作的粒度分,分为表锁和行锁
MyISAM 表锁介绍
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
示例表:
1 | DROP TABLE IF EXISTS `mylock`; |
手动增加表锁
1 | lock table 表名称 read(write),表名称2 read(write); |
查看表上加过的锁
1 | show open tables; |
删除表锁
1 | unlock tables; |
案例分析
加读锁:
1 | -- Session1 |
1 | -- Session2 |
当前session和其他session都可以读该表
当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待
加写锁
当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞
案例结论
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改 操作前,会自动给涉及的表加写锁。
1、对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻 赛对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
2、对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有 当写锁释放后,才会执行其它进程的读写操作
总结:
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
事务(Transaction)及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能 A扣了钱,B却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事物隔离级别
MySQL事物隔离级别
事物隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted)也叫脏读 | 是 | 是 | 是 |
读已提交(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read)默认级别 | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
事物的并发问题
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
mysql默认的事务隔离级别为:repeatable-read
1 | select @@tx_isolation; |
隔离级别详述
1 | select @@tx_isolation; ## 查询当前事物隔离等级 |
1 读未提交
打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表test的初始值
在客户端A的事务提交之前,打开另一个客户端B,更新表account:
这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
此时:一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
2 读已提交
打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的初始值:
在客户端A的事务提交之前,打开另一个客户端B,更新表account:
这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
客户端B的事务提交
客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
3 可重复度
打开一个客户端A,并设置当前事务模式为repeatable read,查询表account
在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交
在客户端A执行步骤(1)的查询:
此时:数据一致性问题解决了,接着执行 更新操作
数据结果变成了1000 (查出来的数据是2000 ,减去500后变成了1000.由此可判断之前的2000不是最新数据,产生了数据不准确问题)
4 串行化
打开一个客户端A,并设置当前事务模式为serializable,开启事物,查询表account的初始值:
打开一个客户端B,并设置当前事务模式为serializable,执行更新操作一直等待
MVCC机制详解
可重复读的 隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作 不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本 号,是当前读(当前版本)。
1 | DROP TABLE IF EXISTS `account`; |
问题:select1 和select2 的查询语句一样,为什么结果不一样
说明:
开启五个个session,前三个开启事物,transaction100 , transaction200, transaction300
transaction100 做更新操作:
1
update test set c1='123' where id = 1; -- 只为生成事物id,假设trx_id为100
transaction200 做更新操作:
1
update test set c1='666' where id = 5; -- 只为生成事物id,假设trx_id为200
transaction300 做更新操作:
1
2
3update account set name = 'lilei300' WHERE id = 1;
commit;
-- 生成事物id300, 然后提交事物此时版本链为:
select1 做查询:
1
2
3select name from account where id = 1;
-- 此时会生成read-view max_id = 300 ,未提交事物数组为[100,200],min_id = 100,;
-- 根据版本链比对规则trx_id=300的是可见的,则结果为”lilei300“
做更新操作:
1
2
3update account set name = 'lilei1' WHERE id = 1;
update account set name = 'lilei2' WHERE id = 1;
-- 连续做更新操作此时版本链为:
select1 做查询:
1
2
3
4select name from account where id = 1;
-- 此时的read-view会沿用第一次生成的read-view
-- max_id = 300 ,未提交事物数组为[100,200],min_id = 100,;
-- 根据版本链比对规则trx_id=300的是可见的,则结果为”lilei300“
transaction100提交事物,transaction200做更新操作:
1
2
3commit; -- transaction100提交事物
update account set name = 'lilei3' WHERE id = 1;
update account set name = 'lilei4' WHERE id = 1;此时版本链为:
select1 做查询:
1
2
3
4select name from account where id = 1;
-- 此时的read-view还会沿用第一次生成的read-view
-- max_id = 300 ,未提交事物数组为[100,200],min_id = 100,;
-- 根据版本链比对规则trx_id=300的是可见的,则结果为”lilei300“
Select2 做查询:
1
2
3
4select name from account where id = 1; --readview:[200],300 lilei2
-- 此时会生成新的read-view
-- max_id = 300 ,未提交事物数组为[200],min_id = 200,;
-- 根据版本链比对规则遍历:trx_id=100 在已提交事物范围内,可见则结果为”lilei2“
注意:
- begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB 表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的
- 删除记录,会在undo日志版本链对应记录里面标记删除
mysql 注意事项
SQL规范所规定的标准,不同的数据库具体的实现可能会有些差异
mysql中默认事务隔离级别是可重复读时并不会锁住读取到的行
事务隔离级别为读提交时,写数据只会锁住相应的行
无索引行锁会升级为表锁
1 | 锁主要是加在索引上,如果对非索引字段更新, 行锁可能会变表锁 |
事务隔离级别为串行化时,读写数据都会锁住整张表
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得啊。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
默认级别情况下间隙锁可以解决幻读
1 | 要避免幻读可以用间隙锁在Session_1下面执行update account set name ='zhuge' where id > 10 and id <=20;,则其他Session没法在这个范围所包含的间隙里插入或修改任何数据 |
行锁分析
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
1 | 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:系统启动后到现在总共等待的次数
对于这5个状态变量,比较重要的主要是:
1 | Innodb_row_lock_time_avg (等待平均时长) |
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
优化建议
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 尽可能减少检索条件范围,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql 尽量放在事务最后执行
- 尽可能低级别事务隔离