Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
一、问题描述:
我的一个商品有 11 个库存,在一次活动中进行秒杀。
如果有 3 个线程:
- 第一个线程直接消费库存,剩余库存为 11 回写到数据库和缓存
stock = 10
- 第二个线程查询缓存库存为 10 ,消费库存回写到数据库和缓存
stock = 9
- 第三个线程查询缓存库存为 10 ,消费库存回写到数据库和缓存
stock = 9
- 第一个线程回写库存到缓存提交成功。如下图所示
二、解决方案
在读写缓存之前,增加一个 redis 的读写锁。
读写锁的特征:
- 读读并行
- 读写互斥
这样就可以巧妙的解决查询缓存数据不一致的问题,而且 lock 具备互斥性,也可以解决 缓存击穿
问题。
看看我的代码(初稿,待优化):
注解定义,主要是定义缓存 key , 超时时间 timeOut 单位:毫秒,操作类型分为:read, write, delete 三种。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiCache {
String key() default "";
long timeOut() default 2000;
String op() default "read"; //read, write, delete
}
aop 拦截,我主要是利用 aop 的方式来对缓存操作进行封装,方便复用。 分为两个步骤:
1、定义 Pointcut
,具体见 multiCache()
方法;
2、定义 Around
,具体见 multiCacheSupport(ProceedingJoinPoint pjp)
方法实现,涵盖了 read
、delete
、write
缓存的三个操作处理。
@Pointcut("@annotation(io.zhengsh.redis.annotation.MultiCache)")
public void multiCache() {
// Pointcut
}
@Around("multiCache()")
public Object multiCacheSupport(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature ms = (MethodSignature) pjp.getSignature();
Method method = ms.getMethod();
MultiCache multiCache = method.getAnnotation(MultiCache.class);
String mkey = generateKey(multiCache.key(), pjp);
try {
if ("read".equals(multiCache.op())) {
String retVal = multiCacheService.read(mkey);
if (retVal != null && !"".equals(retVal.trim())) {
return JSON.parseObject(retVal, method.getReturnType());
}
}
Object proceed = pjp.proceed();
multiCacheService.write(mkey, "delete".equals(multiCache.op()) ? "" : JSON.toJSONString(proceed));
return proceed;
} catch (Throwable ex) {
logger.info("multiCache err key: {}", mkey, ex);
throw ex;
}
}
使用实例:两个方法介绍
1、createOrder
主要是用来创建订单, 消费库存(代码模拟)。缓存是一个删除操作
2、querySku
主要是用来查询库存信息,将查询出来的结果返回给客户端。
@MultiCache(key = "'order.seckill:'+ #orderDto.skuNo", timeOut = 10000, op = "delete")
@GetMapping("/createOrder")
public OrderDto createOrder(OrderDto orderDto) {
//1.参数教研
if (orderDto.getQuantity() == null || orderDto.getQuantity() < 1) {
throw new RuntimeException("unknown error");
}
String key = String.format("order.stock:%s", orderDto.getSkuNo());
Serializable serializable = redisTemplate.opsForValue().decrement(key, orderDto.getQuantity());
if (serializable == null) {
throw new RuntimeException("unknown error");
}
Integer stock = Optional.of(Integer.parseInt(String.valueOf(redisTemplate.opsForValue().get(key)))).orElse(0);
OrderDto resultDto = new OrderDto();
resultDto.setSkuNo(orderDto.getSkuNo());
if (stock >= 0) {
resultDto.setQuantity(orderDto.quantity);
} else {
resultDto.setQuantity(-1);
}
return resultDto;
}
@MultiCache(key = "'order.seckill:'+ #skuNo", timeOut = 10000)
@GetMapping("/querySku/{skuNo}")
public List<SkuDto> querySku(@PathVariable(value = "skuNo") String skuNo) {
Serializable serializable = redisTemplate.opsForValue().get(String.format("order.stock:%s", skuNo));
SkuDto skuDto1 = new SkuDto(skuNo, Optional.of(Integer.parseInt(String.valueOf(serializable))).orElse(0));
return Arrays.asList(skuDto1, new SkuDto("SKU00008", -1));
}
三、总结
Redis 和 MySQL 产生的原因主要是因为在分布式系统,多线程并发操作的时候出现,我的解决方式就是通过分布式读写锁
+ 锁有限期
实现排队解决。