二、实战篇 商户查询缓存

news2025/6/13 19:42:49

源码仓库地址:git@gitee.com:chuangchuang-liu/hm-dingping.git

1、什么是缓存?

缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码

1.1、为什么要使用缓存?

添加缓存后,重复的数据可以直接从缓存中获取,一定程度上降低服务器的压力
但缓存带来的问题:

缓存作用使用缓存成本
降低后端负载数据一致性成本
提高读写效率,提升响应能力维护成本

1.2、如何使用缓存?

实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用
浏览器缓存:主要是存在于浏览器端的缓存
应用层缓存: 可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存
数据库缓存: 在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中
CPU缓存: 当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存
image-20220523212915666.png

2、添加商铺缓存

  • 缓存作用模型

1653322097736.png

  • 核心代码
public Shop queryById(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    if (shopJson != null) {
        // 如果缓存中有,直接返回
        Shop shop = JSON.parseObject(shopJson, Shop.class);
        return shop;
    }
    // 如果缓存中没有,从数据库中查询
    Shop shop = this.getById(id);
    // 如果数据库中没有,返回null
    if (shop == null) {
        return null;
    }
    // 将查询到的数据写入缓存
    stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop));
    return shop;
}

3、缓存更新策略

缓存更新是redis为了节约内存而设计的一种机制,当往redis中插入过多数据,会导致缓存数据过多造成宕机的可能,因此redis会对一些数据进行淘汰。

  • 内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
  • 超时剔除:当给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
  • 主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
/内存淘汰超时剔除主动更新
说明不用我们手动维护,当缓存中的数据过多时,利用redis内存淘汰机制,自动剔除部分数据,下次查询会自动更新当数据过期时,会被自动剔除。下次查询时进行自动更新当数据库中的数据进行更改时,手动地对缓存中的数据进行更新
一致性一般
成本

业务场景:
低一致性需求:使用内存淘汰机制。如商铺类型数据。
高一致性需求:使用主动更新策略,并以超时剔除作为兜底方案。如商铺信息数据更新。

3.1、缓存与数据库不一致问题

当数据发生更改时,数据库更改了而缓存没有同步更新,或者缓存更新了而数据库没有同步更新,都会造成数据不一致的问题。

3.2、不一致问题解决方案

不一致问题有三种解决方案:

  • Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
  • Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
  • Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

分析:
方案一可以实现,但具有一定的维护成本
方案二仍存在较大的一致性问题
方案三会存在数据丢失问题,如缓存数据还没来得及写入数据库,redis就崩溃了,那么这部分数据没有被持久化而丢失
综上,最终选择方案一作为数据不一致问题的解决方案

方案一如何处理实现,仍有三个问题需要考虑:

  • 是删除缓存还是更新缓存?
  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库让缓存失效,再次查询时更新缓存,实现懒加载效果
  • 如何保证缓存操作与数据库操作同时成功或失败?
    • 单体系统:使用事务机制保证操作的原子性
    • 分布式系统:使用TCC等分布式解决方案来保证操作的原子性
  • 先更新数据库再删除缓存,还是先删除缓存再更新数据库?
  • 先删除缓存再操作数据库
  • 先更新数据库再删除缓存

因为缓存操作是快操作、数据库操作是慢操作。第一种方案在并发情况下更加容易出现数据不一致的问题。
1653323595206.png

4、实现商铺数据缓存与数据库的同步修改

核心思路:

  • 查询商铺数据时,如果缓存没有数据,则查询数据库,同步更新到缓存并设置过期时间;
  • 更新数据库数据时,直接删除缓存数据

核心代码

/**
 * 案例:给查询商铺的缓存添加超时剔除和主动更新的策略
 * (先操作数据库后删除缓存)
 * @param id 商铺id
 * @return
 */
@Override
public Shop queryById(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    if (shopJson != null) {
        // 如果缓存中有,直接返回
        Shop shop = JSON.parseObject(shopJson, Shop.class);
        return shop;
    }
    // 如果缓存中没有,从数据库中查询
    Shop shop = this.getById(id);
    // 如果数据库中没有,返回null
    if (shop == null) {
        return null;
    }
    // 将查询到的数据写入缓存
    stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop),
            RedisConstants.SHOP_KEY_TTL, TimeUnit.MINUTES);
    return shop;
}
@Override
@Transactional // 保证数据库操作和缓存操作的原子性
public boolean updateByShopId(Shop shop) {
    if (shop.getId() == null) {
        return false;
    }
    // 更新数据库
    updateById(shop);
    // 更新缓存
    stringRedisTemplate.delete(RedisConstants.SHOP_KEY + shop.getId());
    return true;
}

