秒杀项目之分布式锁运用

news2024/11/25 5:02:37

目录

  • 一、创建Redisson模块
  • 二、模拟高并发场景秒杀下单
    • 2.1 场景模拟
    • 2.2 案例演示
  • 三、JVM级锁与redis级分布式锁
    • 3.1 JVM级锁
    • 3.2 redis级分布式锁
      • 3.2.1 什么是setnx
      • 3.2.2 场景分析
  • 四、redisson分布式锁+源码解读
    • 4.1 什么是Redisson
    • 4.2 Redisson工作原理
    • 4.3 入门案例
  • 五、秒杀项目整合redisson实现分布式锁

一、创建Redisson模块

第1步:基于Spring Initialzr方式创建zmall-redisson模块

第2步:在zmall-redisson模块中添加相关依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--commons-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>

第3步:配置application.yml

server:
  port: 8081
spring:
  redis:
    host: 127.0.0.1
    password: 123456
    database: 1
    port: 6379

二、模拟高并发场景秒杀下单

2.1 场景模拟

@RestController
public class RedissonController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/updateStock")
    public String updateStock() {
		int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
        if (stock > 0) {
           	int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
        return "end";
    }
}

2.2 案例演示

  • 示例一:单线程情况

直接打开浏览器输入:http://localhost:8081/updateStock,查看redis中库存扣减情况。
在这里插入图片描述
在这里插入图片描述

  • 示例二:多线程情况

第1步:配置多启动服务

在这里插入图片描述

第2步:配置nginx,实现负载均衡

upstream tomcats{
	server 127.0.0.1:8081 weight=1;
	server 127.0.0.1:8082 weight=2;
}

server
{
	listen 80;
	server_name localhost;
	
	location / {
		proxy_pass http://tomcats/;
	}
}

在这里插入图片描述
第3步:配置jmeter,实现压测
创建测试用例,循环发送4组线程,每组200个;

查看redis中库存结果为0;查看多服务控制台信息均显示扣减失败,库存不足提示。

  • 结果分析

1)在单线程情况下,调用updatestock方法扣减库存,订单下单正常(没啥好说的)
2)在多线程情况下,调用updatestock方法扣减库存正常,订单下单异常(超卖了)

原因分析:在高并发情况下同时多个线程调用updateStock方法,按照正常思路线程1、线程2、线程3应该是分别实现库存减一(在库存为100的情况下,现在应该剩余97),同时生成三个秒杀订单;然后并发情况下根本不会按照剧本设计来执行,而是出现了线程1、线程2、线程3同时扣减库存,导致库存剩余99,但是订单却产生了3个,说明超卖了。

三、JVM级锁与redis级分布式锁

3.1 JVM级锁

@RestController
public class RedissonController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/updateStock")
    public String updateStock() {
    	//jvm级锁,单机锁
        synchronized (this){
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }
        return "end";
    }
}

在这里插入图片描述

jvm级的同步锁,单机锁。上述同步代码块中,在单机环境下同一时刻只有一个线程能进行秒杀下单库存扣减,完毕之后才能有后续线程进入。但是在分布式环境下依然还是会出现商品超卖情况。

请添加图片描述

3.2 redis级分布式锁

3.2.1 什么是setnx

格式:setnx key value

将key的值设置为value,当且仅当key不存在;若给定的key不存在,则setnx不做任何动作。
setnx是set if not exists(如果不存在,则set)的简写。

setnx "zking" "xiaoliu"      第一次设置有效
setnx "zking" "xiaoliu666"   第二次设置无效

第一次使用setnx设置zking直接成功,第二次使用setnx设置zking则失败,也意味着加锁失败。

redis级分布式锁之setnx使用

@RestController
public class RedissonController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/updateStock")
    public String updateStock() {
    	//使用redis级分布式锁setnx加锁
    	String lockKey="lockKey";
    	Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
    	if(!flag)
    		return "error_code";
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
        //解锁
        stringRedisTemplate.delete(lockKey);
    }
    return "end";
}

测压
在这里插入图片描述
在这里插入图片描述

3.2.2 场景分析

基于以上redis分布式锁setnx的代码,实现场景分析。

  • 问题1:执行扣减库存业务时出现异常,导致无法正常删除锁,从而形成死锁。

解决办法:通过try/catch/finally代码块解决。

