摘要:以短信验证码为例实现缓存。
目录
[TOC]
缓存
在系统访问量越来越大之后,往往最先出现瓶颈的往往是数据库。而为了减少数据库的压力,我们可以选择让产品砍掉消耗数据库性能的需求。当然,我们也可以选择如下方式优化:
艿艿:在这里,我们暂时不考虑优化数据库的硬件,索引等等手段。
- 读写分离。通过将读操作分流到从节点,避免主节点压力过多。
- 分库分表。通过将读写操作分摊到多个节点,避免单节点压力过多。
- 缓存。相比数据库来说,缓存往往能够提供更快的读性能,减小数据库的压力。关于缓存和数据库的性能情况,可以看看如下两篇文章:
那么,在引入缓存之后,我们的读操作的代码,往往代码如下:
1 | |
- 这段代码,是比较常用的缓存策略,俗称“被动写”。整体步骤如下:
- 1)首先,从 Cache 中,读取用户缓存。如果存在,则直接返回。
- 2)然后,从 DB 中,读取用户数据。如果存在,写入 Cache 中。
- 3)最后,返回 DB 的查询结果。
- 可能会有胖友说,这里没有考虑缓存击穿、缓存穿透、缓存并发写的情况。恩,是的,但是这并不在本文的内容范围。感兴趣的,可以看看我的男神超哥写的 《缓存穿透、缓存并发、缓存失效之思路变迁》 文章。嘿嘿~
虽然说,上述的代码已经挺简洁了,但是我们是热爱“偷懒”的开发者,必然需要寻找更优雅(偷懒)的方式。在 Spring 3.1 版本的时候,它发布了 Spring Cache 。关于它的介绍,如下:
FROM 《注释驱动的 Spring Cache 缓存介绍》
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
其特点总结如下:
- 通过少量的配置 annotation 注释即可使得既有代码支持缓存
- 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
- 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
- 支持 AspectJ,并通过其实现任何方法的缓存支持
- 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
- 简单来说,可以像使用
@Transactional声明式事务,使用 Spring Cache 提供的@Cacheable等注解, - 声明式缓存。而在实现原理上,也是基于 Spring AOP 拦截,实现缓存相关的操作。
下面,使用 Spring Cache 将 #getUser(Integer id) 方法进行简化。
1 | |
- 在 UserService 的
#getUser2(Integer id)方法上,我们直接调用 UserMapper ,从 DB 中查询数据。 - 在 UserMapper 的
#selectById(Integer id)方法上,有@Cacheable注解。Spring Cache 会拦截有@Cacheable注解的方法,实现“被动写”的逻辑。
Spring Boot 整合 Redis
在 Spring 的生态中,使用 Spring Data Redis 来实现对 Redis 的数据访问。
市面上已经有 Redis、Redisson、Lettuce 等优秀的 Java Redis 工具库,为什么还要有 Spring Data Redis 呢?
- 对于下层,Spring Data Redis 提供了统一的操作模板(即 RedisTemplate 类),封装了 Jedis、Lettuce 的 API 操作,访问 Redis 数据。
- 所以,实际上,Spring Data Redis 内置真正访问的实际是 Jedis、Lettuce 等 API 操作。
- 对于上层,开发者学习如何使用 Spring Data Redis 即可,而无需关心 Jedis、Lettuce 的 API 操作。
- 甚至,未来如果想将 Redis 访问从 Jedis 迁移成 Lettuce 来,无需做任何的变动。
- 目前,Spring Data Redis 暂时只支持 Jedis、Lettuce 的内部封装,而 Redisson 是由 redisson-spring-data 来提供。

