基于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,一经查实,立即删除!

相关文章

JavaScript 使用random()生成随机数

function myFunction() { var a Math.floor(Math.random()*10);return a;} // 记住 Math.random() 永远不会返回 1。同时因为我们是在用 Math.floor() 向下取整&#xff0c;所以最终我们获得的结果不可能有 20。这确保了我们获得了一个在0到19之间的整数。 把操作连缀起来&…

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;同时为移动用户提供了自定…

Add a All Document Folder

本文出自Simmy的个人blog&#xff1a;西米在线 http://simmyonline.com/archives/54.html right clickSearch Folder-New Search Folder-Custom-Create a custom folder 本文转simmy51CTO博客&#xff0c;原文链接&#xff1a;http://blog.51cto.com/helpdesk/122327&#xff0…

Oracle服务器修改IP后

机房有两套网络&#xff0c;一套办公网&#xff0c;一套机房的内网&#xff0c;办公网可以通过vpn在其他地方访问&#xff0c;内网只能在公司办公室访问。团队有同事去外地办公&#xff0c;开发的时候需要通过客户端直连数据库&#xff0c;于是把数据库服务器的网线换到办公网的…

代理IP对直播平台的影响与关系-国内多IP昙花一现

1.代理IP的作用1>.访问一些单位或团体内部资源&#xff0c;如某大学FTP(前提是该代理地址在该资源 的允许访问范围之内)&#xff0c;使用网络内地址段免费代理服务器&#xff0c;就可以用于对 网络开放的各类FTP下载上传&#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将变得更加强大。 针对任何应用程序的自定义语音命令将…

spring cloud连载第一篇之bootstrap context

1. Spring Cloud Context: Application Context Services&#xff08;应用上下文服务&#xff09; 1.1 The Bootstrap Application Context&#xff08;引导上下文&#xff09; 一个spring cloud应用会创建一个“bootstrap”context&#xff0c;它是主应用的parent context。它…

过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请求…

Android 干货,强烈推荐

本文主要收集 Android开发中常用的干货技术&#xff0c;现做出目录&#xff0c;此文不断更新中&#xff0c;欢迎关注、点赞、投稿。Android 四大组件与布局1. Activity 使用详解2. Service 使用详解3. Broadcast 使用详解4. ContentProvider 使用详解5. 四大布局 使用详解6. Re…

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…

Windows 2003 NTP 时间服务器设置

需要在局域网中架设一台时间同步服务器&#xff0c;统一各客户端及服务器的系统时间&#xff0c;在网上查找大多是基于Linux下的 确&#xff2e;&#xff34;&#xff30;服务器&#xff0e;搜索&#xff0c;实验及总结&#xff0c;写一篇采用Windwos2003自带的W32Time服务用于…

React 深入学习:React 更新队列

path&#xff1a;packages/react-reconciler/src/ReactUpdateQueue.js 更新 export type Update<State> {expirationTime: ExpirationTime, // 到期时间tag: 0 | 1 | 2 | 3, // 更新类型payload: any, // 负载callback: (() > mixed) | null, // 回调函数next: Updat…

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

长时间曝光计算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 …