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

实战前言:上篇博文我总体介绍了我这套视频课程:“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 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文件读取的)更新之后竟然变成了负数(按道理来说,我们写的数据库更新逻辑以及代码判断逻辑没有多大问题啊!!!)

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

实战总结:本篇文章主要基于SpringBoot微服务项目重现了高并发多线程并发访问同一共享资源时出现的问题,学习过程大伙若有相关问题可以加我QQ:1974544863 进行技术交流!若需要该课程的学习,亦可以加QQ进行咨询!如果感兴趣的童鞋,也可以结合课程学习(掌握得更快哦):http://edu.51cto.com/course/15684.html

原文地址:http://blog.51cto.com/13877966/2328630

时间: 2024-10-06 23:59:38

SpringBoot实战实现分布式锁一之重现多线程高并发场景的相关文章

重磅发布-SpringBoot实战实现分布式锁视频教程

概要介绍:历经一个月的时间,我录制的分布式锁实战之SpringBoot实战实现系列完整视频教程终于出世了!在本课程中,我分享介绍了分布式锁出现的背景.实现方式以及将其应用到实际的业务场景中,包括"重复提交"."CRM系统销售人员抢单",并采用当前相当流行的微服务SpringBoot来搭建项目实战实现分布式锁. 课程学习:目前博主已将分布式锁实现以及实际业务场景实战的要点整理成课程,感兴趣的童鞋可以前往学习:http://edu.51cto.com/course/15

springboot+redis实现分布式锁

参考 SpringBoot实现Redis分布式锁 https://www.jianshu.com/p/750ac97eb29e 实现原理 加锁解锁 执行逻辑之前,加锁 执行逻辑之后,删除锁 加锁和删除锁必须是同一个对象的行为. 获取锁删除锁 使用setnx,保证只有一个对象可以设置锁成功,只有一个对象可以拿到锁. (删除锁的时候,必须保证是自己创建的锁,需要验证value. 上面参考文章中,每次设置加锁的时候,设置token,删除锁的时候,对比token) 设置过期时间 设置锁的过期时间,为什么

高并发场景下锁

如何确保一个方法,或者一块代码在高并发情况下,同一时间只能被一个线程执行,单体应用可以使用并发处理相关的 API 进行控制,但单体应用架构演变为分布式微服务架构后,跨进程的实例部署,显然就没办法通过应用层锁的机制来控制并发了.那么锁都有哪些类型,为什么要使用锁,锁的使用场景有哪些?今天我们来聊一聊高并发场景下锁的使用技巧. 锁类别 不同的应用场景对锁的要求各不相同,我们先来看下锁都有哪些类别,这些锁之间有什么区别. 悲观锁(synchronize) Java 中的重量级锁 synchronize

高并发场景下秒杀项目静态锁的使用疑问

题:高并发场景下秒杀项目静态锁的使用疑问场景:我们有一个秒杀平台,可以提供所有接入公司创建的秒杀活动,简单描述如下:1.秒杀10袋洗衣粉,开始时间12:00(项目ID:A001)2.秒杀iPhone5,开始时间12:00(项目ID:A002)3.秒杀水杯,开始时间12:00(项目ID:A003)... ...(项目ID:A004-A009)10.秒杀ThinkPad,开始时间12:00(项目ID:A010) 例如上面,同时有十个秒杀,都是12:00整开始,每个秒杀之间没有任何关系. 按照我之前的

SpringBoot集成redisson分布式锁

官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 1.引用redisson的pom <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.5.0</version> </dependency> 2.定义Lo

springboot整合redisson分布式锁

一.通过maven引入redisson的jar包 <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.5</version> </dependency> 二.在yaml文件中引入redis的相关配置(redis单节点可以读取原有redis配置拼装,如果是主从需另外独立配置,相关属性

SpringBoot集成Redis分布式锁以及Redis缓存

https://blog.csdn.net/qq_26525215/article/details/79182687 集成Redis 首先在pom.xml中加入需要的redis依赖和缓存依赖 <!-- 引入redis依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifa

[转帖]SpringBoot集成redisson分布式锁

https://www.cnblogs.com/yangzhilong/p/7605807.html 前几天同事刚让增加上这一块东西. 百度查一下 啥意思.. 学习一下. 官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 20180226更新:增加tryLock方法,建议后面去掉DistributedLocker接口和其实现类,直接在RedissLockUtil中注入RedissonClient实现类(简单但会丢失

tp框架如何锁表,实现不能高并发的下单

要实例化表 开启事务 连贯查询时候,加上lock(true)