//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
    if(!flag)
    	return "error_code";
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
    if (stock > 0) {
    	int realStock = stock - 1;
    	stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
    	System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
    	System.out.println("扣减失败,库存不足");
    }
}finally{
	//解锁
	stringRedisTemplate.delete(lockKey);
}

在这里插入图片描述
在这里插入图片描述

  • 问题2:执行扣减库存业务是如果Redis服务宕机,基于上述问题1的finally块就无意义了,还是死锁。

解决办法:加锁时设置过期时间,确保原子性

//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking",10,TimeUnit.SECONDS);
    if(!flag)
    	return "error_code";
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
    if (stock > 0) {
    	int realStock = stock - 1;
    	stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
    	System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
    	System.out.println("扣减失败,库存不足");
    }
}finally{
	//解锁
	stringRedisTemplate.delete(lockKey);
}
- 问题3:高并发场景下,线程执行先后顺序无法把控(自己加的锁被其他线程释放掉了,o(╥﹏╥)o)**

**场景分析**> 线程1:业务执行时间15s,加锁时间10s,那么导致业务未执行完成锁被提前释放;
> 线程2:业务执行时间8s,加锁时间10s;
> 线程3:业务执行时间5s,加锁时间10s,那么导致线程2的任务还没有执行完成就是线程3将所删除掉了;
>
> 以此类推,只要是高并发场景一直存在,那么锁一直处于失效状态(永久失效)

**解决办法**:可以在加锁的时候设置一个线程ID,只有是相同的线程ID才能进行解锁操作。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
String clientId= UUID.randomUUID().toString();
try{
  	
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
    if(!flag)
    	return "error_code";
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
    if (stock > 0) {
    	int realStock = stock - 1;
    	stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
    	System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
    	System.out.println("扣减失败,库存不足");
    }
}finally{
	//只有是相同的线程ID时才进行解锁操作
    if(stringRedisTemplate.opsForValue().get(lockKey).equals(clientId)) {
        //业务代码执行完毕删除redis锁(解锁)
        stringRedisTemplate.delete(lockKey);
    }
}

问题4锁要加多次时间才是最合理有效的?

解决办法:redisson,看门狗机制。

四、redisson分布式锁+源码解读

4.1 什么是Redisson

​ Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Redis 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发。

特点

  • 互斥:在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。

  • 防止死锁:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

  • 性能:对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。所以在锁的设计时,需要考虑两点。

    • 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
    • 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
  • 重入:ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用

4.2 Redisson工作原理

请添加图片描述

4.3 入门案例

创建RedissonConfig配置类

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient(){
        Config config=new Config();
        String url="redis://"+host+":"+port;
        config.useSingleServer().setAddress(url).setPassword(password).setDatabase(1);
        return Redisson.create(config);
    }
}

使用redisson分布式锁实现秒杀下单

@RequestMapping("/updateStock")
public String updateStock() {
	String lockKey="lockKey";
	RLock clientLock = redissonClient.getLock(lockKey);
	clientLock.lock();
	try {
		int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
		if (stock > 0) {
			int realStock = stock - 1;
			stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
			System.out.println("扣减成功,剩余库存:" + realStock);
		} else {
			System.out.println("扣减失败,库存不足");
		}
	} finally {
		//解锁
		clientLock.unlock();
	}
	return "end";
}

重新启动jmeter压测,连续发送4组,每组200个请求。查看多服务控制台,结果显示秒杀订单下单正常,无超卖情况发生。

注意:关于redis中信息读取问题
请添加图片描述

效果图如下:
在这里插入图片描述

五、秒杀项目整合redisson实现分布式锁

第1步:在zmall-order模块中配置pom.xml

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>

第2步:创建Redisson配置类

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient(){
        Config config=new Config();
        String url="redis://"+host+":"+port;
        config.useSingleServer().setAddress(url).setPassword(password).setDatabase(1);
        return Redisson.create(config);
    }
}

第3步:整合项目实现Redisson分布式锁

