- 功能核心点
* 经典互联网商品抢购秒杀功能
- 功能api
* 商品秒杀接口
- 数据落地存储方案
* 通过分布式redis减库存
* DB存最终订单信息数据
- api性能调优
* 性能瓶颈在高并发秒杀
* 技术难题在于超卖问题
秒杀系统功能步骤梳理
- 利用 Redis 缓存incr拦截流量
- 首先通过数据控制模块,提前将秒杀商品缓存到读写分离 Redis,并设置秒杀开始标记如下:
"skuId_start": 0 //开始标记1表示秒杀开始
"skuId_count": 10000 //总数
"skuId_access": 12000 //接受抢购数(接受最大抢购数=1.2*商品总数)
- 秒杀开始前,服务集群读取 skuId_start为 0,直接返回未开始。
- 服务时间不一致可能导致流量倾斜
- 数据控制模块将 skuId_start 改为1,标志秒杀开始。
- 当接受下单数达到 sku_count*1.2 后,继续拦截所有请求,商品剩余数量为 0
- 利用Redis缓存加速库存扣量
"skuId_booked": 10000 //总数0开始10000 通过incr扣减库存,返回抢购成功
- 将用户订单数据写入mq
- 监听mq入库
秒杀系统功能api实战(上)
** 后端秒杀网关流量拦截层功能开发 **
- 先判断秒杀是否已经开始
* 初始化时将key:skuId_start_1 value:0_1554046102存入数据库中
- 利用 Redis 缓存incr拦截流量
- 缓存拦截流量代码编写
- 用incr方法原子加
- 通过原子加判断当前skuId_access是否达到最大值
- 思考:是否需要保证获取到值的时候和incr值两个命令的原子性
* 保证原子性的方式,采用lua脚本
* 采用lua脚本方式保证原子性带来缺点,性能有所下降
* 不保证原子性缺点,放入请求量可能大于skuId_access
秒杀系统功能api实战(中)
** 后端秒杀信息校验层功能开发布隆过滤器实现重复购买拦截 **
- 订单信息校验层
* 校验当前用户是否已经买过这个商品
- 需要存储用户的uid
- 存数据库效率太低
- 存Redis value方式数据太大
- 存布隆过滤器性能高且数据量小
- 校验完通过直接返回抢购成功
秒杀系统功能api实战(下)
** 后端秒杀信息校验层功能开发lua脚本实现库存扣除**
- 库存扣除成功,获取当前最新库存
- 如果库存大于0,即马上进行库存扣除,并且访问抢购成功给用户
- 考虑原子性问题
* 保证原子性的方式,采用lua脚本
* 采用lua脚本方式保证原子性带来缺点,性能有所下降
* 不保证原子性缺点,放入请求量可能大于预期值
* 当前扣除库存场景必须保证原子性,否则会导致超卖
- 返回抢购结果
* 抢购成功
* 库存没了 ,抢购失败
初始化库存数据:
set skuId_count_1 10000 set skuId_access_1 12000 set skuId_start_1 0_1571125152
set skuId_booked_1 0
代码:
package com.concurrent.service; import com.concurrent.util.RedisService; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class SeckillService { private static final String secStartPrefix = "skuId_start_"; private static final String secAccess = "skuId_access_"; private static final String secCount = "skuId_count_"; private static final String filterName = "skuId_bloomfilter_"; private static final String bookedName = "skuId_booked_"; @Resource private RedisService redisService; public String seckill(int uid, int skuId) { //流量拦截层 //1、判断秒杀是否开始 0_1554045087 开始标识_开始时间 String isStart = (String) redisService.get(secStartPrefix + skuId); if (StringUtils.isBlank(isStart)) { return "还未开始"; } if (isStart.contains("_")) { Integer isStartInt = Integer.parseInt(isStart.split("_")[0]); Integer startTime = Integer.parseInt(isStart.split("_")[1]); if (isStartInt == 0) { if (startTime > getNow()) { return "还未开始"; } else { //代表秒杀已经开始 redisService.set(secStartPrefix + skuId, 1+""); } } else { return "系统异常"; } } else { if (Integer.parseInt(isStart) != 1) { return "系统异常"; } } //流量拦截 String skuIdAccessName = secAccess + skuId; Integer accessNumInt = 0; String accessNum = (String) redisService.get(skuIdAccessName); if(StringUtils.isNotBlank(accessNum)){ accessNumInt = Integer.parseInt(accessNum); } String skuIdCountName = secCount + skuId; Integer countNumInt = Integer.parseInt((String) redisService.get(skuIdCountName)); if (countNumInt * 1.2 < accessNumInt) { return "抢购已经完成,欢迎下次参与"; } else { redisService.incr(skuIdAccessName); } //信息校验层 if (redisService.bloomFilterExists(filterName + skuId, uid)){ return "您已经抢购过该商品,请勿重复下发!"; }else{ Boolean isSuccess = redisService.getAndIncrLua(bookedName+skuId,skuIdCountName); if(isSuccess){ redisService.bloomFilterAdd(filterName + skuId, uid); //TODO 放入消息队列进行异步入库 return "恭喜您抢购成功!!!"; }else{ return "抢购结束,欢迎下次参与"; } } } private long getNow() { return System.currentTimeMillis() / 1000; } }
RedisService.java
package com.concurrent.util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.*; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Service; import java.io.InterruptedIOException; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Created by Administrator on 2018/10/6. */ @Service public class RedisService { @Autowired private RedisTemplate redisTemplate; private static double size = Math.pow(2, 32); private static final String bloomFilterName = "isVipBloom"; /** * * * @param key * @return */ public Object get(final String key) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); return operations.get(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 自增 * * @param key * @return */ public boolean incr(final String key) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.increment(key,1); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } public Boolean bloomFilterAdd(int value){ DefaultRedisScript<Boolean> bloomAdd = new DefaultRedisScript<>(); bloomAdd.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterAdd.lua"))); bloomAdd.setResultType(Boolean.class); List<Object> keyList= new ArrayList<>(); keyList.add(bloomFilterName); keyList.add(value+""); Boolean result = (Boolean) redisTemplate.execute(bloomAdd,keyList); return result; } public Boolean bloomFilterAdd(String bloomName,int value){ DefaultRedisScript<Boolean> bloomAdd = new DefaultRedisScript<>(); bloomAdd.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterAdd.lua"))); bloomAdd.setResultType(Boolean.class); List<Object> keyList= new ArrayList<>(); keyList.add(bloomName); keyList.add(value+""); Boolean result = (Boolean) redisTemplate.execute(bloomAdd,keyList); return result; } public Boolean bloomFilterExists(int value) { DefaultRedisScript<Boolean> bloomExists = new DefaultRedisScript<>(); bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterExist.lua"))); bloomExists.setResultType(Boolean.class); List<Object> keyList = new ArrayList<>(); keyList.add(bloomFilterName); keyList.add(value + ""); Boolean result = (Boolean) redisTemplate.execute(bloomExists, keyList); return result; } public Boolean bloomFilterExists(String bloomName,int value) { DefaultRedisScript<Boolean> bloomExists = new DefaultRedisScript<>(); bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("bloomFilterExist.lua"))); bloomExists.setResultType(Boolean.class); List<Object> keyList = new ArrayList<>(); keyList.add(bloomName); keyList.add(value + ""); Boolean result = (Boolean) redisTemplate.execute(bloomExists, keyList); return result; }
public Boolean getAndIncrLua(String key,String key2){
DefaultRedisScript<Boolean> bloomExists= new DefaultRedisScript<>();
bloomExists.setScriptSource(new ResourceScriptSource(new ClassPathResource("secKillIncr2.lua")));
bloomExists.setResultType(Boolean.class);
List<Object> keyList= new ArrayList<>();
keyList.add(key);
keyList.add(key2);
Boolean result = (Boolean) redisTemplate.execute(bloomExists,keyList);
return result;
}
}
secKillIncr.lua脚本:
local lockKey = KEYS[1]
local lockKey2 = KEYS[2]
-- get info
local result_1 = redis.call('GET', lockKey)
local maxCount = redis.call('GET', lockKey2)
if tonumber(result_1) < tonumber(maxCount)
then
local result_2= redis.call('INCR', lockKey)
return result_1
else
return 0
end
controller:
package com.concurrent.controller; import com.concurrent.SecondKillApplication; import com.concurrent.service.SeckillService; import org.springframework.boot.SpringApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class SeckillController { @Resource private SeckillService seckillService; @RequestMapping("/redis/seckill") public String secKill(int uid,int skuId){ return seckillService.seckill(uid,skuId); } public static void main(String[] args) { System.out.println(System.currentTimeMillis()/1000); } }