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的乐观锁机制保证在分布式场景下的数据一致性。

    以上。

  • 相关阅读:
    《Programming WPF》翻译 第8章 1.动画基础
    一些被遗忘的设计模式
    《Programming WPF》翻译 第4章 数据绑定
    《Programming WPF》翻译 第3章 控件
    《Programming WPF》翻译 第5章 样式和控件模板
    《Programming WPF》翻译 第7章 绘图
    《Programming WPF》翻译 第9章 自定义控件
    《Programming WPF》翻译 第7章 绘图 (2)
    《Programming WPF》翻译 第8章 前言
    关于Debug和Release之本质区别
  • 原文地址:https://www.cnblogs.com/justuntil/p/10538329.html
Copyright © 2011-2022 走看看