当前位置: 首页 > news >正文

微服务项目【分布式锁】

创建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.1password: 123456database: 0port: 6379

模拟高并发场景秒杀下单

场景模拟

@RestController
public class RedissonController {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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;@Beanpublic 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;@Beanpublic 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.lryc.cn/news/10401.html

相关文章:

  • JavaWeb5-线程常用属性
  • JVM调优及垃圾回收GC
  • JAVA练习53-打乱数组
  • 基于RK3588的嵌入式linux系统开发(三)——Uboot镜像文件合成
  • wireshark抓包后通过工具分包
  • 举个栗子~Tableau 技巧(251):统一多个工作表的坐标轴范围
  • Centos7 调整磁盘空间
  • 小菜版考试系统——“C”
  • Twitter被封号了?最详细的申诉教程在此
  • Docker 安装配置
  • 死锁检测组件-设想
  • 线程池的使用
  • 字节码指令
  • TLS/SSL证书彻底扫盲
  • WGCNA | 值得你深入学习的生信分析方法!~(网状分析-第五步-高级可视化)
  • try catch finally执行顺序
  • 2023年数学建模美赛D题(Prioritizing the UN Sustainability Goals)分析与编程
  • 35岁测试工程师被辞退,给你们一个忠告
  • 华为OD机试题 - 租车骑绿岛(JavaScript)
  • Linux下Python脚本的编写解析fio(minimal格式)(三)
  • 【实战场景二】如何设计一个分布式锁?
  • Java中ThreadLocal类详解
  • 从一致性角度考虑推荐冷启动长尾推荐问题(一)
  • 电脑(Windows)常用快捷键
  • Java类加载器
  • 信号完整性设计规则之单根信号失真最小化
  • Python3 数据结构
  • Compose-Navigation带参传递
  • 【函数栈帧的创建和销毁】 -- 神仙级别底层原理,你学会了吗?
  • Promise的使用及原理