数据库隔离级别
本文转自 http://singo107.iteye.com/blog/1175084
同时参考 http://blog.csdn.net/kaoa000/article/details/15814919
同时参考 http://www.hollischuang.com/archives/934
数据库有四种隔离级别,分别为 Read uncommitted,Read committed,Repeatable read,Serizable。 讲解围绕事务并发。
√:会出现 ×:不会出现
隔离级别 |
脏读 |
不可重复读 |
幻读 |
Read uncommitted |
√ |
√ |
√ |
Read committed |
× |
√ |
√ |
Repeatable read |
× |
× |
√ |
Serializable |
× |
× |
× |
1、Read uncommitted 读未提交
公司发工资了,领导把5000元打到tom的卡上,但是还未提交事务,这时tom查看自己的银行卡,发现自己多了工资5000元,心里想着为什么这次工资少了,但是这时老板发现给tom算错工资了,是10000元,于是事务回滚,将工资修改为10000元,tom再次查银行卡发现自己的工资是10000元,心里总算解闷了。
当隔离级别设置为Read uncommitted 时,容易出现脏读。
2、Read committed 读提交
tom去超市购物,结账时系统读到卡里有10000元,而此时tom的老婆正在网上转账,把tom卡里的10000元转到了另一账户,并在tom前提交了事务,此时系统检查到tom的工资卡里已经没有钱了,tom非常纳闷,明明卡里有钱...
当隔离级别设置为Read committed时,避免了脏读,容易出现不可重复读。大多数数据库的隔离级别设置为Read committed,如Sql Server,Oracle。怎样避免不可重复读,看下一个隔离级别。
3、Repeatable read
当数据库的隔离级别设置为Repeatable read时,可以避免不可重复读,即tom拿着工资卡去消费,系统一旦读工资卡,tom的老婆就不能读工资卡了,tom的老婆也不能在此时转账。Repeatable read避免了不可重复读,但是有可能出现幻读。
tom平时还挺节俭,tom的老婆在银行部门,她经常通过银行系统查看tom的消费记录。有一天,她查到tom的卡消费是80元,但是tom此时正在外面胡吃海喝,消费了1000元,tom的老婆打印账单时显示tom的消费记录是1080元,tom的老婆很诧异,以为出现了幻觉,幻读就这样产生了。
注: MySQL的隔离级别就是Repeatable read。
4、Serializable 序列化
序列化是最高的事务隔离级别,同时花费代价也很高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅避免了脏读,不可重复读,而且避免了幻读。
5、多个事务并发运行时的并发问题
第一类丢失更新:撤销一个事务时,把其他事务已经提交的更新数据覆盖。
脏读:一个事务读到另一个事务未提交的数据。
虚读:一个事务读到另一个事务已提交的新插入的数据。
不可重复读:一个事务读到另一个事务已提交的更新的数据。
第二类丢失更新:这是不可重复读中的特例,一个事务覆盖另一个事务已提交的更新数据。
并发运行的两个事务导致第二类丢失更新:
时间 | 取款事务 | 支票转账事务 |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100,把存款余额改为900元 | |
T6 | 提交事务 | |
T7 | 汇入100元,把存款余额改为1100元 | |
T8 | 提交事务 | |
设置隔离级别的原则
对于多数应用程序,可以优先考虑把数据库系统的隔离级别设置为Read committed,能够避免脏读,而且有较好的并发性能,尽管它能导致不可重复读、幻读、第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
乐观锁和悲观锁是并发控制主要采用的技术手段。数据库、memcache、hibernate、tair等都有类似的概念。
悲观锁
在关系数据库管理系统里,悲观锁是一种并发控制的方式,它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作中某行数据应用了锁,只有等这个事务把锁释放,其他事务才能执行与改锁冲突的操作。
悲观锁主要用于数据争用激烈的环境,以及发生冲突时使用锁保护数据的成本低于回滚事务的成本的环境中。
数据库中悲观锁的流程如下:
- 在对任意记录修改前,先尝试为该记录加上排他锁
- 如果加锁失败,说明该记录已经加上排他锁或者正在被修改,那么当前查询可能要等待或者抛出异常
- 如果成功加锁,那么就可以对该记录做修改,事务完成后就会解锁了
- 期间如果有其他队该记录加锁或修改,都会等待解锁或直接抛出异常
//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_goods where id=1 for update; 开启了排他锁
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;
上面的查询语句中,我们使用了select…for update
的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。
优点与不足:
悲观锁采用的是“先取锁再访问”的保守策略,为数据处理的安全提供了保障。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁机会;另外,在只读事务处理中由于不会产生冲突,所以不需要加锁,这样只能增加系统负载;还有会降低并发性,一个事务如果锁定了某行数据,其他事务就必须等待那行数据被处理完才能处理那行数据。
乐观锁
在关系数据库里,乐观锁是一种并发控制的方法,假设认为数据一般情况下不会造成冲突,所以在数据进行更新提交的时候才正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误信息,让用户决定如何去做,乐观锁不会使用数据库提供的锁机制,实现乐观锁的方式就是记录数据版本。实现数据版本有使用版本号和时间戳两种方式。
数据版本,是数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交数据更新的时候,判断数据库表对应的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
使用版本号实现乐观锁
使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作,并判断当前版本号是不是最新版本号。
1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
优点与不足
乐观锁相信数据之间的竞争是非常小的,因此尽可能直接做下去,知道提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是会遇到问题,例如两个事务都读取了数据库的某一行,经过修改后又写回了数据库,这时就会遇到问题。