Redis 如何实现库存扣减操作和防止被超卖?

电商当项目经验已经非常普遍了,不管你是包装的还是真实的,起码要能讲清楚电商中常见的问题,比如库存的操作怎么防止商品被超卖

解决方案:

  • 基于数据库单库存
  • 基于数据库多库存
  • 基于redis

基于redis实现扣减库存的具体实现

  • 初始化库存回调函数(IStockCallback)
  • 扣减库存服务(StockService)
  • 调用


在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等。

解决方案

  1. 使用mysql数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段。
  2. 还是使用数据库,但是将库存分层多份存到多条记录里面,扣减库存的时候路由一下,这样子增大了并发量,但是还是避免不了大量的去访问数据库来更新库存。
  3. 将库存放到redis使用redis的incrby特性来扣减库存。

分析

在上面的第一种和第二种方式都是基于数据来扣减库存。

基于数据库单库存

第一种方式在所有请求都会在这里等待锁,获取锁有去扣减库存。在并发量不高的情况下可以使用,但是一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源,所以在并发高的情况下这种方式不适用。

基于数据库多库存

第二种方式其实是第一种方式的优化版本,在一定程度上提高了并发量,但是在还是会大量的对数据库做更新操作大量占用数据库资源。

基于数据库来实现扣减库存还存在的一些问题:

  • 用数据库扣减库存的方式,扣减库存的操作必须在一条语句中执行,不能先selec在update,这样在并发下会出现超扣的情况。如:
update number set x=x-1 where x > 0
  • MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。
  • 当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。

基于redis

针对上述问题的问题我们就有了第三种方案,将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题。但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存=总的库存数-已经发放的奖励数,但是如果是异步发奖,需要等到MQ消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。

基于redis实现扣减库存的具体实现

  • 我们使用redis的lua脚本来实现扣减库存
  • 由于是分布式环境下所以还需要一个分布式锁来控制只能有一个服务去初始化库存
  • 需要提供一个回调函数,在初始化库存的时候去调用这个函数获取初始化库存

初始化库存回调函数(IStockCallback )

/** * 获取库存回调 */ public interface IStockCallback { /** * 获取库存 * @return */ int getStock(); }

扣减库存服务(StockService)

