实战——缓存的使用

news2024/10/5 23:29:13

文章目录

  • 前言
  • 概述
  • 实践
    • 一、缓存数据一致
      • 1.更新缓存类
      • 2.删除缓存类
    • 二、项目实践(商城项目)
      • 缓存预热
      • 双缓存机制


前言

对于我们日常开发的应用系统。由于MySQL等关系型数据库读写的并发量是有一定的上线的,当请求量过大时候那数据库的压力一定会上来。

所以采用 MySQL+Redis 这对经典组合来解决高并发问题的。Redis 作为 MySQL 的前置缓存,可以应对绝大部分查询请求,从而在很大程度上缓解 MySQL 并发请求的压力,但是不能一说到缓存脑海中就只有 Redis,这无论在工作还是面试中都不合适,所以我们先全面了解缓存。

注意:缓存不止有redis,需要全面的了解缓存


概述

  • 缓存大体可以分为三类(意思就是在整个系统中我们每一层都是有缓存的):
    • 客户端缓存;
    • 服务端缓存;
    • 网络中的缓存。
  • 根据规模和部署方式缓存也可以分为:
    • 单体缓存;
    • 缓存集群;
    • 分布式缓存。

我们通过对每一层进行缓存来提高系统的稳定性和效率。


实践

一、缓存数据一致

不论是本地缓存还是redis,我们的基本思路就是:当缓存没有命中的时候,我们就去数据库查询,然后直接放到缓存中,供下次查询以加快查询效率。
但是我们在修改数据的时候,就可能造成数据库和缓存数据一致性的问题。
有好几种解决方案,(两大类:一:更新;二:删除)1. 先更新缓存,再更新数据库2. 先更新数据库,再更新缓存3、先删除缓存,后更新数据库4、先更新数据库,后删除缓存

1.更新缓存类

不论先更新数据库还是先更新缓存,这两种方案都不可取。原因就是不论我们先更新谁后更新谁,就会导致我们前面更新成功后,后面更新的那个万一挂了,我们就难以判断是否成功。所以这类更新的方案,对我们来说都是不可取的。

2.删除缓存类

  • 2.1 先删缓存,后更新数据库
    • 问题:

      • 线程A删除缓存后,更新DB,但是DB的事务并没有提交,线程B进来访问。那么就会重新更新缓存(造成缓存和DB数据不一致)
        在这里插入图片描述
    • 解决方案

      • 使用延时双删
        • 1.先删除缓存,再去更新DB
        • 2.当DB更新成功之后,延时个1s,再删除一次缓存

  • 2.2 先更新数据库,后删除缓存(开发中常用的策略)

    • 问题:

      • 在缓存失效的且并发的时候会发生(虽然概率比较小但是还是会发生,因为概率小所以我们在开发中常用)
        在这里插入图片描述
    • 解决方案:

      • 1.设置缓存失效时间。
      • 2.异步延时删除机制(问题在于当我们删除缓存的时候万一失败需要补偿机制来保证缓存一定删除)
        异步补偿删除方案一:
        在这里插入图片描述

    异步补偿删除方案二:

    由于方案一中的业务代码的耦合性较高。使用订阅数据库的binlog

在这里插入图片描述

说到底就是通过数据库的 binlog 来异步淘汰 key,利用工具(canal)将 binlog
日志采集发送到 MQ 中,然后通过 ACK 机制确认处理删除缓存。
先更新 DB,后删除缓存,这种方式,被称为 Cache Aside Pattern,属于缓存更新的设计模式之一。(这是一种最为标准的方案)


二、项目实践(商城项目)

我这边以京东商城为例

在这里插入图片描述

像这样的商城首页一定是并发最高的地方,如果我们每次都去数据库查询,很显然是不满足我们高并发的要求的。

像这种任何用户看到的都是一样的结果的数据,在缓存中的命中率是比较高得,所以我们可以考虑引入缓存的方式。
并且我们这里完全可以分为多个key,如促销,轮播图,标签等。

  1. 首页缓存使用
// 大佬的代码例子
 public HomeContentResult getFromRemote(){
        List<PmsBrand> recommendBrandList = null;
        List<SmsHomeAdvertise> smsHomeAdvertises = null;
        List<PmsProduct> newProducts  = null;
        List<PmsProduct> recommendProducts  = null;
        HomeContentResult result;
        /*从redis获取*/
        if(promotionRedisKey.isAllowRemoteCache()){
            recommendBrandList = redisOpsUtil.getListAll(promotionRedisKey.getBrandKey(), PmsBrand.class);
            smsHomeAdvertises = redisOpsUtil.getListAll(promotionRedisKey.getHomeAdvertiseKey(), SmsHomeAdvertise.class);
            newProducts = redisOpsUtil.getListAll(promotionRedisKey.getNewProductKey(), PmsProduct.class);
            recommendProducts = redisOpsUtil.getListAll(promotionRedisKey.getRecProductKey(), PmsProduct.class);
        }
        /*redis没有则从微服务中获取*/
        if(CollectionUtil.isEmpty(recommendBrandList)
                ||CollectionUtil.isEmpty(smsHomeAdvertises)
                ||CollectionUtil.isEmpty(newProducts)
                ||CollectionUtil.isEmpty(recommendProducts)) {
            result = promotionFeignApi.content(0).getData();
        }else{
            result = new HomeContentResult();
            result.setBrandList(recommendBrandList);
            result.setAdvertiseList(smsHomeAdvertises);
            result.setHotProductList(recommendProducts);
            result.setNewProductList(newProducts);
        }
        return result;
    }

redis 初始化key代码初始化注解@PostConstruct

@Service
@Slf4j
public class PromotionRedisKey {
    // 配置
    @Value ("${namespace.promotion:prmtd}")
    private String promotionNamespace;

    @Value ("${promotion.brand:br}")
    private String brand;

    @Value ("${promotion.newProduct:np}")
    private String newProduct;

    @Value ("${promotion.recProduct:rp}")
    private String recProduct;

    @Value ("${promotion.homeAdvertise:hd}")
    private String homeAdvertise;

    @Value ("${promotion.seckill:sk}")
    private String secKill;
    // 需要初始化的key
    private String brandKey;
    private String newProductKey;
    private String recProductKey;
    private String homeAdvertiseKey;
    private String secKillKey;

    // 全局缓存控制开关
     @Value("${promotion.demo.allowLocalCache:true}")
    private boolean allowLocalCache;

    @Value("${promotion.demo.allowRemoteCache:true}")
    private boolean allowRemoteCache;


    /**
     * 该注解的方法在整个Bean初始化中的执行顺序:
     *
     * Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的初始化方法)
     *
     * 该注解的功能:当依赖注入完成后用于执行初始化的方法,并且只会被执行一次
     */
    @PostConstruct
    public void initKey(){
        brandKey = promotionNamespace + "." + brand;
        newProductKey = promotionNamespace + "." + newProduct;
        recProductKey = promotionNamespace + "." + recProduct;
        homeAdvertiseKey = promotionNamespace + "." + homeAdvertise;
        secKillKey = promotionNamespace + "." + secKill;
        StringBuilder logKeyStr = new StringBuilder();
        logKeyStr.append("[品牌推荐redis主键=").append(brandKey)
                .append("] [新品推荐redis主键=").append(newProductKey)
                .append("] [人气推荐redis主键=").append(recProductKey)
                .append("] [轮播广告redis主键=").append(homeAdvertiseKey)
                .append("] [秒杀redis主键=").append(secKillKey)
                .append("]");
        log.info("促销系统Redis主键配置:{}",logKeyStr);
    }

    public String getBrandKey() {
        return brandKey;
    }

    public String getNewProductKey() {
        return newProductKey;
    }

    public String getRecProductKey() {
        return recProductKey;
    }

    public String getHomeAdvertiseKey() {
        return homeAdvertiseKey;
    }

    public String getSecKillKey() {
        return secKillKey;
    }

   

    public boolean isAllowLocalCache() {
        return allowLocalCache;
    }

