一、难点及解决方案总结
作为一个秒杀系统,常常面临以下问题:
-
秒杀还未开始,就有用户模拟数据提前发送请求进行秒杀或者抢购开始瞬间有人直接利用脚本大量发送请求
-
秒杀前用户不停刷新页面
-
秒杀开始瞬间请求数暴增
-
商品被同一用户重复秒杀
-
超卖超买
-
订单的持久化
解决方案:
- 在后端先判断服务器时间是否在秒杀时间内,如果不是,返回错误信息
- 使用CDN对静态资源进行分发
- 在秒杀开始前,提前将所有的商品信息、库存存入redis中,用户直接从redis中读取商品信息。
- 在redis中对每一个商品,使用商品id作为key,用户id作为value创建一个set集合暂时保存成功的订单信息,用户发送购买请求时,先查看redis中是否已经存在了该用户的订单信息,如果存在返回“重复秒杀”错误。
- 如果没有“重复秒杀”错误,使用分布式乐观锁进行减库存和增加订单。
- 当订单数每达到500的倍数,就开启4个线程对订单进行持久化操作。使用redis不能保证数据的强一致性,如果需要保证数据的强一致性,还是需要使用mysql。
二、系统实现
1、防止提前秒杀
防止提前秒杀是秒杀系统必须考虑的因素,如果商品还没开始出售就被人抢光或者有人直接利用脚本大量发送请求破坏秒杀抢购的公平性会及其影响用户的体验。这里我们采用服务器端判断时间的方式来判断是否能进行秒杀,判断流程如下:
- 用户发送请求
- 判断接收到请求的时间是否在秒杀商品抢购的时间范围内
- 在时间范围内返回经过加密的md5,不在则返回错误信息
- 用户根据返回的md5再次发送请求,服务器判断md5是否正确来判断是否能进行抢购
代码如下:
public class SeckillServiceImpl implements SeckillService {
public Exposer exportSeckillUrl(long seckillId) {
//从redis中读取商品信息
Seckill seckill = redisDao.getSeckill(seckillId);
if (seckill == null) {
//缓存里没找到,去数据库找
seckill = seckillDao.queryById(seckillId);
if (seckill == null){
return new Exposer(false,seckillId);
}else {
redisDao.putSeckill(seckill);
}
}
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
Date cur = new Date();
//不秒杀时间内
if (cur.getTime() > endTime.getTime() ||
cur.getTime() < startTime.getTime()){
return new Exposer(false,seckillId,cur.getTime(),
startTime.getTime(),endTime.getTime());
}
//在秒杀时间内,返回md5
String md5 = getMD5(seckillId);
return new Exposer(true,md5,seckillId);
}
private String getMD5(long seckillId){
//根据商品id生成md5
String base = seckillId + "/" + slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
}
2、分布式乐观锁实现秒杀
我们在redis中通过乐观锁来实现线程的安全,解决超买超卖问题,乐观锁的实现如下:
public class RedisDao {
public boolean lock(String seckillId,String value){
try {
Jedis jedis= jedisPool.getResource();
try {
<