事务
事务数据库区别于文件系统的重要特性之一。在文件系统中,如果正在写文件,但是操作系统突然崩溃,这个文件很可能被破坏。
事务的四大特性
ACID
- 原子性 (Atomicity) :事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
- 一致性 (Consistency) :一致性指事务将数据库从一种状态转变成下一种一致的状态。在事务开始之前和事务结束之后,数据库的完整性约束没有被破坏。
- 隔离性 (Isolation) :事务与事务直接相互隔离,防止数据损坏。由锁来实现的。
- 持久性 (Durability) :一旦事务完成,其结果就是永久性的。无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
并发问题
脏读
B 事务看到了 A 事务中没有提交的数据,然后 A 事务回滚了,B 事务再次读数据时数据看起来被修改了。针对未提交数据。
如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以“看到“该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所”看到“的数据就是一笔脏数据。(脏读又称无效数据的读出,是指在数据库访问中,事务 T1 将某一值修改,然后事务 T2 读取该值,此后 T1 因为某种原因撤销对该值的修改,这就导致了 T2 所读取到的数据是无效的,值得注意的是,脏读一般是针对于 update 操作的)
幻读
同一个事务内读到了不同的数据。
当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行; (读取后又插入了一条新数据)
不可重复读
针对其他提交前后,读取数据本身的对比
不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同如果事务 1 在事务 2 的更新操作之前读取一次数据,在事务 2 的更新操作之后再读取同一笔数据一次,两次结果是不同的。
针对其他提交前后,读取数据条数的对比
幻读问题对应的是插入 INSERT 操作,而不是 UPDATE 操作。
事务的隔离级别
Read unncommitted
读未提交Read committed
读已提交repeatable read
重复读Serializable
序列化
Read uncommitted
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
事例:老板要给程序员发工资,程序员的工资是 3.6 万/月。但是发工资时老板不小心按错了数字,按成 3.9 万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了 3 千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成 3.6 万再提交。
分析:实际程序员这个月的工资还是 3.6 万,但是程序员看到的是 3.9 万。他看到的是老板还没提交事务时的数据。这就是脏读。
那怎么解决脏读呢?Read committed!读提交,能解决脏读问题。
Read committed
读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
事例:程序员拿着信用卡去享受生活 (卡里当然是只有 3.6 万) ,当他埋单时 (程序员事务开启) ,收费系统事先检测到他的卡里有 3.6 万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了 (第二次检测金额当然要等待妻子转出金额事务提交完) 。程序员就会很郁闷,明明卡里是有钱的…
分析:这就是读提交,若有事务对数据进行更新 (UPDATE) 操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。
那怎么解决可能的不可重复读问题?Repeatable read !
Repeatable read
Repeatable read
重复读,就是在开始读取数据 (事务开启) 时,不再允许修改操作【但是可以插入?】
事例:程序员拿着信用卡去享受生活 (卡里当然是只有 3.6 万) ,当他埋单时 (事务开启,不允许其他事务的 UPDATE 修改操作) ,收费系统事先检测到他的卡里有 3.6 万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即 UPDATE 操作。但是可能还会有幻读问题。因为幻读问题对应的是插入 INSERT 操作,而不是 UPDATE 操作。
什么时候会出现幻读?
事例:程序员某一天去消费,花了 2 千元,然后他的妻子去查看他今天的消费记录 (全表扫描 FTS
,妻子事务开启) ,看到确实是花了 2 千元,就在这个时候,程序员花了 1 万买了一部电脑,即新增 INSERT 了一条消费记录,并提交。当妻子打印程序员的消费记录清单时 (妻子事务提交) ,发现花了 1.2 万元,似乎出现了幻觉,这就是幻读。
那怎么解决幻读问题?Serializable
!
Serializable 序列化
Serializable
是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
InnoDB
InnoDB
存储引擎完全符合 ACID 四大特性。
隔离性 (Isolation) 是通过锁来实现的。
原子性 (Atomicity) 、一致性 (Consistency) 、持久性 (Durability) 通过 redo log 和 undo log 来实现的。
事务分类
- 扁平事务 (Flat Transactions)
- 带保存点的扁平事务 (Flat Transactions with
Savepoints
) - 链事务 (Chained Transactions)
- 嵌套事务 (Nested Transactions)
- 分布式事务 (Distributed Transactions)
扁平事务
最简单,最频繁
- 其间操作为原子的,要么都执行,要么都回滚。
- 扁平事务的限制是不能提交或者回滚事务的某一部分。
- 如果回滚事务代价太大,扁平事务就显得不合适,故出现了带保存点的扁平事务。
带保存点的扁平事务
允许在事务执行过程中回滚到同一事务中较早的一个状态。
放弃整个事务不合乎要求,开销太大。保存点用来通知系统应该记住事务当前的状态,以便当发生错误时,事务能回到保存点当时的状态。【其他细节,我这个菜鸡就不记了。】
链事务
保存点模式的一种变种。
嵌套事务
任何事务在顶层事务提交后才提交
任意一个事务的回滚会引起子事务的回滚 (故子事务无持久性)
分布式事务
通常是在一个分布式环境下运行的扁平事务。
事务的实现
原子性 (Atomicity) 、一致性 (Consistency) 、持久性 (Durability) 通过 redo log 和 undo log 来实现的。
redo log
redo log 有什么作用?
mysql
为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到 Boffer Pool (缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步。
那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及执行上面图中红色的操作。这样会导致丢部分已提交事务的修改信息!
所以引入了 redo log 来记录已成功提交事务的修改信息,并且会把 redo log 持久化到磁盘,系统重启之后在读取 redo log 恢复最新数据。
总结:redo log 是用来恢复数据的,用于保障已提交事务的持久化特性。
undo log
undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log 主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者 rollback 操作而回滚的话可以根据 undo log 的信息来进行回滚到没被修改前的状态。
总结:undo log 是用来回滚数据的用于保障 未提交事务的原子性。
基础事务代码
JDBC
事务控制需要保证事务前后拿到的 Connection 是同一个,这样事务的开启关闭才有效。
一个方法中调用了 2 个不同的方法。如:转账操作,transfer 操作中的两个方法都是在同一个线程中执行的。
开启事务,回滚事务获取的是当前线程绑定的连接池;转出,转入操作获取的也是当前线程绑定的连接池;
/**
* 转入转出操作都是在同一个线程中进行的,所以可以用ThreadLocal获取为当前线程绑定的连接池。
* @throws SQLException
*/
public void transfer() throws SQLException {
ThreadLocal<Connection> local = new ThreadLocal<>();
Connection connection = local.get();
connection.setAutoCommit(false);
try {
out();
in();
} catch (Exception e) {
// 出错就回滚
connection.rollback();
} finally {
// 提交
connection.commit();
}
}
public void out() {
// 转出
ThreadLocal<Connection> local = new ThreadLocal<>();
Connection connection = local.get();
// 执行操作
}
public void in() {
// 转入
ThreadLocal<Connection> local = new ThreadLocal<>();
Connection connection = local.get();
// 执行操作
}