[关闭]
@c102zkl 2018-09-11T06:05:35.000000Z 字数 6596 阅读 822

事务的隔离级别以及乐观锁与悲观锁

数据库


事务的四大属性

  1. 原子性(Atomicity)
    原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。

  2. 一致性(Consistency)
    一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。ep:假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。

  3. 隔离型(Isolation)
    隔离性是当多个用户并发访问数据库时,比如同时操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。

  4. 持久性(Durability)
    持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务已经正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成。否则的话就会造成我们虽然看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。这是不允许的。


事务的隔离级别

1.为什么要设置隔离级别

在数据库操作中,在并发的情况下可能出现如下问题:


ep :比如说我需要对外销售1张电影票,且登记一下销售信息到另一张表,至少需要以下3个步骤。

  1. 查询电影票数量 是否满足销售1张电影票SELECET remain_count FROM cinema WHERE film_id = 12345:
  2. 更新电影票数量 UPDATE remain_count = remain_count - 1 FROM cinema WHERE film_id = 12345;
  3. 插入销售信息 INSERT INTO sell_Mes(id,mes) valuse(id,mes);

试想一下如果我们其中的一步被出错了或者被其他操作打乱就很容易出现问题。比如说有两个销售系统A,B在销售同样的票,此时票只剩下1张,A接到订单要售出一张票,他查看电影票的数量大于1,于是要售出的时候,也就是在第一步执行完毕执行第二步的时候,B也接到订单,也看到余票大于1,B也要售出1张票。此时就出现了余票只有1张却售出两张的荒唐的情况出现。

所以一个良好的系统是需要通过ACID测试。


事务的隔离级别有四种,由低到高依次为Read uncommitted(未授权读取、读未提交)、Read committed(授权读取、读提交)、Repeatable read(可重复读取)、Serializable(序列化),这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。隔离级别高的数据库的可靠性高,但并发量低,而隔离级别低的数据库可靠性低,但并发量高,系统开销小。

  • READ UNCIMMITTED (未提交读)

如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。这样就避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据

ep: 还是售票系统,A和B是售票员,他们分别是两个不同窗口的员工,现在售票系统只剩下3张票,此时小明来A这里买3张票,小张来B买票,A查到余票还有就给接了订单,就要执行第三步的时候,B接到小张的请求查询有没有余票。B看到A卖出了3张票,于是拒绝卖票。但是A系统出了问题,第三步执行失败,数据库为保证原子性,数据进行了回滚,也就是说一张票都没卖出去。

总结:这就是事务还没提交,而别的事务可以看到他其中修改的数据的后果,也就是脏读。

  • READ CIMMITTED (提交读)

大多数数据库系统的默认隔离级别是READ CIMMITTED。
读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

ep:还是A和B销售员,余票4张,小明来A请求3张订票单,A受订单,要卖出3张票,上面的销售步骤执行中的时候,小张也来B那里买票,由于A的销售事务执行到一半,B事务没有看到A的事务执行,读到的票数是3,准备接受订单的时候,A的销售事务完成了,此时B的系统变成显示0张票,此时只能拒绝订单了。

总结:这就是A的事务执行到一半,而B看不到他执行的操作,所以看到的是旧数据,也就是不可重复读

  • Repeatable Read(可重复读)

可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,即使第二个事务对数据进行修改,第一个事务两次读到的的数据是一样的。这样就发生了在一个事务内两次读到的数据是一样的,因此称为是可重复读。
读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。这样避免了不可重复读取和脏读,但是有时可能出现幻象读。(读取数据的事务)这可以通过“共享读锁”和“排他写锁”实现。

ep:销售部门有规定,如果销售记录低于规定的值,要扣工资,此时经理在后端控制台查看了一下小明的销售记录,发现销售记录达不到规定的次数,心里暗喜,准备打印好销售清单,理直气壮和小明提出,没想到打印出来的时候发现销售清单里面销售数量增多了几条,刚刚好达到要求,气的经理撕了清单纸。原来是小明在就要打印的瞬间卖出了几张票,因此避过了减工资的血光之灾。

  总结:虽然读取同一条数据可以保证一致性,但是却不能保证没有插入新的数据,就是幻读。

