您现在的位置是:首页 > 文章详情

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

日期:2018-11-29点击:678


实战前言:从本篇文章开始,我将开始介绍"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)

enter image description here



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



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


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

enter image description here


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


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

enter image description here


enter image description here


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


实战分析:“按道理来说,我们写的数据库更新逻辑以及代码判断逻辑没有多大问题啊!!!”,实则不然,其实问题正是出在这两点:数据库更新逻辑 跟 代码判断逻辑 。 欲知问题何在,请听下篇分解!!


实战总结:本篇文章主要基于SpringBoot微服务项目重现了高并发多线程并发访问同一共享资源时出现的问题
附注“SpringBoot实战实现分布式锁”系列博文是 SpringBoot实战实现分布式锁” 视频教程的文字配合讲解,目的在于巩固加深大伙对于分布式锁实现的理解,感兴趣的伙伴可以前往学习:https://www.roncoo.com/course/view/4737639130da4d6892e11190412433a6 




原文链接:https://blog.roncoo.com/article/134420
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章