事务回顾
什么是事务?
事务指的是逻辑上的一组操作,这组操作要么全部成功,要么全部失败。
经典案例
张三账户有1000元,李四账户也有1000元。此时,张三要给李四转账200元,这个操作包括如下步骤:
① 修改张三账户,扣除200元
② 修改李四账户,增加200元
只有以上这两步都完成了,才算转账成功。任何一步失败都是不行的,也就是说①和②是存在一个事务中,并且必须满足事务的一系列特性。
事务的特性(ACID)
① 原子性(Atomicity):指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
以上转账步骤①和②要么一起成功,要么一起失败。
② 一致性(Consistency):指事务前后数据的完整性必须保持一致。
张三给李四转账,但是他俩的账户总额保持不变,不能出现张三账户减少200元,李四账户没增加200元的情况。
③ 隔离性(Isolation):指多个用户并发访问数据库时,一个用户的事务不能被其他用户的事务所干扰,多个并发事务之间数据要相互隔离。
张三的账户在转账的同时,另一个事务也在操作张三的账户,导致数据出现重复操作的情况。
④ 持久性(Durability):指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,即使数据库发生故障也不应该对其有任何影响。
张三给李四转账完成后,这个结果是永久性的,也就是说张三的账户实实在在的减少了200元,而李四的账户实实在在的增加了200元。
事务所面临的问题
ACID这4大特性当中,其实最难理解的是隔离性。正因为它的难理解,才有了所谓的事务隔离级别。没有隔离性,在高并发下就会产生如下问题:
① 脏读:有两个或多个事务同时修改张三的账户,这样就会产生垃圾数据,导致最终的结果跟实际不符。
时间 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | — |
T2 | — | 开始事务 |
T3 | — | 查询余额(1000 元) |
T4 | — | 取出1000元(余额 0 元) |
T5 | 查询余额(0 元) | — |
T6 | — | 撤销事务(余额恢复为 1000 元) |
T7 | 存入500元(余额 500 元) | — |
T8 | 提交事务 | — |
事务A可以理解为:张三正在向李四转账 500 元
事务B可以理解为:李四自己正在取款 1000 元
这两个操作刚好在同一时段进行:
T1-T4:李四取款时,程序代码肯定是先查询李四的账户余额发现有1000元,然后在1000的基础上减去1000,最终李四账户余额为0元。
T5:此时,正好张三在向李四转账500元,程序代码肯定也是先查询李四的账户,发现余额为0元。
T6:李四在取款过程中出现异常回滚,余额重新恢复到1000元。
T7:张三查询到李四账户余额为0后,程序代码直接执行了0+500=500的操作,然后提交事务,最终得出李四的账户余额为500元。
真实的结果应该是:李四的账户余额为1500元,事务B在回滚时,最新的余额没有通知到事务A,这就导致了事务A在T5时间点读出的数据为脏数据。
② 不可重复读:李四第一次查询自己的账户余额为1000元,然后又重新查询了一次,结果变成了0元。钱莫名其妙的就少了1000元,这让李四非常的郁闷。
时间 | 事务A | 事务B |
---|---|---|
T1 | 开始事务 | — |
T2 | — | 开始事务 |
T3 | — | 查询余额(1000 元) |
T4 | 查询余额(1000 元) | — |
T5 | — | 取出1000元(余额 0 元) |
T6 | — | 提交事务 |
T7 | 查询余额(0 元) | — |
T1-T3:事务B查询余额为1000元。
T4:事务A查询余额为1000元。
T5-T6:事务B取出1000元,并且提交了事务。
T7:事务B再次查询了余额,结果变成了0元。尴尬了~~~
③ 幻读:银行工作人员在统计张三的账户总存款时,发现有时候刷新一次,总存款就变得不一样了。
时间 | 事务A(统计总存款) | 事务B(存款) |
---|---|---|
T1 | 开始事务 | — |
T2 | — | 开始事务 |
T3 | 统计总存款(10000元) | — |
T4 | — | 存入100元 |
T5 | — | 提交事务 |
T6 | 统计总存款(10100元) | — |
T1-T3:事务A统计总存款10000元。
T4-T5:事务B存入100元,提交了事务。
T6:事务A统计总存款10100元。
事务的隔离级别
以上提到的事务在高并发下的三种问题可以用一句话描述:
- 脏读:事务A读取了事务B未提交的数据,并在这个基础上又做了其他操作。
- 不可重复读:事务A读取了事务B已提交的更改数据。
- 幻读:事务A读取了事务B已提交的新增数据。
脏读是坚决抵制的,不可重复读和幻读在大多数情况下能够被接受。
有了以上问题的存在,所以事务的隔离级别就扮演着很重要的角色,它就像一面墙一样,隔离不同的事务。
事务隔离级别 | 脏 读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 允许 | 允许 | 允许 |
不可重复读(read-committed) | 禁止 | 允许 | 允许 |
可重复读(repeatable-read) | 禁止 | 禁止 | 允许 |
串行化(serializable) | 禁止 | 禁止 | 禁止 |
JDBC 提供了这四类事务隔离级别,但默认事务隔离级别对不同数据库而言也是不一样的,我们知道JDBC 只是连接java程序与数据库的桥梁而已,那么数据库又是怎么隔离事务的呢?其实就是运用了锁机制,当插入数据时,就锁定表,这叫“锁表”;当更新数据时,就锁定行,这叫“锁行”。