秒杀方案:
1:拦截秒杀的高配刷新操作
解决方案:对秒杀商品页面独立设计,减少动态内容,页面内容静态化,用户请求不需要经过应用服务。
2:减库存操作
在应用端增加 redis 库存,通过redis分布式锁方式,库存的扣减和回滚在redis中处理,提高性能,当高并发的时候我们可以通过分片思想,将一个 sku 的库存分成N片,redis分布式锁只需要随机获取其中一片锁就可以了,N个分片可以支持N个锁,锁之间是隔离的,扣减的时候优先获取库存多的分片锁,回滚的时候优先获取库存少的锁,通过调整权重值的方式实现
3:秒杀服务隔离
1)通过直接改商品价格为秒杀价的方式处理,不再调用营销中心结算和创建订单
2)商品改为单品单售,这样可以去除很多业务场景,让整个下单业务逻辑变的简单,整个下单的io降到最低
4:提供性能方案
1)涉及到数据库查询类的操作,尽量改为redis或者本地缓存
2)部分影响性能的IO操作,在不影响主流程时,可以通过MQ的方式削峰填谷,使得接口处理能够尽量平稳
5:架构设计
6:redis分布式事务和库存扣减
1)redis分布式事务
@Component
@ Slf4j
public class JedisLockUtil {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
/**
* NX -- Only set the key if it does not already exist.
* XX -- Only set the key if it already exist.
*/
private static final String SET_IF_NOT_EXIST = "NX";
/**
* EX = seconds; PX = milliseconds
*/
private static final String SET_WITH_EXPIRE_TIME = "PX";
@Autowired
RedisSeckillUtil redisUtil;
/**
* redis分布式锁锁定
*
* @param activityNo
* @return
*/
public String lock(Long skuId, int sharding) {
String key = getLockKey(skuId, sharding);
String requestId = UUID.randomUUID().toString();
log.info("JedisLockUtil lock init key:{},requestId:{}", key, requestId);
// key是cat:seckill:lock::activityNo
if (!tryGetDistributedLock(key, requestId, 1000, 5)) {
throw new CatException(ResCode.CAT_SECKILL_BUSY);
}
log.info("JedisLockUtil lock success key:{},requestId:{}", key, requestId);
return requestId;
}
/**
* redis分布式锁释放
*
* @param activityNo
* @param requestId
* @return
*/
public boolean releaseLock(Long skuId, Integer sharding, String requestId) {
String key = getLockKey(skuId, sharding);
boolean rs = releaseDistributedLock(key, requestId);
log.info("JedisLockUtil releaseLock success key:{},requestId:{},rs:{}", key, requestId, rs);
return rs;
}
/**
* 分布式锁一个sku从1个改为分片个
*
* @param skuId
* @return
*/
public String getLockKey(Long skuId, int sharding) {
return RedisSeckillConst.LOCK + skuId + RedisSeckillConst.SEPARATE + sharding;
}
/**
* 根据activityNo获取redis的requestId
*
* @param activityNo
* @return
*/
public String getRequestId(String lockKey) {
return redisUtil.get(lockKey, RedisSeckillConst.INDEXDB);
}
/**
* 尝试获取分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
private boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime, int retryTimes) {
for (int i = 0; i < retryTimes; i++) {
String result = redisUtil.tryGetDistributedLock(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime, RedisSeckillConst.INDEXDB);
if (LOCK_SUCCESS.equals(result)) {
return true;
} else {
// 自旋操作
try {
log.info("线程" + Thread.currentThread().getName() + "占用锁失败,自旋等待结果");
Thread.sleep(50);
} catch (InterruptedException e) {
continue;
}
}
}
return false;
}
/**
* 释放分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
private boolean releaseDistributedLock(String lockKey, String requestId) {
if (RELEASE_SUCCESS.equals(redisUtil.releaseDistributedLock(lockKey, requestId, RedisSeckillConst.INDEXDB))) {
return true;
}
return false;
}
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
2)分布式锁拦截器
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
String description() default "";
}
123456
@Component
@Scope
@Aspect
@Order(1)
@Slf4j
//order越小越是最先执行,但更重要的是最先执行的最后结束。order默认值是2147483647
public class RedisLockAspect {
@Autowired
private JedisLockUtil jedisLockUtil;
//Service层切点 用于记录错误日志
@Pointcut("@annotation(io.wharfoo.biztier.cat.domain.service.impl.seckill.RedisLock)")
public void lockAspect() {
}
@Around("lockAspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
boolean isLock = false;
Object obj = null;
String requestId = null;
Long skuId = (Long) joinPoint.getArgs()[0];
Integer sharding = (Integer) joinPoint.getArgs()[3];
try {
if (null != skuId) {
requestId = jedisLockUtil.lock(skuId, sharding);
isLock = true;
}
LogTimeUtil logTimeUtil = new LogTimeUtil();
logTimeUtil.setStartTime();
obj = joinPoint.proceed();
log.info("RedisLockAspectLock skuId:{},sharding:{},cost:{}", skuId, sharding, logTimeUtil.getWasteTime());
} catch (Throwable e) {
e.printStackTrace();
throw e;
} finally {
if (isLock) {//释放锁
jedisLockUtil.releaseLock(skuId, sharding, requestId);
}
}
return obj;
}
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344
3)库存扣减和回滚、sku库存分片
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
private SeckillService seckillService;
@Autowired
private RedisSeckillUtil redisSeckillUtil;
@Autowired
private SeckillSkuRepository seckillSkuRepository;
@Autowired
private ConfigProperties configProperties;
@Autowired
private RedisStockLogRepository redisStockLogRepository;
SeckillSkuConvertor seckillSkuConvertor = SeckillSkuConvertor.INSTANCE;
/**
* 加载 redis缓存 库存
* 获取花猫cat_seckill_sku表数据(sku_id、packingUnitId和stock),30分钟内要开始的活动加载到redis缓存中
* 如果activityNo是一样的,则不需要刷新缓存,如果不一致或者redis中不存在则在活动开始前预加载redis库存时需要刷新redis库存
*
* @return
*/
@ Override
public Object loadRedisStock() {
SeckillSku condition condition = new SeckillSkuCondition();
condition.setBeginTime(new Date(System.currentTimeMillis() + configProperties.getSeckillLoadSkuTime()));
List<SeckillSkuE> list = seckillSkuRepository.queryList(condition);
Map<String, String> hash = new HashMap<>();
if (!CollectionUtils. isEmpty (list)) {
for (SeckillSkuE seckillSkuE : list) {
SeckillSku seckillSkuERedis = getSeckillSkuEByRedis(seckillSkuE.getSkuId(), seckillSkuE.getPackingUnitId());
// 处理redis中的SeckillSku和数据库不一致的场景,需要刷新redis缓存,重新分配库存
if (null == seckillSkuERedis || (null != seckillSkuERedis.getActivityNo() && !seckillSkuERedis.getActivityNo().equals(seckillSkuE.getActivityNo()))) {
Map<String, String> stockHash = new HashMap<>();
// 根据分片数,库存进行分片处理
int sharding = seckillSkuE.getSharding();
// 最后一个用尾差作为库存
int remain = seckillSkuE.getRemainStock();
int remainStock = seckillSkuE.getRemainStock() / sharding;
for (int i = 1; i < sharding; i++) {
remain -= remainStock;
stockHash.put(String.valueOf(i), String.valueOf(remainStock));
}
stockHash.put(String.valueOf(sharding), String.valueOf(remain));
SeckillSku seckillSku = seckillSkuConvertor.entityToDto(seckillSkuE);
hash.put(SeckillSkuE.getRedisEntityField(seckillSku.getSkuId(), seckillSku.getPackingUnitId()), JSON.toJSONString(seckillSku));
if (!org.springframework.util.CollectionUtils.isEmpty(stockHash)) {
redisSeckillUtil.hmset(SeckillSkuE.getRedisKey(seckillSkuE.getSkuId(), seckillSkuE.getPackingUnitId()), stockHash, RedisSeckillConst.INDEXDB);
}
}
}
}
if (!org.springframework.util.CollectionUtils.isEmpty(hash)) {
redisSeckillUtil.hmset(SeckillSkuE.getRedisEntityKey(), hash, RedisSeckillConst.INDEXDB);
}
return null;
}
@Override
public SeckillSku getSeckillSkuEByRedis(Long skuId, Long packingUnitId) {
String entity_key = SeckillSkuE.getRedisEntityKey();
String value = redisSeckillUtil.hget(entity_key, SeckillSkuE.getRedisEntityField(skuId, packingUnitId), RedisSeckillConst.INDEXDB);
if (StringUtil.isNotEmptyAndNotNull(value)) {
return JSON.parseObject(value, SeckillSku.class);
}
SeckillSkuCondition condition = new SeckillSkuCondition();
condition.setSkuId(skuId);
condition.setPackingUnitId(packingUnitId);
List<SeckillSkuE> seckillSkuES = seckillSkuRepository.queryList(condition);
if (CollectionUtils.isNotEmpty(seckillSkuES)) {
Date curDate = new Date();
for (SeckillSkuE seckillSkuE : seckillSkuES) {
if (seckillSkuE.getStartTime().compareTo(curDate) >= 0 && seckillSkuE.getEndTime().compareTo(curDate) <= 0) {
SeckillSku seckillSku = seckillSkuConvertor.entityToDto(seckillSkuE);
redisSeckillUtil.hset(SeckillSkuE.getRedisEntityKey(), SeckillSkuE.getRedisEntityField(seckillSku.getSkuId(), seckillSku.getPackingUnitId()),
JSON.toJSONString(seckillSku), RedisSeckillConst.INDEXDB);
return seckillSku;
}
}
}
return null;
}
@Override
public Map<String, SeckillSku> getSeckillSkuEMapByRedis() {
Map<String, SeckillSku> seckillSkuMap = new HashMap<>();
String entity_key = SeckillSkuE.getRedisEntityKey();
Map<String, String> map = redisSeckillUtil.hgetall(entity_key, RedisSeckillConst.INDEXDB);
if (!org.springframework.util.CollectionUtils.isEmpty(map)) {
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
seckillSkuMap.put(entry.getKey(), JSON.parseObject(entry.getValue(), SeckillSku.class));
}
}
return seckillSkuMap;
}
/**
* 是否是秒杀的sku
*
* @param skuId
* @return
*/
@Override
public boolean isContainSeckillSku(List<String> fieldList) {
String entity_key = SeckillSkuE.getRedisEntityKey();
Map<String, String> values = redisSeckillUtil.hgetall(entity_key, RedisSeckillConst.INDEXDB);
if (!org.springframework.util.CollectionUtils.isEmpty(values)) {
for (String field : fieldList) {
String value = values.get(field);
if (null != value) {
SeckillSku seckillSku = JSON.parseObject(value, SeckillSku.class);
if (seckillSkuConvertor.dtoToEntity(seckillSku).isSeckill()) {
return true;
}
}
}
}
return false;
}
/**
* 获取秒杀的库存
*
* @param skuId
* @return
*/
@Override
public Integer getSeckillSkuStock(Long skuId, Long packingUnitId, int sharding) {
String stockRedisKey = SeckillSkuE.getRedisKey(skuId, packingUnitId);
String stock = redisSeckillUtil.hget(stockRedisKey, String.valueOf(sharding), RedisSeckillConst.INDEXDB);
if (io.wharfoo.biztier.sign.util.StringUtil.isNotEmptyAndNotNull(stock)) {
return Integer.valueOf(stock);
}
Integer remain = 0;
// 获取已锁库存,剩余库存=库存-已锁定库存
SeckillSku seckillSku = seckillService.getSeckillSkuEByRedis(skuId, packingUnitId);
if (null != seckillSku) {
Long lockQuantity = redisStockLogRepository.sumLockedByCondition(seckillSku.getActivityNo(), skuId, packingUnitId, sharding);
if (null == lockQuantity || lockQuantity < 0) {
log.info("getSeckillSkuStock sumLockedByCondition is error activityNo:{}, skuId:{}, packingUnitId:{}, sharding:{}, type:{}, lockQuantity:{}", seckillSku.getActivityNo(), skuId, packingUnitId, sharding, seckillSku.getType(), lockQuantity);
}
if (lockQuantity == null || lockQuantity < 0) {
lockQuantity = 0L;
}
log.info("getSeckillSkuStock sumLockedByCondition activityNo:{}, skuId:{}, packingUnitId:{}, sharding:{}, type:{}, lockQuantity:{}", seckillSku.getActivityNo(), skuId, packingUnitId, sharding, seckillSku.getType(), lockQuantity);
// 如果是最后一个分片,需要根据尾差获取分配的库存,其他的根据库存除以分片数
int shardingStock = seckillSku.getStock() / seckillSku.getSharding();
if (sharding == seckillSku.getSharding()) {
shardingStock = seckillSku.getStock() - (seckillSku.getSharding() - 1) * shardingStock;
}
remain = shardingStock - lockQuantity.intValue();
if (remain < 0) {
remain = 0;
}
}
redisSeckillUtil.hset(stockRedisKey, String.valueOf(sharding), String.valueOf(remain), RedisSeckillConst.INDEXDB);
return remain;
}
/**
* 更新redis库存
* RedisLock:redis分布式锁,更新redis库存前先获取分布式锁
*
* @param skuId
* @param quantity
* @return
*/
@RedisLock
@Override
public Long updateSkuStock(Long skuId, Long packingUnitId, Long quantity, Integer sharding) {
if (quantity == 0) {
throw new CatException(ResCode.CAT_SECKILL_SKU_QUANTITY_EMPTY);
}
// 获取redis库存
Integer stock = getSeckillSkuStock(skuId, packingUnitId, sharding);
// 扣减库存需要校验库存
if (quantity < 0) {
if (stock <= 0) {
throw new CatException(ResCode.CAT_SECKILL_SKU_STOCK_EMPTY);
}
if (stock < Math.abs(quantity)) {
throw new CatException(ResCode.CAT_SECKILL_SKU_STOCK_LOW);
}
}
String key = SeckillSkuE.getRedisKey(skuId, packingUnitId);
Long remain = redisSeckillUtil.hincrBy(key, String.valueOf(sharding), quantity, RedisSeckillConst.INDEXDB);
log.info("updateSeckillSkuStock skuId:{},sharding:{},quantity:{},stock:{},remain:{}", skuId, sharding, quantity, stock, remain);
return remain;
}
/**
* 获取当前的分片
* 如果是扣减库存,获取所有的分片库存,库存小于扣减的库存需要过滤掉,库存值作为权重值,随机值获取后获取当前分片值,优先库存多的先扣减
* 如果是增加库存,获取所有的分片库存,每片分配的库存-库存值作为权重值,随机值获取后获取当前分片值,优先剩余库存少的先增加
*
* @return
*/
public Integer getStockRedisSharding(Long skuId, Long packingUnitId, Long quantity) {
SeckillSku seckillSkuERedis = getSeckillSkuEByRedis(skuId, packingUnitId);
int stock = seckillSkuERedis.getStock() / seckillSkuERedis.getSharding();
List<SeckillSku> shardingStockList = getStockRedis(skuId, packingUnitId);
if (quantity < 0) {
shardingStockList = shardingStockList.stream().filter(e -> e.getRemainStock() >= Math.abs(quantity) && e.getRemainStock() > 0).collect(Collectors.toList());
} else if (quantity > 0) {
shardingStockList = shardingStockList.stream().filter(e -> e.getRemainStock() > 0).map(e -> {
if (e.getSharding().compareTo(seckillSkuERedis.getSharding()) == 0) {
// 计算尾差
e.setRemainStock(seckillSkuERedis.getStock() - (seckillSkuERedis.getSharding() - 1) * stock - e.getRemainStock());
} else {
e.setRemainStock(stock - e.getRemainStock());
}
return e;
}).collect(Collectors.toList());
}
// 获取权重值
int allProportion = 0;
for (SeckillSku seckillSku : shardingStockList) {
allProportion += seckillSku.getRemainStock();
}
// 获取分片值
if (allProportion > 0) {
int random = new Random().nextInt(allProportion);
int curProportion = 0;
for (SeckillSku seckillSku : shardingStockList) {
curProportion += seckillSku.getRemainStock();
if (curProportion >= random) {
return seckillSku.getSharding();
}
}
}
return RedisSeckillConst.SHARDING;
}
/**
* 获取当前skuId的所有的库存
* key:分片值,value:当前分片库存
*
* @param skuId
* @param packingUnitId
* @return
*/
private List<SeckillSku> getStockRedis(Long skuId, Long packingUnitId) {
List<SeckillSku> list = new ArrayList<>();
String stockRedisKey = SeckillSkuE.getRedisKey(skuId, packingUnitId);
Map<String, String> stockMap = redisSeckillUtil.hgetall(stockRedisKey, RedisSeckillConst.INDEXDB);
if (!org.springframework.util.CollectionUtils.isEmpty(stockMap)) {
Iterator<Map.Entry<String, String>> iterator = stockMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
SeckillSku seckillSku = new SeckillSku();
seckillSku.setSharding(NumberUtil.toInt(entry.getKey(), RedisSeckillConst.SHARDING));
seckillSku.setRemainStock(NumberUtil.toInt(entry.getValue(), 0));
list.add(seckillSku);
}
}
return list;
}
}