分布式锁(二)——基于数据库实现分布式锁
作者:互联网
上一篇博客中简单说了说什么是分布式锁,搭建了基本的环境(非常简单)这篇博客就需要开始正式体验分布式锁 了,由于是在单机上开发,没有做集群,但是代码方法的具体实现与集群方面没有二异,只能通过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层面,每一个请求无法更新数据的时候会等待,知道数据更新成功,因此只要数据库的连接数是充足的,则并不会像乐观锁那样出现很多更新失败的情况。**测试结果如下所示:
总结
本篇博客从实例出发,介绍了数据库层面的乐观锁和悲观锁。
谜一样的Coder 发布了122 篇原创文章 · 获赞 35 · 访问量 8万+ 私信 关注标签:基于,code,HTTP,BaseResponse,res,数据库,msg,stock,分布式 来源: https://blog.csdn.net/liman65727/article/details/104088334