在 spring-boot-starter-data-redis 项目 2.X 中,默认使用 Lettuce 作为 Java Redis 工具库
-
个人推荐的话,生产中还是使用 Jedis ,稳定第一。
-
也因此,本节是 Spring Data Redis + Jedis 的组合。
依赖
1 | |
配置文件
在 application.yml 中,添加 Redis 配置,如下:
1 | |
简单测试
创建 Test01 测试类,来测试一下简单的 SET 指令。
1 | |
通过 StringRedisTemplate 类,进行了一次 Redis SET 指令的执行。
执行 #testStringSetKey() 方法这个测试方法。执行完成后,在控制台查询,看看是否真的执行成功了。
1 | |
RedisTemplate
org.springframework.data.redis.core.RedisTemplate 类,从类名上,我们就明明白白知道,提供 Redis 操作模板 API 。核心属性如下:
1 | |
- <1> 处,看到了四个序列化相关的属性,用于 KEY 和 VALUE 的序列化。
- 例如说,我们在使用 POJO 对象存储到 Redis 中,一般情况下,会使用 JSON 方式序列化成字符串,存储到 Redis 中。详细的,我们在 「3. 序列化」 小节中来说明。
- 在上文中,我们看到了
org.springframework.data.redis.core.StringRedisTemplate类,它继承 RedisTemplate 类,使用org.springframework.data.redis.serializer.StringRedisSerializer字符串序列化方式。直接点开 StringRedisSerializer 源码,看下它的构造方法,瞬间明明白白。
<2>处,Lua 脚本执行器,提供 Redis scripting API 操作。- <3>处,Redis 常见数据结构操作类。
- ValueOperations 类,提供 Redis String API 操作。
- ListOperations 类,提供 Redis List API 操作。
- SetOperations 类,提供 Redis Set API 操作。
- ZSetOperations 类,提供 Redis ZSet(Sorted Set) API 操作。
- GeoOperations 类,提供 Redis Geo API 操作。
- HyperLogLogOperations 类,提供 Redis HyperLogLog API 操作。
那么 Pub/Sub、Transaction、Pipeline、Keys、Cluster、Connection 等相关的 API 操作呢?它在 RedisTemplate 自身提供,因为它们不属于具体每一种数据结构,所以没有封装在对应的 Operations 类中。哈哈哈,胖友打开 RedisTemplate 类,去瞅瞅,妥妥的明白。
序列化
RedisSerializer 接口
Redis 序列化接口,用于 Redis KEY 和 VALUE 的序列化。
主要分成四类:
- JDK 序列化方式
- String 序列化方式
- JSON 序列化方式
- XML 序列化方式
项目实践
本小节,我们来分享我们在生产中的一些实践。关于这块,希望大家可以一起讨论,能够让我们的代码更加优雅干净。
4.1 Cache Object
在我们使用数据库时,我们会创建 dataobject 包,存放 DO(Data Object)数据库实体对象。
那么同理,我们缓存对象,怎么进行对应呢?对于复杂的缓存对象,我们创建了 cacheobject 包,和 dataobject 包同层。如:
1 | |
并且所有的 Cache Object 对象使用 CacheObject 结尾,例如说 UserCacheObject、ProductCacheObject 。
4.2 数据访问层
在我们访问数据库时,我们会创建 dao 包,存放每个 DO 对应的 Dao 对应。那么对于每一个 CacheObject 类,我们也会创建一个其对应的 Dao 类。例如说,UserCacheObject 对应 UserCacheObjectDao 类。示例代码如下:
1 | |
<1>处,通过静态变量,声明 KEY 的前缀,并且使用冒号作为间隔<3>处,声明KEY_PATTERN对应的 KEY 拼接方法,避免散落在每个方法中。<2>处,通过@Resource注入指定名字的 RedisTemplate 对应的 Operations 对象,这样明确每个 KEY 的类型。- 剩余的,就是每个方法封装对应的操作。
可能会有胖友问,为什么不支持将 RedisTemplate 直接 Service 业务层调用呢?如果这样,我们业务代码里,就容易混杂着很多 Redis 访问代码的细节,导致很脏乱。我们试着把 RedisTemplate 想象成 Spring JDBCTemplate ,我们一定会声明对应的 Dao 类,访问数据库。所以,同理落。
那么还有一个问题,UserCacheDao 放在哪个包下?目前的想法是,将 dao 包下拆成 mysql、redis 包。这样,MySQL 相关的 Dao 放在 mysql 包下,Redis 相关的 Dao 放在 redis 。
4.3 序列化
在 「3. 序列化」 小节中,我们仔细翻看了每个序列化方式,暂时没有一个能够完美的契合我们的需求,所以我们直接使用最简单的 StringRedisSerializer 作为序列化实现类。而真正的序列化,我们在各个 Dao 类里,自己手动来调用。
例如说,在 UserCacheDao 示例中,已经看到了这么做了。这里还有一个细化点,虽然我们是自己手动序列化,可以自己简单封装一个 JSONUtil 类,未来如果我们想换 JSON 库,就比较方便了。其实,这个和 Spring Data Redis 所做的封装是一个思路。
示例补充
像 String、List、Set、ZSet、Geo、HyperLogLog 等等数据结构的操作,胖友自己去用用对应的 Operations 操作类的 API 方法,就非常容易懂了,我们更多的,补充 Pipeline、Transaction、Pub/Sub、Script 等等功能的示例。
整合 Redisson
简单来说,这是 Java 最强的 Redis 客户端!
- 除了提供了 Redis 客户端的常见操作之外,还提供了 Redis 分布式锁、BloomFilter 布隆过滤器等强大的功能。
依赖
1 | |
配置文件
在 application.yml 中,添加 Redis 配置,如下:
1 | |
和 「2.2 配置文件」 的差异点是:
1)去掉 Jedis 相关的配置项
2)增加 redisson.config 配置
在我们使用 Spring Boot 整合 Redisson 时候,通过该配置项,引入一个外部的 Redisson 相关的配置文件。例如说,示例中,我们引入了 classpath:redisson.yaml 配置文件。它可以使用 JSON 或 YAML 格式,进行配置。
而引入的 redisson.config 对应的配置文件,对应的类是 org.redisson.config.Config 类。因为示例中,我们使用的比较简单,所以就没有做任何 Redisson 相关的自定义配置。
Redis 分布式锁
示例代码对应测试类:LockTest 。
一说到分布式锁,大家一般会想到的就是基于 Zookeeper 或是 Redis 实现分布式锁。相对来说,在考虑性能为优先因素,不需要特别绝对可靠性的场景下,我们会优先考虑使用 Redis 实现的分布式锁。
在 Redisson 中,提供了 8 种分布式锁的实现,具体胖友可以看看 《Redisson 文档 —— 分布式锁和同步器》 。真特码的强大!大多数开发者可能连 Redis 怎么实现分布式锁都没完全搞清楚,Redisson 直接给了 8 种锁,气人,简直了。
本小节,我们来编写一个简单使用 Redisson 提供的可重入锁 RLock 的示例。
创建 LockTest 测试类,编写代码如下:
1 | |
- 整个测试用例,意图是:1)启动一个线程 A ,先去持有锁 10 秒然后释放;2)主线程,也去尝试去持有锁,因为线程 A 目前正在占用着该锁,所以需要等待线程 A 释放到该锁,才能持有成功。
<1>处,注入 RedissonClient 对象。因为我们需要使用 Redisson 独有的功能,所以需要使用到它。<2.1>处,启动线程 A ,然后调用RLock#lock(long leaseTime, TimeUnit unit)方法,加锁以后 10 秒钟自动解锁,无需调用 unlock 方法手动解锁。<2.2>处,简单 sleep 1 秒,保证线程 A 成功持有锁。<3>处,主线程,调用RLock#tryLock(long waitTime, long leaseTime, TimeUnit unit)方法,尝试加锁,最多等待 100 秒,上锁以后 10 秒自动解锁。
执行 #test() 测试用例,结果如下:
1 | |
- 9 秒后(因为我们 sleep 了 1 秒),主线程成功获得到 Redis 分布式锁,符合预期。
Redis 缓存
使用 Redis 实现缓存,有 2 种使用方式:
- 编程式缓存:基于 Spring Data Redis 框架的 RedisTemplate 操作模板。
- 声明式缓存:基于 Spring Cache 框架的
@Cacheable等等注解。
编程式缓存
1 | |
由于 Redisson 提供了分布式锁、队列、限流等特性,所以使用它作为 Spring Data Redis 的客户端。
Spring Data Redis 配置
配置文件中,通过 spring.redis 配置项,设置 Redis 的配置。
1 | |
② 在 YudaoRedisAutoConfiguration (opens new window)配置类,设置使用 JSON 序列化 value 值。

