SpringBoot+Redis的lua脚本实现高性能秒杀下单需求
mhr18 2024-11-05 10:26 36 浏览 0 评论
秒杀抢购可以说是在分布式环境下?个?常经典的案例。
- ?并发时间极短、瞬间?户量?,?瞬间的?QPS把系统或数据库直接打挂,响应失败,导致与这个系统耦合的系统出问题。
- 超卖: 你只有?百件商品,由于是?并发的问题,导致超卖的情况。
目前秒杀的实现方案主要有两种:
- 用redis 将抢购信息进行存储。然后再慢慢消费。 同时,服务器给与用户快速响应。
- 用mq实现,比如RabbitMQ,服务器将请求过来的数据先让RabbitMQ存起来,然后再异步消费。
也可以结合redis与mq的方式,通过redis控制剩余库存,达到快速响应,将满足条件的购买的订单先让RabbitMQ存起来,后续进行异步消费。
整体流程
- 服务器接收到了大量用户请求过来(1s 2000个请求)。比如传了用户信息,产品信息,和购买数量信息。此时 服务器采用redis 的lua 脚本 去调用redis 中间件。lua 脚本的逻辑是减库存,校验库存是否足够。然后迅速给与服务器反馈(库存是否够,够返回 1 ,不够返回 0)。
- 服务器迅速给与用户的请求反馈。提示抢购成功.或者抢购失败。
- 抢购成功将订单信息放入MQ,其余线程接收到MQ的信息后,将订单信息存入DB中 。
- 后面客户就可以查询 mysql 的订单信息了。
代码展示
架构采用springboot+redis+mysql+myBatis.
商品表:
CREATE TABLE `tb_product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'id',
`price` decimal(65,18) NOT NULL DEFAULT '0',
`available_qty` bigint NOT NULL DEFAULT '0' COMMENT '数量',
`title` varchar(1024) NOT NULL DEFAULT '',
`end_time` bigint NOT NULL DEFAULT '0',
`start_time` bigint NOT NULL DEFAULT '0',
`created` bigint NOT NULL DEFAULT '0',
`updated` bigint NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
pom.xml依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
lua 脚本:
//减少库存,校验库存是否充足
local counter = redis.call('hget',KEYS[1],KEYS[2]);
if not counter then
return -1;
end;
local result = counter - ARGV[1];
if(result>=0 ) then
redis .cal1('hset', KEYS[ 1],KEYS[2],result);
return 1;
end;
return 0;
// 库存数量回滚
local counter = redis.call( 'hget',KEYS[1],KEYS[2]);
if not counter then
return -1;
end;
redis .call( 'hincrby', KEYS[ 1], KEYS[2],ARGV[ 1]);
return 1;
核心业务代码展示
- 加载lua脚本
private final static DefaultRedisScript<Long> deductRedisScript = new DefaultRedisScript();
private final static DefaultRedisScript<Long> increaseRedisScript = new DefaultRedisScript();
//加载lua脚本
@PostConstruct
void init() {
//加载削减库存lua脚本
deductRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedDeductInventory.lua")));
deductRedisScript.setResultType(Long.class);
//加载库存回滚lua脚本
increaseRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedIncreaseInventory.lua")));
increaseRedisScript.setResultType(Long.class);
}
- 添加库存到redis
**注意点:**在使用redis集群时,lua脚本中存在多个key时,可以通过hash tag这个方法将不同key的值落在同一个槽位上,hash tag 是通过{}这对括号括起来的字符串,如果下列中{fixed:" + data.getProductId() + "} 作为tag,确保同一个产品的信息都在同一个槽位。
@Resource(name = "fixedCacheRedisTemplate")
private RedisTemplate<String, Long> fixedCacheRedisTemplate;
public void ProductToOngoing(Product data, Long time) {
//设置数量
long number = data.getAvailableQty();
fixedCacheRedisTemplate.opsForHash().putIfAbsent("{fixed:" + data.getProductId() + "}-residue_stock_" + data.getRecordId(),
"{fixed:" + data.getProductId() + "}-residueStock" , number);
String statusKey = "fixed_product_sold_status_"+ data.getRecordId();
long timeout = data.getEndTime() - data.getStartTime();
//添加产品出售状态
fixedCacheRedisTemplate.opsForValue().set(statusKey, 1L, data.getEndTime() - data.getStartTime(), TimeUnit.MILLISECONDS);
}
- 下单&库存校验
//检查库存
public boolean checkFixedOrderQty(Long userId, Long productId, Long quantity, Long overTime) {
Boolean pendingOrder = false;
String userKey = "";
try {
//校验是否开始
String statusKey = "fixed_product_sold_status_" + productId;
Long fixedStartStatus = fixedCacheRedisTemplate.opsForValue().get(statusKey);
if (fixedStartStatus == null || fixedStartStatus != 1L) {
//报错返回,商品未开售
throw new WebException(ResultCode.SALE_HAS_NOT_START);
}
//检查库存数量
Long number = deductInventory(productId, quantity);
if (number != 1L) {
log.warn("availbale num is null:{} {}", productId, number);
throw new WebException(ResultCode.AVAILABLE_AMOUNT_INSUFFICIENT);
}
return true;
} catch (Exception e) {
log.warn("checkFixedOrderQty error:{}", e.getMessage(), e);
throw e;
}
}
//下单
public void createOrder(Long userId, Long productId, BigDecimal price, Long quantity){
boolean check = checkFixedOrderQty(userId, productId, quantity);
try {
if (check) {
//添加MQ等待下单,后续收到推送的线程保存靠DB中
CreateCoinOrderData data = new CreateCoinOrderData();
data.setUserId(userId);
data.setProductId(productId);
data.setPrice(price);
data.setQuantity(quantity);
rabbitmqProducer.sendMessage(1, JSONObject.toJSONString(data));
}
} catch (Exception e) {
//发生异常,库存需要回滚
increaseInventory(recordId, quantity, 1L);
throw e;
}
}
//库存回填
public Long increaseInventory(Long productId, Long num) {
try {
// 构建keys信息,代表hash值中所需要的key信息
List<String> keys = Arrays.asList("{fixed:" + productId + "}-residue_stock_"+ recordId, "{fixed:" + productId + "}-residueStock");
// 执行脚本
Object result = fixedCacheRedisTemplate.execute(increaseRedisScript, keys, num);
log.info("increaseInventory productId :{} num:{} result:{}", productId, num, result);
return (Long) result;
} catch (Exception e) {
log.warn("increaseInventory error productId:{} num:{}", productId, num);
}
return 0L;
}
来源:https://juejin.cn/post/7286264172097781812
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)