数据库事务的四大特性(ACID)
原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
数据库系统支持事务模式
1、自动提交模式
自动提交模式是每个sql语句都是一个独立的事务,当数据库系统执行完一个sql语句后,会自动提交事务
2、手工提交模式必须由数据库的客户程序显示指定事务开始和结束边界
数据库并发问题
1.脏读(dirty read)
A事务读取了B事务尚未提交的更改数据,并且在这个数据基础上进行操作。如果此时恰巧B事务进行回滚,那么A事务读到的数据是根本不被承认的。
以下是一个取款事务和转账事务并发时引起的脏读场景。
时间 | 转账事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 取出500元,把余额改为500元 | |
T5 | 查询账户余额为500元(脏读) | |
T6 | 撤销事务,余额恢复为1000元 | |
T7 | 汇入100元,余额改为600元 | |
T8 | 提交事务 |
在这个场景中,B希望取款500元,而后有撤销了动作,而A往同一个账户转账100元,因为A事务读取了B事务尚未提交的数据,因而导致了账户白白丢失了500元。在Oracle数据中,不会发生脏读的情况。
2.不可重复读(unrepeatable read)
不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致
时间 | 取款事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元,把余额改为900元 | |
T6 | 提交事务 | |
T7 | 查询账户余额为900元 |
在同一个事务中T4和T7时间点读取的账户存款余额不一致
3.幻象读(phantom read)
A事务读取B提交的新增数据,这时A事务将出现幻想读的问题。幻读一般发生在计算统计数据的事务中。举个例子,假设银行系统在同一个事务中两次统计存款的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时两次统计的总金额将不一致。
时间 | 统计金额事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计存款总金额为10000元 | |
T4 | 新增一个存款账户,存款为100元 | |
T5 | 提交事务 | |
T6 | 再次统计存款总金额为10100元(幻象读) |
如果新增的数据刚好满足事务的查询条件,那么这个新数据就会进入事务的视野,因而导致两次统计结果不一致的情况。
幻读和不可重复读是两个容易混淆的概念,
幻读是指读到了其他事物已经提交的新增数据。策略:添加一个表级锁–将整张表锁定
不可重复读是读到了已经提交事务的更改数据(更改或删除)。策略:对操作的数据添加行级锁
4.第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错可能会造成很严重的问题。通过下面的账号取款转账就可以看出来。
时间 | 取款事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账号余额为1000元 | |
T4 | 查询余额为1000元 | |
T5 | 汇入100元,把余额改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元,把余额改为900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000元(丢失更新) |
A事务在撤销时,“不小心”将B事务已经转入账号的金额给抹去了。
5.第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所操作丢失。
时间 | 转账事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账号余额为1000元 | |
T4 | 查询余额为1000元 | |
T5 | 取出100元,把余额改为900元 | |
T6 | 提交事务 | |
T7 | 汇入100元,把余额改为1100元 | |
T8 | 提交事务 | |
T9 | 把余额改为1100元(丢失更新) |
在上面的例子,由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户损失了100元
数据库的锁
悲观锁:数据库总是认为多个数据库并发操作会发生冲突,所以总是要求加锁操作。悲观锁主要表锁、行锁、页锁。
乐观锁:
数据库总是认为多个数据库并发操作不会发生冲突,所以总是不加锁操作。所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。乐观锁的实现方式一般包括使用版本号和时间戳。
表级锁:读锁锁表,会阻碍其他事务修改表数据。写锁锁表会阻碍其他事务读与写。
页级锁:就是对页加锁
行级锁: 共享锁:一个事务对一行的共享只读锁。排它锁:一个事务对一行的排他读写锁。
共享锁:
加锁与解锁:当一个事务执行select语句时,数据库系统会为这个事务分配一把共享锁,来锁定被查询的数据。在默认情况下,数据被读取后,数据库系统立即解除共享锁。例如,当一个事务执行查询“SELECT * FROM accounts”语句时,数据库系统首先锁定第一行,读取之后,解除对第一行的锁定,然后锁定第二行。这样,在一个事务读操作过程中,允许其他事务同时更新 accounts表中未锁定的行。
兼容性:如果数据资源上放置了共享锁,还能再放置共享锁和更新锁。
并发性能:具有良好的并发性能,当数据被放置共享锁后,还可以再放置共享锁或更新锁。所以并发性能很好。
排它锁:
加锁与解锁:当一个事务执行insert、update或delete语句时,数据库系统会自动对SQL语句操纵的数据资源使用独占锁。如果该数据资源已经有其他锁(任何锁)存在时,就无法对其再放置独占锁了。
兼容性:独占锁不能和其他锁兼容,如果数据资源上已经加了独占锁,就不能再放置其他的锁了。同样,如果数据资源上已经放置了其他锁,那么也就不能再放置独占锁了。
并发性能:最差。只允许一个事务访问锁定的数据,如果其他事务也需要访问该数据,就必须等待。
更新锁:
加锁与解锁:当一个事务执行update语句时,数据库系统会先为事务分配一把更新锁。当读取数据完毕,执行更新操作时,会把更新锁升级为独占锁。
兼容性:更新锁与共享锁是兼容的,也就是说,一个资源可以同时放置更新锁和共享锁,但是最多放置一把更新锁。这样,当多个事务更新相同的数据时,只有一个事务能获得更新锁,然后再把更新锁升级为独占锁,其他事务必须等到前一个事务结束后,才能获取得更新锁,这就避免了死锁。
并发性能:允许多个事务同时读锁定的资源,但不允许其他事务修改它。
意向锁(意向共享,意向更新):
在判断每一行是否已经被行锁锁定效率比较低下,因此使用意向锁,当发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
事务隔离级别
Read uncommitted 一个事务可以读取另一个未提交事务的数据 。 缺点:会产生脏读、不可重复读、幻读。
Read committed 一个事务要等另一个事务提交后才能读取数据。 缺点:会产生不可重复读、幻读。 Oracle默认隔离级别
Repeatable read 重复读就是在开始读取数据时,不再允许修改操作。 缺点:会产生幻读。 MySQL默认的隔离级别。
Serializable 序列化 最高的事务隔离级别,在该级别下,事务串行化顺序执行。
缺点:可以解决并发事务的所有问题。但是效率地下,消耗数据库性能,一般不使用。
事务的七种传播行为
REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘
MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。
参考文章: