前言

大流量情况下的库存是老生常谈的问题了,在这里我整理一下mysql和redis应对扣除库存的方案,采用jmeter进行压测。

JMETER设置

库存初始值50,线程数量1000个,1秒以内启动全部,一个线程循环2次,共2000个请求

MySQL方案

初始方案

    <update id="decreaseStock">
UPDATE stock
SET stock_num = stock_num - 1
WHERE id = #{id}
</update>

这种情况下,在并发条件肯定会出现超卖的

进行修改:

    <update id="decreaseStock">
UPDATE stock
SET stock_num = stock_num - 1
WHERE id = #{id} AND stock_num >= 1
</update>

增加AND stock_num >= 1条件,即可避免超卖。

相关代码:

    @PostMapping(value = "/decreaseStock/{id}")
public ResponseEntity<Object> decreaseStock(@PathVariable("id") Integer id) {
int result = stockService.decreaseStock(id);
return result == 1 ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
}

压测情况:

根据Throught可知一秒可以处理200个事务(TPS)

如果说系统的并发量不高,则可以以这种方案进行防止库存超卖,但要注意,在可重复读隔离级别情况下,如果where的条件字段没有索引的话,进行update语句会使整个表被锁住,如果这里使用的where条件不是主键id而是product_name,那么需要给这个字段加索引。

在RR可重复读隔离级别下,如果where条件没有命中索引,那么会基于next-key lock(记录锁和间隙锁的组合)对整个表的所有记录加上这个锁,进行全表扫描,这个时候其他记录想要更新就会被阻塞。

但是不一定是有了索引就不会锁住整个表,这是由优化器决定的,可以使用Explain语句来查看当前语句是走的索引还是全表扫描,如果优化器走的还是全标扫描,可以使用 force index([index_name]) 强制使用某个索引。

改进

在MySQL情况下还能有其他方案来提升性能吗,在不借助Redis的情况(曾经面试招银网络被问了这道题)

我当时给出的回答是,把单个商品的库存比如50个库存,拆分成好几份,一份10个,5份库存,由于秒杀情况下流量很大,可以把这五份库存分别放到五个数据库里面,这样性能至少是原先方案的5倍,那么还会出现新的问题,就是有些问题,负载均衡上的问题,可能会出现某些库里还存在库存,但是请求却没有打进这个数据库,而是打到库存已经没有的数据库里面。我当时的想法是再搞个库存表,这个库存表采集各个商品的总库存以及商品在各个分库里面的库存数量,然后再写个服务,包含负载均衡的算法,将用户的请求平均打到各个分库去,当某个分库的库存达到0的时候,去通知该服务,服务将这个库剔除,使新的请求不会转发过去。实际这种情况也是存在问题的,高并发下库存为0的库来不及被剔除,也会导致请求被打到库存0的库。

Redis方案

将库存暂时放到Redis,然后从Redis进行库存扣减,能大大提升性能

压测结果:

可见性能几乎是MySQL的10倍了,但是这样子在Redis里面会导致超卖

要确保Redis不超买,需要先查询当前的数量,如果大于0则进行扣减,并且查询和扣减需要为原子性,这里就需要借助lua脚本,将这两次操作写到一起。

加了Lua脚本的代码:

    private static final String LUA_DECRESE_STOCK_PATH = "lua/decreseStock.lua";

    @PostMapping(value = "/decreaseStockByRedis/{id}")
public ResponseEntity<Object> decreaseStockByRedis(@PathVariable("id") Integer id) { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
redisScript.setResultType(Long.class); // 执行Lua脚本
Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList(id)); // 返回结果判断
return (result != null && result == 1) ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
}

lua脚本放在resource/lua/decreseStock.lua

local key = KEYS[1]

-- 检查键是否存在
local exists = redis.call('EXISTS', key)
if exists == 1 then
-- 键存在,获取值
local value = redis.call('GET', key)
if tonumber(value) > 0 then
-- 如果值大于0,则递减
redis.call('DECR', key)
return 1 -- 表示递减成功
else
return 0 -- 表示递减失败,值不大于0
end
else
return -1 -- 表示递减失败,键不存在
end

Redis同步库存到MySQL

但是在Redis扣减了库存,总需要同步到MySQL里面

@PostMapping(value = "/decreaseStockByRedis/{id}")
public ResponseEntity<Object> decreaseStockByRedis(@PathVariable("id") Integer id) { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
redisScript.setResultType(Long.class); // 执行Lua脚本
Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(id));
int dataBaselResult = 0;
if (redisResult == 1) {
dataBaselResult = stockService.decreaseStock(id);
}
// 返回结果判断
return (dataBaselResult == 1 && redisResult == 1) ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
}