/** * 扣库存 * */ @Service public class StockService { Logger logger = LoggerFactory.getLogger(StockService.class); /** * 不限库存 */ public static final long UNINITIALIZED_STOCK = -3L; /** * Redis 客户端 */ @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 执行扣库存的脚本 */ public static final String STOCK_LUA; static { /** * * @desc 扣减库存Lua脚本 * 库存(stock)-1:表示不限库存 * 库存(stock)0:表示没有库存 * 库存(stock)大于0:表示剩余库存 * * @params 库存key * @return * -3:库存未初始化 * -2:库存不足 * -1:不限库存 * 大于等于0:剩余库存(扣减之后剩余的库存) * redis缓存的库存(value)是-1表示不限库存,直接返回1 */ StringBuilder sb = new StringBuilder(); sb.append("if (redis.call('exists', KEYS[1]) == 1) then"); sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));"); sb.append(" local num = tonumber(ARGV[1]);"); sb.append(" if (stock == -1) then"); sb.append(" return -1;"); sb.append(" end;"); sb.append(" if (stock >= num) then"); sb.append(" return redis.call('incrby', KEYS[1], 0 - num);"); sb.append(" end;"); sb.append(" return -2;"); sb.append("end;"); sb.append("return -3;"); STOCK_LUA = sb.toString(); } /** * @param key 库存key * @param expire 库存有效时间,单位秒 * @param num 扣减数量 * @param stockCallback 初始化库存回调函数 * @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存 */ public long stock(String key, long expire, int num, IStockCallback stockCallback) { long stock = stock(key, num); // 初始化库存 if (stock == UNINITIALIZED_STOCK) { RedisLock redisLock = new RedisLock(redisTemplate, key); try { // 获取锁 if (redisLock.tryLock()) { // 双重验证,避免并发时重复回源到数据库 stock = stock(key, num); if (stock == UNINITIALIZED_STOCK) { // 获取初始化库存 final int initStock = stockCallback.getStock(); // 将库存设置到redis redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS); // 调一次扣库存的操作 stock = stock(key, num); } } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { redisLock.unlock(); } } return stock; } /** * 加库存(还原库存) * * @param key 库存key * @param num 库存数量 * @return */ public long addStock(String key, int num) { return addStock(key, null, num); } /** * 加库存 * * @param key 库存key * @param expire 过期时间(秒) * @param num 库存数量 * @return */ public long addStock(String key, Long expire, int num) { boolean hasKey = redisTemplate.hasKey(key); // 判断key是否存在,存在就直接更新 if (hasKey) { return redisTemplate.opsForValue().increment(key, num); } Assert.notNull(expire,"初始化库存失败,库存过期时间不能为null"); RedisLock redisLock = new RedisLock(redisTemplate, key); try { if (redisLock.tryLock()) { // 获取到锁后再次判断一下是否有key hasKey = redisTemplate.hasKey(key); if (!hasKey) { // 初始化库存 redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS); } } } catch (Exception e) { logger.error(e.getMessage(), e); } finally { redisLock.unlock(); } return num; } /** * 获取库存 * * @param key 库存key * @return -1:不限库存; 大于等于0:剩余库存 */ public int getStock(String key) { Integer stock = (Integer) redisTemplate.opsForValue().get(key); return stock == null ? -1 : stock; } /** * 扣库存 * * @param key 库存key * @param num 扣减库存数量 * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】 */ private Long stock(String key, int num) { // 脚本里的KEYS参数 List<String> keys = new ArrayList<>(); keys.add(key); // 脚本里的ARGV参数 List<String> args = new ArrayList<>(); args.add(Integer.toString(num)); long result = redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { Object nativeConnection = connection.getNativeConnection(); // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 // 集群模式 if (nativeConnection instanceof JedisCluster) { return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args); } // 单机模式 else if (nativeConnection instanceof Jedis) { return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args); } return UNINITIALIZED_STOCK; } }); return result; } }

调用