image_1cn3bkt7k12sg159t1t4q19d7obra9.png-22.6kB

对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。大多数数据库的默认级别就是Read committed,比如Sql Server , OracleMySQL的默认隔离级别就是Repeatable read


三、悲观锁和乐观锁

虽然数据库的隔离级别可以解决大多数问题,但是灵活度较差,为此又提出了悲观锁和乐观锁的概念。

悲观锁

悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统的数据访问层中实现了加锁机制,也无法保证外部系统不会修改数据。

- 使用场景举例:以MySQL InnoDB为例

商品t_items表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单(此时该商品无法再次下单),那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。
如果不采用锁,那么操作方法如下:

  1. //1.查询出商品信息
  2. select status from t_times where id = 1
  3. //2. 根据商品信息生成订单,并插入订单表t_orders
  4. insert into t_orders (id,goods_id) values(null,1);
  5. //3. 修改商品status为2
  6. update t_times set status=2;

上面这种场景在高并发访问的情况下很可能会出现问题。例如在第一步操作中,查询出来的商品status为1。但是当我们执行第三部Update操作的时候,有可能出现其他人先一步对商品下单把t_items中的status改为2了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单2次,使得数据所以说这种方式不安全的。

在上面的场景中,商品信息从查询出来到修改,中间有一个处理订单的过程,使用悲观锁的原理就是,当我们在查询出t_items信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为t_items被锁定了,就不会出现有第三者来对其进行修改了。需要注意的是,要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。我们可以使用命令设置MySQL为非autocommit模式:set autocommit=0;
设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:

  1. //0.开始事务
  2. begin;/begin work;/start transaction; (三者选一就可以)
  3. //1.查询出商品信息
  4. select status from t_items where id=1 for update; // 注意这里for update
  5. //2.根据商品信息生成订单
  6. insert into t_orders (id,goods_id) values (null,1);
  7. //3.修改商品status为2
  8. update t_items set status=2;
  9. //4.提交事务
  10. commit;/commit work;

上面的begin/commit为事务的开始和结束,因为在前一步我们关闭了mysql的autocommit,所以需要手动控制事务的提交。
上面的第一步我们执行了一次查询操作:select status from t_items where id=1 for update;与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_items表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。需要注意的是,在事务中,只有SELECT ... FOR UPDATELOCK IN SHARE MODE 操作同一个数据时才会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。拿上面的实例来说,当我执行select status from t_items where id=1 for update;后。我在另外的事务中如果再次执行select status from t_items where id=1 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果我是在第二个事务中执行select status from t_items where id=1;则能正常查询出数据,不会受第一个事务的影响。

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键或者索引,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。举例如下:
1、select * from t_items where id=1 for update;
这条语句明确指定主键(id=1),并且有此数据(id=1的数据存在),则采用row lock。只锁定当前这条数据。
2、select * from t_items where id=3 for update;
这条语句明确指定主键,但是却查无此数据,此时不会产生lock(没有元数据,又去lock谁呢?)。
3、select * from t_items where name='手机' for update;
这条语句没有指定数据的主键,那么此时产生table lock,即在当前事务提交前整张数据表的所有字段将无法被查询。
4、select * from t_items where id>0 for update; 或者select * from t_items where id<>1 for update;(注:<>在SQL中表示不等于)
上述两条语句的主键都不明确,也会产生table lock。
5、select * from t_items where status=1 for update;(假设为status字段添加了索引)
这条语句明确指定了索引,并且有此数据,则产生row lock。
6、select * from t_items where status=3 for update;(假设为status字段添加了索引)
这条语句明确指定索引,但是根据索引查无此数据,也就不会产生lock。

悲观锁小结

悲观锁并不是适用于任何场景,它也有它存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。所以与悲观锁相对的,我们有了乐观锁。

乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以只会在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定如何去做。实现乐观锁一般来说有以下2种方式:

使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注