概述: 这次完善项目的时候加入了秒杀功能。这个功能要考虑的地方挺多的,我在网上找了一些资料,然后把这个功能大致完成了。但是还有很多地方我没考虑到,有些地方实现的不是很好,等以后再回头看对这个功能进行进一步的完善吧。
记录一下我现在实现的一些功能: redis做缓存: 因为秒杀这个功能并发性是很大的,所以如果在秒杀的时候直接对mysql数据库进行操作,数据库可能承受不住,所以一般情况下都会用redis做缓存,将秒杀商品的信息存到redis里面,当redis里面查不到数据的时候再进入mysql查询,如果查询到数据,就将mysql中的数据写入到redis。
库存问题: 在扣减内存时,要先判断一个用户是否已经购买过,确定未购买再进行扣减内存。但因为查询数据库和更新数据库不是原子性操作,在并发性很高的情况下,可能会出现超卖的情况。这时候可以使用lua脚本,查询和扣减内存的操作均在lua中进行,可以保证原子性。
key[1]:用来存储已经进行秒杀过的用户id
key[2]:存储秒杀商品内存
ARGV[1]:用户id
ARGV[2]:订单信息类转化为的Json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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进行查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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<String> 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(); } } }
1 2 3 4 5 6 public boolean setNx (String key,String value,long timeout,TimeUnit unit) { return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit); }
设置过期时间,防止死锁。
如果在进行删除操作之前,一个锁刚好到过期时间,此时另一个请求正好加锁,就会释放掉别人的锁,所以加锁的时候要加userValue,在释放锁的时候判断一下是否是自己的锁,如果是,再将锁释放。因为在get和del之间可能也会出现异常,所以也要保证原子性,就在使用了lua脚本。
1 2 3 4 5 6 7 8 9 10 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做消息队列,这一部分完成的不是很理想,因为目前我对线程这一块的知识了解的特别特别特别模糊,等以后我学习了更多知识后再对这一部分进行完善。
目前的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @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){ try { Thread.sleep(200 ); } catch (InterruptedException ex) { ex.printStackTrace(); } break ; } } } }
@PostConstruct:在类启动的时候运行。
构造方法 ——> @Autowired —— > @PostConstruct ——> 静态方法 (按此顺序加载)