数据库并发事务
数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。为了解决这些并发事务的问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题。
事务特性:ACID
- 原子性(Atomicity):
- 一致性(Consistent):
- 隔离性(Isolation):
- 持久性(Durable):
事务并发可能带来的问题
- 丢失更新或者脏写:两个事务同时更新一个数据,后提交的事务会把先提交的结果
- 脏读:A事务读到了B事务未提交的数据。 如果B事务回滚,A则读到了一个无效数据。
- 不可重复读:在一个事务中连续对同一数据读多次,读到的结果不一致。(其他事务做了修改)
- 幻读:在一个事务中,相同的条件多次统计记录条数,个数不一致。(其他事务新增了满足其条件的数据)
事务隔离级别
-- 看当前数据库的事务隔离级别
show variables like 'tx_isolation';
-- 设置事务隔离级别
set tx_isolation='REPEATABLE-READ';
Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别。
- 读未提交:
- 读已提交(RC):
- 可重复读(RR):使用了MVCC机制, select是读的事务开始时的快照版,update则是当前读,读的是数据库中最新的值。
- 串行化:
在一个事务中, 读到的内容都是事务开始时的一个快照, 即读取的内容不变; 但如果当前事务对一个记录做修改(排他锁,其他事务无法对这个记录做修改), 修改的记录再重新读取,读到的是最新的。
问题:隔离级别底层是怎么实现的呢?
- 乐观锁不要用在可重复读级别, 要在更小的两个隔离级别用
- 通过快照读取,但并不是每个事务都创建一个对应的快照
-
RC对并发要求高;RR对同一时间维度要求高
锁
间隙锁:锁的是两个值之间的空隙。
临键锁(Next-key Locks):行锁和间隙锁的组合,包含范围的边界。
乐观锁:java中用版本号实现。update tableA set amount = 200 where id = 1 and version=1;
悲观锁:java在sql中直接执行。update tableA set amount = amount + 500 where id=1
页锁:只有BDB存储引擎支持叶锁,锁的资源比行锁多,比表锁少。考虑索引树种的一个节点,就是对应一页,相对于对这一页加锁。
读/写锁/意向锁
-
-- 手动增加表锁 lock table 表名称 read(write), 表名称2 read(write); -- 查看表上加过的锁 show open tables; -- 删除表锁 unlock tables; -- 共享锁/读锁 select * from T where id=1 lock in share mode -- 排他锁/写锁 select * from T where id=1 for update
- 读锁:其他事务可读,不可写(阻塞); 当前事务可读,不可写
- 写锁:其他事务不可读,不可写(阻塞);当前事务可读,可写
-
意向锁(Intention Lock):当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有行锁了,其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁。这个标识就是意向锁。相当于对表加锁。 分为意向共享锁(IS)和意向排他锁(IX)
行/表锁
行锁添加方式
1. 更新操作where条件使用索引列
>update account set balance = 800 where name ='lilei';
2. lock in share mode(共享锁)
>select * from T where id = 1 lock in share mode;
3. 排他锁for update
>select * from test_innodb_lock where a=2 for update
无索引行锁会升级为表锁
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁。
session1 执行:update account set balance = 800 where name ='lilei';
session2 对该表任一行操作都会阻塞住
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。注意:并不是直接将整张表加上表锁,因为有的记录可能被其他事务锁住。 (那这种情况是否能加锁成功呢?)
RR级别会升级为表锁,RC级别不会升级为表锁。(为什么?)
行锁竞争分析
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:系统启动后到现在总共等待的次数
对于这几个状态变量,比较重要的主要是:
Innodb_row_lock_time_avg (等待平均时长)
Innodb_row_lock_waits (等待总次数)
Innodb_row_lock_time(等待总时长)
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
InnoDB与MYISAM的最大不同
- InnoDB支持事务(TRANSACTION)
- InnoDB支持行级锁
问题:为啥MyISAM不支持行级锁?
- MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
- InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
锁相关几个问题:
1、Mysql默认的隔离级别是可重复读,有办法可以解决幻读吗?
答:间隙锁在某些情况下可以解决幻读
那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,
在Session_1下面执行 update account set name ='zhuge' where id > 8 and id <18;,则其他Session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在(3,20]区间都无法修改数据,注意最后那个20也是包含在内的。(3,20]整个区间可以叫做临键锁。
间隙锁是在可重复读隔离级别下才会生效。间隙锁是为了解决幻读问题,所以作用在可重复读级别。实际测试下来确实可可重复读级别间隙锁才生效。
2、多个查询操作,是否要放在事务中?
答:这个问题主要是考察事务并发问题, 以及解决这些问题需要的隔离级别。
如果要保证读取数据的一致性,那必须放在事务中, 且这个事务是可重复读的隔离级别。
3、事务中,有读和写操作, 哪个放前?
答:更新等涉及到加锁的操作尽可能放在事务靠后的位置。目的是减少锁住的时间。
锁优化
- 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 尽可能减少检索条件范围,避免间隙锁
- 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
- 尽可能用低的事务隔离级别
MVCC多版本并发控制
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
undo日志版本链
指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。
readview和可见性算法
readview和可见性算法其实就是记录了sql查询那个时刻数据库里提交和未提交所有事务的状态。
RR隔离级别,事务里每次执行查询操作readview都是使用第一次查询时生成的readview。保障事务内对同一个数据的查询查的结果都一致。
RC隔离级别,事务里每次执行查询操作readview都会按照数据库当前状态重新生成readview.保障读取到其他事务提交的最新的结果。
事务开始时间
begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作或加排它锁操作(比如select...for update)的语句,事务才真正启动,才会向mysql申请真正的事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。