Java教程:分析数据库与缓存的双写问题(三)

开课吧开课吧锤锤2021-02-22 15:13

    80、90后是这个时代最焦虑的一代人,大部分人都是,上有老,下有小,有房贷,车贷,卡贷。身上的大山很重,很多人的工资是不足以负担着这些的,所以想要给家人,给自己一个好的生活,就要从事一份薪资高的工作,在众多工种中,程序员的工资最高,很多人都在向往着这份工作,但是普通的程序员,也不会有非常可观的薪资,只有那种技术尖端人才,才可以,事业名利双丰收,那怎么样才能做到行业的顶尖呢?,那就呀不断的提升自己,今天就为大家带来分析数据库与缓存的双写问题,希望对您攀登事业顶峰有所帮助。

Java

    缓存和数据库一致性实战

    实战:先删除缓存,再更新数据库

    终于到了实战,我们在秒杀项目的代码上增加接口:先删除缓存,再更新数据库

    OrderController中新增:

/**

 * 下单接口:先删除缓存,再更新数据库

 * @param sid

 * @return

 */

@RequestMapping("/createOrderWithCacheV1/{sid}")

@ResponseBody

public String createOrderWithCacheV1(@PathVariable int sid) {

    int count = 0;

    try {

        // 删除库存缓存

        stockService.delStockCountCache(sid);

        // 完成扣库存下单事务

        orderService.createPessimisticOrder(sid);

    } catch (Exception e) {

        LOGGER.error("购买失败:[{}]", e.getMessage());

        return "购买失败,库存不足";

    }

    LOGGER.info("购买成功,剩余库存为: [{}]", count);

    return String.format("购买成功,剩余库存为:%d", count);

}

    stockService中新增:

@Override

public void delStockCountCache(int id) {

    String hashKey = CacheKey.STOCK_COUNT.getKey() + "_" + id;

    stringRedisTemplate.delete(hashKey);

    LOGGER.info("删除商品id:[{}] 缓存", id);

}

    其他涉及的代码都在之前三篇文章中有介绍,并且可以直接去Github拿到项目源码,就不在这里重复贴了。

    实战:先更新数据库,再删缓存

    如果是先更新数据库,再删缓存,那么代码只是在业务顺序上颠倒了一下,这里就只贴OrderController中新增:

/**

 * 下单接口:先更新数据库,再删缓存

 * @param sid

 * @return

 */

@RequestMapping("/createOrderWithCacheV2/{sid}")

@ResponseBody

public String createOrderWithCacheV2(@PathVariable int sid) {

    int count = 0;

    try {

        // 完成扣库存下单事务

        orderService.createPessimisticOrder(sid);

        // 删除库存缓存

        stockService.delStockCountCache(sid);

    } catch (Exception e) {

        LOGGER.error("购买失败:[{}]", e.getMessage());

        return "购买失败,库存不足";

    }

    LOGGER.info("购买成功,剩余库存为: [{}]", count);

    return String.format("购买成功,剩余库存为:%d", count);

}

    实战:缓存延时双删

    如何做延时双删呢,最好的方法是开设一个线程池,在线程中删除key,而不是使用Thread.sleep进行等待,这样会阻塞用户的请求。

    更新前先删除缓存,然后更新数据,再延时删除缓存。

    OrderController中新增接口:

// 延时时间:预估读数据库数据业务逻辑的耗时,用来做缓存再删除

private static final int DELAY_MILLSECONDS = 1000;

/**

 * 下单接口:先删除缓存,再更新数据库,缓存延时双删

 * @param sid

 * @return

 */

@RequestMapping("/createOrderWithCacheV3/{sid}")

@ResponseBody

public String createOrderWithCacheV3(@PathVariable int sid) {

    int count;

    try {

        // 删除库存缓存

        stockService.delStockCountCache(sid);

        // 完成扣库存下单事务

        count = orderService.createPessimisticOrder(sid);

        // 延时指定时间后再次删除缓存

        cachedThreadPool.execute(new delCacheByThread(sid));

    } catch (Exception e) {

        LOGGER.error("购买失败:[{}]", e.getMessage());

        return "购买失败,库存不足";

    }

    LOGGER.info("购买成功,剩余库存为: [{}]", count);

    return String.format("购买成功,剩余库存为:%d", count);

}

    OrderController中新增线程池:

// 延时双删线程池

private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

/**

 * 缓存再删除线程

 */

private class delCacheByThread implements Runnable {

    private int sid;

    public delCacheByThread(int sid) {

        this.sid = sid;

    }

    public void run() {

        try {

            LOGGER.info("异步执行缓存再删除,商品id:[{}], 首先休眠:[{}] 毫秒", sid, DELAY_MILLSECONDS);

            Thread.sleep(DELAY_MILLSECONDS);

            stockService.delStockCountCache(sid);

            LOGGER.info("再次删除商品id:[{}] 缓存", sid);

        } catch (Exception e) {

            LOGGER.error("delCacheByThread执行出错", e);

        }

    }

}

    来试验一下,请求接口createOrderWithCacheV3:

Java

    日志中,做到了两次删除:

