基于redis实现的扣减库存

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

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

解决方案

  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 )

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

扣减库存服务(StockService)

/*** 扣库存** @author yuhao.wang*/
@Service
public class StockService {Logger logger = LoggerFactory.getLogger(StockService.class);/*** 不限库存*/public static final long UNINITIALIZED_STOCK = -3L;/*** Redis 客户端*/@Autowiredprivate 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();// 将库存设置到redisredisTemplate.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()) {// 获取到锁后再次判断一下是否有keyhasKey = 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>() {@Overridepublic 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;}}

调用

/*** @author yuhao.wang*/
@RestController
public class StockController {@Autowiredprivate StockService stockService;@RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object stock() {// 商品IDlong commodityId = 1;// 库存IDString 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() {// 商品IDlong commodityId = 1;// 库存IDString redisKey = "redis_key:stock:" + commodityId;return stockService.getStock(redisKey);}@RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object addStock() {// 商品IDlong commodityId = 2;// 库存IDString redisKey = "redis_key:stock:" + commodityId;return stockService.addStock(redisKey, 2);}
}

源码: https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-stock-redis 工程

参考:

  • http://www.cnblogs.com/billyxp/p/3701124.html
  • http://blog.csdn.net/jiao_fuyou/article/details/15504777
  • https://www.jianshu.com/p/48c1a92fbf3a
  • https://www.zhihu.com/question/268937734

转载于:https://my.oschina.net/xiaominmin/blog/3060257

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

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

相关文章

plex 乱码_Plex Media Center现在支持播客

plex 乱码Plex is adding beta support for podcasts to iOS, Android, Roku, and Plex Web today, alongside a custom home screen for mobile users. Plex现在为iOS&#xff0c;Android&#xff0c;Roku和Plex Web的播客添加了beta支持&#xff0c;同时为移动用户提供了自定…

ios12彻底关闭siri_Siri正在iOS 12中获取自定义语音操作

ios12彻底关闭siriSiri is about to get a lot more powerful. Custom voice commands for any app will allow you to say “Hey Siri, I lost my keys” to instantly launch an app that will help you find them. Siri将变得更加强大。 针对任何应用程序的自定义语音命令将…

过Postfix构建Exchange Server 2010邮件网关部署系列三:安装Exchange 2010先决性条件

1.将Exchange Server 2010服务器加入域。 2.在“开始”菜单上&#xff0c;依次导航到“所有程序”>“附件”>“Windows PowerShell”。打开提升的 Windows PowerShell 控制台并运行以下命令&#xff1a; Import-Module ServerManager 3.使用 Add-WindowsFeature cmdlet 安…

gmail收件箱标签设置_通过多个收件箱实验室有效管理您的Gmail

gmail收件箱标签设置Most people have more than one email account and if you are using Gmail it’s easy to get things set up so that all of your messages can be accessed in the same place. But if you would prefer to keep things ‘together yet separate’ the …

清华生命学院 2017 就业报告:就业率仅 51%

时间&#xff1a;20170406 一、截至目前生命学院整体就业情况 1.1 系统就业率 1.2 实际排查就业率 (6092)/(68230)51.06%二、本科生就业排查 2017 届本科生 68 人&#xff0c;已确定去向 60 人&#xff08;已登记去向 32 人&#xff09; 2.1 确定去向的 60 人中 国内深造 35 人…

程序改变了命运,程序生活一天比一天好,对未来也充满了希望

为什么80%的码农都做不了架构师&#xff1f;>>> 我出生在内蒙古自治区兴安盟扎赉特旗宝力根花苏木&#xff0c;那里是少数民族蒙古族聚居区&#xff0c;20-30年前与现代城市文明有些差距。当还在读小学的时在中学当数学老师的爸爸去深圳出差学习&#xff0c;顺路在…

powershell 变量_极客学院:学习PowerShell变量,输入和输出

powershell 变量As we move away from simply running commands and move into writing full blown scripts, you will need a temporary place to store data. This is where variables come in. 随着我们不再只是运行命令而转而编写完整的脚本&#xff0c;您将需要一个临时位…

offsetTop、offsetLeft、offsetWidth、offsetHeight、style中的样式

< DOCTYPE html PUBLIC -WCDTD XHTML StrictEN httpwwwworgTRxhtmlDTDxhtml-strictdtd> 假设 obj 为某个 HTML 控件。 obj.offsetTop 指 obj 距离上方或上层控件的位置&#xff0c;整型&#xff0c;单位像素。 obj.offsetLeft 指 obj 距离左方或上层控件的位置&#xff0…

Mock2 moco框架的http协议get方法Mock的实现

首先在Chapter7文件夹下再新建一个startGet.json startget.json代码如下&#xff0c;因为是get请求&#xff0c;所以要写method关键字&#xff0c;有两个&#xff0c;一个是有参数&#xff0c;一个是无参数的请求。 [{"description":"模拟一个没有参数的get请求…

imessage_如何在所有Apple设备上同步您的iMessage

imessageMessages in iCloud lets you sync your iMessages across all of your Apple devices using your iCloud account. Here’s how to set it up. 通过iCloud中的消息&#xff0c;您可以使用iCloud帐户在所有Apple设备上同步iMessage。 设置方法如下。 Apple announced t…

“.Net 社区大会”(dotnetConf) 2018 Day 1 主题演讲

Miguel de Icaza、Scott Hunter、Mads Torgersen三位大咖给大家带来了 .NET Core ,C# 以及 Xamarin的精彩内容&#xff1a;6月份已经发布了.NET Core 2.1, 大会上Scott Hunter 一开始花了大量的篇幅回顾.NET Core 2.1的发布&#xff0c;社区的参与度已经非常高&#xff0c;.NET…

长时间曝光计算_如何拍摄好长时间曝光的照片

长时间曝光计算In long exposure photography, you take a picture with a slow shutter speed—generally somewhere between five and sixty seconds—so that any movement in the scene gets blurred. It’s a way to show the passage of time in a single image. Let’s …

深度学习入门3

CNN 第一周&#xff1a; title: edge detection example 卷积核在边缘检测中的应用&#xff0c;可解释&#xff0c;卷积核的设计可以找到像素列突变的位置 把人为选择的卷积核参数&#xff0c;改为学习参数&#xff0c;可以学到更多的特征 title: padding n * n图片&#xff0c…

图像大小调整_如何在Windows中调整图像和照片的大小

图像大小调整Most image viewing programs have a built-in feature to help you change the size of images. Here are our favorite image resizing tools for Windows. We’ve picked out a built-in option, a couple of third party apps, and even a browser-based tool.…

Spring Data JPA例子[基于Spring Boot、Mysql]

阅读目录 关于Spring Data关于Spring Data子项目关于Spring Data Jpa例子&#xff0c;Spring Boot Spring Data Jpa运行、测试程序程序源码参考资料关于Spring Data Spring社区的一个顶级工程&#xff0c;主要用于简化数据&#xff08;关系型&非关系型&#xff09;访问&am…

The way of Webpack learning (IV.) -- Packaging CSS(打包css)

一&#xff1a;目录结构 二&#xff1a;webpack.config.js的配置 const path require(path);module.exports {mode:development,entry:{app:./src/app.js},output:{path:path.resolve(__dirname,dist),publicPath:./dist/,//设置引入路径在相对路径filename:[name].bundle.js…

文本文档TXT每行开头结尾加内容批处理代码

文本文档TXT每行开头结尾加内容批处理代码 读A.TXT ,每行开头加&#xff1a;HTMLBodytxt HTMLBodytxt chr(10) aaaaaaaa结尾加&#xff1a;bbbbbbbb处理后的文档写入到B.TXT For /f "delims" %%i in (a.txt) do echo HTMLBodytxt HTMLBodytxt chr(10) aaaaaaaa%%…

windows运行对话框_如何在Windows运行对话框中添加文本快捷方式?

windows运行对话框Windows comes prepackaged with a ton of handy run-dialog shortcuts to help you launch apps and tools right from the run box; is it possible to add in your own custom shortcuts? Windows预包装了许多方便的运行对话框快捷方式&#xff0c;可帮助…

Zabbix 3.0 安装

Zabbix 3.0 For CentOS6安装 1 概述2 安装MySQL3 安装WEB4 安装Zabbix-Server5配置WEB1概述 对于3.0&#xff0c;官方只提供CentOS7的RPM包&#xff0c;Ubuntu的DEB包&#xff0c;对于CentOS6&#xff0c;默认不提供RPM包&#xff0c;为了照顾到使用CentOS6的兄弟们&#xff0c…

[Hadoop in China 2011] 中兴:NoSQL应用现状及电信业务实践

http://tech.it168.com/a2011/1203/1283/000001283154.shtml 在今天下午进行的NoSQL系统及应用分论坛中&#xff0c;中兴云计算平台研发总工、中兴通讯技术专家委员会专家高洪发表主题演讲“NoSQL技术的电信业务实践”&#xff0c;介绍了NoSQL的发展现状及其在电信业务中的应用…