@Transactional
@Override
public JsonResponseBody<?> createKillOrder(User user, Integer pid) {
    //6.根据秒杀商品ID和用户ID判断是否重复抢购
    Order order = redisService.getKillOrderByUidAndPid(user.getId(), pid);
    if(null!=order)
    	return new JsonResponseBody<>(JsonResponseStatus.ORDER_REPART);

    RLock clientLock = redissonClient.getLock("scekill:goods:" + pid);
    clientLock.lock();
    try {
        //7.Redis库存预减
        long stock = redisService.decrement(pid);
        if (stock < 0) {
        	redisService.increment(pid);
        	return new JsonResponseBody<>(JsonResponseStatus.STOCK_EMPTY);
        }
        //创建订单
        order = new Order();
        order.setUserId(user.getId());
        order.setLoginName(user.getLoginName());
        order.setPid(pid);

        //将生成的秒杀订单保存到Redis中
        redisService.setKillOrderToRedis(pid, order);
        //将生成的秒杀订单推送到RabbitMQ中的订单队列中
        rabbitTemplate.convertAndSend(RabbitmqOrderConfig.ORDER_EXCHANGE,
        RabbitmqOrderConfig.ORDER_ROUTING_KEY, order);
    }catch (Exception e){
        e.printStackTrace();
        throw new BusinessException(JsonResponseStatus.ORDER_ERROR);
    }finally {
    	clientLock.unlock();
    }
    return new JsonResponseBody<>();
}

重新启动jmeter压测。

启动的服务:

nacos
MySQL
redis
RabbitMQ
Nginx
zmall-gateway
zmall-user
zmall-product
zmall-order
zmall-RabbitMQ
zmall-cart

最终结果如下

优化后-加Redisson:500个并发:qps在140-160之间
优化后-不加Redisson:500个并发:qps在250之间
优化前:500个并发,qps在50左右(参考第五次课压测)

在这里插入图片描述

在这里插入图片描述

注意:实际项目上线,分布式项目是多节点,所以很有必要加Redisson,否则多个微服务节点之间会出现重复下单秒杀;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
进行测压

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/346313.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

StarRocks 企业行|走进 58 同城,探索极速统一 3.0 时代的企业实践

新的一年&#xff0c;新的征程。随着 StarRocks 项目的演进&#xff0c;StarRocks 也迈入了极速统一 3.0 的时代。极速 OLAP 极速数据湖分析将带给企业什么价值&#xff1f;StarRocks 的用户又是如何在企业内打造专属的大数据平台&#xff0c;让数据驱动业务增长和优化&#x…

尚医通(十三)后台医院管理模块

目录一、医院管理模块需求分析1、医院列表2、医院详情二、医院列表功能(接口)1、添加service分页接口与实现2、添加controller方法3、service_cmn模块提供接口3.1 添加service接口与实现3.2 添加controller4、封装Feign服务调用4.1 搭建service_client父模块4.2 在service_clie…

论文投稿指南——中文核心期刊推荐(园艺)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

Spring3之注解(Annotation)

简介 前面介绍的都是通过 xml 文件配置的方式&#xff0c;Spring 也可以通过注解的方式添加配置&#xff0c;在实际开发过程中&#xff0c;最佳实践是&#xff1a;属于 Spring 的系统配置配置在 xml 文件中&#xff0c;而像注入 bean 之类的配置则通过注解的方式&#xff1b;这…

IDEA根据wsdl生成java代码(Generate Java Code from WSDL)以及乱码问题的解决

目录 一、根据wsdl生成java代码 1、创建待存放java代码的目录&#xff0c;点击“帮助”>“查找操作”&#xff0c;打开查找窗口&#xff1b; 2、输入wsdl并查找&#xff0c;点击“从WSDL生成Java代码”&#xff0c;打开新的窗口&#xff1b; 3、选择wsdl文件&#xff0c…

LeetCode 2.两数相加

原题链接 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表…

工作记录------lombok中@Data包含哪些功能?

工作记录------lombok中Data包含哪些功能&#xff1f; 在实体类中加上Data后&#xff0c;实体类有哪些增强&#xff1f; Data public class BaseProcedure {TableId(value "id", type IdType.ASSIGN_UUID)private String id;private String procedureCode;写上Da…

字节青训营——秒杀系统设计学习笔记(二)

一、两次MD5加密设计 加密&#xff1a;出于安全考虑 第一次 &#xff08;在前端加密&#xff0c;客户端&#xff09;&#xff1a;密码加密是&#xff08;明文密码固定盐值&#xff09;生成md5用于传输&#xff0c;目的&#xff0c;由于http是明文传输&#xff0c;当输入密码若…