Java

    实战:删除缓存重试机制

    上文提到了,要解决删除失败的问题,需要用到消息队列,进行删除操作的重试。这里我们为了达到效果,接入了RabbitMq,并且需要在接口中写发送消息,并且需要消费者常驻来消费消息。Spring整合RabbitMq还是比较简单的,我把简单的整合代码也贴出来。

    pom.xml新增RabbitMq的依赖:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-amqp</artifactId>

</dependency>

    写一个RabbitMqConfig:

@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);

        LOGGER.info("DelCacheReceiver开始删除缓存: " + message);

        stockService.delStockCountCache(Integer.parseInt(message));

    }

}

    OrderController中新增接口:

/**

 * 下单接口:先更新数据库,再删缓存,删除缓存重试机制

 * @param sid

 * @return

 */

@RequestMapping("/createOrderWithCacheV4/{sid}")

@ResponseBody

public String createOrderWithCacheV4(@PathVariable int sid) {

    int count;

    try {

        // 完成扣库存下单事务

        count = orderService.createPessimisticOrder(sid);

        // 删除库存缓存

        stockService.delStockCountCache(sid);

        // 延时指定时间后再次删除缓存

        // cachedThreadPool.execute(new delCacheByThread(sid));

        // 假设上述再次删除缓存没成功,通知消息队列进行删除缓存

        sendDelCache(String.valueOf(sid));

    } catch (Exception e) {

        LOGGER.error("购买失败:[{}]", e.getMessage());

        return "购买失败,库存不足";

    }

    LOGGER.info("购买成功,剩余库存为: [{}]", count);

    return String.format("购买成功,剩余库存为:%d", count);

}

    访问createOrderWithCacheV4:

Java

    可以看到,我们先完成了下单,然后删除了缓存,并且假设延迟删除缓存失败了,发送给消息队列重试的消息,消息队列收到消息后再去删除缓存。

    实战:读取binlog异步删除缓存

    我们需要用到阿里开源的canal来读取binlog进行缓存的异步删除。

    扩展阅读

    更新缓存的的DesignPattern有四种:

    Cacheaside

    Readthrough

    Writethrough

    Writebehindcaching,

    小结

    引用陈浩《缓存更新的套路》最后的总结语作为小结:

    分布式系统里要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率

    缓存系统适用的场景就是非强一致性的场景,所以它属于CAP中的AP,BASE理论。

    异构数据库本来就没办法强一致,只是尽可能减少时间窗口,达到最终一致性。

    还有别忘了设置过期时间,这是个兜底方案

    结束语

    本文总结并探讨了缓存数据库双写一致性问题。

    文章内容大致可以总结为如下几点:

    对于读多写少的数据,请使用缓存。

    为了保持数据库和缓存的一致性,会导致系统吞吐量的下降。

    为了保持数据库和缓存的一致性,会导致业务代码逻辑复杂。

    缓存做不到绝对一致性,但可以做到最终一致性。

    对于需要保证缓存数据库数据一致的情况,请尽量考虑对一致性到底有多高要求,选定合适的方案,避免过度设计。

    以上内容由开课吧老师Java中文社群提供,更多Java教程尽在开课吧广场Java教程频道。更多免费课程可以关注公众号“码农集散地”

上一篇:关于Spring-ApplicationContext解读(一)下一篇:Java教程:分析数据库与缓存的双写问题(二)

最新文章

文章图0

如何在大规模 Kubernetes 集群上实现高 SLO?

Pod创建成功率:这是一个非常重要的指标,蚂蚁集团一周的Pod创建量在百万级别,如果成功率波动会造成大量Pod失败,同时Pod成功率下跌也是集群异常的最直观反映;

2021-03-05 16:20:04

文章图1

MySQL不建议delete删除数据的原因是什么?(二)

MySQL内部不会真正删除空间,而且做标记删除,即将delflag:N修改为delflag:Y,commit之后会会被purge进入删除链表,如果下一次insert更大的记录,delete之后的空间不会被重用,如果插入的记录小于等于delete的记录空会被重用,这块内容可以通过知数堂的innbloc

2021-03-05 11:45:44

文章图2

MySQL不建议delete删除数据的原因是什么?(一)

物理上主要由系统用户数据文件,日志文件组成,数据文件主要存储MySQL字典数据和用户数据,日志文件记录的是datapage的变更记录,用于MySQLCrash时的恢复。

2021-03-05 11:36:22

文章图3

Java教程:MySQL如何设计索引更高效?(五)

同时也介绍了如何更好做MySQL索引设计,包括前缀索引,复合索引的顺序问题以及MySQL8.0推出的索引跳跃扫描,我们都知道,索引可以加快数据的检索,减少IO开销,会占用磁盘空间,是一种用空间换时间的优化手段,同时更新操作会导致索引频繁的合并分裂,影响索引性能,在实际的业务开发中,如何根据业务场景去

2021-03-05 11:29:12

文章图4

Java教程:MySQL如何设计索引更高效?(四)

在单列索引不能很好的过滤数据的时候,可以结合where条件中其他字段来创建复合索引,更好的去过滤数据,减少IO的扫描次数,举个例子:业务需要按照时间段来查询交易记录,有如下的SQL:

2021-03-05 11:25:02