关键词:
电商当项目经验已经非常普遍了,不管你是包装的还是真实的,起码要能讲清楚电商中常见的问题,比如库存的操作怎么防止商品被超卖
- 解决方案
- 分析
- 基于数据库单库存
- 基于数据库多库存
- 基于redis
- 基于redis实现扣减库存的具体实现
- 初始化库存回调函数(IStockCallback)
- 扣减库存服务(StockService)
- 调用
在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等。
解决方案
- 使用mysql数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段。
- 还是使用数据库,但是将库存分层多份存到多条记录里面,扣减库存的时候路由一下,这样子增大了并发量,但是还是避免不了大量的去访问数据库来更新库存。
- 将库存放到redis使用redis的incrby特性来扣减库存。
分析
在上面的第一种和第二种方式都是基于数据来扣减库存。
基于数据库单库存
第一种方式在所有请求都会在这里等待锁,获取锁有去扣减库存。在并发量不高的情况下可以使用,但是一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源,所以在并发高的情况下这种方式不适用。
基于数据库多库存
第二种方式其实是第一种方式的优化版本,在一定程度上提高了并发量,但是在还是会大量的对数据库做更新操作大量占用数据库资源。
基于数据库来实现扣减库存还存在的一些问题:
- 用数据库扣减库存的方式,扣减库存的操作必须在一条语句中执行,不能先selec在update,这样在并发下会出现超扣的情况。如:
- MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。
- 当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。
基于redis
针对上述问题的问题我们就有了第三种方案,将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题。但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存=总的库存数-已经发放的奖励数,但是如果是异步发奖,需要等到MQ消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。项目实战(点击下载):SpringBoot+SpringCloud+Mybatis+Vue电商项目实战
基于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 客户端
*/
@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;
调用
/** * @author yuhao.wang
*/
@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);
redis如何实现库存扣减操作和防止被超卖?(代码片段)
...c;欢迎star~Github地址:https://github.com/Tyson0314/Java-learning电商当项目经验已经非常普遍了,不管你是包装的还是真实的,起码要能讲清楚电商中常见的问题,比如库存的操作怎么防止商品被超卖解决方案:基于数... 查看详情
redis分布式锁扣减库存弊端:吞吐量低,解决方法:使用分段锁分布式分段锁并发扣减库存--代码实现(代码片段)
packagetech.codestory.zookeeper.aalvcai.ConcurrentHashMapLock;importlombok.AllArgsConstructor;importlombok.Getter;importlombok.Setter;importorg.redisson.Redisson;importorg.redisson.api.RBucket;importo 查看详情
利用redis设计库存系统的苦与乐
...:在seckill场景下,性能总是被要求越高越好我们来看下如何利用Redis来解决上面的三个问题。一.库存安全利用Redis来做库存扣减,避免超限的"方法"很多,坑也很多,我们先来看下常用的陷阱有哪些。1.先获取当前库存值... 查看详情
库存扣减和订单自动失效
...重要的。以后一定加强锻炼。切入正题,最近项目中需要实现在线挂号功能,初步设计把排班生成的号源看做库存,挂的号看做一个个的订单,生成了订单自动锁号,十分钟不支付自动取消订单,退回号源。排班那一套就不做详... 查看详情
redis分布式锁的5个坑,真是又大又深(代码片段)
...工期就在眼前只能硬着头皮上了。脑子浑浑噩噩的时候,写的就不能叫代码,可以直接叫做Bug。我就熬夜写了一个bug被骂惨了。由于是做商城业务,要频繁的对商品库存进行扣减,应用是集群部署,为避免并发造成库存超买超卖... 查看详情
读库存扣减系列文章有感
...方案? 第一篇文章中着重描述了扣减库存的并发问题如何解决,如何保证幂等。 文章首先解决的是如何做到幂等,因为“扣减”库存一定是一个非幂等的 查看详情
spring整合curator实现分布式锁
...现库存卖超的现象。这时候,就需要我们使用分布式锁来实现。实现分布式锁的方法有很多种。redis,zk都可以。但 查看详情
redis分布式锁篇
...式系统中多个线程访问共享数据时数据的安全性举例:在电商系统中,用户在进行下单操作的时候需要扣减库存。为了提高下单操作的执行效率,此时需要将库存的数据存储到Redis中。订单服务每一次生成订单之前需要查询一下... 查看详情
redis分布式锁的5个坑,真是又大又深(代码片段)
...工期就在眼前只能硬着头皮上了。脑子浑浑噩噩的时候,写的就不能叫代码,可以直接叫做Bug。我就熬夜写了一个bug被骂惨了。由于是做商城业务,要频繁的对商品库存进行扣减,应用是集群部署,为避免并发造成库存超买超卖... 查看详情
面试官:“看你简历上写熟悉handler机制,那聊聊idlehandler吧?”(代码片段)
作者:承香墨影一.序Handler机制算是Android基本功,面试常客。但现在面试,多数已经不会直接让你讲讲Handler的机制,Looper是如何循环的,MessageQueue是如何管理Message等,而是基于场景去提问,看看你对Ha... 查看详情
哈哈,简历上写了个精通redis,结果...
...光荣下岗”了。只能收拾收拾低落的心情,开始整理简历,突然发现这些年的经验写到纸上真的是一文不值,都是各种xxx管理系统…大概 查看详情
电商学习目录
1.电商系统了解 什么是SPU、SKU、SKC、ARPU https://www.cnblogs.com/shoshana-kong/p/9656460.html2.电商系统整体框架3. 库存扣减4.秒杀系统设计 查看详情
电商 秒杀系统 设计思路和实现方法(代码片段)
电商 秒杀系统 设计思路和实现方法2017年05月26日00:06:35阅读数:36621秒杀业务分析正常电子商务流程(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货秒杀业务的特性(1)低廉价格... 查看详情
如何保证分布式系统的消息最终一致性
参考技术A针对楼主的问题:下面我们以电商购物支付流程中,在各大参与者系统中可能会遇到分布式事务问题的场景进行详细的分析!如上图所示,假设三大参与平台(电商平台、支付平台、银行)的系统都做了分布式系统架... 查看详情
全网最全-谷粒商城项目-面试总结-简历优化
项目名称:书阁”图书商城管理系统、微盟电子商城网络交易系统、高校闲置资源交易系统购物在“e”零售商城平台、惠农通—智慧农资商城、农产品轻量级微商城系统项目简介:本系统采用微服务架构设计,在分布式环境下利... 查看详情
分布式事务框架---tcc(代码片段)
...业务场景介绍咱们先来看看业务场景,假设你现在有一个电商系统,里面有一个支付订单的场景。那对一个订单支付之后,我们需要做下面的步骤:更改订单的状态为“已支付”扣减商品库存给会员增加积分创建销售出库... 查看详情
分布式一致性的想法
...大家一起探讨下常见一致性问题下订单减库存在我们做的电商系统中,会有这样的一个场景:用户下单购买某个商品,然后进行扣减商品库存的场景。如果先下订单,然后扣减库存,会导致超卖如果下订单失败,扣减库存成功,... 查看详情
无标题(代码片段)
摘要如果你要开发一个电商库存系统,最担心的是什么?闭上眼睛想下,当然是高并发和防超卖了!本文给出一个统筹考虑如何高并发和防超卖数据准确性的方案。读者可以直接借鉴本设计,或在此基础上做... 查看详情