5、缓存穿透

5.1、什么是缓存穿透?

缓存穿透指的是:当请求到达redis和数据库均未命中,且客户端发来大量这种恶意请求,造成数据库服务宕机的问题成为缓存穿透。

5.2、缓存穿透解决方案

常见的缓存穿透解决方案有两种:

  • 缓存空对象
  • 布隆过滤器
优点缺点
缓存空对象实现简单造成内存存储大量垃圾数据;存在短期的不一致
布隆过滤器内存占用较少,没有多余key实现复杂存在误判可能

缓存空对象思路分析:当redis和数据库均未命中,则向redis中对应的key缓存一条空对象,当再次查询同一key时,直接返回空对象即可,减少数据库的压力。
布隆过滤器: 布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,但因为其实通过hash的思想进行判断,会存在误判的可能。

总的来说,如果布隆过滤器判断不存在,那这条数据就是真的不存在;如果判断存在,但数据不一定就真的存在。

1653326156516.png

5.3、缓存空对象编码实现

  • 核心代码
public Shop queryById(Long id) {
// 从缓存中查询
String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
if (StrUtil.isNotBlank(shopJson)) {
    // 如果缓存中有,直接返回
    Shop shop = JSON.parseObject(shopJson, Shop.class);
    return shop;
}
// 判断redis是否缓存空值 shopJson != null,说明其是""空字符串
if (shopJson !=null){
    return null;
}
// 如果缓存中未命中,从数据库中查询
Shop shop = this.getById(id);
// 如果数据库中没有,返回null
if (shop == null) {
    // 数据库也未命中,缓存null值到redis
    stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, SHOP_NULL_VALUE,
                                          RedisConstants.SHOP_NULL_VALUE_TTL, TimeUnit.MINUTES);
    return null;
}
// 将查询到的数据写入缓存
stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop),
                                      RedisConstants.SHOP_KEY_TTL, TimeUnit.MINUTES);
return shop;
}

5.4、补充说明

缓存穿透的解决方案当然不止前面说的这两种,还可以考虑:

  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

6、缓存雪崩

6.1、什么是缓存雪崩?

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

6.2、解决方案

  • 在设置热点key时,设置不同的TTL,避免热点key同时失效
  • 添加redis集群,提供高可用服务
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

7、缓存击穿

7.1、什么是缓存击穿?

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

逻辑分析:在线程1重建缓存过程的同时,其他大量线程也都查询缓存,然后走查询数据库重建缓存这样的过程,对数据库服务造成巨大压力。
1653328022622.png
解决方案:

  • 互斥锁

只有获取到锁资源的线程可以重建缓存,其他线程则需要一直等待重试,知道缓存中有数据或拿到锁资源。
1653328288627.png

  • 逻辑过期

在设置热点key时不设置TTL,而是在数据本身中设置一个逻辑过期时间,这样在redis中这个数据基本可以认为永不失效。通过代码来判断数据是否失效,当线程1查询数据失效时,尝试获取锁资源,开启独立线程进行缓存重建,而自己则返回旧数据。
同时,其他大量线程查询数据失效时,也尝试获取锁资源。如果失败,则不等待,直接返回旧数据即可!
1653328663897.png

  • 方案优缺点比对
优点缺点
互斥锁实现简单没有额外内存消耗保证数据一致性性能较差,大量线程需要等待存在死锁风险
逻辑过期线程无需等待,服务可用性好不保证一致性;实现复杂;存在额外内存消耗(存储逻辑过期时间)

7.2、利用互斥锁解决缓存击穿

  • 核心思路

image.png

  • 核心代码
private Shop queryWithMutexLock(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    if (StrUtil.isNotBlank(shopJson)) {
        // 如果缓存中有,直接返回
        Shop shop = JSON.parseObject(shopJson, Shop.class);
        return shop;
    }
    // 判断redis是否缓存空值 shopJson != null,说明其是""空字符串
    if (shopJson != null) {
        return null;
    }
    try {
        // 获取互斥锁
        if (!getMutexLock(id)) {
            // 未获取到锁,等待一段时间后重试
            Thread.sleep(50);
            queryWithMutexLock(id);
        }

        // Double Check
        shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            // 如果缓存中有,直接返回
            Shop shop = JSON.parseObject(shopJson, Shop.class);
            return shop;
        }
        // 判断redis是否缓存空值 shopJson != null,说明其是""空字符串
        if (shopJson != null) {
            return null;
        }

        // 从数据库中查询,进行缓存重建
        Shop shop = this.getById(id);
        // 数据库也未命中,缓存null值到redis
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, SHOP_NULL_VALUE,
                    RedisConstants.SHOP_NULL_VALUE_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 将查询到的数据写入缓存
        stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(shop),
                RedisConstants.SHOP_KEY_TTL, TimeUnit.MINUTES);
        return shop;
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 释放互斥锁
        releaseMutexLock(id);
    }
}
/**
 * 获取互斥锁
 * @param id
 * @return
 */