    public boolean isAllowRemoteCache() {
        return allowRemoteCache;
    }
}



通过以上首页对缓存的使用,提出一些思考如下:
缓存一定是离用户越近越好,依据这个原则,首页还有优
化的空间,从上面的访问路径可以看到,首页服务需要到 Redis 集群中获得数据用以展示,能不能将缓存的数据再提前呢?于是我们在首页服务内引入了应用级缓存 Caffeine。

什么叫近???
在这里插入图片描述

TIPS:Caffeine 基于 Google 的 Guava Cache,提供一个性能卓越的本地缓存(local cache) 实现, 也是 SpringBoot 内置的本地缓存实现,有资料表明 Caffeine性能是 Guava Cache 的 6 倍

Caffeine使用

@Configuration
public class CaffeineCacheConfig {

    @Bean(name = "promotion")// 缓存预热(指定bean名称)
    public Cache<String, HomeContentResult> promotionCache() {
        int rnd = ThreadLocalRandom.current().nextInt(10);
        return Caffeine.newBuilder()
                // 设置最后一次写入经过固定时间过期
                .expireAfterWrite(30 + rnd, TimeUnit.MINUTES)
                // 初始的缓存空间大小
                .initialCapacity(20)
                // 缓存的最大条数
                .maximumSize(100)
                .build();
    }

    /*以双缓存的形式提升首页的访问性能,这个备份缓存其实设置为永不过期更好,
    * 可以作为首页的降级和兜底方案
    * 为了说明缓存击穿和分布式锁这里设置了一个过期时间*/
    @Bean(name = "promotionBak")
    public Cache<String, HomeContentResult> promotionCacheBak() {
        int rnd = ThreadLocalRandom.current().nextInt(10);
        return Caffeine.newBuilder()
                // 设置最后一次访问经过固定时间过期
                .expireAfterAccess(41 + rnd, TimeUnit.MINUTES)
                // 初始的缓存空间大小
                .initialCapacity(20)
                // 缓存的最大条数
                .maximumSize(100)
                .build();
    }

    /*秒杀信息在首页的缓存*/
    @Bean(name = "secKill")
    public Cache<String, List<FlashPromotionProduct>> secKillCache() {
        int rnd = ThreadLocalRandom.current().nextInt(400);
        return Caffeine.newBuilder()
                // 设置最后一次写入经过固定时间过期
                .expireAfterWrite(500 + rnd, TimeUnit.MILLISECONDS)
                // 初始的缓存空间大小
                .initialCapacity(20)
                // 缓存的最大条数
                .maximumSize(100)
                .build();
    }

