众所周知,数据库事务具有隔离性,理论上来说事务之间的执行不应该相互产生影响,其对数据库的影响应该和它们串行执行时一样。然而完全的隔离性会导致系统并发性能很低,降低对资源的利用率,因而实际上对隔离性的要求会有所放宽,这也会一定程度造成对数据库一致性要求降低。
常见的事务隔离级别有四种,从低到高依次是:
事务的隔离级别越低,可能出现的并发异常越多,但通常而言系统能提供的并发能力就越强。
读未提交(Read uncommitted),B事务可以读取到未提交的A事务中操作的数据,会导致脏读,为解决脏读,引入读已提交。
读已提交(Read committed),B事务要等A事务提交后才能读取到A事务中操作的数据,但是依然会导致不可重复读的问题(即B事务在A事务提交前后读取的数据不一致)。
可重复读(Repeatable read),一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,Mysql是通过快照版本来实现可重复读的,即事务开始时就拿到了一个版本号,别的事务操作的数据版本号不一样,就不会影响当前事务重复读取的数据,但是会看不到别的事务提交的数据。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。
序列化(Serializable),最高的事务隔离级别,事务串行化顺序执行,但是效率低下,比较耗数据库性能,一般不使用。
脏写 | 脏读 | 不可重复读 | 幻读 | 丢失更新 | |
读未提交 | 可能 | 可能 | 可能 | 可能 | |
读已提交 | 可能 | 可能 | 可能 | ||
可重复读 | 可能 | ||||
串行化 |
另外,还有SQLServer中的快照(Snapshot)、已提交读快照(Read Committed Snapshot)两种常见隔离级别。
1、脏写
A事务可以修改另外B事务未提交的修改过的数据。任何一种隔离级别都不允许脏写的情况发生,解决方案是写操作时采用排它锁,A事务对某条数据进行写操作时,该记录加排它锁,B事务对该条数据的写操作必须等待。
2、脏读
如果一个事务A向数据库写了数据,但事务还没提交或终止,另一个事务B就看到了事务A写进数据库的数据,并且在这个数据基础上进行操作。如果此时恰巧A事务进行回滚,那么B事务读到的数据是根本不被承认的。
如下:账户应为150,脏读导致结果为130.
3、不可重复读
不可重复读指的是由于事务A对数据的操作,导致事务B前后两次读取到的结果不一致。
4、幻读
A事务读取B提交的新增数据,这时A事务将出现幻想读的问题。幻读一般发生在计算统计数据的事务中。举个例子,假设银行系统在同一个事务中两次统计存款的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时两次统计的总金额将不一致。
幻读和不可重复读是两个容易混淆的概念,前者是指读到了其他事物已经提交的新增数据,而后者是读到了已经提交事务的更改数据(更改或删除)。为了避免这两种情况,采取的策略是不同的:防止读到更改数据,只需对操作的数据添加行级锁,阻止操作过程中的数据发生变化,而防止读到新增数据,则往往需要添加表级锁或者间隙锁。
5、丢失更新
当多个事务并发写同一数据时,先执行的事务所写的数据会被后写的覆盖,这也就是更新丢失。除此之外,更新丢失主要发生在read-modify-write类型的事务当中:就是要先查询数据,然后计算新的数据,最后写回新的数据。
如下两种情形都可直接导致数据出错:
更新丢失的解决方案:
排它锁:select for update,这样在读取数据时就不允许其他事务的更新操作。
乐观锁:版本号机制,增加一个版本或者时间戳的列,对于进行修改行时先获取版本号,修改的时候比较一下版本号是否一致,一致再进行修改并将版本号加一更新。
/**------SQL--------------查询前一个版本号和需要操作的数据----------------------*/
SELECT account,last_modify FROM `user` WHERE user_id = '1dce2f04ed664f5ca74b9e6cacf74e1b';
/**------Java-------------数据处理和准备----------------------*/
USER user = select.fetchOne();
double newAount = user.account + 100;
long timestampNow = System.currentTimeMillis();
/**------SQL--------------根据上一个版本号更新数据,如果版本号不对说明已被另外的事务并发更新----------------------*/
UPDATE `user` SET account = newAcount,last_modify = timestampNow WHERE last_modify = user.last_modify AND user_id = '1dce2f04ed664f5ca74b9e6cacf74e1b';
---查询Mysql隔离级别
select @@transaction_isolation;
---设置Mysql隔离级别为READ COMMITTED
set session transaction isolation level read committed;
---查询SQLServer隔离级别
DBCC USEROPTIONS;
---设置SQLServer隔离级别为REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
---先开启一个事务
declare
trans_id Varchar2(100);
begin
trans_id := dbms_transaction.local_transaction_id( TRUE );
end;
---查询Oracle的隔离级别
SELECT CASE BITAND(t.flag, POWER(2, 28)) WHEN 0 THEN 'READ COMMITTED' ELSE 'SERIALIZABLE' END AS isolation_level FROM v$transaction t;
---设置Oracle隔离级别为SERIALIZABLE---执行报错
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Oracle的隔离级别只有两种---SERIALIZABLE和READ COMMITTED