一、问题描述:
我的一个商品有 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 产生的原因主要是因为在分布式系统,多线程并发操作的时候出现,我的解决方式就是通过分布式读写锁
+ 锁有限期
实现排队解决。