微服务项目【分布式锁】

news2024/10/2 6:33:52

创建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: 0
    port: 6379

模拟高并发场景秒杀下单

场景模拟

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

案例演示

  • 示例一:单线程情况

直接打开浏览器输入:http://localhost:8081/updateStock,查看redis中库存扣减情况。

  • 示例二:多线程情况

第1步:配置多启动服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IJol4PhL-1676642300167)(images\20220824220250.jpg)]

第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级分布式锁

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OCvM5BJf-1676642300170)(images\2022-08-24_144236.png)]

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

redis级分布式锁

什么是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";
}

场景分析

基于以上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:高并发场景下,线程执行先后顺序无法把控(自己加的锁被其他线程释放掉了

场景分析

线程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分布式锁+源码解读

什么是Redisson

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

特点

  • 互斥:在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
  • 防止死锁:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
  • 性能:对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。所以在锁的设计时,需要考虑两点。
    • 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
    • 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
  • 重入:ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。

Redisson工作原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XW61ORr2-1676642300172)(images\1090617-20190618183025891-1248337684.jpg)]

入门案例

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

秒杀项目整合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(0);
        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压测。

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

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

相关文章

java程序设计-ssm博客管理系统

博客管理系统是一个用于创建、管理和发布博客文章的应用程序。它通常包括一个后台管理界面&#xff0c;用于管理用户、文章、评论、标签等数据。同时&#xff0c;它还包括一个前端界面&#xff0c;用于展示博客文章并提供交互功能&#xff0c;例如评论和分享。 博客管理系统可…

Linux/Ubuntu安装部署Odoo15仓管系统,只需不到十步---史上最成功

sudo apt-get update sudo apt install postgresql -y sudo apt-get -f install sudo dpkg -i /home/ubuntu/odoo_15.0.latest_all.deb —报错再次执行上一条命令再执行 —安装包地址&#xff1a;http://nightly.odoo.com/15.0/nightly/deb/–翻到最下面 sudo apt-get ins…

NIFI大数据进阶_内嵌ZK模式集群2_实际操作搭建NIFI内嵌模式集群---大数据之Nifi工作笔记0016

然后我们开始来搭建nifi集群,可以看到之前我们上传上来的安装包 然后我们因为当前目录有了,我先去解压到其他目录 这里解压到/export/soft下面去 然后进去soft去看看,可以看到已经有了 然后我们说我们要搭建3个nifi的集群,那么这个时候,需要复制3份,但是 我们为了方便这里先…

SpringBoot 日志文件

(一)日志文件有什么用&#xff1f;除了发现和定位问题之外&#xff0c;我们还可以通过日志实现以下功能&#xff1a;记录用户登录日志&#xff0c;以便分析用户是正常登录还是恶意破解用户。记录系统的操作日志&#xff0c;以便数据恢复和定位操作 。记录程序的执行时间&#x…

字节码指令

目录 2.1 入门 2.2 javap 工具 2.3 图解方法执行流程 1&#xff09;原始 java 代码 2&#xff09;编译后的字节码文件 3&#xff09;常量池载入运行时常量池 4&#xff09;方法字节码载入方法区 5&#xff09;main 线程开始运行&#xff0c;分配栈帧内存 6&#xff09;…

百度地图API添加自定义标记解决单html文件跨域

百度地图API添加自定义标记解决单html文件跨域 因为要往百度地图上添加一些标注点&#xff0c;而且这些标注点要用自定义的图片&#xff0c;而且只能使用单html文件&#xff0c;不能使用服务器&#xff08;也别问为什么&#xff0c;就是这么个需求&#xff09;&#xff0c;做起…

互联网大厂测开面试记,二面被按地上血虐,所幸Offer已到手

在互联网做了几年之后&#xff0c;去大厂“镀镀金”是大部分人的首选。大厂不仅待遇高、福利好&#xff0c;更重要的是&#xff0c;它是对你专业能力的背书&#xff0c;大厂工作背景多少会给你的简历增加几分竞争力。 如何备战面试的&#xff1f; 第一步&#xff1a;准备简历…

第三方软件测试机构▏软件性能测试的测试流程和指标简析

软件性能是衡量软件产品质量的重要指标之一&#xff0c;性能测试也是软件测试中不可或缺的重要流程&#xff0c;主要测试软件性能方面的质量&#xff0c;它是一种非功能性的测试。进行性能测试是为了保障软件能够在期望的负载下运行良好&#xff0c;并且通过发现性能问题来消除…

【IP课堂】Ip地址如何进行精准定位?

通过Ip地址定位&#xff0c;是目前网络上最常见的定位方式。当然&#xff0c;也是最简单的定位方式。其实方法大多都是雷同的&#xff0c;通过Ip定位&#xff0c;就目前网上公开的技术。如通过搜索关键词“定位&#xff0c;定位查询&#xff0c;Ip定位”等&#xff0c;只能查询…

