分布式锁(二)——基于数据库的分布式锁实例

上一篇博客中简单说了说什么是分布式锁,搭建了基本的环境(非常简单)这篇博客就需要开始正式体验分布式锁 了,由于是在单机上开发,没有做集群,但是代码方法的具体实现与集群方面没有二异,只能通过JMeter模拟多线程达到高并发的效果。

模拟业务场景

1、模拟数据库中商品库存销售的SQL

  <!--更新库存-->
  <update id="updateStock" parameterType="com.learn.lockmodel.entity.ProductLock">
    update product_lock
    set stock = stock - #{stock,jdbcType=INTEGER}
    where id = #{id,jdbcType=INTEGER}
  </update>

2、商品销售的实体

其中的BindingResult就是校验结果,之前介绍过,传送门:spring boot中的参数校验 其中的BaseResponse是封装的统一消息处理对象,具体代码如下:

@Data
public class BaseResponse<T> {

    private Integer code;
    private String msg;
    private T data;

    public BaseResponse(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }

    public BaseResponse(StatusCode statusCode, T data) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = data;
    }

    public BaseResponse(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public BaseResponse(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

5、postman进行验证

商品id:10010,商品库存:1000
在这里插入图片描述
利用postman构建如下请求
在这里插入图片描述
请求发送成功之后会看到库存正常减少
在这里插入图片描述

JMeter模拟高并发

JMeter在我看来能模拟我们开发中用到的所有场景,功能似乎比postman强大的多,Apache JMeter 介绍 下载地址:JMeter的各个版本下载地址

安装

安装就是一个Easy到爆的东西,解压完成之后,进入到bin目录下,点击jmeter.bat批处理脚本(windows环境下,linux环境下点击jmeter.sh),就可以启动jmeter
在这里插入图片描述

加入HTTP信息头管理器

鼠标反键->添加->配置元件->HTTP信息头管理器。
在这里插入图片描述
之后需要在HTTP头中增加HTTP的数据类型
在这里插入图片描述
这一点与postman不同,添加更加方便。

加入HTTP请求头

鼠标右键->添加->取样器->HTTP请求
在这里插入图片描述
加入HTTP请求之后,需要配置url,端口等参数。
在这里插入图片描述
这里的请求参数,我们采用的动态配置,JMeter可以通过动态配置变量,自动获取指定文件中的数据,这个就需要添加相应的数据文件了。

加入测试数据文件

鼠标右键->添加->配置元件在这里插入图片描述
添加文件之后,我们需要指定CSV文件,并指定变量名称:
在这里插入图片描述

这也就是为什么在HTTP请求中指定${stock}变量的原因。这里的数据CSV文件中我们就指定了两个数据——2,4。

设置线程组

忘了提一下,在打开JMeter的时候,默认就会有一个测试计划,在给测试计划中添加了线程组之后,才可以添加HTTP信息管理头,HTTP请求头和测试数据文件。
在这里插入图片描述
设置相关线程组:
在这里插入图片描述

发起请求

发起请求,1000个线程处理完成之后,我们会发现如下情况
在这里插入图片描述
库存变成负数,这是不能忍的(虽然一定程度下这只能算是多线程的一种简单模拟,但是如果这些代码放在多个服务器上,多个JVM中执行,还是会出现库存负数的情况)。

乐观锁与悲观锁

关于乐观锁和悲观锁的介绍这里也不再赘述,乐观锁引入了一个版本号的概念,如果版本号不一致则表示已经有其他的操作对其进行了修改,数据就变成了脏数据。悲观锁无非就是让所有的请求进行排队。这些基本概念已经有大牛总结的非常到位了,百度一搜一大堆,这里就直接附上一个链接即可。乐观锁与悲观锁简介

基于数据库的实现

数据库是所用应用程序的数据来源,在数据库阶段完成数据的访问限制,自然能实现分布式锁的操作。

乐观锁的实现

引入版本号的字段之后,在更新是匹配版本号与本线程是否一致,如果一致则更新数据,如果不一致则放弃更新。在基础的代码基础上,我们只需要修改SQL就可以实现。

<!--更新库存-乐观锁v1-->
<update id="updateStockV1" parameterType="com.learn.lockmodel.entity.ProductLock">
  update product_lock set stock = stock - #{stock,jdbcType=INTEGER},version=version+1
  where id = #{id,jdbcType=INTEGER} and version = #{version,jdbcType=INTEGER} and stock > 0
</update>

where 条件中加入了version的判断,在更新的时候同时让版本号+1。

业务代码无需大的更改,只需要调用指定的数据访问代码即可。

@RequestMapping(value=prefix+"/db/update/optimistic",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBaseOptimisticLock(@RequestBody @Validated ProductLockDto productLockDto,BindingResult bindingResult){
    if(bindingResult.hasErrors()){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    BaseResponse result = new BaseResponse(StatusCode.Ok);
    try{
        log.debug("当前请求数据:{}",productLockDto);
        int res = dataLockService.updateStockWithOptimisticLock(productLockDto);
        if(res<=0){//如果数据库层面更新失败,则直接购买失败
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("更新商品库存失败,异常信息为:{}",e.fillInStackTrace());
        result = new BaseResponse(StatusCode.Fail);
    }
    return result;
}

测试

为了便于计算,我们将所有的购买个数变成1,然后发起多个线程,进行压力测试。

并发2000个线程,初始化库存50000,启动JMeter测试,如果版本号+库存=50000则表示数据正常(只有在购买数都为1的时候才可以用这个公式进行验证,任何时候公式都成立才表示数据正确)
在这里插入图片描述
测试结果如下图
在这里插入图片描述

悲观锁的实现

我们回本溯源一下,出现数据不符合逻辑的情况,其实就是数据出现了脏读读取情况,如果用悲观锁实现,就需要在读取数据的时候,加上X 锁,关于数据库的X锁和S锁可以参见这篇博客——MySql(三)——事务和锁

加入X锁

利用for update语句,给数据库读取的时候增加上X锁,这样就能避免数据出现脏读的情况。

  <!--根据主键查询for update 悲观锁-->
  <select id="selectByPKForNegative" resultType="com.learn.lockmodel.entity.ProductLock">
    SELECT <include refid="Base_Column_List"/> FROM product_lock
    WHERE id=#{id} FOR UPDATE
  </select>

加入for update语句,用于加入X锁。

业务代码层面需要做的变更依旧很少,只是需要变更数据查询的逻辑就可以了。如下所示:

/*
* 悲观锁的更新操作
 * @param dto
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public int updateStockNegativeLock(ProductLockDto dto){
    int res = 0;
    //获取库存数据的时候,加上X锁
    ProductLock negativeLockEntity = lockMapper.selectByPKForNegative(dto.getId());
    if(negativeLockEntity!=null && negativeLockEntity.getStock().compareTo(dto.getStock())>=0){
        negativeLockEntity.setStock(dto.getStock());
        res = lockMapper.updateStockForNegative(negativeLockEntity);
        if(res>0){//抢购成功
            log.info("下单抢购商品成功,stock{}",negativeLockEntity.getStock());
        }else{
            log.error("抢购失败");
        }
        return res;
    }
    return res;
}

controller的实例:

 /*
 * 悲观锁更新数据库
 * @param productLockDto
 * @param bindingResult
 * @return
 */
@RequestMapping(value=prefix+"/db/update/negative",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBaseNegativeLock(@RequestBody @Validated ProductLockDto productLockDto,BindingResult bindingResult){
    if(bindingResult.hasErrors()){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    BaseResponse result = new BaseResponse(StatusCode.Ok);
    try{
        log.debug("当前请求数据:{}",productLockDto);
        int res = dataLockService.updateStockNegativeLock(productLockDto);
        if(res<=0){//如果数据库层面更新失败,则直接购买失败
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("更新商品库存失败,异常信息为:{}",e.fillInStackTrace());
        result = new BaseResponse(StatusCode.Fail);
    }
    return result;
}

**逻辑上这种锁是直接加载MySQL层面,每一个请求无法更新数据的时候会等待,知道数据更新成功,因此只要数据库的连接数是充足的,则并不会像乐观锁那样出现很多更新失败的情况。**测试结果如下所示:

在这里插入图片描述

总结

本篇博客从实例出发,介绍了数据库层面的乐观锁和悲观锁。

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/104088334