Redis实现优惠券秒杀功能

Redis实现优惠券秒杀功能

  • 全局唯一ID
  • 实现优惠券秒杀下单
  • 实现一人一单
  • 分布式锁
  • Redis秒杀优化
  • Redis消息队列

全局唯一ID

订单表使用数据库自增ID的缺点:
1、ID规律性太明显。
2、受单表数据量限制,需要分库分表时数据库自增ID失效。
全局ID生成器:在分布式系统中生成全局唯一ID的工具,需要满足以下特性:
1、唯一性
2、高可用
3、高性能
4、递增性
5、安全性
例如Redis的string类型的incr命令。
但是为了ID安全性,不能直接使用Redis自增的数值,而是拼接其他信息。
例如,使用Long类型,占8个字节,64bit位,1bit符号位,前31bit使用时间戳,支持69年使用时间,后32位使用序列号,每秒产生2^32个不同ID。
基于Redis实现全局唯一ID例子:

@Component
public class RedisIdWorker {@Autowiredprivate StringRedisTemplate stringRedisTemplate;private static final long BEGIN_TIMESTAMP = 1704067200l;//序列号位数private static final int COUNT_BITS = 32;public long nextId(String keyPrefix) {//生成时间戳LocalDateTime now = LocalDateTime.now();long nowSeconds = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSeconds - BEGIN_TIMESTAMP;//生成序列号,每天2^32个IDString date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));Long count = stringRedisTemplate.opsForValue().increment("incr:" + keyPrefix + ":" + date);return timestamp << COUNT_BITS | count;}public static void main(String[] args) {LocalDateTime time = LocalDateTime.of(2024, 1, 1, 0, 0, 0);long seconds = time.toEpochSecond(ZoneOffset.UTC);System.out.println(seconds);}
}

实现优惠券秒杀下单

优惠券分为平价券和特价券,平价券可以任意购买,特价券需要秒杀抢购。这里针对特价券而言。
tb_voucher包含平价券和特价券,tb_seckill_voucher只描述特价券,包含库存,生效时间,失效时间。
秒杀下单需要判断:
1、秒杀是否开始或者是否已经结束
2、库存是否足够,足够才扣减库存并且创建订单
功能实现后,高并发场景下出现了超卖,剩余库存是负数!原因是多个线程先查询库存,后续进行扣减,整个操作不是原子性的。
解决:
1、加悲观锁
比如synchronized,Lock,或者分布式锁。缺点是开销较大。
2、加乐观锁:
版本号法,每次修改时判断和刚刚查询的版本号是否一致,修改后版本号+1。优点是性能好,缺点是有可能出现少买。
判断库存大于0即可,判断条件为where id = ? and stock > 0; 完美解决。

实现一人一单

同一优惠券,一个用户只能下一单。
先判断订单表中是否存在用户id和优惠券id的记录,如果记录数大于0,则返回失败。
缺点,用户还是能下多单,因为多个线程判断成功后可以继续购买,和后续扣减库存,生成订单不是原子性操作。
这只能使用悲观锁,因为乐观锁只能适用修改,但是这里是新增操作。
这里对用户id加锁,但是为了防止对象变化,使用字符串,并且调用intern方法在字符串常量池中取对象。
锁的范围应该是整个事务提交之后,防止事务还没提交,但是锁被释放其他线程又加锁下单。
事务失效,使用this.createVoucherOrder(),需要拿到当前对象的代理对象。
需要添加依赖AspectJ,启动类添加注解暴露代理对象。

分布式锁

之前的锁都是本地锁,考虑分布式情况下,多个机器的锁不共享,需要使用分布式锁。
分布式锁参考博客。
为什么Redisson没有锁自动续期?
这里碰到一个问题,观察分布式锁的ttl时发现没有自动续期,而是过期删除了,这是因为使用Debug模式,因此Redisson没有自动续期。

Redis秒杀优化

原本流程:
1、查询优惠券
2、判断秒杀库存
3、查询订单
4、校验一人一单
5、减库存
6、创建订单
优化:
1、将秒杀库存和订单信息缓存到Redis中
2、保存优惠券id,用户id,订单id到阻塞队列,返回订单id
3、异步读取阻塞队列信息,完成下单
库存使用string类型,订单信息使用set类型,并且使用lua脚本保证原子性。
基于阻塞队列的异步秒杀存在哪些问题?
1、使用的是JVM的阻塞队列,内存有限。
2、如果宕机,内存数据丢失。
解决:使用消息队列。