搭建 Access Token 缓存
以访问令牌 Access Token 的缓存来举例子,讲解项目中是如何使用 Spring Data Redis 框架的。

引入依赖
在 yudao-module-system-server 模块中,引入 yudao-spring-boot-starter-redis 技术组件。
1 | |
OAuth2AccessTokenDO
新建 OAuth2AccessTokenDO (opens new window)类,访问令牌 Access Token 类。

友情提示:
- ① 如果值是【简单】的 String 或者 Integer 等类型,无需创建数据实体。
- ② 如果值是【复杂对象】时,建议在
dal/dataobject包下,创建对应的数据实体。
RedisKeyConstants
为什么要定义 Redis Key 常量?
- 每个
yudao-module-xxx模块,都有一个 RedisKeyConstants 类,定义该模块的 Redis Key 的信息。- 目的是,避免 Redis Key 散落在 Service 业务代码中,像对待数据库的表一样,对待每个 Redis Key。
- 通过这样的方式,如果想要了解一个模块的 Redis 的使用情况,只需要查看 RedisKeyConstants 类即可。
在 yudao-module-system 模块的 RedisKeyConstants (opens new window)类中,新建 OAuth2AccessTokenDO 对应的 Redis Key 定义 OAUTH2_ACCESS_TOKEN。

