一、表结构设计
item_id | stock_count |
---|---|
1 | 10 |
每个用户下单,进行update,对这一行的库存进行扣减
二、如何防止超卖?
不能先查库存在进行扣减,多个线程会有超卖现象,使用如下SQL语句:
update stock set stock_count=stock_count-1
where item_id=1 and stock_count>=1
这条SQL语句持有了这个行锁,底层是串行执行的
三、库存数据放在Redis里的问题
库存是否要redis里面去操作,需要考虑redis本身的高可用问题
扣减库存的同时还要同时写一张库存流水表(分布式事务的解决方案),这两个操作是在一个事务当中的,如果把库存放到redis中,写库存流水表无法去保证原子性。
四、如何解决高并发问题?
并发资源的争抢,提升并发度:降低锁的粒度、降低锁持有的时间
1、降低锁的粒度
可以采用分库分表方案
2、降低锁持有的时间
内存里面设计一个队列,200ms的请求都放到一个队列里面,用户同步等待。让200ms内的多个用户请求都放在队列中,将这个队列请求合并后批量的进行一次数据的IO,批量的进行一次扣减
计数商品的并发请求,如果并发数超过了阈值,才会去创建队列。
五、难点问题解决
1、异步合并请求的时候报错,上游超时了
这个时候有可能上游服务显示没有扣减成功,但是其实已经扣减成功了,导致数据不一致,上游发出消息(携带订单号)对库存进行回滚,这是上面的那个库存流水表就起作用了。类似seata,如果已经插入库存流水表,针对这条记录进行回滚。
2、库存只有1件了,但是内存合并之后,需要去扣3件
异步线程合并去扣减不成功,退化成一个循环,每个用户购买的数量从大到小进行扣减,尽量保证买的多的用户线扣减成功,
3、优化点:降低锁持有的时间
事务(耗时t1 + t2):
update 库存表 耗时t1
insert 库存流水表 耗时t2
or
事务(耗时t1):
insert 库存流水表 耗时t2
update 库存表 耗时t1
选择第二种事务的SQL执行顺序,锁持有时间更短。
4、库存也是有缓存的,如何保证数据库和缓存的数据一致性?
cache aside 先更行数据库,在删缓存
改造为:监听binlog,代码不用关心缓存
5、缓存命中率低,刚更新为一个值,马上有更新成另外的值
监听binlog的时候,设计一个时间窗口,一个商品在5s之内有更新,只在5s结束的时候去更新缓存,避免频繁覆盖
6、binlog是并行过来的,后面的可能先被监听到,消息乱序的问题?
在表里面增加版本号,每次更新对版本号进行+1,监听binlog的时候,判断下redis的版本号,判断大于的还是更新,避免老的binlog把新的缓存值覆盖掉
7、异步刷新之后真实的库存和页面的库存有延迟?库存有没有比数量更重要
一旦扣库存失败,会把缓存里的数据同步更新成0
8、多个线程去判断版本号,会不会出现并发安全问题?
需要加分布式锁
9、内存队列很难横向扩容
数据库层做排队,应用层只能做到单机排队,单用用技术本身很多,这种排队方式控制并发仍然有限,所以如果能在数据库层做全局排队是最理想的,淘宝的数据库团队开发了针对这种MySQL的innoDB层的patch,可以做到数据库层上对单行记录做到并发排队。