local voucherId = ARGV[1]
local userId = ARGV[2]
local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherIdif (tonumber(redis.call('get', stockKey)) <= 0) then--库存不足,返回1return 1
endif (redis.call('sismember', orderKey, userId) == 1) then--重复下单,返回2return 2
endredis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
--成功,返回0
return 0
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();@PostConstructprivate void init() {SECKILL_ORDER_EXECUTOR.execute(new VoucherOrderHandler());}private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {VoucherOrder voucherOrder = orderTasks.take();handlerVoucherOrder(voucherOrder);} catch (Exception e) {log.error("处理订单异常", e);}}}}private void handlerVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();RLock lock = redissonClient.getLock("lock:order:" + userId);boolean isLock = lock.tryLock();if (!isLock) {log.error("不允许重复下单");return;}try {proxy.createVoucherOrder(voucherOrder);} finally {lock.unlock();}}//由于有扣减库存和写入订单,因此使用事务@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {//一人一单Long userId = voucherOrder.getUserId();Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();if (count > 0) {log.error("用户已经购买过一次!");return ;}//扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder.getVoucherId()).ge("stock", 1).update();if (!success) {log.error("库存不足!");return ;}//写入订单save(voucherOrder);}private IVoucherOrderService proxy;@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString());int r = result.intValue();if (r != 0) {return Result.fail(r == 1 ? "库存不足!" : "不能重复下单!");}//保存信息到阻塞队列VoucherOrder voucherOrder = new VoucherOrder();//使用全局唯一ID生成订单ID,而不是使用数据库自增IDlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);orderTasks.add(voucherOrder);proxy = (IVoucherOrderService) AopContext.currentProxy();return Result.ok(0);}}
}

Redis消息队列

1、基于List实现消息队列。
使用lpush结合rpop;或者rpush结合lpop,为了实现阻塞效果,取出时使用blopo和brpop。
2、基于PubSub实现消息队列。
在Redis2.0引入的消息传递模型。消费者可以订阅多个channel。
subcribe channel
publish channel msg
psubscribe pattern
3、基于stream实现消息队列。
在Redis5.0引入的新数据类型,可以实现功能完善的消息队列,并且可以持久化。
xadd key id field value,发送消息
xread count streams key id
缺点:ID为$表示读取最新消息,但是如果多条消息到达,只会读取最新的那条。
消费者组。

使用stream实现消息队列,实现异步秒杀下单
1、创建stream类型的消息队列,名为stream.orders
2、修改lua脚本,向stream.orders中添加消息,包含优惠券id,用户id,订单id
3、项目启动时,开启线程任务,尝试获取stream.orders小夏,完成下单
创建消息队列命令:

XGROUP CREATE stream.orders g1 0 MKSTREAM

lua脚本

local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherIdif (tonumber(redis.call('get', stockKey)) <= 0) then--库存不足,返回1return 1
endif (redis.call('sismember', orderKey, userId) == 1) then--重复下单,返回2return 2
end--扣库存
redis.call('incrby', stockKey, -1)
--下单
redis.call('sadd', orderKey, userId)--发送到消息队列
redis.call("xadd", "stream.orders", "*", "userId", userId, "voucherId", voucherId, "id", orderId)--成功,返回0
return 0
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();@PostConstructprivate void init() {SECKILL_ORDER_EXECUTOR.execute(new VoucherOrderHandler());}private class VoucherOrderHandler implements Runnable {String queueName = "streams.order";@Overridepublic void run() {while (true) {try {List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create(queueName, ReadOffset.lastConsumed()));if (list == null || list.isEmpty()) {continue;}MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handlerVoucherOrder(voucherOrder);stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());} catch (Exception e) {log.error("处理订单异常", e);handlePendingList();}}}private void handlePendingList() {while (true) {try {List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1),StreamOffset.create(queueName, ReadOffset.from("0")));if (list == null || list.isEmpty()) {break;}MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handlerVoucherOrder(voucherOrder);stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());} catch (Exception e) {log.error("处理pending-list异常", e);try {Thread.sleep(50);} catch (InterruptedException ex) {ex.printStackTrace();}}}}}// private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);//// private class VoucherOrderHandler implements Runnable {////     @Override//     public void run() {//         while (true) {//             try {//                 VoucherOrder voucherOrder = orderTasks.take();//                 handlerVoucherOrder(voucherOrder);//             } catch (Exception e) {//                 log.error("处理订单异常", e);//             }//         }//     }// }private void handlerVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();RLock lock = redissonClient.getLock("lock:order:" + userId);boolean isLock = lock.tryLock();if (!isLock) {log.error("不允许重复下单");return;}try {proxy.createVoucherOrder(voucherOrder);} finally {lock.unlock();}}//由于有扣减库存和写入订单,因此使用事务@Transactionalpublic void createVoucherOrder(VoucherOrder voucherOrder) {//一人一单Long userId = voucherOrder.getUserId();Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();if (count > 0) {log.error("用户已经购买过一次!");return ;}//扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder.getVoucherId()).ge("stock", 1).update();if (!success) {log.error("库存不足!");return ;}//写入订单save(voucherOrder);}private IVoucherOrderService proxy;@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();//使用全局唯一ID生成订单ID,而不是使用数据库自增IDlong orderId = redisIdWorker.nextId("order");Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(), String.valueOf(orderId));int r = result.intValue();if (r != 0) {return Result.fail(r == 1 ? "库存不足!" : "不能重复下单!");}proxy = (IVoucherOrderService) AopContext.currentProxy();return Result.ok(0);}}
}

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

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

