实现一个高并发的Redis分布式锁

news/2024/2/21 10:35:27

1. 无锁场景

下面是一个扣减库存逻辑, 由于查库存和扣减库存两个操作不是原子的,明显存在并发超卖问题

    // 假设初始库存200@GetMapping("/stock")public String stock(@RequestParam(value = "name", defaultValue = "World") String name) {String key = "product:101";Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));if (stock > 0) {stock = stock - 1;redisTemplate.opsForValue().set(key, stock.toString());System.out.println("成功扣减库存, 还剩" + stock);} else {throw new RuntimeException("缺货");}return "200";}

压测结果: 1000人抢200库存商品, 卖出731件,存在超卖问题

2. 单机环境,加synchronized锁

    private static Object STOCK_LOCK = new Object();// 假设初始库存200@GetMapping("/stock")public String stock(@RequestParam(value = "name", defaultValue = "World") String name) {String key = "product:101";synchronized (STOCK_LOCK) {Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));if (stock > 0) {stock = stock - 1;redisTemplate.opsForValue().set(key, stock.toString());System.out.println("成功扣减库存, 还剩" + stock);return "200";}}throw new RuntimeException("缺货");}

压测结果:1000人抢200库存商品, 卖出200件,用例成功

3. 分布式环境,加synchronized锁

准备:这里启动两个节点, 用nginx负载均衡

压测结果:1000人抢200库存商品, 卖出310件,存在超卖问题

4. 分布式环境,redis setnx分布式锁

基础版

主要代码逻辑:

  1. 用setIfAbsent(setnx封装)加锁,同时设置超时时间,锁力度到具体商品
  2. 获取锁后执行减库存逻辑
  3. 执行成功释放锁

代码:

// 假设初始库存200@GetMapping("/stock2")public String stock2(@RequestParam(value = "name", defaultValue = "World") String name) {String key = "product:101";String lockKey = "lock:" + key;Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);if (result) {try {Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));if (stock > 0) {stock = stock - 1;redisTemplate.opsForValue().set(key, stock.toString());System.out.println("成功扣减库存, 还剩" + stock);return "200";}} finally {redisTemplate.delete(lockKey);}}throw new RuntimeException("缺货");}

压测结果:1000人抢200库存商品, 卖出182件,剩余库存18件,业务正常

在低并发,服务器理想情况下, 业务正常,但是还存在一些问题

问题1

现在写死的锁过期时间30秒,但是在服务器压力大时, 接口耗时不稳定, 可能超过过期时间, 锁自动失效, 可能导致超卖

解决:锁续命, 开启一个后台线程, 如果业务没执行完,给锁延长过期时间.

问题2

A线程业务执行完, 准备释放锁时, 肯能刚好锁自动过期,这时候B线程进来抢占到锁正在执行业务,A线程开始删除锁, 此时其他线程都可能去拿到锁,保证不了同步

