实战前言:上篇博文我总体介绍了我这套视频课程:“SpringBoot实战实现分布式锁” 总体涉及的内容,从本篇文章开始,我将开始介绍其中涉及到的相关知识要点,感兴趣的小伙伴可以关注关注学习学习!!工欲善其事,必先利其器,介绍分布式锁使用的前因后果之前,得先想办法说清楚为啥需要分布式锁以及如何才需要将分布式锁搬上用场!!
其中,该课程的学习链接:http://edu.51cto.com/course/15684.html
感兴趣的童鞋可以前往观看学习!!!
实战概要:故而此文将介绍一下分布式锁出现的背景以及如何才能将分布式锁搬上用场(即如何重新多线程高并发的场景)。
实战内容:
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 product_lock
set stock = stock - #{stock,jdbcType=INTEGER}
where id = #{id,jdbcType=INTEGER}
8、至此简单的抢单系统/商城秒杀系统的抢单场景就大致模拟好了,下面我们采用Jmeter测试工具来模拟这一高并发场景,Jmeter的相关设置如下:
(1)首先我们设置1s并发100个线程,后面你可以在这里设置1000、10000甚至更多个线程!
(2)接着我们设置 “HTTP信息头管理器” ,因为我们的抢单接口接收的媒体类型是 json格式的post请求!
(3)接着我们创建 “HTTP请求” ,设置我们的项目上下文、端口以及我们的请求接口路径跟方法体(ProductLockDto的字段:商品的id跟需要抢的量stock)
(4)最后我们设置stock字段来源于我们配置的CSV数据文件设置中读取的变量stock 的值,即代表我们的用户可以任意随机的下单一定的量!!
9、最后,我们点击这一按钮,即开启了 1s 内启动100个并发线程对设定的产品进行 “抢” 的请求。
10、这个时候,我们先对这一产品的库存量在数据库进行设置,我们设置为 100,即现有的库存量为100。理论情况下,不管发生多少次的“哄抢”,“最终的库存应当是被抢完而且应当是恰好被抢完,而且需要发送相应的短信/通知告知用户抢到了!!”,然后,现实是很残酷的(当你按下那一个start run的按钮时,数据库最终出现的结果却不是我们预期的那样!!)
11、下面是抢单接口的打印日志以及数据库最终对这一商品更新的结果:
15、你会惊讶的看到,100个库存在随机产生的100个线程(每个线程库存2或者5-csv文件读取的)更新之后竟然变成了负数(按道理来说,我们写的数据库更新逻辑以及代码判断逻辑没有多大问题啊!!!)
实战分析:“按道理来说,我们写的数据库更新逻辑以及代码判断逻辑没有多大问题啊!!!”,实则不然,其实问题正是出在这两点:数据库更新逻辑 跟 代码判断逻辑 。 欲知问题何在,请听下回分解!!
实战总结:本篇文章主要基于SpringBoot微服务项目重现了高并发多线程并发访问同一共享资源时出现的问题,学习过程大伙若有相关问题可以加我QQ:1974544863 进行技术交流!若需要该课程的学习,亦可以加QQ进行咨询!如果感兴趣的童鞋,也可以结合课程学习(掌握得更快哦):http://edu.51cto.com/course/15684.html