Java 实现秒杀-数据库和缓存数据一致性

数据库和缓存数据一致性

1、先删缓存,再更新数据库
请求A更新操作,请求B查询操作
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况就会导致数据不一致。如果不采用给缓存设置过期时间策略,该数据永远是脏数据。

/**
 * 先删除缓存,再更新数据库
 */
@RequestMapping("/delCacheUp/{id}")
@ResponseBody
public String delCacheUp(@PathVariable int id) {
	int count = 0;
    try {
        // 删除库存缓存
        stockService.delStockCountCache(id);
        // 完成扣库存下单
        orderService.createPessimisticOrder(id);
    } catch (Exception e) {
        logger.error("购买失败:[{}]", e.getMessage());
        return "购买失败,库存不足!";
    }
    return String.format("购买成功,剩余库存为:%d", count);
}
@Override
public void delStockCountCache(int id) {
    String hashKey = CacheKey.STOCK_COUNT.getKey() + "_" + id;
    RedisUtil.delete(hashKey);
    logger.info("删除商品id:[{}] 缓存", id);
}

2、先更新数据库,再删缓存
请求A查询操作,请求B更新操作
(1)缓存刚好失效
(2)请求A查询数据库,得到一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
步骤(3)的写操作比步骤(2)的读数据库耗时更短。依然会有问题,问题出现的可能性会因为上述原因,变得比较低!

/**
 * 先更新数据库,再删缓存
 */
@RequestMapping("/upDelCache/{id}")
@ResponseBody
public String upDelCache(@PathVariable int id) {
    int count = 0;
    try {
        // 完成扣库存下单事务
        orderService.createPessimisticOrder(id);
        // 删除库存缓存
        stockService.delStockCountCache(id);
    } catch (Exception e) {
        LOGGER.error("购买失败:[{}]", e.getMessage());
        return "购买失败,库存不足";
    }
    logger.info("购买成功,剩余库存为: [{}]", count);
    return String.format("购买成功,剩余库存为:%d", count);
}

以上两种方式:没法做到强一致性,只能做到最终一致性

3、延时双删
(1)先淘汰缓存
(2)再写数据库
(3)休眠一秒,再次淘汰缓存(将一秒内所造成的缓存脏数据,再次删除)
采用这种同步淘汰策略,吞吐量降低怎么办?
那就将第二次删除作为异步

最好的方法是开设一个线程池,在线程中删除key,而不是使用Thread.sleep进行等待,这样会阻塞用户的请求。
写数据的休眠时间在读数据业务逻辑的耗时基础上,加几百ms即可。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

如果删除缓冲失败了,怎么搞?
那就采用删除缓冲重试机制,使用mq消息队列

/**
 * 先删除缓存,再更新数据库,缓存延时再删
 */
@RequestMapping("/doubleDel/{id}")
@ResponseBody
public String doubleDel(@PathVariable int id) {
    int count;
    try {
        // 删除库存缓存
        stockService.delStockCountCache(id);
        // 完成扣库存下单事务
        count = orderService.createPessimisticOrder(id);
        // 延时指定时间后再次删除缓存
        cachedThreadPool.execute(new delCacheByThread(id));
    } catch (Exception e) {
        logger.error("购买失败:[{}]", e.getMessage());
        return "购买失败,库存不足";
    }
    logger.info("购买成功,剩余库存为: [{}]", count);
    return String.format("购买成功,剩余库存为:%d", count);
}
//延迟时间
private static final int DELAY_MILLSECONDS = 1000;
// 延时双删线程池
private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue());

/**
 * 缓存再删除线程
 */
private class delCacheByThread implements Runnable {
    private int id;
    public delCacheByThread(int id) {
        this.sid = id;
    }
    public void run() {
        try {
            LOGGER.info("异步执行缓存再删除,商品id:[{}], 首先休眠:[{}] 毫秒", sid, DELAY_MILLSECONDS);
            Thread.sleep(DELAY_MILLSECONDS);
            stockService.delStockCountCache(id);
            logger.info("再次删除商品id:[{}] 缓存", id);
        } catch (Exception e) {
            logger.error("delCacheByThread执行出错", e);
        }
    }
}

重试机制:

@Configuration
public class RabbitMqConfig {
    @Bean
    public Queue delCacheQueue() {
        return new Queue("delCache");
    }
}
@Component
@RabbitListener(queues = "delCache")
public class DelCacheReceiver {
    private static final Logger logger = LoggerFactory.getLogger(DelCacheReceiver.class);
    @Autowired
    private StockService stockService;

    @RabbitHandler
    public void process(String message) {
        logger.info("DelCacheReceiver收到消息: " + message + ",开始删除缓存");
        stockService.delStockCountCache(Integer.parseInt(message));
    }
}
/**
 * 先更新数据库,再删缓存,删除缓存重试机制
 */
@RequestMapping("/upDelRep/{id}")
@ResponseBody
public String upDelRep(@PathVariable int id) {
    int count;
    try {
        // 完成扣库存下单事务
        count = orderService.createPessimisticOrder(id);
        // 删除库存缓存
        stockService.delStockCountCache(sid);
        // 延时指定时间后再次删除缓存
        // cachedThreadPool.execute(new delCacheByThread(id));
        // 假设上述再次删除缓存没成功,通知消息队列进行删除缓存
        sendDelCache(String.valueOf(id));

    } catch (Exception e) {
        LOGGER.error("购买失败:[{}]", e.getMessage());
        return "购买失败,库存不足";
    }
    LOGGER.info("购买成功,剩余库存为: [{}]", count);
    return String.format("购买成功,剩余库存为:%d", count);
}

4、MySQL的读写分离
一个请求A进行更新操作,另一个请求B进行查询操作
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值
依旧使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
重试机制1(不可取,造成代码侵入):
在这里插入图片描述
重试机制2(需要使用阿里开源canal):
在这里插入图片描述
概念
Canal 是一款基于 MySQL 数据库增量日志解析的开源项目。它模拟了 MySQL 的 slave 节点,通过解析 MySQL 的 binlog 日志来获取数据库的变更信息,如数据的插入、更新和删除操作。这些变更信息可以被 Canal 捕获并发送到其他存储系统、消息队列或者进行自定义的处理,从而实现数据的实时同步和其他相关业务逻辑。
架构
Canal 主要由 Server 和 Client 两部分组成。
Canal Server 负责连接到 MySQL 数据库,解析 binlog 日志。它包括了多个组件,如 instance(实例),每个 instance 可以对应一个或多个数据库表的解析。
Canal Client 则从 Canal Server 获取解析后的变更数据,并进行后续的处理。例如,可以将数据发送到 Kafka 消息队列或者存储到 Elasticsearch 中。
原理
MySQL 的 binlog 是一种二进制格式的日志文件,记录了数据库的所有更改操作。Canal 通过伪装成 MySQL 的 slave 向 MySQL master 发送 dump 协议请求 binlog 数据。然后,它利用自己的解析引擎对 binlog 进行解析,将二进制数据转换为可读的变更记录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值