秒杀场景:比如选课系统。当然并发不多,只是举个例子。
1、每个老师只有40个名额,但是有4000个学生并发来选某一个老师得课。
原则: 把请求拦截在系统上游。延长请求过程时间。
一、前端验证:
1、js限制每秒只能发送一个请求。
2、按钮或连接点击之后几秒不能再点击,
3、验证码。
4、答题。答题要生成题库,复杂了。
5、不跳转页面,直接发送json数据,ajax异步操作,省html资源。
二、站点层:
限制用户id请求次数,一秒内只能透过10个请求,防止别人for循环请求。
具体:随手写的代码,没有经过测试。
关于这里,可以看这里:(一)redis限流
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
String id = "123";
RBucket<Boolean> bucket = redisson.getBucket("blackId:" + id);
// 是否是黑名单得
if(bucket.get() == true){
// 或者返回最近的一个请求的缓存
return;
}
RAtomicLong idAccessNum = redisson.getAtomicLong(id);
while (true){
long num = idAccessNum.get();
// 没有初始化,则设置过期时间
// 这种方式,并非准确10秒10次就到了阈值,可能前10秒已经有9次,然后后10秒又请求了9次,
// 最长可能会有18次。但是我们不需要准确的10次,所以无影响。
// 如果想准确的统计,可以使用zset
if(num == 0){
idAccessNum.expire(10, TimeUnit.SECONDS);
}
// 超过了10次,则进入黑名单
if(num > 10){
// 加入黑名单,30秒之后不能再访问
bucket.set(true, 30, TimeUnit.SECONDS);
// 或者返回最近的一个请求的缓存
return;
}
// 这里自旋,只针对同一个用户id,因为理论上同一个用户的请求是线性的,所以这里自旋
// 不会对性能造成很大的影响
boolean updateSuccess = idAccessNum.compareAndSet(num, num + 1);
// 是否更新成功
if(updateSuccess){
break;
}
}
// 放行
三、服务层
站点层拦截了一部分之后,到了服务层的就是真实的有效的请求了。
直接用redis收集已经下单的数量,库存有多少,则放多少请求进来,其他,都返回失败。
真正进来的的请求之后, 如果压力还大,可以进入mq,但是一旦进入了mq, 则说明是异步的,前端需要轮询查询订单是否下单成功(这里不适合websocket,因为这个轮询不会很久,没必要弄一个websocket),或者,容错性比较大,个别错误了无所谓。
也可以,进入线程池,但是进入线程池,一旦,服务挂了,数据就丢失了。
String userId = "123";
String storeId = "345";
// 用户是否已经下过订单
RBucket<Long> isOrdered = redisson.getBucket(storeId + ":" + userId);
if(isOrdered.get() == 0){
// 查询数据库,然后更新缓存,然后判断是否已经下过订单
} else if(isOrdered.get() == 2){
//已经下过订单
return;
}
// 获取库存id
RBucket<Long> storeBucket = redisson.getBucket(storeId + ":num");
Long storeNum = storeBucket.get();
// 缓存没有,则去查数据库
if(storeNum == 0){
// 查数据库
// 没有则去查数据库(如果并发太高,这里有缓存穿透),可以分布式锁控制
// 可以更新库存的时候,直接写入缓存。
}
RBucket<Long> inNumBucket = redisson.getBucket("storeId:inNum");
while (true){
Long inNum = inNumBucket.get();
// 订单已经满了,则返回,抢购失败
if(inNum >= storeNum){
return;
}
// 减少库存
boolean isSuccess = inNumBucket.compareAndSet(inNum, inNum - 1);
if(isSuccess){
break;
}
}
// 下单成功
// 如果压力还大,可以进入mq,但是一旦进入了mq,
// 则说明是异步的,前端需要轮询查询订单是否下单成功
// 这里保证幂等性,因为,如果同一个用户,落到两台机器上,则可能都会进入这里
// 比如,insert唯一键,失败了,则回滚,并且,回滚库存,加1
四、对于读请求
就加缓存,加机器,再不行就限流抛弃一部分请求。
站点层,可以cdn加速。
五、业务折衷
1、比如,先抢到一个许可证,之后用这个许可证去购买。
这样,在第一个抢票环节,就直接往mq种放,不需要考虑消息是否执行完成,马上返回抢票成功。
缺点:用户体验极差。真的有这种并发量,还不如允许mq异步,然后页面跳转然后轮询呢。
2、对于当前库存数量,不精确数字,比如12306的显示有票。
3、把抢购,分时段,比如12306的分批放票。
六、问题:
一、如果一个用户多个请求,落到不同的服务器上,将会造成,下多个订单的问题。
解决方案:
1、数据库做唯一键,保证幂等性。
数据库插入失败之后,回滚把库存加回去
2、如果是update操作,j则加where条件乐观锁机制,返回影响0条,则回滚库存。
3、或者,分布式锁,锁住这个用户商品抢购,然后去查询是否下单成功。
总之,保证幂等性。
网友评论