上一篇简单的介绍了 Spring Cache 的使用方法,用的是默认缓存管理器
ConcurrentMapCacheManager, 本篇文章介绍 Spring Cache 如何集成 Redis
[TOC]
集成
我们只需引入依赖 spring-boot-starter-data-redis
,Spring Boot 扫描到后,RedisCacheConfiguration 自动配置生效,redis 缓存管理器
RedisCacheManager 就会 取代 默认缓存管理器
ConcurrentMapCacheManager 成了 CacheManager 的实现
环境/版本一览:
- 开发工具:Intellij IDEA 2018.2.2
- springboot: 2.0.5.RELEASE
- jdk:1.8.0_171
- maven:3.3.9
- spring-boot-starter-cache:2.0.5.RELEASE
- spring-boot-starter-data-redis:2.0.5.RELEASE
1、pom.xml
1 | <dependencies> |
2、application.yml
1 | spring: |
3、config
自定义缓存方式的 key
与 value
的序列化方式
1 | package com.fatal.config; |
4、entity
实体必须实现序列化
1 | package com.fatal.entity; |
LocalDateTime 的序列化和反序列化
LocalDateTime 比较特殊,由于没有无参构造器,所以它需要使用特定的序列化器
LocalDateTimeSerializer
和 反序列化LocalDateTimeDeserializer
5、dto
1 | package com.fatal.dto; |
6、dao
###IUserDao.java
1 | package com.fatal.dao; |
UserDaoImpl.java
1 | package com.fatal.dao.impl; |
7、service
###IUserService.java
1 | package com.fatal.service; |
UserServiceImpl.java
1 | package com.fatal.service.impl; |
8、Application
开启缓存
1 | package com.fatal; |
9、Test
1 | package com.fatal; |
10、测试
测试一
先运行 Chapter16ApplicationTests.testSelectById()
然后运行 Chapter16ApplicationTests.testCache()
结果和我们期望的一致,可以看到增删改查中,第一次查询是没有日志输出的,因为它直接从缓存中获取的数据,而添加、修改、删除都是会进入方法内执行具体的业务代码,然后通过切面去删除掉Redis中的缓存数据。其中 # 号代表这是一个 SpEL 表达式,此表达式可以遍历方法的参数对象,具体语法可以参考 Spring 的相关文档手册。
测试二
运行Chapter16ApplicationTests.testCacheManager()
测试三
运行Chapter16ApplicationTests.testSelectById()
查看 redis 图形化界面,LocalDateTime
序列化成功
测试四
运行两次 Chapter16ApplicationTests.testListUserWithParam()
(换个参数)
再运行 Chapter16ApplicationTests.testSelectById()
初始化一下缓存数据
再运行 Chapter16ApplicationTests.testRemove()
,测试删除
测试五
用缓存的时候,缓存与数据库双写一致性是比较重要的,读的话,先从数据库读,然后存到缓存中再响应。那么更新的时候,如何保证缓存与数据库双写一致性。在低流量的前提下,有一种解决方案,就是先删除缓存之后再更新数据库。为什么不是先更新之后再删除呢?因为如果更新的过程中出现问题,导致删除缓存的逻辑没有执行,就会出现缓存数据库双写不一致问题。
接下来,测试解决方案
这个测试方法会先将数据查出来,放到缓存中;在 lowFlowRateWithUpdate(..)
的方法执行前打个断点,方法体里边也打个断点
debug 运行 Chapter16ApplicationTests.testLowFlowRateWithUpdate()
,如下:
执行到 lowFlowRateWithUpdate(..)
方法前,我们用 Redis 图形化界面
看看数据
数据查出来并且已经放缓存中了,接下来放行,来到 lowFlowRateWithUpdate(..)
方法体中,用 Redis 图形化界面
将数据库 reload 一下看看结果
缓存在执行更新数据库操作之前正常删除了。
测试六
上面那套方案只适用于低流量的情况下,高并发情况下,更新数据用上面那套方案有什么问题呢?
假设现在有一条请求是更新数据的,在删除缓存的时候,更新操作还没执行完,出现了另一个请求读取数据,从数据库读取未更新前的数据,并写到缓存中,那么这个时候就会出现 缓存与数据库双写不一致的问题了。怎么解决呢?
我自己的思路是:先删除缓存,然后更新数据库,更新完数据再删除缓存,以确保更新操作能将缓存正常删除。
具体实现上边写好了,接下来看看怎么测试,模拟出这种情况吧。
这个测试方法也会先将数据查出来,放到缓存中;和上边一样,在 highConcurrencyWithUpdate(..)
方法执行之前打个断点,方法里边也加个断点。
debug 运行 Chapter16ApplicationTests.testHighConcurrencyWithUpdate(..)
执行到 highConcurrencyWithUpdate(..)
方法前,我们用 Redis 图形化界面
看看数据
数据查出来并且已经放缓存中了,接下来放行,来到 highConcurrencyWithUpdate(..)
方法体中,用 Redis 图形化界面
将数据库 reload 一下看看结果,数据正常
这些和上边也一样,但是为了测试流程的完整性,还是贴出来吧,方便自己日后看。
来到这里之后,我们需要另起一个线程去查询数据,执行 Chapter16ApplicationTests.testSelectById()
用 Redis 图形化界面
将数据库 reload 一下
缓存又查出来了,数据正常,接下来,我们将 highConcurrencyWithUpdate(..)
的断点放行了,再用 Redis 图形化界面
将数据库 reload 一下看看结果
缓存数据正常删除了。这样在高并发的情况下更新数据也能保证 缓存与数据双写一致性 了。
参考资料
SpringBoot非官方教程 | 第十三篇:springboot集成spring cache
一起来学SpringBoot | 第十篇:使用Spring Cache集成Redis
总结
Cache 注解用法可参考上一篇
Redis 作为缓存实现,保证缓存与数据库双写一致性可以使用上边的方式,当然咯,不管是不是 Spring Cache 作为缓存门面,逻辑都一样:更新数据库前后都执行删除缓存操作即可。
源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter16