【秒杀系统】从零开始打造简易秒杀系统(一):防止超卖
@RequestMapping("/createWrongOrder/{sid}")
@ResponseBody
public String createWrongOrder(@PathVariable int sid) {
LOGGER.info("购买物品编号sid=[{}]", sid);
int id = 0;
try {
id = orderService.createWrongOrder(sid);
LOGGER.info("创建订单id: [{}]", id);
} catch (Exception e) {
LOGGER.error("Exception", e);
}
return String.valueOf(id);
}
@Override
public int createWrongOrder(int sid) throws Exception {
//校验库存
Stock stock = checkStock(sid);
//扣库存
saleStock(stock);
//创建订单
int id = createOrder(stock);
return id;
}
private Stock checkStock(int sid) {
Stock stock = stockService.getStockById(sid);
if (stock.getSale().equals(stock.getCount())) {
throw new RuntimeException("库存不足");
}
return stock;
}
private int saleStock(Stock stock) {
stock.setSale(stock.getSale() + 1);
return stockService.updateStockById(stock);
}
private int createOrder(Stock stock) {
StockOrder order = new StockOrder();
order.setSid(stock.getId());
order.setName(stock.getName());
int id = orderMapper.insertSelective(order);
return id;
}
在JMeter里启动1000个线程,无延迟同时访问接口。模拟1000个人,抢购100个产品的场景。点击启动:
卖出了14个,库存减少了14个,但是每个请求Spring都处理了,创建了1000个订单。
避免超卖问题:更新商品库存的版本号
为了解决上面的超卖问题,我们当然可以在Service层给更新表添加一个事务,这样每个线程更新请求的时候都会先去锁表的这一行(悲观锁),更新完库存后再释放锁。可这样就太慢了,1000个线程可等不及。
我们需要乐观锁。
一个最简单的办法就是,给每个商品库存一个版本号version字段
<update id="updateByOptimistic" parameterType="cn.monitor4all.miaoshadao.dao.Stock">
update stock
<set>
sale = sale + 1,
version = version + 1,
</set>
WHERE id = #{id,jdbcType=INTEGER}
AND version = #{version,jdbcType=INTEGER}
</update>
发起并发购买请求
卖出去了39个,version更新为了39,同时创建了39个订单。我们没有超卖,可喜可贺。
【秒杀系统】零基础上手秒杀系统(二):令牌桶限流 + 再谈超卖
接口限流
在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。
所以秒杀系统会尽量选择独立于公司其他后端系统之外进行单独部署,以免秒杀业务崩溃影响到其他系统。
除了独立部署秒杀业务之外,我们能够做的就是尽量让后台系统稳定优雅的处理大量请求。
接口限流实战:令牌桶限流算法
- 令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。
- 大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
@Controller
public class OrderController {
private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);
@Autowired
private StockService stockService;
@Autowired
private OrderService orderService;
//每秒放行10个请求
RateLimiter rateLimiter = RateLimiter.create(10);
@RequestMapping("/createWrongOrder/{sid}")
@ResponseBody
public String createWrongOrder(@PathVariable int sid) {
int id = 0;
try {
id = orderService.createWrongOrder(sid);
LOGGER.info("创建订单id: [{}]", id);
} catch (Exception e) {
LOGGER.error("Exception", e);
}
return String.valueOf(id);
}
/**
* 乐观锁更新库存 + 令牌桶限流
* @param sid
* @return
*/
@RequestMapping("/createOptimisticOrder/{sid}")
@ResponseBody
public String createOptimisticOrder(@PathVariable int sid) {
// 阻塞式获取令牌
//LOGGER.info("等待时间" + rateLimiter.acquire());
// 非阻塞式获取令牌
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
LOGGER.warn("你被限流了,真不幸,直接返回失败");
return "购买失败,库存不足";
}
int id;
try {
id = orderService.createOptimisticOrder(sid);
LOGGER.info("购买成功,剩余库存为: [{}]", id);
} catch (Exception e) {
LOGGER.error("购买失败:[{}]", e.getMessage());
return "购买失败,库存不足";
}
return String.format("购买成功,剩余库存为:%d", id);
}
}
代码中,RateLimiter rateLimiter = RateLimiter.create(10);这里初始化了令牌桶类,每秒放行10个请求。
在接口中,可以看到有两种使用方法:
阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,就在这里阻塞住,等待令牌的发放。
非阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,会尝试等待设置好的时间(这里写了1000ms),其会自动判断在1000ms后,这个请求能不能拿到令牌,如果不能拿到,直接返回抢购失败。如果timeout设置为0,则等于阻塞时获取令牌。