分布式锁

news2025/7/6 13:35:04

目录

1. 模拟高并发场景秒杀下单

1.1 导入依赖

1.2 配置application.yml文件

1.3 场景模拟

1.4 案例演示

2. JVM级锁与redis级分布式锁

2.1 JVM级锁

3. redis级分布式锁

3.1 什么是setnx

3.2 场景分析

4.  redisson分布式锁

4.1 什么是Redisson

4.2 Redisson工作原理

4.3 入门案例


1. 模拟高并发场景秒杀下单

1.1 导入依赖

<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>

1.2 配置application.yml文件

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

1.3 场景模拟

@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";
    }
}

1.4 案例演示

  • 示例一:单线程情况  

直接打开浏览器输入: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个,说明超卖了。


2. JVM级锁与redis级分布式锁

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

重新启动jmeter压测,连续发送4组,每组200个请求。  


3. redis级分布式锁

3.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 场景分析

基于以上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,看门狗机制。  


4.  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(0);
        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个请求。查看多服务控制台,结果显示秒杀订单下单正常,无超卖情况发生。

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

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

相关文章

有关于decoder中的past_key_values参数

我们都知道&#xff0c;encoder-decoder模型在进行generate的时候&#xff0c;会重复地调用decoder &#xff08;i.e., auto-regressive&#xff09;。 也就是&#xff0c;上一个step decoder的预测结果&#xff0c;作为下一个step decoder的输入。 这个时候&#xff0c;由于…

Axios(一) +Promise自定义封装36-42

1. axios 是什么? 1. 前端最流行的 ajax 请求库 2. react/vue 官方都推荐使用 axios 发 ajax 请求 3. 文档: https://github.com/axios/axios 1.2. axios 特点 1. 基于 xhr promise 的异步 ajax 请求库 2. 浏览器端/node 端都可以使用 3. 支持请求&#xff0f;响应拦截器 4…

从零开始,开启属于你的 RTE 漫游之旅!丨漫游指南 x 即将启航

&#x1f914; 什么是「开发者漫游指南」&#xff1f; 「开发者漫游指南」邀请热爱前端开发、关心音视频领域发展、希望进入音视频行业、乐于和大家一起交流成长的小伙伴&#xff0c;通过「开发者漫游指南」与社区共同成长&#xff0c;帮助更多的开发者在实时音视频领域取得进…

Linux中gdb的使用

文章目录gdb的使用方法启动gdb之前的准备工作下载gdb拥有一个带有调试信息的可执行程序正式启动gdb展示源码&#xff08;要先看到源码才知道接下来的步骤……&#xff09;打断点显示所有断点信息运行程序逐过程&#xff08;VS中的F10&#xff09;逐语句&#xff08;VS中的F11&a…

实验四、格子世界(Grid World)

一、实验目的 &#xff08;1&#xff09;熟悉动态规划算法中策略评估过程&#xff1b; &#xff08;2&#xff09;了解如何对问题进行建模处理&#xff0c;包括环境、状态、动作、奖惩值的初始化&#xff1b; 二、实验内容与要求 &#xff08;1&#xff09;掌握动态算法基本…

华为机试_HJ27 查找兄弟单词【中等】

目录 描述 输入描述&#xff1a; 输出描述&#xff1a; 解题过程 提交代码 学习代码 代码一 收藏点 描述 定义一个单词的“兄弟单词”为&#xff1a;交换该单词字母顺序&#xff08;注&#xff1a;可以交换任意次&#xff09;&#xff0c;而不添加、删除、修改原有的字…

Linux系统 PHP安装expect扩展详解

今天继续给大家介绍服务器运维相关知识&#xff0c;本文主要内容是Linux系统 PHP安装expect扩展详解。 一、expect简介 expect是基于tcl语言开发的&#xff0c;用于实现自动和交互式任务进行通信&#xff0c;而无须人的干预。expect是建立在tcl基础上的一个工具&#xff0c;还…

Navicat--对比和同步MySQL表结构的方法

原文网址&#xff1a;Navicat--对比和同步MySQL表结构的方法_IT利刃出鞘的博客-CSDN博客 简介 本文介绍如何使用Navicat对比和同步MySQL表结构的方法。 实际项目中会遇到这样的场景&#xff1a;将测试环境的表结构同步到生产环境。 工具> 结构同步 选择源数据库和目标数据…

tensorflow 基本概念和基本操作

op和tensor之间的关系 op是graph上的节点&#xff0c;线就是tensor。 op输入tensor&#xff0c;同时也产出下游的tensor 作为每一个tensor&#xff0c;都会有一个op的属性(attribute)&#xff0c;该op就代表着这个tensor是被什么计算产出的。举个例子&#xff1a; In [74]: w…