相关文章

docker 修改运行容器环境变量

文章目录 前言第一步&#xff1a;查看Docker Root目录第二步&#xff1a;查到容器的长id&#xff08;container id&#xff09;第三步&#xff1a;停止容器第四步&#xff1a;编辑修改环境变量env第五步&#xff1a;重载服务的配置文件第六步&#xff1a;重启docker 总结 前言 …

力扣416. 分割等和子集

Problem: 416. 分割等和子集 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 该题目可以归类为0-1背包问题&#xff0c;具体到细节可以再归纳为背包是否装满问题 1.首先判断数组元素和的奇偶性&#xff08;奇数则不能划分&#xff09; 2.我们定义一个二维布尔类型数组…

【opencv】opencv透视变换和ocr识别实验

实验环境&#xff1a;anaconda、jupyter notebook 实验用到的包opencv、numpy、matplotlib、tesseract 一、opencv透视变换 原图 图片是我拍的耳机说明书&#xff0c;哈哈哈哈&#xff0c;你也可以使用自己拍的照片&#xff0c;最好是英文内容&#xff0c;tesseract默认识别英…

iOS Xcode Debug View Hierarchy 查看视图层级结构

前言 我们难免会遇到接手别人项目的情况&#xff0c;让你去改他遗留的问题&#xff0c;想想都头大&#xff0c;&#x1f602;可是也不得不面对。作为开发者只要让我们找到出问题的代码文件&#xff0c;我们就总有办法去解决它&#xff0c;那么如何快速定位问题对应的代码文件呢…

FullCalendar日历组件集成实战(4)

背景 有一些应用系统或应用功能&#xff0c;如日程管理、任务管理需要使用到日历组件。虽然Element Plus也提供了日历组件&#xff0c;但功能比较简单&#xff0c;用来做数据展现勉强可用。但如果需要进行复杂的数据展示&#xff0c;以及互动操作如通过点击添加事件&#xff0…

python如何做一个服务器fastapi 和flask

用 fastapi 方式的话 from fastapi import FastAPIapp FastAPI()app.get("/api") def index():return "hello world"然后需要安装 uvicorn 并执行下面的命令 uvicorn server:app --port 8000 --reload最终 如果是用 flask 直接写下面的代码 # -*- cod…

IP代理网络协议介绍

在IP代理页面上&#xff0c;存在HTTP/HTTPS/Socks5三种协议。它们都是客户端与服务器之间交互的协议。 HTTP HTTP又称之为超文本传输协议&#xff0c;在因特网使用范围广泛。它是一种请求/响应模型&#xff0c;客户端向服务器发送请求&#xff0c;服务器解析请求后对客户端作出…

【玩转Google云】自动化构建和推送 Docker 镜像到 GCP Artifact Registry

