MySQL乐观锁在分布式场景下的实践

 

背景

在电商购物的场景下,当我们点击购物时,后端服务就会对相应的商品进行减库存操作。在单实例部署的情况,我们可以简单地使用JVM提供的锁机制对减库存操作进行加锁,防止多个用户同时点击购买后导致的库存不一致问题。

但在实践中,为了提高系统的可用性,我们一般都会进行多实例部署。而不同实例有各自的JVM,被负载均衡到不同实例上的用户请求不能通过JVM的锁机制实现互斥。

因此,为了保证在分布式场景下的数据一致性,我们一般有两种实践方式:一、使用MySQL乐观锁;二、使用分布式锁。

本文主要介绍MySQL乐观锁,关于分布式锁我在下一篇博客中介绍。

乐观锁简介

乐观锁(Optimistic Locking)与悲观锁相对应,我们在使用乐观锁时会假设数据在极大多数情况下不会形成冲突,因此只有在数据提交的时候,才会对数据是否产生冲突进行检验。如果产生数据冲突了,则返回错误信息,进行相应的处理。

那我们如何来实现乐观锁呢?一般采用以下方式:使用版本号(version)机制来实现,这是乐观锁最常用的实现方式。

版本号

那什么是版本号呢?版本号就是为数据添加一个版本标志,通常我会为数据库中的表添加一个int类型的"version"字段。当我们将数据读出时,我们会将version字段一并读出;当数据进行更新时,会对这条数据的version值加1。当我们提交数据的时候,会判断数据库中的当前版本号和第一次取数据时的版本号是否一致,如果两个版本号相等,则更新,否则就认为数据过期,返回错误信息。我们可以用下图来说明问题:

如图所示,如果更新操作如第一个图中一样顺序执行,则数据的版本号会依次递增,不会有冲突出现。但是像第二个图中一样,不同的用户操作读取到数据的同一个版本,再分别对数据进行更新操作,则用户的A的更新操作可以成功,用户B更新时,数据的版本号已经变化,所以更新失败。

代码实践

我们对某个商品减库存时,具体操作分为以下3个步骤:

  1. 查询出商品的具体信息

  2. 根据具体的减库存数量,生成相应的更新对象

  3. 修改商品的库存数量

为了使用MySQL的乐观锁,我们需要为商品表goods加一个版本号字段version,具体的表结构如下:

1
2
3
4
5
6
7
CREATE TABLE `goods` (
   `id`  int ( 11 ) NOT NULL AUTO_INCREMENT,
   `name` varchar( 64 ) NOT NULL DEFAULT  '' ,
   `remaining_number`  int ( 11 ) NOT NULL,
   `version`  int ( 11 ) NOT NULL,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT= 2  DEFAULT CHARSET=utf8;

 

 

Goods类的Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
* 商品名字
      */
     private  String name;
 
     /**
      * 库存数量
      */
     private  Integer remainingNumber;
 
     /**
      * 版本号
      */
     private  Integer version;
 
     @Override
     public  String toString() {
         return  "Goods{"  +
                 "id="  + id +
                 ", name='"  + name + '\ ''  +
                 ", remainingNumber="  + remainingNumber +
                 ", version="  + version +
                 '}' ;
     }
}

 

 

GoodsMapper.java:

1
2
3
4
5
public  interface  GoodsMapper {
 
     Integer updateGoodCAS(Goods good);
 
}

 

 

GoodsMapper.xml如下:

1
2
3
4
5
6
7
8
9
<update id= "updateGoodCAS"  parameterType= "com.ztl.domain.Goods" >
         <![CDATA[
           update goods
           set `name`=#{name},
           remaining_number=#{remainingNumber},
           version=version+ 1
           where id=#{id} and version=#{version}
         ]]>
     </update>

 

 

GoodsService.java 接口如下:

1
2
3
4
5
public  interface  GoodsService {
 
     @Transactional
     Boolean updateGoodCAS(Integer id, Integer decreaseNum);
}

 

 

GoodsServiceImpl.java类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public  class  GoodsServiceImpl  implements  GoodsService {
 
     @Autowired
     private  GoodsMapper goodsMapper;
 
     @Override
     public  Boolean updateGoodCAS(Integer id, Integer decreaseNum) {
         Goods good = goodsMapper.selectGoodById(id);
         System.out.println(good);
         try  {
             Thread.sleep( 3000 );      //模拟并发情况,不同的用户读取到同一个数据版本
         catch  (InterruptedException e) {
             e.printStackTrace();
         }
         good.setRemainingNumber(good.getRemainingNumber() - decreaseNum);
         int  result = goodsMapper.updateGoodCAS(good);
         System.out.println(result ==  1  "success"  "fail" );
         return  result ==  1 ;
     }
}

 

 

GoodsServiceImplTest.java测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith (SpringRunner. class )
@SpringBootTest
public  class  GoodsServiceImplTest {
 
     @Autowired
     private  GoodsService goodsService;
 
     @Test
     public  void  updateGoodCASTest() {
         final  Integer id =  1 ;
         Thread thread =  new  Thread( new  Runnable() {
             @Override
             public  void  run() {
                 goodsService.updateGoodCAS(id,  1 );     //用户1的请求
             }
         });
         thread.start();
         goodsService.updateGoodCAS(id,  2 );             //用户2的请求
 
         System.out.println(goodsService.selectGoodById(id));
     }
}

 

 

输出结果:

1
2
3
4
5
Goods{id= 1 , name= '手机' , remainingNumber= 10 , version= 9 }
Goods{id= 1 , name= '手机' , remainingNumber= 10 , version= 9 }
success
fail
Goods{id= 1 , name= '手机' , remainingNumber= 8 , version= 10 }

 

 

代码说明:

在updateGoodCASTest()的测试方法中,用户1和用户2同时查出id=1的商品的同一个版本信息,然后分别对商品进行库存减1和减2的操作。从输出的结果可以看出用户2的减库存操作成功了,商品库存成功减去2;而用户1提交减库存操作时,数据版本号已经改变,所以数据变更失败。

这样,我们就可以通过MySQL的乐观锁机制保证在分布式场景下的数据一致性。

以上。

转载于:https://www.cnblogs.com/justuntil/p/10538329.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值