mysql 的悲观锁和乐观锁

mysql 的悲观锁和乐观锁

       如果客户端在同一时间内访问同一条数据对数据进行操作时,这时候就会产生数据不一致的问题,我们可以使用mysql的加锁机制,这样可以在一定程度上解决并发访问的问题,mysql中有两种常见的锁,分别为悲观锁和乐观锁,悲观锁和乐观锁是一种机制不是指具体的锁。

一:悲观锁:

悲观锁,正如其名,它指的是对数据被外界修改持悲观的态度,认为数据是不安全的,总有人在修改,在需要操作数据时,会先将数据锁住,然后在操作数据。

悲观锁通过常用的select … for update操作来实现悲观锁,悲观锁的流程(一锁二查三更新):

举个栗子:
假设商品表中有一个字段quantity表示当前该商品的库存量。假设有一件Dulex套套,其id为100,quantity=8个;如果不使用锁,那么操作方法

/step1: 查出商品剩余量
 select quantity from items where id=100;
//step2: 如果剩余量大于0,则根据商品信息生成订单
 insert into orders(id,item_id) values(null,100);
 //step3: 修改商品的库存
 update Items set quantity=quantity-1 where id=100;

这种写法在高并发环境下可能出现问题,导致库存为负数的情况(产品多卖)

      使用悲观锁,就可以解决这个问题,在上面的场景中,商品信息从查询出来到修改,中间有一个生成订单的过程,使用悲观锁的原理就是,当我们在查询出items信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为数据被锁定了,就不会出现有第三者来对其进行修改了。而这样做的前提是需要将要执行的SQL语句放在同一个事物中,否则达不到锁定数据行的目的。

如下:
//step1: 查出商品状态
select quantity from items where id=100 for update;
//step2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);
//step3: 修改商品的库存
update Items set quantity=quantity-2 where id=100;
      select...for update是MySQL提供的实现悲观锁的方式。此时在items表中,id为100的那条数据就被我们锁定了,其它的要执行select quantity from items where id=100 for update的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

        需要注意的是:  当我执行select quantity from items where id=100 for update后。如果我是在第二个事务中执行select quantity from items where id=100(不带for update)仍能正常查询出数据,不会受第一个事务的影响。另外,MySQL还有个问题是select...for update语句执行中所有扫描过的行都会被锁上,因此在MySQL中用悲观锁务必须确定走了索引,而不是全表扫描,否则将会将整个数据表锁住

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

 
 
 

2. 乐观锁

       乐观锁相对悲观锁而言,它认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回错误信息,让用户决定如何去做。接下来我们看一下乐观锁在数据表和缓存中的实现。

实现:
 
  1:版本号控制:   利用数据版本号(version)机制是乐观锁最常用的一种实现方式。一般通过为数据库表增加一个数字类型的 “version” 字段,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,返回更新失败。
 
   2:时间戳控制 :    还可以使用时间戳字段,该方法同样是在表中增加一个时间戳字段,和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
举个栗子:
//step1: 查询出商品信息
select (quantity,version) from items where id=100;
//step2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);
//step3: 修改商品的库存
update items set quantity=quantity-1,version=version+1 where id=100 and version=#{version};

service体实现:

       int tryTimes = 0;
          while (true) {
             tryTimes++;
             if (更新数据库的返回值int != 0) {
                 // 说明更新成功,直接退出
                 break;
             }
             if (tryTimes == 200) {
                // 达到最大重试次数,退出
                 break;
             }
             try {
                // 休息一段时间后再重试
                 TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                 e.printStackTrace();
                Thread.currentThread().interrupt();
            }
         }
         LOGGER.info("tryTimes: {}", tryTimes);
     }
原文地址:https://www.cnblogs.com/dw3306/p/12950415.html