直接按照上述代码来写,删Redis后同时将库存同步到MySQL,相当于使用了Redis性能又没有提升。

其实选择了Redis来进行库存扣减,那么MySQL的库存并不需要去实时进行更新,只需要库存达到最终一致性即可,即先对Redis的库存进行更新,然后再异步同步到MySQL的库存。

如果使用spring的异步线程来解决,会不会出现同步MySQL失败导致数据最终不一致呢,在流量很多的情况下,系统本身就处于压力大的情况,再使用异步线程会占用额外的资源,最好的方法是引入MQ,把库存的同步信息交给MQ,MQ再交到消费系统,进行减库存的操作,由MQ保证消息被消费,实现最终一致性。

部分代码如下,由MQ product发出,再由consumer进行消费:

    private final DecreaseStockProduce decreaseStockProduce;

    @PostMapping(value = "/decreaseStockByRedis/{id}")
public ResponseEntity<Object> decreaseStockByRedis(@PathVariable("id") String id) { DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
redisScript.setResultType(Long.class); // 执行Lua脚本
Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(id));
if (redisResult == 1) {
// 发送消息
try {
DecreaseStockEvent decreaseStockEvent = DecreaseStockEvent.builder()
.id(id)
.build();
SendResult sendResult = decreaseStockProduce.sendMessage(decreaseStockEvent);
if (!Objects.equals(sendResult.getSendStatus(), SendStatus.SEND_OK)) {
log.error("消息发送错误,请求参数:{}", id);
}
} catch (Exception e) {
log.error("消息发送错误,请求参数:{}", id, e);
}
} // 返回结果判断
return (redisResult == 1) ? new ResponseEntity<>("decreaseStock successfully", HttpStatus.OK) : new ResponseEntity<>("decreaseStock failed", HttpStatus.OK);
}

MQ [TIMEOUT_CLEAN_QUEUE] broker busy问题

这里直接压测会报下面的错误,并且这个时候查看redis库存已经减到0,到是MySQL只减到了37

针对MQ [TIMEOUT_CLEAN_QUEUE] broker busy问题,需要去修改MQ的broker.conf文件

针对TIMEOUT_CLEAN_QUEUE broker busy问题,需要去修改MQ的broker.conf文件,上述的201ms超时了,我这里将等待时间改为400,并且将线程数设置为64,这个线程数可以根据实际压测情况进行调整。

# 发消息线程池数量
sendMessageThreadPoolNums=64
# 拉消息线程池数量
pullMessageThreadPoolNums=64
waitTimeMillsInSendQueue=400

现在再进行压测,发现tps能跑到1000,相比直接入库mysql的200已经是提升很大了。

虽然性能提高,也实现库存的同步,但这个性能下还是会存在一些问题:

比如MQ消息发送失败、或者MySQL库存扣减失败,并且实际情况还有订单的生成和库存之间的一致性也要考虑。

对于上述这些问题,可以查看我的另外一篇博客:

RocketMQ事务消息在订单创建和库存扣减的使用 - Scotyzh - 博客园 (cnblogs.com)

mysql和redis库存扣减和优化的更多相关文章

  1. 利用redis实现分布式事务锁,解决高并发环境下库存扣减

    利用redis实现分布式事务锁,解决高并发环境下库存扣减   问题描述: 某电商平台,首发一款新品手机,每人限购2台,预计会有10W的并发,在该情况下,如果扣减库存,保证不会超卖 解决方案一 利用数据 ...

  2. Java+Redis 通过Lua 完成库存扣减,创建消息队列,异步处理消息--实战

    需要完成功能 借助redis Stream 数据结构实现消息队列,异步完成订单创建,其中涉及到了缓存(击穿,穿透,雪崩),锁(Redisson),并发处理,异步处理,Lua脚本 IDE:IDEA 20 ...

  3. 自实现CAS原理JAVA版,模拟下单库存扣减

    在做电商系统时,库存是一个非常严格的数据,根据CAS(check and swap)原来下面对库存扣减提供两种方法,一种是redis,一种用java实现CAS. 第一种 redis实现: 以下这个类是 ...

  4. redis分布式锁扣减库存弊端: 吞吐量低, 解决方法:使用 分段锁 分布式分段锁并发扣减库存--代码实现

    package tech.codestory.zookeeper.aalvcai.ConcurrentHashMapLock; import lombok.AllArgsConstructor; im ...

  5. 电商中的库存管理实现-mysql与redis

        库存是电商系统的核心环节,如何做到不少卖,不超卖是库存关心的核心业务问题.业务量大时带来的问题是如何更快速的处理库存计算. 此处以最简模式来讨论库存设计. 以下内容只做分析,不能直接套用,欢迎 ...

  6. EF+MySQL乐观锁控制电商并发下单扣减库存,在高并发下的问题

    下订单减库存的方式 现在,连农村的大姐都会用手机上淘宝购物了,相信电商对大家已经非常熟悉了,如果熟悉电商开发的同学,就知道在买家下单购买商品的时候,是需要扣减库存的,当然有2种扣减库存的方式, 一种是 ...

  7. mysql常见优化,更多mysql,Redis,memcached等文章

    mysql常见优化 http://www.cnblogs.com/ggjucheng/archive/2012/11/07/2758058.html 更多mysql,Redis,memcached等文 ...

  8. 笔记:如何使用postgresql做顺序扣减库存

    如何使用postgresql做顺序扣减库存 Ⅰ.废话在前面 首先这篇笔记源自于最近的一次需求,这个临时性需求是根据两份数据(库存数据以及出库数据) 算出实际库存给到业务,至于库存为什么不等于剩余库存, ...

  9. 【连载】redis库存操作,分布式锁的四种实现方式[三]--基于Redis watch机制实现分布式锁

    一.redis的事务介绍 1. Redis保证一个事务中的所有命令要么都执行,要么都不执行.如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行.而一旦客户端发 ...

  10. SpringMVC+Mybatis+MySQL配置Redis缓存

    SpringMVC+Mybatis+MySQL配置Redis缓存 1.准备环境: SpringMVC:spring-framework-4.3.5.RELEASE-dist Mybatis:3.4.2 ...