Redis6 主从复制

Redis6 主从复制1. 什么是主从复制2. 能做什么3. 配置1主2从3.1 配置3.2 启动redis3.3 配置主从关系4.常见问题4.1 一主二仆5. 主从复制原理6. 薪火相传7. 反客为主1. 什么是主从复制 主机数据更新后根据配置和策略&#xff0c; 自动同步到备机的master/slaver机制&#xff0c…

C++--数据结构--最短路径--Dijkstra--Bellman-Ford算法--Floyd-Warshall算法--高阶0713 14

注&#xff1a;本篇所用的某些未在本文中实现的函数&#xff0c;或不明确的类&#xff0c;均在上篇博客中有详细过程&#xff0c;因篇幅问题不再赘述。 C--数据结构--图的相关概念及模拟实现--高阶0712_Gaze&#xff01;的博客-CSDN博客 1. Dijkstra算法 Dijkstra算法需要开辟…

spring-boot 接收form表单 多文件加多字段数据(postman在form-data格式下传数组和集合)

前言 该博客多用于记录自己的问题 在写项目的时候遇到这种业务情况&#xff1a; 需要保存整个页面的数据&#xff0c;数据包含多个字段信息和多个文件 结合网上的处理思路&#xff0c;我最终实现了这种业务需求并整理一下 前端单独提交字段和文件比较方便简单&#xff0c;本人…

带你读AI论文丨针对文字识别的多模态半监督方法

摘要&#xff1a;本文提出了一种针对文字识别的多模态半监督方法&#xff0c;具体来说&#xff0c;作者首先使用teacher-student网络进行半监督学习&#xff0c;然后在视觉、语义以及视觉和语义的融合特征上&#xff0c;都进行了一致性约束。本文分享自华为云社区《一种针对文字…

构建高性能内存队列:Disruptor

1、 背景 Java中有哪些队列 ArrayBlockingQueue 使用ReentrantLock LinkedBlockingQueue 使用ReentrantLock ConcurrentLinkedQueue 使用CAS 等等 我们清楚使用锁的性能比较低&#xff0c;尽量使用无锁设计。接下来就我们来认识下Disruptor。 2、Disruptor简单使用 github地…

Web3中文|可判115年监禁的FTX创始人SBF即将被引渡到美国

巴哈马总检察长办公室在宣布逮捕FTX前CEO Sam Bankman-Fried时&#xff0c;指出他很可能应美国要求被引渡。 一个多星期后&#xff0c;美国广播公司新闻报道称 &#xff0c;SBF于12月20日签署了引渡文件。 另据彭博社12月20日的一份报告称&#xff0c;该交易所创始人SBF于12月…

Centos7安装配置Minio

Background 官方下载地址&#xff1a;https://github.com/minio/minio/releases 这里给出本次使用的一个版本&#xff1a;minio-2021-05-11T23:27:41Z&#xff0c;提取码&#xff1a;king 1、下载minio文件夹 其他的版本的相关命令可能发生变化&#xff0c;这里只是针对我提供…

基于GIS的生态安全格局构建之生态阻力面的建立

GIS前沿 一、数据来源介绍 &#xff08;一&#xff09;土地利用数据 土地利用数据来自国土资源三次调查数据&#xff08;2018年&#xff09;&#xff0c;根据研究需要对其进行分析处理。 &#xff08;二&#xff09;生态安全等级数据 利用对从生态属性和生态干扰两方面选择的…

Junit5 + YAML 轻松实现参数化和数据驱动,让 App 自动化测试更高效(一)

登录&#xff1a;不同的用户名&#xff0c;不同的密码&#xff0c;不同的组合都需要做登录场景的测试&#xff0c;正常的排列组合下可能会产生多个用例 搜索&#xff1a;不同的搜索条件产生不同的搜索结果&#xff0c;搜索也是常见的测试项&#xff0c;单个搜索参数或者多种搜…

Java---正则表达式

目录 一、正则表达式的介绍 二、正则表达式的基本语法 &#xff08;1&#xff09;字符类 &#xff08;2&#xff09;预定义符 &#xff08;3&#xff09;数量词 三、正则表达式的具体实例 &#xff08;1&#xff09;判断电话号码是否符合规则 &#xff08;2&#xff09;…

git push踩坑记录【看注意事项】

记录一次git push的踩坑过程&#xff08;详细在注意事项里&#xff0c;列出了具体的解决办法&#xff09;。 push远程仓库命令 使用命令 git init git add . git commit -m "提交说明写在这里" git remote add origin gitgithub.com:xxx/surgical-robot.git git p…