@RestController public class StockController { @Autowired private StockService stockService; @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object stock() { // 商品ID long commodityId = 1; // 库存ID String redisKey = "redis_key:stock:" + commodityId; long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId)); return stock >= 0; } /** * 获取初始的库存 * * @return */ private int initStock(long commodityId) { // TODO 这里做一些初始化库存的操作 return 1000; } @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object getStock() { // 商品ID long commodityId = 1; // 库存ID String redisKey = "redis_key:stock:" + commodityId; return stockService.getStock(redisKey); } @RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Object addStock() { // 商品ID long commodityId = 2; // 库存ID String redisKey = "redis_key:stock:" + commodityId; return stockService.addStock(redisKey, 2); } }

获取更多干货内容,记得关注我哦。

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

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

相关文章

AI实体侦测极速部署:5分钟比本地快10倍

AI实体侦测极速部署&#xff1a;5分钟比本地快10倍 1. 为什么你需要云端AI实体侦测 当你突然接到一个紧急项目演示需求&#xff0c;需要快速展示AI实体侦测能力时&#xff0c;传统本地部署方式往往会让你陷入困境。想象一下这样的场景&#xff1a;你需要安装CUDA、配置Python…

专科生必看!10个高效降AIGC工具推荐,轻松过审不踩坑

专科生必看&#xff01;10个高效降AIGC工具推荐&#xff0c;轻松过审不踩坑 AI降重工具&#xff0c;帮你轻松应对论文查重难题 随着AI技术的快速发展&#xff0c;越来越多的专科生在撰写论文时会使用AI辅助工具来提高写作效率。然而&#xff0c;AI生成的内容往往存在明显的“AI…

StructBERT模型自动化部署:Ansible实战

StructBERT模型自动化部署&#xff1a;Ansible实战 1. 引言&#xff1a;中文情感分析的工程落地挑战 在自然语言处理&#xff08;NLP&#xff09;的实际应用中&#xff0c;中文情感分析是企业级AI服务中最常见的需求之一。无论是用户评论监控、客服对话情绪识别&#xff0c;还…

学霸同款10个AI论文平台,助你搞定研究生论文写作!

学霸同款10个AI论文平台&#xff0c;助你搞定研究生论文写作&#xff01; AI 工具如何成为论文写作的得力助手 在研究生阶段&#xff0c;论文写作不仅是学术能力的体现&#xff0c;更是一项需要大量时间与精力投入的任务。随着 AI 技术的不断进步&#xff0c;越来越多的 AI 工具…

2026年牛客网最热门的Java岗面试八股文汇总

今天也不搞那些花里胡哨的了&#xff0c;单纯的总结了一下今年面试被问得最多的1000道题&#xff0c;说1000道就是1000道&#xff0c;一题都不少&#xff0c;希望对还没找到合适工作的同学有所帮助。 本套面试宝典从近一百套最新一线互联网公司面试题中精选而出&#xff0c;涵…

实时行为分析深度解析:云端GPU性能翻倍价格减半

实时行为分析深度解析&#xff1a;云端GPU性能翻倍价格减半 引言&#xff1a;当安全分析遇上GPU加速 作为安全分析师&#xff0c;你是否经常面对这样的困境&#xff1a;每天需要处理TB级的日志数据&#xff0c;本地机器跑个简单查询都要等半小时&#xff0c;更别提复杂的实时…

AI实体侦测模型竞赛:云端环境公平对决

AI实体侦测模型竞赛&#xff1a;云端环境公平对决 引言 想象一下&#xff0c;你是一名计算机专业的大学生&#xff0c;参加了一场AI模型竞赛。你熬夜优化算法&#xff0c;却在提交时发现——因为你的笔记本电脑性能不如其他同学的顶级显卡&#xff0c;最终成绩差了整整20%。这…

当我不想再为「小决定」消耗注意力时,我做了一个很小的工具

写这篇文章的起因&#xff0c;其实很简单。 有一天我发现&#xff0c;自己一天中被打断最多的&#xff0c;并不是复杂的问题&#xff0c;而是一些本来不值得认真思考的小决定&#xff1a; 先做哪个任务&#xff1f;午饭吃什么&#xff1f;几个方案里随便选一个&#xff0c;从哪…

零售货架AI巡检:云端自动识别缺货,1周快速验证

零售货架AI巡检&#xff1a;云端自动识别缺货&#xff0c;1周快速验证 引言&#xff1a;当货架管理遇上AI 连锁超市的区域经理王明最近很头疼。每周巡店时&#xff0c;总发现某些热销商品莫名其妙缺货&#xff0c;补货不及时导致销售额直接损失。更麻烦的是&#xff0c;IT部门…

AI监测系统容灾设计:保证99.99%可用性的架构

AI监测系统容灾设计&#xff1a;保证99.99%可用性的架构 1. 为什么需要高可用AI监测系统 关键基础设施&#xff08;如电力、交通、通信网络&#xff09;的安全监测系统一旦出现故障&#xff0c;可能导致严重后果。传统监测系统存在两个致命缺陷&#xff1a; 单点故障风险&am…

边缘AI与云端协同:智能侦测的混合架构实践

边缘AI与云端协同&#xff1a;智能侦测的混合架构实践 引言&#xff1a;为什么需要混合架构&#xff1f; 在物联网时代&#xff0c;智能摄像头、传感器等设备每天产生海量数据。传统方案面临两难选择&#xff1a;全部数据上传云端会导致延迟高、带宽成本大&#xff1b;仅靠本…

智能监控DIY指南:200元打造专业级AI安防系统

智能监控DIY指南&#xff1a;200元打造专业级AI安防系统 1. 为什么选择AI智能监控&#xff1f; 作为别墅业主&#xff0c;你可能既想要专业级的安全防护&#xff0c;又不愿支付高昂的商业监控方案费用。传统监控系统存在几个痛点&#xff1a; 被动录像&#xff1a;只能事后查…

基于PLC的智能农业温室大棚控制系统设计(源码+万字报告+讲解)(支持资料、图片参考_相关定制)

摘 要 温室技术具有合理利用农业资源、保护生态环境、提高农产品产量及在国际市场竞争力等优势&#xff0c;已成为当前国际前沿性研究领域。如何利用自动控制系统有效地提高温室环境控制水平和现代化管理程度&#xff0c;是温室技术研究的重要课题之一。随着过程控制技术、通讯…

内部威胁AI检测实战:从零到报告仅需3步,云端GPU按秒计费

内部威胁AI检测实战&#xff1a;从零到报告仅需3步&#xff0c;云端GPU按秒计费 引言&#xff1a;当企业遇到"内鬼"危机时 想象一下这个场景&#xff1a;周一早晨&#xff0c;公司安全负责人突然接到匿名举报&#xff0c;称某核心部门员工正在泄露商业机密。传统调…

中文情感分析WebUI优化:交互体验提升

中文情感分析WebUI优化&#xff1a;交互体验提升 1. 背景与需求&#xff1a;中文情感分析的现实挑战 在自然语言处理&#xff08;NLP&#xff09;的实际应用中&#xff0c;中文情感分析是企业洞察用户反馈、监控舆情、优化客服系统的重要技术手段。然而&#xff0c;尽管已有大…

StructBERT情感分析案例:社交媒体舆情监控系统搭建

StructBERT情感分析案例&#xff1a;社交媒体舆情监控系统搭建 1. 引言&#xff1a;中文情感分析的现实需求与技术挑战 在社交媒体、电商平台和用户评论系统中&#xff0c;海量的中文文本数据每天都在产生。如何从这些非结构化文本中快速识别公众情绪倾向&#xff0c;已成为企…

中文文本情感分析Web服务开发:StructBERT轻量版案例

中文文本情感分析Web服务开发&#xff1a;StructBERT轻量版案例 1. 引言&#xff1a;中文情感分析的现实需求与技术挑战 在社交媒体、电商评论、用户反馈等场景中&#xff0c;海量中文文本蕴含着丰富的情绪信息。如何高效、准确地识别这些情绪倾向&#xff0c;已成为企业洞察…

中文情感分析模型部署:StructBERT

中文情感分析模型部署&#xff1a;StructBERT 1. 引言&#xff1a;中文情感分析的现实需求 在当今数字化时代&#xff0c;用户生成内容&#xff08;UGC&#xff09;呈爆炸式增长&#xff0c;从电商平台评论、社交媒体发言到客服对话记录&#xff0c;海量中文文本背后蕴含着丰…

中文文本情感分析模型部署:StructBERT完整指南

中文文本情感分析模型部署&#xff1a;StructBERT完整指南 1. 引言&#xff1a;中文情感分析的现实需求 在当今数字化时代&#xff0c;用户生成内容&#xff08;UGC&#xff09;呈爆炸式增长&#xff0c;社交媒体评论、电商评价、客服对话等场景中蕴含着海量的情感信息。如何…

AI侦测模型选型指南:5大方案云端横向评测

AI侦测模型选型指南&#xff1a;5大方案云端横向评测 1. 为什么需要云端AI侦测模型选型&#xff1f; 想象你是一家电商平台的技术负责人&#xff0c;突然接到老板通知&#xff1a;"下周上线假货识别功能&#xff01;"这时候你需要快速评估各种AI侦测模型的效果、速…