事务
定义: 比如ABCD四个业务,作为一个事务,他们要么一起都执行完毕,要么都不执行。(只要有一个不成功,那么所有的都不可以成功)
四个特性
ACID
原子性(Atomicity)
整个事务中的所有操作,要么全都完成,要么全部不完成。
事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态。
一致性(Consistency)
事务必须保持系统处于一致的状态,不管在任何时间并发事务有多少。
比如转账,ABCDE五个人每个人初始状态 有 100元,总额是500元。 互相转账的操作下,
要保持住整体系统的总额依然是500元。
隔离性(Isolation)
隔离状态进行事务操作,使他们在给定的时间内 像只有自己在操作一样。
如果有两个事务,在同一时间内,执行相同的操作,事务的隔离性要求 保证每一个事务在系统中认为只有
该事务在使用系统。
这种属性有时候叫串行化, 为了防止事务操作间的混淆,必须串行化或者序列化请求,使得同一时间仅有一个请求请求于同一数据
持久性(Durability)
由于一项操作通常会包含许多子操作,而这些子操作可能会因为硬件的损坏或其他因素产生问题,要正确实现ACID并不容易。ACID建议数据库将所有需要更新以及修改的资料一次操作完毕,但实际上并不可行。
目前主要有两种方式实现ACID:第一种是Write ahead logging,也就是日志式的方式(现代数据库均基于这种方式)。第二种是Shadow paging。
相对于WAL(write ahead logging)技术,shadow paging技术实现起来比较简单,消除了写日志记录的开销, 恢复的速度也快(不需要redo和undo)。shadow paging的缺点就是事务提交时要输出多个块,这使得提交的开销很大,而且以块为单位,很难应用到允许多个事务并发执行的情况——这是它致命的缺点。
WAL 的中心思想是对数据文件 的修改(它们是表和索引的载体)必须是只能发生在这些修改已经 记录了日志之后 -- 也就是说,在日志记录冲刷到永久存储器之后. 如果我们遵循这个过程,那么我们就不需要在每次事务提交的时候 都把数据页冲刷到磁盘,因为我们知道在出现崩溃的情况下, 我们可以用日志来恢复数据库:任何尚未附加到数据页的记录 都将先从日志记录中重做(这叫向前滚动恢复,也叫做 REDO) 然后那些未提交的事务做的修改将被从数据页中删除 (这叫向后滚动恢复 - UNDO)。
什么是WAL
"In computer science, write-ahead logging (WAL) is a family of techniques for providing atomicity and durability (two of the ACID properties) in database systems."——维基百科
在计算机领域,WAL(Write-ahead logging,预写式日志)是数据库系统提供原子性和持久化的一系列技术。
在使用WAL的系统中,所有的修改都先被写入到日志中,然后再被应用到系统状态中。通常包含redo和undo两部分信息。
为什么需要使用WAL,然后包含redo和undo信息呢?举个例子,如果一个系统直接将变更应用到系统状态中,那么在机器掉电重启之后系统需要知道操作是成功了,还是只有部分成功或者是失败了(为了恢复状态)。如果使用了WAL,那么在重启之后系统可以通过比较日志和系统状态来决定是继续完成操作还是撤销操作。
redo log
redo log称为重做日志,每当有操作时,在数据变更之前将操作写入redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。
undo log
undo log称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。
MySQL中用redo log来在系统Crash重启之类的情况时修复数据(事务的持久性),而undo log来保证事务的原子性。
隔离问题
脏读
一个事务读到另一个事务未提交的数据
不可重复读
一个事务读到另一个事务已经提交的数据。
是指:一个事务范围内,做了俩次相同的查询,却返回了不同的结果。
起因: 这是由于一个事务在查询时,系统中另一个事务修改的提交而引起的。
例子: 比如事务T1 读取某一数据,这个时候事务T2读取并修改了这一数据,T1为了读取
值然后进行校验,而再次读取该数据,就会得到不同的结果。
幻读
幻读是指一个事务不是独立执行时发生的情况(像是受到了别的事务的干扰)
例子:
背景: 事务A读取与搜索条件匹配的若干行,事务B以插入或删除的方式来修改事务A搜索到的结果集,
然后提交。
情形: 事务A对一个表中的数据做了修改,然后与此同时,这个事务B也对这个结果集刚好做了insert操作,
添加了一行新数据, 那么事务A还没提交之前,发现多了一行数据没有修改,就好像产生了幻觉一样.
解决方法: 一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
在数据库定义的四种隔离级别中
最高隔离级别SERIALIZABLE_READ可以保证不出现幻读的问题。
Repeatable Read (RR)
隔离界别
MySQL InnoDB事务的隔离级别有四级,默认是“可重复读”(REPEATABLE READ)。
- 未提交读(READ UNCOMMITTED)。另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据(脏读)。
- 提交读(READ COMMITTED)。本事务读取到的是最新的数据(其他事务提交后的)。问题是,在同一个事务里,前后两次相同的SELECT会读到不同的结果(不重复读)。
- 可重复读(REPEATABLE READ)。在同一个事务里,SELECT的结果是事务开始时时间点的状态,因此,同样的SELECT操作读到的结果会是一致的。但是,会有幻读现象(稍后解释)。
- 串行化(SERIALIZABLE)。读操作会隐式获取共享锁,可以保证不同事务间的互斥。
测试Mysql的隔离级别的幻读
创建表和检查引擎
mysql> show create table t_bitflyG;
CREATE TABLE `t_bitfly` (
`id` bigint(20) NOT NULL default '0',
`value` varchar(32) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk
mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+-----------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+-----------------+
开启两个mysql窗口,模拟两个事务A,B
情景:模拟由于事务B在事务A执行中commit了一条新增记录的操作
t Session A Session B
|
| START TRANSACTION; |START TRANSACTION;
| |
| SELECT * FROM t_bitfly; |
| empty set |
| | INSERT INTO t_bitfly
| | VALUES (1, 'a');
|
| SELECT * FROM t_bitfly; |
| empty set |
| | COMMIT;
| |
| SELECT * FROM t_bitfly;
| empty set
|
| INSERT INTO t_bitfly VALUES (1, 'a');
| ERROR 1062 (23000):
| Duplicate entry '1' for key 1
(shit, 刚刚明明告诉我这没有这条记录的)
结果如下:
实验证明:RR级别确实解决了不可重复读的隔离问题,因为最后一次读取的时候还是没有读取到事务B
提交后的新纪录。
但是自己插入(或更新)时都不成功。
mysql对于RR级别能解决幻读的官方解释:
http://dev.mysql.com/doc/refman/5.0/en/innodb-record-level-locks.html
By default, InnoDB operates in REPEATABLE READ transaction isolation level and with the innodb_locks_unsafe_for_binlog system variable disabled. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows (see Section 13.6.8.5, “Avoiding the Phantom Problem Using Next-Key Locking”).
意思是:可以通过 Next-Key Locking来解决幻读
关键点在于,是InnoDB默认对一个普通的查询也会加next-key locks,还是说需要应用自己来加锁呢?如果单看这一句,可能会以为InnoDB对普通的查询也加了锁,如果是,那和序列化(SERIALIZABLE)的区别又在哪里呢?
MySQL manual里还有一段:
13.2.8.5. Avoiding the Phantom Problem Using Next-Key Locking (http://dev.mysql.com/doc/refman/5.0/en/innodb-next-key-locking.html)
To prevent phantoms,
InnoDB
uses an algorithm called next-key locking that combines index-row locking with gap locking.You can use next-key locking to implement a uniqueness check in your application: If you read your data in share mode and do not see a duplicate for a row you are going to insert, then you can safely insert your row and know that the next-key lock set on the successor of your row during the read prevents anyone meanwhile inserting a duplicate for your row. Thus, the next-key locking enables you to “lock” the nonexistence of something in your table.
我的理解是说,InnoDB提供了next-key locks,但需要应用程序自己去加锁。manual里提供一个例子:
SELECT * FROM child WHERE id > 100 FOR UPDATE;
这样,InnoDB会给id大于100的行(假如child表里有一行id为102),以及100-102,102+的gap都加上锁。
可以使用show innodb status来查看是否给表加上了锁。
事务A 事务B
start transaction; start transaction
select *from t_bitfly where id<=1 for update;
insert into t_bitfly values(2,'b');
Query OK, 1 row affected (0.36 sec)
//成功了! 这里证明如果事务A加锁的结果集范围不包含
事务B想要插入的key的范围的话(2不在id<2的范围),
则可以成 功执行
insert into t_bitfly values(0,'z');
Lock wait timeout exceeded; try restarting transaction
//意味着事务A不提交的时候,事务B因为范围锁永远无法
执行。
其他评论:
·1、第一个例子,事务b提交以后,事务a没有读取到(没有出现幻读),至于插入失败,是因为主键不唯一,这个就算是可见也一定不会成功的。
·2、第二个例子,查询并没有幻读,但是update之后出现了多余的数据,是因为update的时候,是会更新next-key的版本号的,如果update加入条件,只更新查询出来的id为1的数据,后续查询,还是查不到另外一条的(没有幻读,update更新了版本号,所以查询出来的数据是合法的)
后面的例子是你加锁的例子,没有问题。但是这样会大大的消耗了性能,其实你做的是SERIALIZABLE做的事情。
还有一点,你可能对next-key locks的理解有些偏差,所谓next-key locks并不是真的加锁,只是通过版本号,做了数据隔离,而版本号(当前版本,删除版本两个)是mysql的innodb自己维护的隐藏列。这种隔离是对查询的隔离,更新删除还有插入,都有自己的版本号维护,来保证查询的正确性。
mysql的savepoint实现可选择性的回滚
ABCD 一个事务
Connection conn = null;
try{
//1 获得连接
conn = ...;
//2 开启事务
conn.setAutoCommit(false);
A
B
C
D
//3 提交事务
conn.commit();
} catche(){
//4 回滚事务
conn.rollback();
}
AB(必选),CD(可选)
场景举例:比如AB是转账操作,CD是银行发短信的操作
Connection conn = null;
Savepoint savepoint = null; //保存点,记录操作的当前位置,之后可以回滚到指定的位置。(可以回滚一部分)
try{
//1 获得连接
conn = ...;
//2 开启事务
conn.setAutoCommit(false);
A
B
savepoint = conn.setSavepoint();
C
D
//3 提交事务
conn.commit();
} catche(){
if(savepoint != null){ //CD异常
// 回滚到CD之前
conn.rollback(savepoint);
// 提交AB
conn.commit();
} else{ //AB异常
// 回滚AB
conn.rollback();
}
}