# 商城秒杀接口
**Repository Path**: coderman_hero/mall-seckill-interface
## Basic Information
- **Project Name**: 商城秒杀接口
- **Description**: 商城秒杀接口,里面介绍了如何保证并发条件下,订单接口如何引起超卖以及如何放置超卖的解决方案
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2023-10-25
- **Last Updated**: 2023-10-25
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 商城秒杀接口
## 描述
商城秒杀接口
## 技术栈
springboot/nacos/mybatis-plus.
## 秒杀步骤
一般情况下,购买的步骤为:
(已经到了秒杀时间的前提条件)
1.根据商品id查询库存
2.如果存在库存,扣除库存。
3.扣除成功,创建订单
4.用户支付等步骤(支付失败,增加库存)
## 数据库设计
```mysql
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁,版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO stock(`name`,count,sale,version) VALUE('小米8',14,0,0);
-- ----------------------------
-- Table structure for stock_order
-- ----------------------------
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`sid` int(11) NOT NULL COMMENT '库存ID',
`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
## 秒杀问题及解决方案
#### 秒杀问题描述:
在并发量不大的情况下,不容易出现,订单的数量大于库存数量,但是,一旦出现高并发,就容易出现超卖的问题。
只是一般的查询订单和扣除库存,就会出现超卖,下图就是在订库存只有12的情况下,但是订单数量却远远大于库存。
使用jmeter进行测试,测试接口为localhost:8081/generator/stockorder/createWrongOrder/1


#### 秒杀解决方案:
#### 1悲观锁解决方案(该方法还可以解决分布式微服务的问题)
悲观锁的解决方案本文没有采用,大致原理如下:
###### 1.1对接口进行加锁
数据库存储接口字段,并将接口字段设置为primary key,线程每次进入前,都会尝试插入createWrongOrder关键字到数据库,如果插入成功,表明这个时候只有一个线程在创建订单,继续进行订单创建,如果插入失败,订单创建直接失败。遇到任何异常,从数据库删除(释放)createWrongOrder。考虑到mysql的性能,可以将mysql替换成redis,redis天然支持高并发,可以使用redis的集合进行数据插入,插入成功证明获取到锁,并且要为锁设置过期时间。不管创建订单与否,都要记得删除锁
###### 1.2 对商品库存的id进行加锁
只需要对库存id进行加锁,也就是同一时间只允许一个库存id被一个线程操作,加锁原理和对接口加锁一致。
#### 2 乐观锁解决
在仓库里增加version关键字,每次修改库存时,这样在高并发下,如果有其他线程修改了库存,那么version就会改变,这次订单就会失败,这样能够保证同一时间段下,只有一个订单会成功
```xml
update stock
sale =sale+1,
version = version+1
WHERE id = #{stock.id}
AND version = #{stock.version}
```
使用jmeter进程测试,测试接口 localhost:8081/generator/stockorder/createOptimisticOrder/1,测试后发现,虽然会出现部分线程没有抢占到资源,但是不会出现超卖的问题。库存12,最后创建的也是12个订单。




#### 3对乐观锁进行优化
在高并发情况下,mysql面临大量的查询以及大量抢占式修改,无疑会增加服务器的压力,为了解决这个问题,我们可以对接口做一些限流。
常见限流的方法:
1)Tomcat 使用 maxThreads来实现限流。 当请求的并发大于此值(maxThreads)时,请求就会排队执行,这样就完成了限流的目的。
```xml
```
2)Nginx的limit_req_zone和 burst来实现速率限流。
- `$binary_remote_addr`:binary_目的是缩写内存占用,remote_addr表示通过IP地址来限流
- `zone`:iplimit是一块内存区域(记录访问频率信息),20m是指这块内存区域的大小
- `rate`: **12r/s** = 12 request / second**,类似于1200/m(每分钟1200次请求)
- `burst`: burst=20,设置一个大小为20的缓存区域,当大量请求到来,请求数量超过限流频率时,将其放入缓冲区域
- `nodelay`: 缓冲区满了后直接返回503异常
```
http {
limit_req_zone $binary_remote_addr_zone=iplimit:10m rate=12r/s;
server {
server_name www.nginx-lyntest.com;
listen 80;
location /access-limit/ {
proxy_pass http://127.0.0.1:8081/;
# 根据ip地址限制流量
limit_req_zone=iplimit burst=20 nodelay;
}
}
}
```
3)Nginx的limit_conn_zone和 limit_conn两个指令控制并发连接的总数。
4)时间窗口算法借助 Redis的有序集合可以实现。
```
利用redis zset,key为限流id,score存储限流的时间,每次请求要保证,前面1000ms最多只有150个请求。key的过期时间为1000ms
```
5)漏桶算法可以使用Redis利用热地-Cell来实现。
水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
```
cl.throttle sixj:reply 10 10 60 1
第一个参数 sixj:reply #key
第二个参数 10 #漏斗容量
第三、四个参数 10 60 #每60秒10次操作
第五个参数 1 #每次进入漏斗的量(可选参数,默认为1)
返回结果
1)(integer) 0 #0表示允许,1表示拒绝
2)(integer)10 #漏斗容量
3)(integer)9 #漏斗剩余空间
4)(integer)-1 #如果被拒绝了,需要多长时间再试(漏斗有空间了,单位秒)
5)(integer)6 #多长时间后,漏斗完全空出来
如果被拒绝了,就需要丢弃或重试,直接取出返回结果的第四个值进行sleep即可,如果不想阻塞线程,也可以异步定时任务来重试。
```
6)令牌算法可以解决Google的guava包来实现。
```java
//每秒放行10个请求
private RateLimiter rateLimiter = RateLimiter.create(10);
if(!rateLimiter.tryAcquire(10, TimeUnit.MILLISECONDS)){
return R.error().put("data","you are throttled");
}
```
## 缓存与数据库双写问题
缓存是为了更快的查询,会把数据放入缓存,那么再执行更新的时候,就会有个问题,到底是先更新缓存还是先更新数据库呢,更新了数据库,那么缓存是旧的,更新了缓存,但是数据库又是旧的,就造成不一致性问题。
解决方案:
延时双删,先淘汰缓存,再写缓存,休眠一定时间后,再次淘汰缓存。这种情况遇到读写分离数据库,一样可能造成读写一致性问题。
```
请求A执行写入请求,删除缓存
请求A再将数据写入mysql
请求A再次删除缓存
请求B查询缓存没有,又去mysql查询,这是由于没有完成主从同步,查询到的是旧的值
请求B又将旧的值写入缓存。
数据库完成主从同步,从库变成新值。
```
### 删除缓存有可能失败怎么办?
交给中间件,中间件有失败重试机制。
## 接口安全
1限流,防止大量请求。
2 秒杀到达秒杀时间
3为了防止刷单,必要时需要对接口进行隐藏,用户请求时,向暴露在外的一个接口,商品id和用户信息通过md5加椒盐进行加密,用户信息用户不能完全知道,而且椒盐信息也属于保密,所以用户就没办法伪造md5码,但是这样用户还是可以通过同一个md5进行多次请求,这个时候就需要借助redis了,如果解密后的用户信息以及库存都还有,就将用户id和商品id组成的key存入redis(要么购满成功key失效,要么时间过期key失效,如果key没有失效,用户进来一次,key对应的value+1,如果超过一定次数,用户就不能再购买这件商品了,防止恶意抢单。不过一般key值过期时间为半小时,这个时间段抢购已经结束了。
## 后续待补充