    /*秒杀信息在首页的缓存备份,提升首页的访问性能*/
    @Bean(name = "secKillBak")
    public Cache<String, List<FlashPromotionProduct>> secKillCacheBak() {
        int rnd = ThreadLocalRandom.current().nextInt(400);
        return Caffeine.newBuilder()
                // 设置最后一次写入经过固定时间过期
                .expireAfterWrite(100 + rnd, TimeUnit.MILLISECONDS)
                // 初始的缓存空间大小
                .initialCapacity(20)
                // 缓存的最大条数
                .maximumSize(100)
                .build();
    }
}



    @Autowired
    @Qualifier("promotion") // 指定注入相关对象和上面的配置一致,就是在容器中拿对应的值
    private Cache<String, HomeContentResult> promotionCache;

   
    @Override
    public HomeContentResult cmsContent(HomeContentResult content) {
        //获取推荐专题
        content.setSubjectList(homeDao.getRecommendSubjectList(0,4));
        return content;
    }

    /*处理首页推荐品牌和商品内容*/
    @Override
    public HomeContentResult recommendContent(){
        /*品牌和产品在本地缓存中统一处理,有则视为同有,无则视为同无*/
        final String brandKey = promotionRedisKey.getBrandKey();
        final boolean allowLocalCache = promotionRedisKey.isAllowLocalCache();
        /*先从本地缓存中获取推荐内容*/
        HomeContentResult result = allowLocalCache ?
                promotionCache.getIfPresent(brandKey) : null;
        if(result == null){
            result = allowLocalCache ?
                    promotionCacheBak.getIfPresent(brandKey) : null;
        }
        /*本地缓存中没有*/
        if(result == null){
            log.warn("从本地缓存中获取推荐品牌和商品失败,可能出错或禁用了本地缓存[allowLocalCache = {}]",allowLocalCache);
            // 去redis中去,没有再去DB中取
            result = getFromRemote();
            if(null != result) {
                promotionCache.put(brandKey,result);
                promotionCacheBak.put(brandKey,result);
            }
        }
        /* 处理秒杀内容*/
        final String secKillKey = promotionRedisKey.getSecKillKey();
        List<FlashPromotionProduct> secKills = secKillCache.getIfPresent(secKillKey);
        if(CollectionUtils.isEmpty(secKills)){
            secKills = secKillCacheBak.getIfPresent(secKillKey);
        }
        if(CollectionUtils.isEmpty(secKills)){
            /*极小的概率出现本地两个缓存同时失效的问题,
            从远程获取时,只从Redis缓存中获取,不从营销微服务中获取,
            避免秒杀的流量冲垮营销微服务*/
            secKills = getSecKillFromRemote();
            if(!CollectionUtils.isEmpty(secKills)) {
                secKillCache.put(secKillKey,secKills);
                secKillCacheBak.put(secKillKey,secKills);
            }else{
                /*Redis缓存中也没有秒杀活动信息,此处用一个空List代替,
                * 其实可以用固定的图片或信息,作为降级和兜底方案*/
                secKills = new ArrayList<FlashPromotionProduct>();
            }
        }
        result.setHomeFlashPromotion(secKills);
        // fixme CMS本次不予实现,设置空集合
        result.setSubjectList(new ArrayList<CmsSubject>());
        return result;
    }

缓存预热

缓存预热是为了防止,我们的项目刚发版,可能请求过多造成的数据库压力过大的情况。

使用Spring的启动化机制

@Component
@Slf4j
// 缓存预热,使用Spring启动化机制CommandLineRunner
public class preheatCache implements CommandLineRunner {

    @Autowired
    private HomeService homeService;

    @Override
    public void run(String... args) throws Exception {
        for (String str : args) {
            log.info("系统启动命令行参数: {}", str);
        }
    // 缓存预热,可以包括本地缓存或者redis
        homeService.preheatCache();
    }

}

双缓存机制

在这里插入图片描述

为了数据的一致性,本地 Caffeine和redis设置了过期时间,Redis 集群中的数据也会在数据变动后被除。当数据同时过期的时候。可能有以下的情况本地缓存去redis去取数据;本地缓存取DB取数据。那么在耗时上就会产生毛刺现象。