在现代软件开发中,持续集成和持续部署(CI/CD)已成为提高开发效率和代码质量的重要手段。而 Jenkins 作为流行的 CI/CD 工具,在自动化构建和部署流程中扮演了关键角色。本文将详细介绍如何利用 Jenkins 构建一个简单的 Docker 镜像,并将其推送到 Google Cloud Platform (GC…

20240514基于深度学习的弹性超材料色散关系预测与结构逆设计

论文&#xff1a;Dispersion relation prediction and structure inverse design of elastic metamaterials via deep learning DOI&#xff1a;https://doi.org/10.1016/j.mtphys.2022.100616 1、摘要 精心设计的超材料结构给予前所未有的性能&#xff0c;保证了各种各样的具…

成功的自定义类加载器应用案例

1、热部署和热更新 在一些复杂的系统中&#xff0c;尤其是大型分布式应用、中间件和Web服务器中&#xff0c;需要快速地上线和更新服务。通过自定义类加载器&#xff0c;可以实现热部署和热更新&#xff0c;即在不重启应用的情况下&#xff0c;动态地加载新的类或更新已存在的…

Linux学习笔记(Socket)

Linux-Socket 1、基础知识2、服务端3、客户端4、读写操作4.1、读写函数4.2、阻塞IO和非阻塞IO 5、例程 1、基础知识 socket用于计算机之间的网络通信&#xff0c;无论是构建服务器还是客户端&#xff0c;我们仅需要三个信息&#xff0c;服务器的ip地址&#xff0c;对应进程的端…

OpenAI 新发布的 GPT-4o,有血有肉的Ai来了,可实时语音视频交互

今天&#xff0c;OpenAI又又又开发布会了。 在大众心里&#xff0c;现在也基本上都知道&#xff0c;奥特曼是一个贼能PR的人。 每一次的PR的时间点&#xff0c;都拿捏的极其到位&#xff0c;精准的狙击其他厂商。比如说上一次Sora&#xff0c;其实你会发现从头到尾就是一个PR的…

奥维地图下载高清影像的两种方式!以及ArcGIS、QGIS、GlobalMapper、自编工具下载高清影像的方法推荐!

今天来介绍一下奥维互动地图是如何下载高清影像的&#xff0c;也不是多了不起的功能&#xff01;有朋友问&#xff0c;加上这个软件确实用的人多。 下载的高清数据在ArcGIS中打开的效果&#xff01; 开始介绍奥维之前我们也介绍一下我们之前介绍的几个方法&#xff0c;没有优劣…

zabbix触发器配置定期生效教程

在企业生产过程中&#xff0c;并非所有的设备都需要全天候、满负载运转&#xff0c;也有些仅需要周期性的运转即可。例如&#xff0c;在某家企业&#xff0c;有一批这样的机器&#xff0c;每天都会在固定的时间跑批量任务&#xff0c;期间&#xff0c;机器的CPU使用率会有明显的…

Pytorch学习-利用Dataset类定义自己的数据集

定义自己的数据集类需要继承torch.utils.data中的Dataset类 主要实现两个方法&#xff0c;即__len__和__getitem__ from torch.utils.data import Dataset class VOCDataSet(Dataset):#初始化def __init__(self):pass#返回数的长度def __len__(self):pass#返回样本和标签def …

LeetCode 126题:单词接龙 II

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

CMake配置安装gdal3.6.2库

安装GDAL 3.6.2库可以通过使用CMake来完成&#xff0c;尤其是在Windows平台上。这里我将提供一个详细的步骤指南&#xff0c;包括如何配置CMake以及如何使用它来构建和安装GDAL。前提是你已经安装了CMake和相应的编译器&#xff08;如Visual Studio或GCC&#xff09;。 ### 步骤…

联软安渡 UniNXG 安全数据交换系统 任意文件读取漏洞复现

0x01 产品简介 联软安渡UniNXG安全数据交换系统,是联软科技自研的业内融合网闸、网盘和DLP的一体机产品,它同时支持多网交换,查杀毒、审计审批、敏感内容识别等功能,是解决用户网络隔离、网间及网内数据传输、交换、共享/分享、存储的理想安全设备,具有开创性意义。 UniN…

什么是BI看板?选择BI看板制作工具时一定要考虑这些方面

BI看板也称为商业智能仪表板&#xff0c;是一种直观的数据可视化工具&#xff0c;它将关键业务指标&#xff08;KPIs&#xff09;和数据以图表、图形和表格的形式集中展示&#xff0c;使用户能够快速获取企业运营的实时概览。 这种数据可视化方式不仅使得复杂的数据信息易于理…

Python图嵌入信息潜在表征算法

&#x1f4dc;用例 &#x1f4dc;Python社群纽带关系谱和图神经 | &#x1f4dc;C和Python通信引文道路社评电商大规模行为图结构数据模型 | &#x1f4dc;角色图嵌入学习 | &#x1f4dc;图全局结构信息学习 | &#x1f4dc;图编码解码半监督学习 | &#x1f4dc;富文本表征学…