数据库事务
隔离级别是是数据库中概念,我们先了解一下数据库事务相关的知识。
数据库事务具有一下4个基本特征(ACID):
① Atomi(原子性):事务中包含的操作被看做一个整体的业务单元,这个业务单元中的操作要么完全成功,要么完全失败。
②Consistency(一致性):事务在完成时,必须是所有的数据都保持一致状态,在数据库中所有的修改都基于事务,保证了数据的完整性和一致性。
③Isolation(隔离性):当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
这里的隔离性也是就是下面我们要说的隔离级别,为了减少事务在修改数据上的相互影响。
④Durability(持久性):一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
隔离级别
1.未提交读
未提交读(read uncommitted)是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,优点在于并发能力高,适合对数据一致性没有要求而追求高并发的场景,但是会出现脏读。为了克服脏读提出读写提交的隔离级别。
脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据
时刻 | 事务1 | 事务2 | 备注 |
T0 | ······ | ······ | 商品库存初始化为2 |
T1 | 读取库存为2 | ||
T2 | 扣减库存 | 库存为1 | |
T3 | 扣减库存 | 库存为0,读取事务1未提交的库存数据 | |
T4 | 提交事务 | 库存保存为0 | |
T5 | 回滚事务 | 库存为0,结果错误 |
2.提交读
读写提交(read committed),实质一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。可以克服脏读的现象,但是会出现不可重读的现象。为了克服不可重读提出可重复读的隔离级别。
克服脏读
时刻 | 事务1 | 事务2 | 备注 |
T0 | ······ | ······ | 商品库存初始化为2 |
T1 | 读取库存为2 | ||
T2 | 扣减库存 | 库存为1 | |
T3 | 扣减库存 | 库存为0,读取不到事务1未提交的库存数据 | |
T4 | 提交事务 | 库存保存为1 | |
T5 | 回滚事务 | 库存为1,结果正确 |
不可重读
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
时刻 | 事务1 | 事务2 | 备注 |
T0 | ······ | ······ | 商品库存初始化为1 |
T1 | 读取库存为1 | ||
T2 | 扣减库存 | 事务未提交,当前库存为1 | |
T3 | 读取库存为1 | 认为可扣减 | |
T4 | 提交事务 | 库存变为0 | |
T5 | 扣减库存 | 库存为0,无法扣减 |
3.可重复读
可重复读(Repeatable Read),目标是克服读写提交中出现的不可重复读的现象,但是会导致幻读的现象。为了克服幻读数据库提出串行化的隔离级别。
克服不可重读
时刻 | 事务1 | 事务2 | 备注 |
T0 | ······ | ······ | 商品库存初始化为1 |
T1 | 读取库存为1 | ||
T2 | 扣减库存 | 事务未提交,当前库存为1 | |
T3 | 尝试读取库存 | 不允许读取,等待事务1提交 | |
T4 | 提交事务 | 库存变为0 | |
T5 | 读取库存 | 库存为0,无法扣减 |
幻读
幻读是事务非独立执行时发生的一种现象。幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
时刻 | 事务1 | 事务2 | 备注 |
T0 | 读取库存50件 | 商品库存初始化为100,现在已经销售50件,库存50件 | |
T1 | 读取库存为1 | 查询交易记录,50笔 | |
T2 | 扣减库存 | ||
T3 | 插入1笔交易记录 | ||
T4 | 提交事务 | 库存49件,交易记录21笔 | |
T5 | 打印交易记录,51笔 | 这里与查询的不一致,在事务2看来有1笔试虚幻的,与之前查询的不一致 |
4.串行化
串行化(Serializable):是数据库最高的隔离级别,它要求所有的sql都按顺序执行,这样就克服了上述隔离级别出现的各种问题,所以他能够完全保证数据的一致性。
总结
异常现象 | 脏读 | 不可重复读 | 幻读 |
未提交读 | √ | √ | √ |
读写提交 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
可以看到,四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,性能就越低。例如Serializable级别,就是以锁表的方式使得其他的线程只能在锁外等待。
MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。
在选择隔离级别时,要考虑的不单单是数据一致性的问题,还要考虑系统性能的问题。当然java也给出了很多解决方案,如使用乐观锁方式,或者不再使用数据库而且使用Redis作为数据载体的手段等。
备注,以上内容部分参考书籍《深入浅出SpringBoot2.X》