随机推荐

  1. MAUI+Masa Blazor APP 各大商店新手发布指南-华为篇

    目录 前言 准备材料 一.企业认证 二.审核资料 审核注意事项 总结 前言 AppGallery Connect(简称AGC)是华为应用市场推出的应用一站式服务平台,致力于为开发者提供应用创意.开发. ...

  2. 聊聊大数据框架的数据更新策略: COW,MOR,MOW

    大数据框架下,常用的数据更新策略有三种: COW: copy-on-write, 写时复制; MOR: merge-on-read, 读时合并; MOW: merge-on-write, 写时合并; ...

  3. 吉特日化MES & 再谈原料标签

    在前面之前提到的标签的选择和设计,原料标签可以做到如下几点:     1 原料标签是一物一码还是一码多物:在美妆类的原料建议原料标签采用一物一码,对于大液洗之类的产品原料如果能够做到一物一码最佳,但是 ...

  4. Chrome扩展开发实战:快速填充表单

    大家好,我是 dom 哥.我正在写关于 Chrome 扩展开发的系列文章,感兴趣的可以 点个小星星 . 填表单是打工人经常面对的场景,作为一个前端,我经常开发一些PC端的页面,它们主要由表单和表格构成 ...

  5. 文心一言 VS 讯飞星火 VS chatgpt (157)-- 算法导论12.3 4题

    四.用go语言,删除操作可交换吗?可交换的含义是,先删除 x 再删除 y 留下的结果树与先除 y 再删除 x 留下的结果树完全一样.如果是,说明为什么? 否则,给出一个反例. 文心一言: 在Go语言中 ...

  6. k8s安装etcd出现Job for etcd.service failed......"journalctl -xe" for details.

    错误如下 先按照提示,输入 journalctl -xe 看一些详细信息 1.针对:start request repeated too quickly for etcd.service 错误,解决办 ...

  7. 神经网络优化篇:详解正则化(Regularization)

    正则化 深度学习可能存在过拟合问题--高方差,有两个解决方法,一个是正则化,另一个是准备更多的数据,这是非常可靠的方法,但可能无法时时刻刻准备足够多的训练数据或者获取更多数据的成本很高,但正则化通常有 ...

  8. 掌握语义内核(Semantic Kernel):如何精进你的提示词工程

    在人工智能的海洋里,大型语言模型(LLM AI)是高速发展的一艘巨轮,而有效地与其沟通和指导其行为的锚,正是提示语(prompts).提示语是我们提供给模型的输入或查询,以期获取特定的响应.当今,提示 ...

  9. [LitCTF 2023]1zjs

    打开环境: 一个魔方♂ 习惯性打开 F12,之后发现有个./dist/index.umd.js Ctrl u 打开 把这个蓝色的点开 0.o? 这里眼神好的话就能看到有个" /f@k3f1a ...

  10. AI与低代码解锁无限可能

    前言 近年来,人工智能(AI)和低代码开发技术逐渐成为数字化转型的重要推动力.AI作为一项具有革命性潜力的技术,正在改变我们生活的方方面面.而低代码开发则提供了一种快速构建应用程序的方法,使得开发者无 ...