解决: 释放锁时,判断只有加锁线程才有资格去删除锁

    @GetMapping("/stock3")public String stock3(@RequestParam(value = "name", defaultValue = "World") String name) {String key = "product:101";String lockKey = "lock:" + key;String clientId = UUID.randomUUID().toString();Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);if (result) {try {Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));if (stock > 0) {stock = stock - 1;redisTemplate.opsForValue().set(key, stock.toString());System.out.println("成功扣减库存, 还剩" + stock);return "200";}} finally {// 只能删除自己加的锁, 不让其他线程删if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {/* ...  */redisTemplate.delete(lockKey);}}}throw new RuntimeException("缺货");}

问题3

但是问题2还没彻底解决, 因为比较clientId和删除锁这两个操作不是原子的, 如果中间卡顿,卡顿期间锁刚好自动过期,其他线程占有锁, 这里再执行删除锁就会误删别人锁.

解决: 可用lua脚本执行批量命令,保证原子性

Redisson分布式锁

Redisson是专门处理分布式场景使用Redis的组件, 里面就封装了锁续命,只删自己加的锁,lua脚本,锁重入等功能.

示例:

@Beanpublic Redisson redisson(RedisProperties redisProperties) {// 此为单机模式Config config = new Config();config.useClusterServers().setNodeAddresses(redisProperties.getCluster().getNodes().stream().map(node -> "redis://" + node).collect(Collectors.toList()));return (Redisson) Redisson.create(config);}@Autowiredprivate Redisson redisson;// 假设初始库存200@GetMapping("/stock4")public String stock4(@RequestParam(value = "name", defaultValue = "World") String name) {String key = "product:101";String lockKey = "lock:" + key;RLock rLock = redisson.getLock(lockKey);// 尝试加锁, 加锁失败会间歇阻塞再次加锁, 直至成功rLock.lock();try {Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(key));if (stock > 0) {stock = stock - 1;redisTemplate.opsForValue().set(key, stock.toString());System.out.println("成功扣减库存, 还剩" + stock);return "200";}} finally {rLock.unlock();}throw new RuntimeException("缺货");}

压测结果:1000人抢200库存商品, 卖出200件,用例成功

ReadLock


https://www.xjx100.cn/news/3118877.html

相关文章

C语言——实现一个计算m~n(m<n)之间所有整数的和的简单函数。

#include <stdio.h>int sum(int m, int n) {int i;int sum 0;for ( i m; i <n; i){sum i;}return sum;}int main() { int m, n;printf("输入m和n&#xff1a;\n");scanf("%d,%d", &m, &n);printf("sum %d\n", sum(m, n)…

大数据学习(26)-spark SQL核心总结

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 承认自己的无知&#xff0c;乃是开启智慧的大门 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博主哦&#x1f91…

详解原生Spring当中的额外功能开发MethodBeforeAdvice与MethodInterceptor接口!

&#x1f609;&#x1f609; 学习交流群&#xff1a; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 &#x1f96d;&#x1f96d;3&#xff1a;QQ群&#xff1a;583783…

PTApt——2023年软件设计综合实践_7(数据结构)

6-1 递增的整数序列链表的插入 本题要求实现一个函数&#xff0c;在递增的整数序列链表&#xff08;带头结点&#xff09;中插入一个新整数&#xff0c;并保持该序列的有序性。 答案&#xff1a; 语言选C(gcc) List Insert(List L, ElementType X) {List tmp (List) mal…

flink源码分析之功能组件(四)-slotpool组件II

简介 本系列是flink源码分析的第二个系列&#xff0c;上一个《flink源码分析之集群与资源》分析集群与资源&#xff0c;本系列分析功能组件&#xff0c;kubeclient&#xff0c;rpc&#xff0c;心跳&#xff0c;高可用&#xff0c;slotpool&#xff0c;rest&#xff0c;metrics&…

FastApi接收不到Apifox发送的from-data字符串_解决方法

接收不到Apifox发送的from-data字符串_解决方法 问题描述解决方法弯路总结弯路描述纵观全局小结 问题描述 这里写了一个接口&#xff0c;功能是上传文件&#xff0c;接口参数是file文件和一个id字符串 gpt_router.post("/uploadfiles") async def create_upload_fi…

Web安全漏洞分析-XSS(中)

随着互联网的迅猛发展&#xff0c;Web应用的普及程度也愈发广泛。然而&#xff0c;随之而来的是各种安全威胁的不断涌现&#xff0c;其中最为常见而危险的之一就是跨站脚本攻击&#xff08;Cross-Site Scripting&#xff0c;简称XSS&#xff09;。XSS攻击一直以来都是Web安全领…

编程零基础算法 | 四、循环和选择结构——1572. 矩阵对角线元素的和

一、题目链接 1572. 矩阵对角线元素的和 二、题目简介 给你两个整数&#xff0c;n 和 start 。 数组 nums 定义为&#xff1a;nums[i] start 2*i&#xff08;下标从 0 开始&#xff09;且 n nums.length 。 请返回 nums 中所有元素按位异或&#xff08;XOR&#xff09;后…