private boolean getMutexLock(Long id) {
    String key = "lock:shop" + id;
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    // 不要直接返回包装类,存在空指针异常问题,需要使用工具类BooleanUtil.isTrue()方法进行校验
    return BooleanUtil.isTrue(flag);
}
/**
 * 释放互斥锁
 * @param id
 */
private void releaseMutexLock(Long id) {
    String key = "lock:shop" + id;
    stringRedisTemplate.delete(key);
}

7.3、利用逻辑过期解决缓存击穿

  • 核心思路

1653360308731.png

  • 核心代码
/**
 * 查询商铺信息(缓存击穿-逻辑过期)
 *
 * @param id
 * @return
 */
private Shop queryWithLogicExpireTime(Long id) {
    // 从缓存中查询
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
    // 判断是否过期
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    if (StrUtil.isNotBlank(shopJson)) {
        // 未过期,返回商铺
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            return shop;
        }
    }

    try {
        // 尝试获取互斥锁
        if (!getMutexLock(id)) {
            // 未获取到锁,返回旧商品信息
            return shop;
        }

        // Double Check
        shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
        if (StrUtil.isNotBlank(shopJson)) {
            // 检查缓存是否存在,且是否过期
            shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.SHOP_KEY + id);
            if (StrUtil.isNotBlank(shopJson)) {
                // 判断是否过期
                redisData = JSONUtil.toBean(shopJson, RedisData.class);
                shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
                // 未过期,返回商铺
                if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
                    return shop;
                }
            }
        }

        // 从数据库中查询,进行缓存重建
        shop = this.getById(id);
        // 数据库也未命中,返回null
        if (shop == null) {
            return null;
        }
        // 将查询到的数据写入缓存,设置逻辑过期
        RedisData newRedisData = new RedisData();
        newRedisData.setData(shop);
        newRedisData.setExpireTime(LocalDateTime.now().plusMinutes(RedisConstants.SHOP_KEY_TTL));
        stringRedisTemplate.opsForValue().set(RedisConstants.SHOP_KEY + id, JSON.toJSONString(newRedisData));
        return shop;
    } catch (Exception e) {
        throw new RuntimeException(e);
    } finally {
        // 释放互斥锁
        releaseMutexLock(id);
    }
}

8、简单封装缓存工具类

