一件商品只有100个库存,如今有1000或者更多的用户来购买,每一个用户计划同时购买1个到几个不等商品。如何保证库存在高并发的场景下是安全的。 1.很少发 2.很多发程序员
1.下单 2.下单同时预占库存 3.支付 4.支付成功真正减扣库存 5.取消订单 6.回退预占库存redis
方案一:加入购物车的时候去预占库存。 方案二:下单的时候去预占库存。 方案三:支付的时候去预占库存。数据库
方案一:加入购物车并不表明用户必定会购买,若是这个时候开始预占库存,会致使想购买的没法加入购物车。而不想购买的人一直占用库存。显然这种作法是不可取的。 方案二:商品加入购物车后,选择下单,这个时候去预占库存。用户选择去支付说明了,用户购买欲望是比 方案一 要强烈的。订单也有一个时效,例如半个小时。超过半个小时后,系统自动取消订单,回退预占库存。 方案三:下单成功去支付的时候去预占库存。只有100个用户能支付成功,900个用户支付失败。用户体验很差,就像你走了一条光明大道,一路通畅,忽然被告知此处不通行。并且支付流程也是一个比较复杂的流程,若是和减库存放在一块儿,将会变的更复杂。缓存
因此综上所述: 选择方案二比较合理。安全
/**
* 先生成 token 保存到 Redis
* token 做为 key , 并设置过时时间 时间长度 根据任务需求
* value 为数字 自增判断 是否使用过
*
* @param user
* @return
*/
public String createToken(User user) {
String key = "placeOrder:token:" + user.getId();
String token = UUID.randomUUID().toString();
//保存到Redis
redisService.set(key + token, 0, 1000L);
return token;
}
/**
* 校验下单的token是否有效
* @param user
* @param token
* @return
*/
public Boolean checkToken(User user, String token) {
String key = "placeOrder:token:" + user.getId();
if (null != redisService.get(key + token)) {
long times = redisService.increment(key + token, 1);
if (times == 1) {
//利用increment 原子性 判断是否 该token 是否使用
return true;
} else {
// 已经使用过了
}
//删除
redisService.remove(key + token);
}
return false;
}
复制代码
同一个用户或者多个用户同时抢购一个商品的时候,咱们如何作到并发安全减扣库存?bash
数据库操做商品库存:网络
/**
* Created by Administrator on 2017/9/8.
*/
public interface ProductDao extends JpaRepository<Product, Integer> {
/**
* @param pid 商品ID
* @param num 购买数量
* @return
*/
@Transactional
@Modifying
@Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1")
int reduceStock1(Integer pid, Integer num);
/**
* @param pid 商品ID
* @param num 购买数量
* @return
*/
@Transactional
@Modifying
@Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1 and availableNum - ?2 >= 0")
int reduceStock2(Integer pid, Integer num);
}
复制代码
下单:并发
/**
* 下单操做1
*
* @param req
*/
private int place(PlaceOrderReq req) {
User user = userDao.findOne(req.getUserId());
Product product = productDao.findOne(req.getProductId());
//下单数量
Integer num = req.getNum();
//可用库存
Integer availableNum = product.getAvailableNum();
//可用预约
if (availableNum >= num) {
//减库存
int count = productDao.reduceStock1(product.getId(), num);
if (count == 1) {
//生成订单
createOrders(user, product, num);
} else {
logger.info("库存不足 3");
}
return 1;
} else {
logger.info("库存不足 4");
return -1;
}
}
/**
* 下单操做2
*
* @param req
*/
private int place2(PlaceOrderReq req) {
User user = userDao.findOne(req.getUserId());
Product product = productDao.findOne(req.getProductId());
//下单数量
Integer num = req.getNum();
//可用库存
Integer availableNum = product.getAvailableNum();
//可用预约
if (availableNum >= num) {
//减库存
int count = productDao.reduceStock2(product.getId(), num);
if (count == 1) {
//生成订单
createOrders(user, product, num);
} else {
logger.info("库存不足 3");
}
return 1;
} else {
logger.info("库存不足 4");
return -1;
}
}
复制代码
方法1 :框架
不考虑库存安全的写法:dom
/**
* 方法 1
* 减可用
* 加预占
* 库存数据不安全
*
* @param req
*/
@Override
@Transactional
public void placeOrder(PlaceOrderReq req) {
place1(req);
}
复制代码
分析: 在高并的场景下,假设库存只有 2 件 ,两个请求同时进来,抢购改商品,购买数量都是 2. A请求 此时去获取库存,发现库存恰好足够,执行扣库存下单操做。 在 A 请求为完成的时候(事务未提交),B请求 此时也去获取库存,发现库存还有2. 此时也去执行扣库存,下单操做。
库存剩 2 ,可是卖出了 4 。最终数据库库存数量将变为 -2 ,因此库存是不安全的。
方法2:
这个操做能够保证库存数据是安全的。
/**
* 方法 2
* 减可用
* 加预占
* 库存数据不安全
*
* @param req
*/
@Override
@Transactional
public void placeOrder(PlaceOrderReq req) {
place2(req);
}
复制代码
分析: 在方法1 的基础上 ,更新库存的语句,增长了可用库存数量 大于 0, availableNum - num >= 0 ;实质是使用了数据库的乐观锁来控制库存安全,在并发量不是很大的状况下能够这么作。可是若是是秒杀,抢购,瞬时流量很高的话,压力会都到数据库,可能拖垮数据库。
方法3:
该方法也能够保证库存数量安全。
/**
* 方法 3
* 采用 Redis 锁 通一个时间 只能一个 请求修改 同一个商品的数量
* <p>
* 缺点并发不高,同时只能一个用户抢占操做,用户体验很差!
*
* @param req
*/
@Override
public void placeOrder2(PlaceOrderReq req) {
String lockKey = "placeOrder:" + req.getProductId();
Boolean isLock = redisService.lock(lockKey);
if (!isLock) {
logger.info("系统繁忙稍后再试!");
return 2;
}
//place2(req);
place1(req);
//这两个方法均可以
redisService.unLock(lockKey);
}
复制代码
分析:
利用Redis 分布式锁, 强制控制 同一个商品,同时只能一个请求处理下单。 其余请求返回 ‘系统繁忙稍后再试!’; 强制把处理请求串行化,缺点并发不高 ,处理比较慢,不适合抢购等方案 。 用户体验也很差,明明看到库存是充足的,就是强不到。 相比方案2减轻了数据库的压力。
方法4 :
能够保证库存安全,知足高并发处理,可是相对复杂一点。
/**
* 方法 4
* 商品的数量 等其余信息 先保存 到 Redis
* 检查库存 与 减小库存 不是原子性, 以 increment > 0 为准
*
* @param req
*/
@Override
public void placeOrder3(PlaceOrderReq req) {
String key = "product:" + req.getProductId();
// 先检查 库存是否充足
Integer num = (Integer) redisService.get(key);
if (num < req.getNum()) {
logger.info("库存不足 1");
} else{
//不可在这里下单减库存,不然致使数据不安全, 状况相似 方法1;
}
//减小库存
long value = redisService.increment(key, -req.getNum().longValue());
//库存充足
if (value >= 0) {
logger.info("成功抢购 ! ");
//TODO 真正减 扣 库存 等操做 下单等操做 ,这些操做可用经过 MQ 或 其余方式
place2(req);
} else {
//库存不足,须要增长刚刚减去的库存
redisService.increment(key, req.getNum().longValue());
logger.info("库存不足 2 ");
}
}
复制代码
分析: 利用Redis increment 的原子操做,保证库存安全。 事先须要把库存的数量等其余信息保存到Redis,并保证更新库存的时候,更新Redis。 进来的时候 先 get 库存数量是否充足,再执行 increment。以 increment > 0 为准。 检查库存 与 减小库存 不是原子性的。 检查库存的时候技术库存充足也不可下单;不然形成库存不安全,原来相似 方法1. increment 是个原子操做,已这个为准。
redisService.increment(key, -req.getNum().longValue()) >= 0 说明库存充足,能够下单。
redisService.increment(key, -req.getNum().longValue()) < 0 的时候 不能下单,次数库存不足。而且须要 回加刚刚减去的库存数量,不然会致使刚才减扣的数量 一直买不出去。数据库与缓存的库存不一致。
次方法能够知足 高并抢购等一些方案,真正减扣库存和下单能够异步执行。
订单取消后,可利用MQ 回退库存等。