JAVA语言之SpringBoot实战实现分布式锁一之重现多线程高并发场景
小标 2019-04-25 来源 : 阅读 1303 评论 0

摘要:本文主要向大家介绍了JAVA语言之SpringBoot实战实现分布式锁一之重现多线程高并发场景,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

本文主要向大家介绍了JAVA语言之SpringBoot实战实现分布式锁一之重现多线程高并发场景,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

JAVA语言之SpringBoot实战实现分布式锁一之重现多线程高并发场景

实战概要:故而此文将介绍一下分布式锁出现的背景以及如何才能将分布式锁搬上用场(即如何重新多线程高并发的场景)。


实战内容:
1、“同一时刻多个线程高并发下访问共享资源”的场景在当前互联网产品或者项目下并不少见,这一场景随之带来的问题便显而易见:这一共享资源在并发访问的前后出现了数据不一致或者并非预期出现的结果的现象!!简而言之,这种现象其实就是大伙熟悉的 “高并发多线程访问共享资源时需要加同步代码块”的口头语(甚至可以说是面试时常见的对白了!)


2、单体应用时代加“同步锁”常见的方式是利用jdk天然提供的类/组件:ReentrantLock或者Synchronized,但在分布式系统架构下项目一般以微服务的方式开发、独立部署甚至集群部署,当不同的服务或者集群环境同一服务不同实例发生对共享资源的高并发访问时,ReentrantLock或者Synchronized 的方式将很难解决 “高并发导致数据不一致或者并发预期出现的结果”的问题!!


3、于是乎,“分布式锁”便出现了,“分布式锁”其实只是一解决方案,并非一专有组件或者类,实现这一解决方案仍旧需要借助额外的组件或者中间件来辅助,甚至某些情况下,需要借助数据库级别的方式来实现。总体来说,目前较为流行的解决方式还是有很多种,在我的视频课程或者文章中,我将介绍一下几种方式来实战实现 “分布式锁”
(1)数据库级别锁-乐观悲观锁
(2)基于Redis的原子操作实现分布式锁
(3)基于Zookeeper实战实现分布式锁
(4)基于Redisson实战实现分布式锁


4、既然我们知道分布式锁出现的背景以及其相应的实战实现方式,那我们回到本篇文章的核心内容:重现多线程高并发访问共享资源的场景


5、下面我们以“商城系统/秒杀系统抢单”场景为例,借助Jmeter测试工具,基于SpringBoot微服务项目重现高并发多线程访问共享资源的场景!即:重现1秒内100线程、1000线程、10000线程等充当抢单请求对一商品进行抢单!!!


6、这一场景其实很像“抢微信红包”、“某一商城如小米商城饥饿营销时抢手机”等业务场景。下面我们大概模拟重现其中的核心逻辑-即抢单的过程:建库-spring_boot_distribute,建一商品信息表语句如下(mysql5.6版本):


CREATE TABLE `product_lock` ( 
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`product_no` varchar(255) DEFAULT NULL COMMENT '产品编号',
`stock` int(11) DEFAULT NULL COMMENT '库存量',
`version` int(11) DEFAULT NULL COMMENT '版本号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_unique` (`id`) USING BTREE
ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='产品信息表';


并在其中录入一个商品的信息(主要是库存 stock 的设置)!

7、接着采用IDEA的SpringBoot Initializr组件构建多模块的SpringBoot微服务项目,并开发一Controller跟一Service,采用Mybatis逆向工程生成上面那张数据库表对应的entity、mapper、mapper.xml,相关代码以及截图如下:


@Service
public class DataLockService {

private static final Logger log= LoggerFactory.getLogger(DataLockService.class);

@Autowired
private ProductLockMapper lockMapper;

/**
 * 正常更新商品库存 - 重现了高并发的场景
 * @param dto
 * @return
 * @throws Exception
 */
@Transactional(rollbackFor = Exception.class)
public int updateStock(ProductLockDto dto) throws Exception{
    int res=0;

    ProductLock entity=lockMapper.selectByPrimaryKey(dto.getId());
    if (entity!=null && entity.getStock().compareTo(dto.getStock())>=0){
        entity.setStock(dto.getStock());
        return lockMapper.updateStock(entity);
    }

    return res;
}}


DataLockController代码如下:


@RestController
public class DataLockController {

private static final Logger log= LoggerFactory.getLogger(DataLockController.class);

private static final String prefix="lock";

@Autowired
private DataLockService dataLockService;

/**
 * 更新商品库存-1
 * @param dto
 * @param bindingResult
 * @return
 */
@RequestMapping(value = prefix+"/data/base/positive/update",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse dataBasePositive(@RequestBody @Validated ProductLockDto dto, BindingResult bindingResult){
    if (bindingResult.hasErrors()){
        return new BaseResponse(StatusCode.InvalidParam);
    }

    BaseResponse response=new BaseResponse(StatusCode.Ok);
    try {
        log.debug("当前请求数据:{} ",dto);

        int res=dataLockService.updateStock(dto);
        if (res<=0) {
            return new BaseResponse(StatusCode.Fail);
        }
    }catch (Exception e){
        log.error("发生异常:",e.fillInStackTrace());
        response=new BaseResponse(StatusCode.Fail);
    }
    return response;
}}


ProductLockDto 代码如下:


@Data
@ToString
public class ProductLockDto {
@NotNull
private Integer id;

@NotNull
private Integer stock=1;}


Mybatis逆向工程生成的那三个组件就不贴了,在这里贴一下 DataLockService调用的ProductLockMapper更新库存的方法以及动态sql的写法:


ProductLockMapper类的方法:    int updateStock(ProductLock lock);


ProductLockMapper.xml的动态sql:


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


8、至此简单的抢单系统/商城秒杀系统的抢单场景就大致模拟好了,下面我们采用Jmeter测试工具来模拟这一高并发场景,Jmeter的相关设置如下:
(1)首先我们设置1s并发100个线程,后面你可以在这里设置1000、10000甚至更多个线程!


(2)接着我们设置 “HTTP信息头管理器” ,因为我们的抢单接口接收的媒体类型是 json格式的post请求!


(3)接着我们创建 “HTTP请求” ,设置我们的项目上下文、端口以及我们的请求接口路径跟方法体(ProductLockDto的字段:商品的id跟需要抢的量stock)


(4)最后我们设置stock字段来源于我们配置的CSV数据文件设置中读取的变量stock 的值,即代表我们的用户可以任意随机的下单一定的量!!


(5)其中的csv文件是长这样的:


9、最后,我们点击这一按钮,即开启了 1s 内启动100个并发线程对设定的产品进行 “抢” 的请求。


10、这个时候,我们先对这一产品的库存量在数据库进行设置,我们设置为 100,即现有的库存量为100。理论情况下,不管发生多少次的“哄抢”,“最终的库存应当是被抢完而且应当是恰好被抢完,而且需要发送相应的短信/通知告知用户抢到了!!”,然后,现实是很残酷的(当你按下那一个start run的按钮时,数据库最终出现的结果却不是我们预期的那样!!)


11、下面是抢单接口的打印日志以及数据库最终对这一商品更新的结果:


15、你会惊讶的看到,100个库存在随机产生的100个线程(每个线程库存2或者5-csv文件读取的)更新之后竟然变成了负数(按道理来说,我们写的数据库更新逻辑以及代码判断逻辑没有多大问题啊!!!)


本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注编程语言JAVA频道!


本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程