OAuth2AccessTokenRedisDAO
新建 OAuth2AccessTokenRedisDAO (opens new window)类,是 OAuth2AccessTokenDO 的 RedisDAO 实现。
- 添加 Repository 注解
- 注入 RedisTemplate Bean
- 格式化 Redis Key
- 如果是复杂对象,需要使用 JSON 序列化

OAuth2TokenServiceImpl
在 OAuth2TokenServiceImpl (opens new window)中,只要注入 OAuth2AccessTokenRedisDAO Bean,非常简洁干净的进行 OAuth2AccessTokenDO 的缓存操作,无需关心具体的实现。

声明式缓存
Spring Cache 声明式缓存
相比来说 Spring Data Redis 编程式缓存,Spring Cache 声明式缓存的使用更加便利,一个 @Cacheable 注解即可实现缓存的功能。
1 | |
依赖
1 | |
Spring Cache 配置
① 在 application.yaml (opens new window)配置文件中,通过 spring.redis 配置项,设置 Redis 的配置。
1 | |
② 在 YudaoCacheAutoConfiguration (opens new window)配置类,设置使用 JSON 序列化 value 值。

常见注解
@Cacheable 注解
@Cacheable (opens new window)注解:添加在方法上,缓存方法的执行结果。执行过程如下:
- 首先,判断方法执行结果的缓存。如果有,则直接返回该缓存结果。
- 然后,执行方法,获得方法结果。
- 之后,根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
- 最后,返回方法结果。
注解在方法上,表示该方法的返回结果是可以缓存的。
- 也就是说,该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法。
- 参数相同:因为缓存不关心方法的执行逻辑,它能确定的是:
- 对于同一个方法,如果参数相同,那么返回结果也是相同的。
- 但是如果参数不同,缓存只能假设结果是不同的,
- 所以对于同一个方法,程序运行过程中,使用了多少种参数组合调用过该方法,理论上就会生成多少个缓存的 key(当然,这些组合的参数指的是与生成 key 相关的)。
参数:
- 提供两个参数来指定缓存名:value、cacheNames,二者选其一即可。
- key:
- unless:
1 | |
@CachePut 注解
注解,添加在方法上,缓存方法的执行结果。
不同于 @Cacheable 注解,它的执行过程如下:
- 首先,执行方法,获得方法结果。也就是说,无论是否有缓存,都会执行方法。
- 然后,根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
- 最后,返回方法结果。
@CacheEvict 注解
@CacheEvict (opens new window)注解,添加在方法上,删除缓存。
不使用 allEntries 属性,但是想批量删除一些缓存,怎么办?
可参考 https://t.zsxq.com/phOrM (opens new window)帖子,手动删除一些。
1 | |
搭建 Role 角色缓存
在 RoleServiceImpl (opens new window)中,使用 Spring Cache 实现了 Role 角色缓存,采用【被动读】的方案。原因是:

