在电商购物的场景下, 当我们点击购物时, 后端服务就会对相应的商品进行减库存操作. 在单实例部署的情况, 我们可以简单地使用 JVM 提供的锁机制对减库存操作进行加锁, 防止多个用户同时点击购买后导致的库存不一致问题.
但在实践中, 为了提高系统的可用性, 我们一般都会进行多实例部署. 而不同实例有各自的 JVM, 被负载均衡到不同实例上的用户请求不能通过 JVM 的锁机制实现互斥.
因此, 为了保证在分布式场景下的数据一致性, 我们一般有两种实践方式: 一, 使用 MySQL 乐观锁; 二, 使用分布式锁.
本文主要介绍 MySQL 乐观锁, 关于分布式锁我在下一篇博客中介绍.
乐观锁简介
乐观锁 (Optimistic Locking) 与悲观锁相对应, 我们在使用乐观锁时会假设数据在极大多数情况下不会形成冲突, 因此只有在数据提交的时候, 才会对数据是否产生冲突进行检验. 如果产生数据冲突了, 则返回错误信息, 进行相应的处理.
那我们如何来实现乐观锁呢? 一般采用以下方式: 使用版本号 (version) 机制来实现, 这是乐观锁最常用的实现方式.
版本号
那什么是版本号呢? 版本号就是为数据添加一个版本标志, 通常我会为数据库中的表添加一个 int 类型的 "version" 字段. 当我们将数据读出时, 我们会将 version 字段一并读出; 当数据进行更新时, 会对这条数据的 version 值加 1. 当我们提交数据的时候, 会判断数据库中的当前版本号和第一次取数据时的版本号是否一致, 如果两个版本号相等, 则更新, 否则就认为数据过期, 返回错误信息.
代码实践
我们对某个商品减库存时, 具体操作分为以下 3 个步骤:
查询出商品的具体信息
根据具体的减库存数量, 生成相应的更新对象
修改商品的库存数量
为了使用 MySQL 的乐观锁, 我们需要为商品表 goods 加一个版本号字段 version, 具体的表结构如下:
- 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 代码:
- /* 商品名字
- */
- 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:
- public interface GoodsMapper {
- Integer updateGoodCAS(Goods good);
- }
GoodsMapper.xml 如下:
- <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 接口如下:
- public interface GoodsService {
- @Transactional
- Boolean updateGoodCAS(Integer id, Integer decreaseNum);
- }
GoodsServiceImpl.java 类如下:
- @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 测试类
- @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);
- }
- });
- thread.start();
- goodsService.updateGoodCAS(id, 2);
- System.out.println(goodsService.selectGoodById(id));
- }
- }
输出结果:
- 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://juejin.im/post/5afec34df265da0b745269db