分享大厂对于缓存操作的封装

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 作者shigen分享了关于Redis缓存的封装,以避免常见问题如穿透、击穿、雪崩。封装包括四个文件:CacheEnum、CacheLoader、CacheService和CacheServiceImpl。CacheEnum用于统一管理缓存名和过期时间,CacheService定义缓存操作接口,CacheServiceImpl是实现类,使用Semaphore解决缓存击穿问题。

hello,伙伴们好久不见,我是shigen。发现有两周没有更新我的文章了。也是因为最近比较忙,基本是993了。

缓存大家再熟悉不过了,几乎是现在任何系统的标配,并引申出来很多的问题:缓存穿透、缓存击穿、缓存雪崩.......哎,作为天天敲业务代码的人,哪有时间天天考虑这么多的破事。直接封装一个东西,我们直接拿来就用岂不是美哉。看了项目组的代码,我也忍不住 diy 了,对于增删就算了,就是 get set 的 API 调用,修改?直接删了重新添加吧,哪有先查缓存再去修改保存的。难点就在于缓存的查询,要不缓存的穿透、击穿、雪崩会诞生对吧。
我们先看下缓存的逻辑:

是的,其实就是这么简单,剩下的就是考虑一下缓存穿透问题,最常见的处理方式就是加锁。这里我采用的是信号量 Semaphore。
好的,现在展示我的代码,代码结构如下:

.
├── CacheEnum.java                                    -- 缓存枚举
├── CacheLoader.java                                -- 缓存加载接口
├── CacheService.java                                -- 缓存服务
└── CacheServiceImpl.java                     -- 缓存服务实现类

1 directory, 4 files

ok,现在我们一一讲解下:

CacheEnum

主要的代码:

public enum CacheEnum {
   
   
                       /** 用户token缓存 */
                       USER_TOKEN("USER_TOKEN", 60, "用户token"),
                       /** 用户信息缓存 */
                       USER_INFO("USER_INFO", 60, "用户信息"),;

    /** 缓存前缀 */
    private final String  cacheName;
    /** 缓存过期时间 */
    private final Integer expire;
    /** 缓存描述 */
    private final String  desc;

其他的就是 get/set 方法,这里不做展示。主要解决的痛点就是缓存过期时间的统一管理、缓存名称的统一管理。

CacheService

这里边就是定义了缓存操作的接口:

public interface CacheService {
   
   

    /**
     * 获取缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @param type 缓存类型
     * @return 缓存值
     * @param <T> 缓存类型
     */
    <T> T get(String cacheName, String key, Class<T> type);

    /**
     * 获取缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @param type 缓存类型
     * @param loader 缓存加载器
     * @return 缓存值
     * @param <T> 缓存类型
     */
    <T> T get(String cacheName, String key, Class<T> type, CacheLoader<T> loader);

    /**
     * 删除缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     */
    void delete(String cacheName, String key);

    /**
     * 设置缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @param value 缓存值
     */
    void set(String cacheName, String key, Object value);
}

其实就是一些增删查的方法。只不过这里我们更加关注的是:缓存的名称,缓存的 key,缓存对象,缓存对象的类型。
在 22 行这里用到了CacheLoader 接口,其实就是处理缓存对象在缓存中拿不到的问题,它的定义也很简单:

@FunctionalInterface
public interface CacheLoader<V> {
   
   
    /**
     * 加载缓存
     * @param key 缓存key
     * @return 缓存值
     */
    V load(String key);
}

就一个方法,直接使用上 lambda 表达式,下边的测试类会讲到。

CacheServiceImpl

@Slf4j
@Service
public class CacheServiceImpl implements CacheService {
   
   

    /** Semaphore */
    private static final Semaphore        CACHE_LOCK = new Semaphore(100);

    /** 缓存操作 */
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取缓存key
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @return 缓存key
     */
    private String getCacheKey(String cacheName, String key) {
   
   
        Assert.isTrue(StrUtil.isNotBlank(cacheName), "cacheName不能为空");
        Assert.isTrue(StrUtil.isNotBlank(key), "key不能为空");
        Assert.isTrue(CacheEnum.getByCacheName(cacheName) != null, "需要使用CacheEnum枚举创建缓存");
        return cacheName + ":" + key;
    }

    @Override
    public <T> T get(String cacheName, String key, Class<T> type) {
   
   
        Object value = redisTemplate.opsForValue().get(getCacheKey(cacheName, key));
        if (value != null) {
   
   
            return type.cast(value);
        }
        return null;
    }

    @Override
    public <T> T get(String cacheName, String key, Class<T> type, CacheLoader<T> cacheLoader) {
   
   
        try {
   
   
            // 获取锁, 防止缓存击穿
            CACHE_LOCK.acquire();
            String cacheKey = getCacheKey(cacheName, key);
            Object value = redisTemplate.opsForValue().get(cacheKey);
            if (value != null) {
   
   
                return type.cast(value);
            }
            value = cacheLoader.load(cacheKey);
            if (value != null) {
   
   
                this.set(cacheName, key, value);
                return type.cast(value);
            }
        } catch (InterruptedException e) {
   
   
            log.warn("获取锁失败");
        } finally {
   
   
            CACHE_LOCK.release();
        }
        return null;
    }

    @Override
    public void delete(String cacheName, String key) {
   
   
        redisTemplate.opsForValue().getOperations().delete(getCacheKey(cacheName, key));
    }

    @Override
    public void set(String cacheName, String key, Object value) {
   
   
        String cacheKey = getCacheKey(cacheName, key);
        CacheEnum cacheEnum = CacheEnum.getByCacheName(cacheName);
        Assert.isTrue(cacheEnum != null, "需要使用CacheEnum枚举创建缓存");
        redisTemplate.opsForValue().set(cacheKey, value, cacheEnum.getExpire(), TimeUnit.SECONDS);
    }
}

这里就是接口的具体实现。需要注意:

