这次完善项目的时候加入了秒杀功能。这个功能要考虑的地方挺多的,我在网上找了一些资料,然后把这个功能大致完成了。但是还有很多地方我没考虑到,有些地方实现的不是很好,等以后再回头看对这个功能进行进一步的完善吧。
因为秒杀这个功能并发性是很大的,所以如果在秒杀的时候直接对mysql数据库进行操作,数据库可能承受不住,所以一般情况下都会用redis做缓存,将秒杀商品的信息存到redis里面,当redis里面查不到数据的时候再进入mysql查询,如果查询到数据,就将mysql中的数据写入到redis。
在扣减内存时,要先判断一个用户是否已经购买过,确定未购买再进行扣减内存。但因为查询数据库和更新数据库不是原子性操作,在并发性很高的情况下,可能会出现超卖的情况。这时候可以使用lua脚本,查询和扣减内存的操作均在lua中进行,可以保证原子性。
key[1]:用来存储已经进行秒杀过的用户id
key[2]:存储秒杀商品内存
ARGV[1]:用户id
ARGV[2]:订单信息类转化为的Json
local userKey=KEYS[1];
local stockKey=KEYS[2];
local userId=ARGV[1];
local orderMsg=ARGV[2];
local userExists=redis.call('sismember',userKey,userId)
if(tonumber(userExists)==1) then
return -2;
end;
if (redis.call('exists', stockKey) == 1) then
local stock = tonumber(redis.call('get',stockKey));
if (stock > 0) then
redis.call('incrby', stockKey, -1);
redis.call('sadd',userKey,userId);
redis.call('lpush','orderList',orderMsg);
return 1;
end;
return 0;
end;
return -1;
-2:用户已经秒杀过,不能再进行秒杀
-1:秒杀商品在redis里面不存在,返回后对数据库进行查询,如果商品存在将商品信息存入redis,不存在直接返回
0:库存不足,直接返回
1:秒杀成功,将库存减一,用户id存入已经秒杀过的用户中
在秒杀时先从redis数据库中查询数据,如果数据不存在就进入mysql查询。但是在高并发的情况下,一瞬间有很多请求同时进行,此时在redis中查询不到数据,就会全部进入mysql查询,会造成缓存击穿。解决缓存击穿有好几种方案,例如:热数据设置永不过期、加分布式锁。这里我使用了分布式锁,就是让很多请求同时查询不到数据的时候,只允许一个请求对mysql进行操作,这个请求将mysql中的数据读取到redis之后,其他请求再进入redis进行查询。
while(result.equals(MagicIntegerEnum.STOCK_NO_EXIST.getKey())){
String lockKey="killLock";
result = luaUtil.runLuaScript("Stock.lua",keyList,userId.toString(),orderJson);
String userValue = UUID.randomUUID().toString();
boolean lock = redisUtil.setNx(lockKey, userValue, 30, TimeUnit.SECONDS);
if (lock) {
SecKill kill = killDao.getByProId(proId, 1);
List list=new ArrayList<>();
list.add(lockKey);
if(kill==null){
luaUtil.runLockLua("Lock.lua",list,userValue);
return ReturnUtil.error("无秒杀商品");
}
int size=0;
if(redisUtil.hasKey(userKey)) {
size = redisUtil.setMembers(userKey).size();
}
int stock=kill.getKillStock()-size;
redisUtil.set(killKey, Integer.toString(stock));
luaUtil.runLockLua("Lock.lua",list,userId.toString());
}else {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
*只有在 key 不存在时设置 key 的值,将值 value 关联到 key ,设置过期时间
*/
public boolean setNx(String key,String value,long timeout,TimeUnit unit){
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}
设置过期时间,防止死锁。
如果在进行删除操作之前,一个锁刚好到过期时间,此时另一个请求正好加锁,就会释放掉别人的锁,所以加锁的时候要加userValue,在释放锁的时候判断一下是否是自己的锁,如果是,再将锁释放。因为在get和del之间可能也会出现异常,所以也要保证原子性,就在使用了lua脚本。
local lockKey=KEYS[1];
local userValue=ARGV[1];
local exist=redis.call('exists', lockKey);
if(tonumber(exist)==0) then
return 0;
end;
if(redis.call('get',lockKey)==userValue) then
return tonumber(redis.call('del',lockKey));
end;
return 1;
在秒杀之后下单的操作其实并发性并没有那么高,我在网上找了一些资料,发现很多时候秒杀下单的时候会采用mq消息队列进行异步处理。但是因为我对这个不是很了解,就使用了redis的list做消息队列,这一部分完成的不是很理想,因为目前我对线程这一块的知识了解的特别特别特别模糊,等以后我学习了更多知识后再对这一部分进行完善。
目前的代码:
@PostConstruct
private void init(){
SKILL_ORDER.submit(new OrderHandle());
}
public class OrderHandle implements Runnable{
@Override
public void run() {
String queue="orderList";
while (true) {
try {
String orderJson = redisUtil.lBRightPop(queue, 1, TimeUnit.MINUTES);
if (orderJson == null || "".equals(orderJson)) {
continue;
}
Order order = JSON.parseObject(orderJson, Order.class);
int i = (int) ((Math.random() * 9 + 1) * 100000);
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMss");
String format = localDateTime.format(formatter);
String code = format + order.getUserId() + order.getProId() + order.getAddId() + i;
SecKill kill = killDao.getByProId(order.getProId(), 1);
order.setOrderCode(code);
order.setOrderPrice(kill.getKillPrice());
orderDao.insertOrder(order);
String orderKey="killOrder:"+order.getOrderId();
redisUtil.setEx(orderKey,"1",10,TimeUnit.MINUTES);
}
catch (Exception e){
// e.printStackTrace();
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;
}
}
}
}
@PostConstruct:在类启动的时候运行。
构造方法 ——> @Autowired —— > @PostConstruct ——> 静态方法 (按此顺序加载)