- 【被动读】相对能够保证 Redis 与 MySQL 的一致性
- 绝大数数据不需要放到 Redis 缓存中,采用【主动写】会将非必要的数据进行缓存
友情提示:
如果你未学习过 MySQL 与 Redis 一致性的问题,可以后续阅读 《Redis 与 MySQL 双写一致性如何保证? 》 (opens new window)文章。
① 执行 #getRoleFromCache(...) 方法,从 MySQL 读取数据后,向 Redis 写入缓存。

② 执行 #updateRole(...) 或 #deleteRole(...) 方法,在更新或者删除 MySQL 数据后,从 Redis 删除缓存。

补充说明:
如果在多个项目里,使用了 Redis 想通 db 的话,可以通过 spring.cache.redis.key-prefix 解决,可见 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/998/(opens new window)
过期时间
Spring Cache 默认使用 spring.cache.redis.time-to-live 配置项,设置缓存的过期时间,项目默认为 1 小时。
- 如果想自定义过期时间,可以在
@Cacheable注解中的cacheNames属性中,添加#{过期时间}后缀,单位是秒。

实现的原来,参考 《Spring @Cacheable 扩展支持自定义过期时间 》 (opens new window)文章。
Redis 监控
yudao-module-infra 的 redis (opens new window)模块,提供了 Redis 监控的功能。
点击 [基础设施 -> 监控中心 -> Redis 监控] 菜单,可以查看到 Redis 的基础信息、命令统计、内存信息。如下图所示:

管理后台 - Redis 监控
1 | |
整合 Redis 缓存步骤
以短信验证码为例。security 权限。
依赖
1 | |
配置文件
在 SpringBoot 配置文件 application.yml 中:
- 在
spring节点下添加 Redis 连接配置;
1 | |
- 在根节点下添加 Redis 自定义 key 的配置:前缀 String、过期时间;
1 | |
添加 RedisService 接口
- 用于定义常用 Redis 操作:
get()、set()、expire()等; - 注入
StringRedisTemplate,实现接口; - 没有 DAO 层,更简略
1 | |
XxxRedisDAO 实现
另一种实现结构
- JsonUtils.parseObject()
1 | |
XxxRedisDAO 被 Service 调用
1 | |
UmsMemberService 接口
- 生成验证码时,将自定义的 Redis 键值 + 手机号生成一个 Redis 的 key,
- 发送验证码:创建发送短信的客户端,提供参数调用对应方法即可。
- 提供阿里云账号(AccessKey ID)和密码(AccessKey Secret)登录后,选择对应的开发测试短信签名和模版,填充对应的短信内容(模版参数),将短信发送到指定的手机号。
- 发送成功后,以验证码为 value 存入到 Redis Session 中,并设置过期时间(如120s);
- 校验验证码时,检查传入的验证码格式,根据手机号码来获取 Redis 里存储的验证码、及过期时间,与传入的比对。
Service 接口返回 String + Controller 返回 CommonResult;
1 | |
发送短信
1 | |
接口
1 | |
UmsMemberController
- 添加
UmsMemberController,根据电话发送验证码的接口和校验验证码的接口; :控制层方法中@Redis@Redis标注的参数(如,@Redis(key = "redisKey") String redisValue),值应从 Redis 中获取,不用从请求参数中获取。
<img //src=”../assets/v2-a226f96dff62a33e3b705e07b51dc319_720w.jpg” alt=”img” />
1 | |
数据库与缓存的一致性
如果未学习过 MySQL 与 Redis 一致性的问题,可以后续阅读 《Redis 与 MySQL 双写一致性如何保证? 》 (opens new window)文章。