当前位置:网站首页>基于Redis(SETNX)实现分布式锁,案例:解决高并发下的订单超卖,秒杀
基于Redis(SETNX)实现分布式锁,案例:解决高并发下的订单超卖,秒杀
2022-07-31 15:36:00 【emgexgb_sef】
高并发的情况下订单超卖/秒杀问题
这里的问题就是在同一时间发过来了两个请求。这两个用户购买同一件数量的商品。线程之间没有互斥。也就是你运行你的。我运行我的。导致在线程1还未实际扣除库存的时候。线程2也获取到了和线程1相同的数量的商品。它两同时扣除库存。这样就出现了订单超卖的问题!!!
解决订单超卖/秒杀的办法
分布式锁
分布式锁就是在多个服务集群的模式下 每个服务下的线程可以做到互斥的效果 这就是分布式锁 并且分布式锁还要达到 高性能 高可见 高互斥 高可用 这样才叫分布式锁
SETNX的作用
setnx就是使用redis实现分布式锁的一种方式 它的特点就是当有重复的锁key查询缓存就会返回false 或者空 那我们就可以利用它这种机制去实现多个服务同时调用同一个方法让所有服务都有互斥的效果
代码实现创建锁
private final static String SUO_KEY = "cs:suo:";
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 获取锁
* je: 订单唯一ID
* @return
*/
public boolean huoqu(Long je) {
// 生成uuid 做锁的value
String uuid = UUID.randomUUID().toString();
// stringRedisTemplate.opsForValue().setIfAbsent(SUO_KEY + je, uuid,60,TimeUnit.SECONDS)
// setIfAbsent() :在java中使用setnx就需要调用setIfAbsent()他会返回一个状态值
// 根据你当前传送的key 判断是否存在。如果存在返回false。表示锁已被其他线程创建
// 根据你当前传送的key 判断是否存在。如果存在返回true。表示锁已被当前创建成功
// SUO_KEY + je :锁的唯一key
// uuid:value值
// 60,TimeUnit.SECONDS :失效时间 一定要设置失效时间。防止业务代码异常无法释放锁。可以靠失效时间自动释放
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(SUO_KEY + je, uuid,60,TimeUnit.SECONDS));
}
代码实现释放锁
/**
* 释放锁
* 释放锁就等于将对应的锁key 在缓存中删除即可
* @return
*/
public void shanc(Long je) {
stringRedisTemplate.delete(SUO_KEY + je);
}
代码实现解决订单超卖/秒杀
package com.macro.mall.service.impl;
import com.macro.mall.common.api.CommonResult;
import com.macro.mall.common.api.ResultCode;
import com.macro.mall.dao.RedisSetNxDAO;
import com.macro.mall.service.RedisSetNxService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class RedisSetNxServiceImpl implements RedisSetNxService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedisSetNxDAO redisSetNxDAO;
private final static String SUO_KEY = "cs:suo:";
@Override
public CommonResult cs(Long uid, Long je) {
// 获取锁
boolean huoQu = huoqu(je);
if (!huoQu) {
try {
// 没有获取到 持续获取 也可以直接返回一个状态 让他重新请求
while (!huoqu(je)){
// 没有获取到锁 休眠一段时间在重新获取
// 目的是为了和上一个线程的业务代码错开
// 避免不必要的cpu消耗
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
throw new RuntimeException("异常了!!!!!!!");
}
}
// 获取到了锁 执行代码
try {
// 业务代码
// 查询剩余库存
Long num = redisSetNxDAO.findKc(je);
if (num == null || num < 1) {
// 库存不足。返回状态
System.out.println("库存不足!!!!!");
return CommonResult.failed(ResultCode.FAILED, "库存不足!!!!!");
}
// 库存足够 减库存
// 获取当前线程ID
Long id = Thread.currentThread().getId();
// 扣减库存
redisSetNxDAO.findDele(je);
// 生成订单 记录基本信息
redisSetNxDAO.saveKc(uid, je, Long.valueOf(i));
} catch (Exception e) {
throw new RuntimeException("异常了!!!!!!!");
} finally {
// 锁释放 释放锁一定要放在 finally 下
// 防止业务代码报错。导致锁无法释放。造成死锁
shanc(je);
}
return CommonResult.success(ResultCode.SUCCESS);
}
/**
* 获取锁
*
* @return
*/
public boolean huoqu(Long je) {
String uuid = UUID.randomUUID().toString();
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(SUO_KEY + je, uuid,60,TimeUnit.SECONDS));
}
/**
* 释放锁
*
* @return
*/
public void shanc(Long je) {
stringRedisTemplate.delete(SUO_KEY + je);
}
}
SETNX特点
setnx是redis的锁机制 意思是只能存在一个唯一的key 如果缓存中已经存在这个key 在向缓存保存相同的key那么就会返回一个false 代表锁以被占有 只有获取锁的线程删除了这个key 其他线程才可以继续获取锁 这样就实现了 多线程之间互斥的效果。
注意!!!
锁误删1
因为设置每个锁的key都会带有一个失效的时间 这么做的目的就是因为怕业务代码出了问题报错了 没有执行删除锁 导致锁阻塞 所以我们可以先给这个锁加一个失效的时间 这样就算业务代码不删除锁 它自己也会释放 但是存在的问题就是 如果的业务代码指定的时间超过了 锁的失效时间 导致 你业务没执行完 锁已经释放了 这时候其他的线程就会获取到这个锁 这时候你这个业务执行完了 执行了删除锁 但是你删除的是线程2的锁 所以这时候锁又被释放了 这种恶性的循环解决方法
每次存到锁key的value值保证一个唯一 也就是这个value只有这一个线程的 那么当失效失效时间过了 其他线程已经拿到了锁 你执行删除锁的时候 先拿你锁的value值。判断你设置的锁key是否存在 如果存在判断是否是和你当前线程设置的value值 如果一致 表示这个锁还是你当时设置的锁 如果不是 表示锁已经释放过 并且已经被其他的线程重新的设置 那么你就不能在执行删除锁锁误删2
还有一种就是每次删除锁的时候 都会先判断value值是否相等 相等的删除 不等的不删除 那么如果在你判断完成后 是相等的 并且在删除锁的时候 由于某种原因导致代码阻塞了一段时间 并且时间超过了锁的失效时间 这时候锁被释放了 其他线程获取到了这个锁 而这时候 如果线程不阻塞了 那么就会执行删除锁 因为已经走过了判断条件。所以就会直接执行删除锁操作。其实你删除的已经是其他线程重新设置的锁 这样就又会出现恶性循环的问题解决方法
如果想要解决这种问题 可以直接将查询和删除放到一起执行 但是redis中这两个操作本身就是分为两部分操作的 所以不可以在代码中实现同步操作 只能使用lua脚本保证同步操作 lua脚本就可以实现 将所有的Redis命令都放到一块然后顺序执行 这样只要操作了查询就必然会执行删除 中间不会有任何打断
后期有时间更新RedisSon实现分布式锁 性能更强大
ps:
菜鸟一枚 一直在学习 从未敢停止
先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦
边栏推荐
- 工程水文学复习资料
- Getting Started with TextBlock Control Basic Tools Usage, Get Started
- 浏览器自带的拾色器
- 多主复制的适用场景(1)-多IDC
- 国内市场上的BI软件,到底有啥区别
- C language "the third is" upgrade (mode selection + AI chess)
- ansible学习笔记02
- 实现防抖与节流函数
- Kubernetes principle analysis and practical application manual, too complete
- WeChat chat record search in a red envelope
猜你喜欢
What is the difference between BI software in the domestic market?
Female service community product design
TRACE32——常用操作
国内市场上的BI软件,到底有啥区别
TRACE32 - Common Operations
TextBlock控件入门基础工具使用用法,取上法入门
01 邂逅typescript,环境搭建
[Meetup Preview] OpenMLDB+OneFlow: Link feature engineering to model training to accelerate machine learning model development
腾讯云部署----DevOps
t-sne 数据可视化网络中的部分参数+
随机推荐
json到底是什么(c# json)
网银被盗?这篇文章告诉你如何安全使用网银
基于ABP实现DDD
hough变换检测直线原理(opencv霍夫直线检测)
Internet banking stolen?This article tells you how to use online banking safely
复制延迟案例(1)-最终一致性
Snake Project (Simple)
国内市场上的BI软件,到底有啥区别
01 Encounter typescript, build environment
Kubernetes principle analysis and practical application manual, too complete
Getting Started with TextBlock Control Basic Tools Usage, Get Started
四象限时间管理有多好用?
Precautions and solutions when SIGABRT error is reported
Replication Latency Case (3) - Monotonic Read
Synchronized和volatile 面试简单汇总
BGP综合实验(建立对等体、路由反射器、联邦、路由宣告及聚合)
radiobutton的使用
TRACE32——常用操作
Implement anti-shake and throttling functions
C语言”三子棋“升级版(模式选择+AI下棋)