为了避免以上情况,我们使用双缓存的结构。

    @Bean(name = "promotion")// 缓存预热
    public Cache<String, HomeContentResult> promotionCache() {
        int rnd = ThreadLocalRandom.current().nextInt(10);
        return Caffeine.newBuilder()
                // 设置最后一次写入经过固定时间过期
                .expireAfterWrite(30 + rnd, TimeUnit.MINUTES)
                // 初始的缓存空间大小
                .initialCapacity(20)
                // 缓存的最大条数
                .maximumSize(100)
                .build();
    }

    备份缓存要随着主缓存的变动而变动

    /*以双缓存的形式提升首页的访问性能,这个备份缓存其实设置为永不过期更好,
    * 可以作为首页的降级和兜底方案
    * 为了说明缓存击穿和分布式锁这里设置了一个过期时间*/
    @Bean(name = "promotionBak")
    public Cache<String, HomeContentResult> promotionCacheBak() {
        int rnd = ThreadLocalRandom.current().nextInt(10);
        return Caffeine.newBuilder()
                // 设置最后一次访问经过固定时间过期
                .expireAfterAccess(41 + rnd, TimeUnit.MINUTES)
                // 初始的缓存空间大小
                .initialCapacity(20)
                // 缓存的最大条数
                .maximumSize(100)
                .build();
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/363392.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3717: yuyu学数数

描述yuyu开始学数数了&#xff0c;她要爸爸给他一些火柴棍&#xff0c;她要拼出很多数来。yuyu每次说要拼什么数字&#xff0c;爸爸就得想想要给她几根&#xff0c;好累啊&#xff0c;于是就只好写程序了。输入输入数据有多组&#xff0c;每组占一行&#xff0c;每行一个非负整…

K_A12_030 基于STM32驱动Pulse sensor心率模块 上位机与OLED0.96双显示

K_A12_030 基于STM32驱动Pulse sensor心率模块 上位机与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明STM32 ADC采集:四、部分代码说明1、接线引脚定义STM32F103C8T6Pulse sensor心率模块五、基础知识学习与相关资料下载六、视频效果展示与程序资料获取七、…

计算机内存数值存储方式-原码、反码、补码

计算机内存数值存储方式 1、原码 一个数的原码(原始的二进制码)有如下特点&#xff1a; ①最高位做为符号位&#xff0c;0表示正,为1表示负 ②其它数值部分就是数值本身绝对值的二进制数 ③负数的原码是在其绝对值的基础上&#xff0c;最高位变为1 下面数值以1字节的大小描述…

Nginx——Nginx的基础原理

摘要 Nginx 是俄罗斯人编写的十分轻量级的 HTTP 服务器,是一个高性能的HTTP和反向代理服务器&#xff0c;同时也是一个 IMAP/POP3/SMTP 代理服务器。Nginx 是由俄罗斯人 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的&#xff0c;它已经在该站点运行超过两年半了。…

Jetson Xavier nx(ubuntu18.04)安装rtl8152网卡驱动和8192网卡驱动

含义 Bus 002 : 指明设备连接到哪条总线。 Device 003 : 表明这是连接到总线上的第二台设备。 ID : 设备的ID&#xff0c;包括厂商的ID和产品的ID&#xff0c;格式 厂商ID&#xff1a;产品ID。 Realtek Semiconductor Corp. RTL8153 Gigabit Ethernet Adapter:生产商名字和设备…

力扣-寻找用户推荐人

大家好&#xff0c;我是空空star&#xff0c;本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;584. 寻找用户推荐人二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.其他总结前言 一、题目&#xff1a…

详解Linux多线程中锁、条件变量、信号量

一文读懂Linux多线程中互斥锁、读写锁、自旋锁、条件变量、信号量 Hello、Hello大家好&#xff0c;我是木荣&#xff0c;今天我们继续来聊一聊Linux中多线程编程中的重要知识点&#xff0c;详细谈谈多线程中同步和互斥机制。 同步和互斥 互斥&#xff1a;多线程中互斥是指多个…

SpringBoot+ActiveMQ-发布订阅模式(消费端)

ActiveMQ消息中间件的发布订阅模式 主题 topictopic生产端案例(配合topic消费端测试)&#xff1a;SpringBootActiveMQ Topic 生产端ActiveMQ版本&#xff1a;apache-activemq-5.16.5案例源码:SpringBootActiveMQ-发布订阅DemoSpringBoot集成ActiveMQ Topic消费端的pom.xml<?…

git之工作区暂存区和仓库区

工作区暂存区和仓库区 工作区 对于添加、修改、删除文件的操作&#xff0c;都发生在工作区中 暂存区 暂存区指将工作区中的操作完成小阶段的存储&#xff0c;是版本库的一部分 仓库区 仓库区表示个人开发的一个小阶段的完成 仓库区中记录的各版本是可以查看并回退的但是在暂…

uniapp小程序基于Android+vue校园考研论坛php

系统功能 本灾情救援系统主要包括两大功能模块&#xff0c;即用户功能模块和管理员功能模块。 &#xff08;1&#xff09;管理员模块&#xff1a;主要模块包括首页&#xff0c;个人中心&#xff0c;会员管理&#xff0c;话题分类管理&#xff0c;考研论坛管理&#xff0c;系统管…

python—xlwt模块详解

一、前言 xlwt模块是python中专门用于写入Excel的拓展模块&#xff0c;可以实现创建表单、写入指定单元格、指定单元格样式等人工实现的功能&#xff0c;一句话就是人使用excel实现的功能&#xff0c;这个扩展包都可以实现。 二、基础操作 1、创建workbook(创建excel) #创建…

识别项目风险的7种方法

成功的项目经理有一个共同的特点&#xff1a;能够识别和管理风险。本文主要讨论识别项目风险的七个方法。 何时识别风险 在项目开始的时候&#xff0c;风险暴露是最大的&#xff0c;不确定性很高。因为项目在开始的时候&#xff0c;信息比较少。明智的项目经理会在项目早期就…

python基础 | Numpy基础

文章目录&#x1f4da;数组操作&#x1f407;np数组的构造&#x1f955;np数组的构造&#x1f955;特殊补充&#x1f407;np数组的变形和合并&#x1f955;转置&#x1f955;合并操作&#x1f955;维度变换&#x1f407;np数组的切片和索引&#x1f955;一维数组索引与切片&…

嵌套走马灯Carousel

Carousel 的应用很广泛&#xff0c;基础用法这里不多做阐述&#xff0c;感兴趣的可以去element-gui了解Carousel 组件。 今天主要是梳理嵌套走马灯的逻辑&#xff0c;背景如下&#xff1a; 需要对项目做一个展示&#xff0c;项目可能有一个或多个&#xff0c;同时一个项目可能…

2023爱分析·云原生智能运维中台市场厂商评估报告:秒云(miaoyun.io)

目录 1. 研究范围定义 2. 云原生智能运维中台市场定义 3. 厂商评估&#xff1a;秒云&#xff08;miaoyun.io&#xff09; 4. 入选证书 1. 研究范围定义 数字化时代&#xff0c;应用成为企业开展各项业务的落脚点。随着业务的快速发展&#xff0c;应用的功能迭代变得越…

Scala集合详解(第七章:集合、数组、列表、set集合、map集合、元组、队列、并行)(尚硅谷笔记)

集合第七章:集合7.1 集合简介7.1.1 不可变集合继承图7.1.2 可变集合继承图7.2 数组7.2.1 不可变数组7.2.2 可变数组7.2.3 不可变数组与可变数组的转换7.2.4 多维数组7.3 列表 List7.3.1 不可变 List7.3.2 可变 ListBuffer7.4 Set 集合7.4.1 不可变 Set7.4.2 可变 mutable.Set7.…

蓝牙运动耳机什么牌子的好、运动蓝牙耳机排行榜推荐

近些年&#xff0c;户外运动兴起&#xff0c;运动耳机迎来爆发增长&#xff0c;拒绝运动乏味&#xff0c;追求健康运动方式&#xff0c;已经成为当下年轻人的共同诉求。跑步骑行听音乐&#xff0c;已经是运动爱好者再熟悉不过的操作&#xff0c;很多人在运动中离不开音乐的节奏…

双检测人脸防伪识别方法(活体检测+人脸识别+关键点检测+人像分割)

双检测人脸防伪识别=人脸检测+活体检测+人脸识别 1.人脸关键点+语义分割 使用mediapipe进行视频人脸关键点检测和人像分割: import time import cv2 import mediapipe as mp import numpy as npmp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solution…

定了:Python3.7,今年停止更新~

大家好&#xff0c;这里是程序员晚枫。 今天给大家分享一个来自Python官网的重要消息&#xff1a;Python3.7马上就要停止维护了&#xff0c;请不要使用了&#xff01; 官网链接&#xff1a;https://devguide.python.org/versions/ 停更的后果是什么&#xff1f; 周末翻阅Py…

从功能测试(点点点)到进阶自动化测试,实现薪资翻倍我只用了3个月时间

前言 从事测试工作已3年有余了&#xff0c;今天想聊一下自己刚入门时和现在的今昔对比&#xff0c;虽然现在也没什么成就&#xff0c;只能说笑谈一下自己的测试生涯&#xff0c;各位看官就当是茶余饭后的吐槽吧&#xff0c;另外也想写一写自己的职场感想&#xff0c;希望对刚开…