zoukankan      html  css  js  c++  java
  • 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的乐观锁机制保证在分布式场景下的数据一致性。

    以上。

  • 相关阅读:
    Java学习笔记二.2
    Java学习笔记二.1
    Java学习笔记一
    cookie和session笔记
    编码知识笔记
    新手前端笔记之--css盒子
    新手前端笔记之--初识css
    新手前端笔记之--必备的标签
    新手前端笔记之--初识html标签
    二叉树总结
  • 原文地址:https://www.cnblogs.com/justuntil/p/10538329.html
Copyright © 2011-2022 走看看