一、事务
事务是由一组SQL语句组成的逻辑处理单元,是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。事务具有以下4个属性,通常简称为事务的ACID属性:
原子性(ATOMICITY) | 事务是一个原子操作单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不可能只执行其中的一部分操作。 |
一致性(Consistent) | 一致性是指事务讲数据库从一种一致性状态转换到另外一种一致性状态,在事务开始之前和事务结束后数据库数据的完整性没有被破坏。 以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。 |
隔离性(Isolation) | 隔离性要求一个事务对数据库中数据的修改,在未提交完成前对于其它事务是不可见的 |
持久性(Durable) | 一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失 |
二、并发一致性问题
1、更新丢失(Lost Update)
第一类更新丢失 | 由于某个事务的回滚操作,参与回滚的旧数据将其他事务的数据更新覆盖了。 |
第二类更新丢失 | 关于多个事务同时更新一行数据导致的问题 |
1.1 第一类更新丢失
标准定义的所有隔离级别都不允许第一类丢失更新发生
比如如下两个事务,事务一先开启查询账户有1000元,然后准备存款100元,使其账户变为1100,此时事务尚未结束,其后,事务二发生了转账,并提交了事务,使账户金额变为900,而事务一并不知情,最后事务一没有提交,而是回滚了事务,将账户金额重新设置为1000。但其实,账户已经被转走了100元,这种回滚导致了更新丢失。
时间序号 | 事务一 | 事务二 |
---|---|---|
T1 | 开启事务 | 开启事务 |
T2 | 查询账户money=1000 | 查询账户money=1000 |
T3 | 存款100,money=1100 | |
T4 | ||
T5 | 转账100,money=900 | |
T6 | 提交事务,money=900 | |
T7 | 取消事务,回滚,money=1000 |
1.2 第二类更新丢失
T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。
例如,两个程序员修改同一java文件。每程序员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖前一个程序员所做的更改。
解决方法:如果在一个程序员完成并提交事务之前,另一个程序员不能访问同一文件,则可避免此问题。
如下表所示,事务一和事务二都更新一行数据,他们事务开始的时候都查询到账户有1000元,然后都往账户添加了100元,最后大家都提交了各自的事务,结果却是错误的。事务一完成之后,money正常的结果值应该是1200,而现在提交事务之后值是1100,则事务2的修改被丢失了。
时间序号 | 事务一 | 事务二 |
---|---|---|
T1 | 开启事务 | 开启事务 |
T2 | 查询账户money=1000 | 查询账户money=1000 |
T3 | 存款100,money=1100 | |
T4 | ||
T5 | 存款100,money=1100 | |
T6 | 提交事务,money=1100 | |
T7 | 提交事务,money=1100 |
第二类更新丢失的问题,如果数据库用户使用方式不对,是有可能出现问题的。
通常有两种方式可以解决这个问题:
#悲观锁
#悲观锁就是锁定要更新的这一行,然后在事务提交之前不让其他事务对该行数据做任何操作
#直至释放行锁
select money from account where id=10 for update
money = money + 100
update account set money = money where id =10
#乐观锁
#乐观锁是在并发的表上加一个version字段,更新的时候只有版本号大于当前版本号才能更新成功
select money,version from account where id=10 for update
money = money + 100
version = version + 1
update account set money = money where id =10 and version > version
2、脏读
事务B读取到了事务A已修改但尚未提交的的数据,还在这个数据基础上做了操作。此时,如果A事务回滚Rollback
,B读取的数据无效,不符合一致性要求。
解决办法: 把数据库的事务隔离级别调整到 READ_COMMITTED。
T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
3、不可重复读(Non-Repeatable Reads)
在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。
简单概括一句话:一个事务范围内两个相同的查询却返回了不同数据。
解决办法: 如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。把数据库的事务隔离级别调整到REPEATABLE_READ
T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
4、幻读
一个事务T1按相同的查询条件重新读取以前检索过的数据,却发现其他事务T2插入了满足其查询条件的新数据,这种现象就称为“幻读”。(和可重复读类似,但是事务 T2 的数据操作仅仅是插入和删除,不是修改数据,读取的记录数量前后不一致)
j简单概括一句话:事务A 读取到了事务B提交的新增数据,不符合隔离性。
解决办法: 如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。把数据库的事务隔离级别调整到 SERIALIZABLE_READ
。
T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
三、事务隔离级别
"脏读"、"不可重复读"和"幻读",其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
MYSQL查看当前数据库的事务隔离级别:show variables like 'tx_isolation';
隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻影读 | 第二类更新丢失 |
---|---|---|---|---|---|
读未提交 | 最低级别 | √ | √ | √ | × |
读已提交 | 语句级 | × | √ | √ | × |
可重复读 | 事务级 | × | × | √ | √ |
可串行化 | 最高级别,事务级 | × | × | × | √ |
1、读未提交 (Read Uncommitted)
最低的隔离等级,允许其他事务看到没有提交的数据,会导致脏读。
读未提交存在 【脏读问题】 | 公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有2000元,singo空欢喜一场。 出现上述情况,即我们所说的脏读,两个并发的事务,“事务A:领导给singo发工资”、“事务B:singo查询工资账户”,事务B读取了事务A尚未提交的数据。 |
2、读已提交 (Read Committed)
被读取的数据可以被其他事务修改,这样可能导致不可重复读。
也就是说,事务读取的时候获取读锁,但是在读完之后立即释放(不需要等事务结束),释放读锁之后,就可能被其他事务修改数据。【而写锁则是事务提交之后才释放】该等级也是 SQL Server 默认的隔离等级。
读已提交存在 【不可重复读的问题】 | singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为何...... 出现上述情况,即我们所说的不可重复读,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接的更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。 |
3、可重复读(Repeatable Read)
所有被 Select 获取的数据都不能被修改,这样就可以避免一个事务前后读取数据不一致的情况。但是却没有办法控制幻读,因为这个时候其他事务不能更改所选的数据,但是可以增加数据,即前一个事务有读锁但是没有范围锁(引申:现在主流数据库都使用 MVCC 并发控制,使用之后RR
(可重复读)隔离级别下是不会出现幻读的现象。)
MYSQL默认是REPEATABLE-READ
。
可重复读存在 【幻读的问题】 | singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额(select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction ... ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出现了幻觉,幻读就这样产生了。 |
4、串行化(Serializable)
所有事务一个接着一个的执行,这样可以避免幻读 (phantom read),对于基于锁来实现并发控制的数据库来说,串行化要求在执行范围查询的时候,需要获取范围锁,如果不是基于锁实现并发控制的数据库,则检查到有违反串行操作的事务时,需回滚该事务。
5、总结
- 读未提交: 一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交: 一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读 : 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化: 顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
四个级别逐渐增强,每个级别解决一个问题,事务级别越高,性能越差,大多数环境(Read committed
就可以用了)