首先我就用华为官网 来说明秒杀的 整体流程,在华为官网某个秒杀场次点击商品信息
我们就会跳转到商品详情页中来,具体这里的技术难点 我上一篇文章也说了, 然后点击 立即抢购
点击抢购 到达商品确认页--->之后点击下单 付款
也就是秒杀这里的业务流程就是(在某个活动下选择 某个场次 ,在某个场次下选择具体的商品,比如 618活动选择12 点到2点的场次, 在12点到2点的场次下选择 华为Pro20)
(活动--->场次---->具体的商品) 点击商品---->商品详情页---->(立即抢购)商品确认页---------->{确认下单}
这就是秒杀这里的业务流程
秒杀的业务特点就是
呢么 在秒杀中我们应该注意一些什么技术问题呢?
商品详情页就是 之前说过了 采用nignx+lua+本地缓存 +redis 实现三级缓存策略
.对用户的频率实现限制 不能让用户频繁访问
秒杀 1.高并发解决超卖问题 首先我们要明白超卖问题是怎么产生的
--- table Info 我们的表结构
CREATE TABLE `tb_product_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`product_id` bigint(32) NOT NULL COMMENT '商品ID',
`number` INT(8) NOT NULL DEFAULT 0 COMMENT '库存数量',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`modify_time` DATETIME NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_pid` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品库存表';
// ProductStock pojo
class ProductStock {
private Long productId; //商品id
private Integer number; //库存量
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
}
-------------<<<< 不考虑并发的时候 我们(select )查库存--->(update) 更改库存--->(insert ) 新增订单
多线程并发情况下,会存在超卖的可能。
/**
* 更新库存(不考虑并发)
* @param productId
* @return
*/
public boolean updateStockRaw(Long productId){
// 查库存
ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
if (product.getNumber() > 0) {
// 更改
int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
if(updateCnt > 0){ //更新库存成功
return true;
}
}
return false;
}
看以上的图 假设线程1 和线程2 同时查询到了 此时的库存有100个
线程1 查询 100库存 进行update 更改库存(扣减库存) 更新为99
如果线程1 此时由于网络原因阻塞了, 此时
线程2 查询 100库存 进行update 更改库存(扣减库存) 更新为99,
因为在线程1 没有更新结束的时候 线程2就开始了查询 这样就发生了超卖 ,
我卖了2件商品 但是库存中只是扣减了一次
我们可以使用mysql的悲观锁来解决 select ... for update
使用mysql 的select ...for update 悲观锁来解决超卖问题 悲观锁具有排他性
我当前的事务没有提交或者回滚, 你其他线程是不可以操作这一条数据的
/**
* 更新库存(使用悲观锁)
* @param productId
* @return
*/
public boolean updateStock(Long productId){
//先锁定商品库存记录
ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE", productId);
if (product.getNumber() > 0) {
int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
if(updateCnt > 0){ //更新库存成功
return true;
}
}
return false;
}
/**
* 下单减库存---------------->乐观锁 我们也可以使用Mysql 的乐观锁 ci
* @param productId
* @return
*/
public boolean updateStock(Long productId){
int updateCnt = 0;
while (updateCnt == 0) {
ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
if (product.getNumber() > 0) {
.................... 这里 我们更改的时候加上 之前查询的数量如果是100的化 就更改 , 或者加个版本号 或者加上uuid
updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber());
if(updateCnt > 0){ //更新库存成功
return true;
}
} else { //update fill 更改失败
return false;
}
}
return false;
}
1. 无论是乐观锁还是悲观锁 我们都要操作db, 都要上锁 会影响性能的 而且我们db的性能是优先的
比如说1w个人来抢购,商品有100个, 就会有1w个人来操作db ,但是我此时只想让100 个人操作db,不让(1w-100)个人操作db
我们可以采用redis 做预热
我们同样可以使用本地缓存 标志位true 加上redis 做预热来解决当前性能问题 这样我们的查询db 只会查询一部分 减少了db的压力