  1. 在获得完整的缓存 key 的时候,我们其实对于缓存的 cacheName 做了验证,参见上代码块 21 行,不允许自己定义缓存的 cacheName,统一在枚举类中定义。
  2. 因为 tair 的资源有点不好申请,这里使用的 redis 作为缓存的工具,结合 spring-boot-starter-data-redis 作为操作的 API。
  3. 应对缓存穿透问题,这里使用的是Semaphore 信号量。

别的就没什么好说的,现在我们来测试一下我们的封装是否管用。

测试代码

设置缓存

测试用定义的枚举类创建缓存:

    @Test
    void set() {
   
   
        cacheService.set(CacheEnum.USER_INFO.getCacheName(), "10001", getFakeUser("10001"));
    }

是没问题的,不用枚举类创建:

    @Test
    void testSetSelfDefinedCacheName() {
   
   
        cacheService.set("user", "10001", getFakeUser("10001"));
    }

直接异常出来了:

java.lang.IllegalArgumentException: 需要使用CacheEnum枚举创建缓存

读取缓存

读取缓存,我的 API 中分为两种情况:直接读取,没有就算了;读取缓存,没有的话再从 DB 中拿。对应的单测如下:

    @Test
    void testGet() {
   
   
        UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
            UserEntity.class);
        log.info("user: {}", user);
    }

    @Test
    void testGetWithCacheLoader() {
   
   
        UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
            UserEntity.class, new CacheLoader<UserEntity>() {
   
   
                @Override
                public UserEntity load(String key) {
   
   
                    return getFakeUser("10001");
                }
            });
        log.info("user: {}", user);
    }

    @Test
    void testGetWithSimpledCacheLoader() {
   
   
        UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
            UserEntity.class, key -> getFakeUser(key));
        log.info("user: {}", user);
    }

第三种就是对于 lambda 接口的简化写法。
基于以上的方式,我们操作缓存就变得更加容易了。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
23小时前
|
存储 缓存 Dart
Flutter&鸿蒙next 封装 Dio 网络请求详解:登录身份验证与免登录缓存
本文详细介绍了如何在 Flutter 中使用 Dio 封装网络请求,实现用户登录身份验证及免登录缓存功能。首先在 `pubspec.yaml` 中添加 Dio 和 `shared_preferences` 依赖,然后创建 `NetworkService` 类封装 Dio 的功能,包括请求拦截、响应拦截、Token 存储和登录请求。最后,通过一个登录界面示例展示了如何在实际应用中使用 `NetworkService` 进行身份验证。希望本文能帮助你在 Flutter 中更好地处理网络请求和用户认证。
108 1
|
6月前
|
存储 缓存 NoSQL
【Go语言专栏】Go语言中的Redis操作与缓存应用
【4月更文挑战第30天】本文探讨了在Go语言中使用Redis进行操作和缓存应用的方法。文章介绍了Redis作为高性能键值存储系统,用于提升应用性能。推荐使用`go-redis/redis`库,示例代码展示了连接、设置、获取和删除键值对的基本操作。文章还详细阐述了缓存应用的步骤及常见缓存策略,包括缓存穿透、缓存击穿和缓存雪崩的解决方案。利用Redis和合适策略可有效优化应用性能。
135 0
|
3月前
|
缓存 程序员
封装一个给 .NET Framework 用的内存缓存帮助类
封装一个给 .NET Framework 用的内存缓存帮助类
|
4月前
|
缓存 Java Spring
Guava缓存工具类封装和使用
Guava缓存工具类封装和使用
101 0
|
5月前
|
缓存 负载均衡 NoSQL
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
77 1
|
4月前
|
存储 算法 缓存
高并发架构设计三大利器:缓存、限流和降级问题之使用RateLimiter来限制操作的频率问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用RateLimiter来限制操作的频率问题如何解决
|
5月前
|
缓存 分布式计算 关系型数据库
数据管理DMS操作报错合集之当进行RDS实例的可用区迁移时,提示“缓存清理”是什么意思
数据管理DMS(Data Management Service)是阿里云提供的数据库管理和运维服务,它支持多种数据库类型,包括RDS、PolarDB、MongoDB等。在使用DMS进行数据库操作时,可能会遇到各种报错情况。以下是一些常见的DMS操作报错及其可能的原因与解决措施的合集。
|
5月前
|
缓存 运维 Devops
阿里云云效操作报错合集之在构建过程中,Docker尝试从缓存中获取某个文件(或计算缓存键)时遇到了问题,该如何处理
本合集将整理呈现用户在使用过程中遇到的报错及其对应的解决办法,包括但不限于账户权限设置错误、项目配置不正确、代码提交冲突、构建任务执行失败、测试环境异常、需求流转阻塞等问题。阿里云云效是一站式企业级研发协同和DevOps平台,为企业提供从需求规划、开发、测试、发布到运维、运营的全流程端到端服务和工具支撑,致力于提升企业的研发效能和创新能力。
|
5月前
|
缓存 NoSQL Java
Redis系列学习文章分享---第四篇(Redis快速入门之Java客户端--商户查询缓存+更新+双写一致+穿透+雪崩+击穿+工具封装)
Redis系列学习文章分享---第四篇(Redis快速入门之Java客户端--商户查询缓存+更新+双写一致+穿透+雪崩+击穿+工具封装)
66 0
|
6月前
|
缓存 NoSQL 数据库
[Redis]——数据一致性,先操作数据库,还是先更新缓存?
[Redis]——数据一致性,先操作数据库,还是先更新缓存?
147 0