《狂飙》壁纸太帅,Python自动切换太酷(8)

小朋友们好&#xff0c;大朋友们好&#xff01;我是猫妹&#xff01;要说最近什么电视剧最火&#xff1f;非《狂飙》莫属。《狂飙》剧名来自毛主席诗词“国际悲歌歌一曲&#xff0c;狂飙为我从天落”。导演借用“狂飙”二字来比喻剧中的扫黑除恶大风暴。据了解&#xff0c;《狂…

店铺微信公众号怎么创建?

有些小伙伴问店铺微信公众号怎么创建&#xff0c;在解答这个问题之前&#xff0c;先简单说说店铺和微信公众号关系&#xff1a; 店铺一般是指小程序店铺&#xff0c;商家通过小程序店铺来卖货&#xff1b;微信公众号则是一个发布信息的平台。但是两者之间可以打通&#xff0c;…

如何去选择合适的自动化测试工具?

自动化测试是高质量软件交付领域中最重要的实践之一。在今天的敏捷开发方法中&#xff0c;几乎任一软件开发过程都需要在开发阶段的某个时候进行自动化测试&#xff0c;以加速回归测试的工作。自动化测试工具可以帮助测试人员以及整个团队专注于自动化工具无法处理的各自任务&a…

搭建Hexo博客-第5章-一些我用到的小技巧

搭建Hexo博客-第5章-一些我用到的小技巧 搭建Hexo博客-第5章-一些我用到的小技巧 搭建Hexo博客-第5章-一些我用到的小技巧 这大概是这组博客地最后一篇文章了&#xff0c;在这篇文章里&#xff0c;我想和大家分享一些我写博客时用到的小技巧。 Vs Code 我用的编辑器是 Vs C…

SpringBoot-自动配置-@Import注解与@EnableAutoConfiguration注解

Import注解 Enable* 底层依赖于 Import 注解导入一些类&#xff0c;使用 Import 导入的类会被 Spring 加载到 IOC 容器中Import 提供了4种用法&#xff1a; 1.导入Bean2.导入配置类3.导入ImportSelector实现类&#xff1b;一般用于加载配置文件中的类4.导入ImportBeanDefinitio…

阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由

说在前面 在微服务的应用开发中&#xff0c;DDD 用得越来越普及。 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;DDD是一个非常、非常高频的交流话题。 最近&#xff0c;有小伙伴面试阿里时&#xff0c;遇到一个面试题&#xff1a; 谈谈你对DDD的理解&#xff1f; 小伙…

我应该在我的博客上写什么? 介绍如何撰写初学者容易担心的文章

我想有很多人开了博客&#xff0c;但想不起来写作&#xff0c;无法取得进展。 博客的主题和文章的内容不会仅仅通过写你想做的事情来工作。 重要的是要了解用户想要阅读的内容以及人们可能收集的内容&#xff0c;并将其与您想要编写的内容很好地匹配。 这一次&#xff0c;我…

怎么拥有一个帅气的 CMD 命令窗口 ❓ - Windows

自从拥有这样一个炫酷的命令窗口&#xff0c;我都舍不得关掉它了 关于我为什么我要闲的去 “打扮” 一个命令窗口&#xff0c;这要从星期五下午的一场 摸鱼 &#x1f420; 开始&#xff0c;当时我要创建一个 vue ts vite 的项目练练手&#xff0c;为新项目开始做准备&#x…

spring-web InvocableHandlerMethod 源码分析

说明 本文基于 jdk 8, spring-framework 5.2.x 编写。author JellyfishMIX - github / blog.jellyfishmix.comLICENSE GPL-2.0 类层次 HandlerMethod&#xff0c;处理器的方法的封装对象。HandlerMethod 只提供了处理器的方法的基本信息&#xff0c;不提供调用逻辑。 Invoca…

大数据面试小抄

项目地址&#xff1a;https://github.com/GTyingzi/BigDATA 该项目是自己在学习大数据过程中整理、总结下来的一份面试小抄。涵盖Hadoop、Spark、Flink、Hive、HBae、Kafka、ES、Zookeeper等。 开源给大家&#xff0c;若感觉不错欢迎star~ 摘取Flink部分如下文章目录FlinkFli…

Zebra ZT410 ZT411 导入中文字体

1.设置--发送到打印机 2.字体--添加--下载 --windows 字体库下载到zebra 打印机 3.字体--工具--调用 ZEBRA驱动包中驱动。 4.老版打印机导入方式 Zebra ZPL条形码打印机上如何下载和使用TrueType或External字体-敏用数码(上海北京济南洛阳)|专注于条码数据处理 (chongshang.co…