Linux进程线程管理

目录 存储管理 linux内存管理基本框架 系统空间管理和用户空间管理 进程与进程调度 进程四要素 用户堆栈的扩展 进程三部曲&#xff1a;创建&#xff0c;执行&#xff0c;消亡 系统调用exit(),wait() 内核中的互斥操作 存储管理 linux内存管理基本框架 系统空间管理…

sql手工注入练习拿flag

sql手工注入练习拿flag 记录一下自己重新开始学习web安全之路⑤。 1、找注入点 ①url ②搜索框 ③登录框 2、找交互点 用单引号判断是否存在交互点&#xff0c;发现回显不正常&#xff0c;说明url存在有交互点 3、判断类型&#xff08;char类型&#xff09; 利用and 11 和…

Linux Shell脚本讲解

目录 Shell脚本基础 Shell脚本组成 Shell脚本工作方式 编写简单的Shell脚本 Shell脚本参数 Shell脚本接收参数 Shell脚本判断用户参数 文件测试与逻辑测试语句 整数测试比较语句 字符串比较语句 Shell流程控制 if条件判断语句 单分支 双分支 多分支 for循环语句…

第五章.与学习相关技巧—参数更新的最优化方法(SGD,Momentum,AdaGrad,Adam)

第五章.与学习相关技巧 5.1 参数更新的最优化方法 神经网络学习的目的是找到使损失函数的值尽可能小的参数&#xff0c;这是寻找最优参数的问题&#xff0c;解决这个问题的过程称为最优化。很多深度学习框架都实现了各种最优化方法&#xff0c;比如Lasagne深度学习框架&#xf…

Vue中使用天地图

Vue项目引入天地图 在vue的静态资源目录下的index.html中引入天地图的底图&#xff0c;开发天地图源码路径&#xff1a;天地图API 方法一&#xff1a;加载天地图&#xff0c;引用&#xff1a;public/index.html <script type"text/javascript" src"http:/…

来来来,手摸手写一个hook

hello&#xff0c;这里是潇晨&#xff0c;今天就带着大家一起来手写一个迷你版的hooks&#xff0c;方便大家理解hook在源码中的运行机制&#xff0c;配有图解&#xff0c;保姆级的教程&#xff0c;只求同学一个小小的&#x1f44d;&#xff0c;&#x1f436;。 第一步&#xf…

【软件测试】团队测试技术体现,遇到不可复现bug处理方式......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试技术带来的是PP…

ThingsBoard-规则链-check alarm status

1、概述 从今天开始,专门讲解ThingsBoard的规则链,每一个节点都会详细讲解,并且配套案例,让大家都能理解,喜欢请点个关注。今天讲的是筛选器的第第一个节点【check alarm status】,意思是检测告警状态。 2、节点理解 2.1、概述 【check alarm status】节点如图所示:…

解立方根-蓝桥杯

题目 分析 主要是运用二分法使复杂度减低。 代码 #include<iostream> #include<iomanip> #include<cmath> using namespace std; #define double long double const double eps 1e-12; int main() {int T 1;cin >> T;while (T--){double n;cin &g…

LMS 最佳实践:学习管理系统中的知识管理!

企业需要在整个组织中收集、存储和传播知识。我们被信息淹没了&#xff0c;因此应该充分利用学习管理系统&#xff08;LMS&#xff09;来组织业务资产并支持知识管理&#xff08;KM&#xff09;战略。LMS 不仅仅是一个捕获电子学习单元和多项选择题的系统。它很可能没有充分发挥…

代码随想录算法训练营第二十八天 | 491.递增子序列,46.全排列,47.全排列 II

一、参考资料递增子序列题目链接/文章讲解&#xff1a;https://programmercarl.com/0491.%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97.html 视频讲解&#xff1a;https://www.bilibili.com/video/BV1EG4y1h78v 全排列题目链接/文章讲解&#xff1a;https://programmercarl.…

从零学架构-基础部分

一、架构的基础将学习的架构设计知识总结出来&#xff0c;分享给大家。1.1什么是架构架构和框架是什么关系&#xff1f;有什么区别?Linux有架构&#xff0c;MySQL有架构&#xff0c;JVM也有架构&#xff0c;应该关注哪个架构&#xff1f;金融有架构&#xff0c;支付有架构&…