Redis 和 MySQL 双写不一致问题

2,174 阅读2分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

一、问题描述:

我的一个商品有 11 个库存,在一次活动中进行秒杀。

如果有 3 个线程:

  1. 第一个线程直接消费库存,剩余库存为 11 回写到数据库和缓存 stock = 10
  2. 第二个线程查询缓存库存为 10 ,消费库存回写到数据库和缓存 stock = 9
  3. 第三个线程查询缓存库存为 10 ,消费库存回写到数据库和缓存 stock = 9
  4. 第一个线程回写库存到缓存提交成功。如下图所示

二、解决方案

在读写缓存之前,增加一个 redis 的读写锁。

读写锁的特征:

  1. 读读并行
  2. 读写互斥

这样就可以巧妙的解决查询缓存数据不一致的问题,而且 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) 方法实现,涵盖了 readdeletewrite 缓存的三个操作处理。

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