@Component
public class CacheUtil {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheUtil(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, String value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    public void delete(String key) {
        stringRedisTemplate.delete(key);
    }
}

希望以上内容,可以帮助到大家!

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

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

相关文章

icp许可证年报入口在哪?icp许可证年报流程详细介绍

近期拥有ICP许可证的企业负责人都会收到各地方通管局下发的年报通知,需要在每年的1-3月份报送年报信息,最晚报送时间是2024年3月31日。 对于刚申请或者刚接触这方面的朋友来说,可能连icp许可证年报入口在哪都不知道,更不用说后面…

OpenStack之keystone(用户认证)

Keystone(认证) Keystone 概述 1)管理用户及其权限 2)维护OpenStack Services 的 Endpoint 3)Authentication(认证)和 Authorization(授权) keystone的名词概念 1.User(用户或服务&#xf…

springboot256基于springboot+vue的游戏交易系统

游戏交易系统设计与实现 摘 要 在如今社会上,关于信息上面的处理,没有任何一个企业或者个人会忽视,如何让信息急速传递,并且归档储存查询,采用之前的纸张记录模式已经不符合当前使用要求了。所以,对游戏交…

制定一份完美的测试计划,让您的产品质量更上一层楼!

大家好,我是彭于晏。今天学习测试计划如何书写。 虽然很多人日常工作中都知道测试计划是什么,但是写好测试计划,其实并不容易。今天就来一起学习下测试计划如何书写。 什么是测试计划? 测试计划是一份为软件产品所准备的详细文档…

揭秘接口测试:完整流程指南!

在讲接口测试之前,首先需要给大家申明下:接口测试对于测试人员而言,非常非常重要,懂功能测试接口测试,就能在企业中拿到一份非常不错的薪资。 这么重要的接口测试,一般也是面试笔试必问。为方便大家更好的…

Qt 实现诈金花的牌面值分析工具

诈金花是很多男人最爱的卡牌游戏 , 每当你拿到三张牌的时候, 生活重新充满了期待和鸟语花香. 那么我们如果判断手中的牌在所有可能出现的牌中占据的百分比位置呢. 这是最终效果: 这是更多的结果: 在此做些简单的说明: 炸弹(有些地方叫豹子) > 同花顺 > 同花 > 顺…

【Web】浅聊Java反序列化之Rome——关于其他利用链

目录 前言 JdbcRowSetImpl利用链 BasicDataSource利用链 Hashtable利用链 BadAttributeValueExpException利用链 HotSwappableTargetSource利用链 前文:【Web】浅聊Java反序列化之Rome——EqualsBean&ObjectBean-CSDN博客 前言 Rome中ToStringBean的利用…

ElasticSearch 底层读写原理

ElasticSearch 底层读写原理 ​ 写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。 1、ES写入数据的过程 1.选择任意一个DataNode发送请求&#xff0c…

山泉还可以申请商标不,现阶段通过率如何!

在32类类别啤酒饮料是许多生产水企业主要申请注册的类别,那现在山泉在这个类别还可以申请注册商标不,山泉在这个类别基本上是通用词,首先是需要前面词具有显著性,没的相同或近似才可以。 经普推知产老杨检索发现,在32…

数据分析-Pandas数据画箱线图

数据分析-Pandas数据画箱线图 数据分析和处理中,难免会遇到各种数据,那么数据呈现怎样的规律呢?不管金融数据,风控数据,营销数据等等,莫不如此。如何通过图示展示数据的规律? 数据表&#xff…

全网最最最详细centos7如何安装docker教程

在CentOS 7上安装Docker主要包括以下步骤: 1. 卸载旧版本的Docker 首先,需要确保系统上没有安装旧版本的Docker。可以通过以下命令来卸载它们: sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-late…

阿珊详解Vue Router的守卫机制

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…

nginx代理minio客户端

错误方式 在点击桶名查看文件时, 会一直处于loading加载中 worker_processes 1; #设置 Nginx 启动的工作进程数为 1。events {worker_connections 1024; ##设置每个工作进程的最大并发连接数为 1024。 }http {include mime.types; #该文件定义了文件扩展名和 MIME 类型…

AJAX学习(一)

版权声明 本文章来源于B站上的某马课程,由本人整理,仅供学习交流使用。如涉及侵权问题,请立即与本人联系,本人将积极配合删除相关内容。感谢理解和支持,本人致力于维护原创作品的权益,共同营造一个尊重知识…

全网最新最全的Jmeter接口测试:jmeter_定时器

固定定时器 如果你需要让每个线程在请求之前按相同的指定时间停顿,那么可以使用这个定时器;需要注意的是,固定定时器的延时不会计入单个sampler的响应时间,但会计入事务控制器的时间 1、使用固定定时器位置在http请求中&#xf…

CVHub | 初识langchain,3分钟快速了解!

本文来源公众号“CVHub”,仅用于学术分享,侵权删,干货满满。 原文链接:初识langchain 1 什么是langchain langchain[1]是一个用于构建LLM-Based应用的框架,提供以下能力: 上下文感知:可以为LLM链接上下文…

汽车零部件制造中的信息抽取技术:提升效率与质量的关键

一、引言 在汽车制造业中,零部件的生产是整个制造流程的关键一环。这些零部件,包括但不限于制动系统、转向系统和传动系统,是确保汽车安全、可靠运行的基础。为了满足现代汽车工业对效率和质量的严格要求,制造商们纷纷投入到高度…

jvm堆概述

《java虚拟机规范》中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。 一个JVM实例只存在一个堆内存(就是new 出来一个对象),java内存管理的核心区域 java堆区在jvm启动的时候就被创建,空间大小确定。是jvm管理的最大一…

探索代理服务器:保护您的网络安全与隐私

🍎个人博客:个人主页 🏆个人专栏:Linux ⛳️ 功不唐捐,玉汝于成 目录 前言 正文 隐藏真实IP地址: 访问控制: 加速访问速度: 过滤内容: 突破访问限制&#xff1…

深入浅出计算机网络 day.1 概论③ 电路交换、分组交换和报文交换

人无法同时拥有青春和对青春的感受 —— 04.3.9 内容概述 01.电路交换、分组交换和报文交换 02.三种交换方式的对比 一、电路交换、分组交换和报文交换 1.电路交换 计算机之间的数据传送是突发式的,当使用电路交换来传送计算机数据时,其线路的传输效率一…