众所周知,金融业是一个直接和钱打交道的行业。通俗的讲,金融公司提供的是中介服务,并从中收取中介费,盈利模式是“本金”——“收益”。而实体经济的盈利模式是“本金”—“产品”—“收益”。所以,在金融业中对金额是非常敏感的,下面以“取款”、“转账”和“查询余额”等银行业务为例来说明,如何通过数据库事务保证该类业务正确,完整和安全的进行。
一. 数据库事务简介
数据库事务(Transaction)是指:作为单个逻辑工作单元执行的一些列操作。事务内的操作要么全部执行,要么全部不执行,回到初始状态。事务必须具备下面四点属性(ACID)
1. 原子性(Atomic):事务内的操作必须是全部执行,或者全部不执行,回滚到初始状态。
2. 一致性(Consistent):事务在完成后,数据库内的数据是一致的,没有脏数据产生。
3. 隔离性(Isolation):事务在并发执行时,事务之间的修改必须是隔离的,好比事务是串行执行的一样。
4. 持久性(Duration):事务完成后,对数据的修改是持久的。
这是个人理解的数据库事务,详细的定义和介绍可以参照维基百科。
二. 数据库并发问题
同一时间可能存在多个用户访问数据库,并做相应的操作。必然就会存在数据库中同一份数据被多个事务同时访问,如果处理不好,总体来说会引入五类问题:3类读问题(脏读、不可重复读和幻读)和2类写问题(第一类丢失更新和第二类丢失更新)。下面以“取款”、“转账”和“查询余额”三类业务来解释出现这五类问题的场景:
1. 脏读(Dirty Read)
举一个生活中的例子,结巴A来到超市买东西,老板B问他:“抽中华烟吗?”。顾客A说“我…抽…抽…抽”,老板B忙给他拿了包烟过来,这时结巴A终于憋出了后半句“抽不起啊”。在这个生活场景中,老板B对结巴A进行了脏读。
脏读的现象是:事务B读取了事务A没有提交的更改数据,并做了相应的操作。此时,事务A回滚,那么事务B基于之前数据的操作都是错误的。来看看转账和取款的例子:
时间 | 取款事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 余额1000元,转出100元,余额改为900元 | |
T4 | 余额900元(脏读) | |
T5 | 撤销事务 | |
T6 | 取款100元,余额改为800元 | |
T7 | 提交事务 |
从上述例子中可以看出,脏读导致账户损失了100元。
2. 不可重复读(Unrepeatable Read)
还是刚才生活中的例子,A问B:“还有中华烟吗?”,B说:“还有一包,要吗?”,A思索了一下,此时B又去招待其他顾客了,而且将烟卖给了另一个顾客。等A想好,并等B忙完后说“给我来一包吧”,B说:“买完了”,然后两人开始吵架了。在这个场景中,在A身上就发生了不可重复读问题。
不可重复读现象是:事务A在事务B提交某条更新数据的前后,对该条数据访问时,会产生不一致的情况。来看看,查询和转账业务
时间 | 取款事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 余额1000元 | |
T4 | 查询余额1000元 | |
T5 | 转出100元,余额改为900元 | |
T6 | 提交事务 | |
T7 | 查询余额900元 | |
T8 | 提交事务 |
从上面例子中可以看出,在同一事务A中,T4和T7查询的结果不一致。
3. 幻读(Phantom Read)
还是刚才生活中的例子,A问B:“店里有多少中华烟?”,B说:“还有2包,都要么?”,这时A在思索一下。恰巧,这时另一个顾客C退了一包回来。等A想好后说:“我都要了”,其实此时店里一共还有三包烟了。在这个场景中,在A身上发生了幻读的情况。
幻读的现象是:事务A在事务B插入某条数据的前后,对数据记录进行统计时,读取到事务B新插入的一条记录。幻读容易与不可重复读混淆,幻读指的是读取了新插入的数据,不可重复读指的是读取了新更新(修改,删除)的数据。在数据库中,防止这两类问题所采取的策略是不同的,不可重复读采取行级锁防止更新,而幻读需要采取表级锁防止插入。来看看“统计账户总额”和“开户”业务
时间 | 统计账户总额事务A | 开户事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询总额为1000元 | |
T4 | 新增一个账户,并存款100元 | |
T5 | 提交事务 | |
T6 | 查询总额为1100元 | |
T7 | 提交事务 |
从上面例子中可以看出,在同一事务A中,T3和T6统计的结果不一致。
4. 第一类丢失更新(Lost Update)
还是刚才生活中的例子,不过角色要丰富一下,顾客变成2个:A1和A2,老板变成2个:B1和B2。场景是这样的,A1问B1:“还有多少中华烟”,B1清点了一下说:“还有2包”。A2问了B2同样的问题,B2也回答说是“还有2包”,然后A2买了1包。但是B2并没有告知B1。后来,A1也要了一包烟,不一样的事,不久A1又把烟给退了,可是B1还以为店里有2包烟。在这个场景中,B1身上发生了第一类更新问题。
第一类更新问题指的是:事务A撤销时,导致事务B已提交的修改被覆盖。来看看取款和转账业务:
时间 | 取款事务A | 转账事务B |
T1 | 事务开始 | |
T2 | 事务开始 | |
T3 | 查询余额1000元 | |
T4 | 查询余额1000元,并转入100元余额1100元 | |
T5 | 提交事务 | |
T6 | 取出100元,余额900元 | |
T7 | 撤销事务 | |
T8 | 余额恢复为1000元 |
从上面例子中可以看出,事务A撤销后,导致事务B已提交的修改被覆盖
5. 第二类丢失更新(Second Lost Update)
和第一类丢失更新不同的是,第二类丢失更新指的是:事务A提交的结果,将事务B提交的结果覆盖了。仍然以上面的取款和转账业务为例:
时间 | 取款事务A | 转账事务B |
T1 | 事务开始 | |
T2 | 事务开始 | |
T3 | 查询余额1000元 | |
T4 | 查询余额1000元,并转入100元余额1100元 | |
T5 | 提交事务 | |
T6 | 取出100元,余额900元 | |
T7 | 提交事务 | |
T8 | 余额为900元 |
从上面例子中可以看出,事务A提交后,导致事务B已提交的修改被覆盖。