Spring优雅整合Redis缓存
“小明,多系统的session共享,怎么处理?”“Redis缓存啊!” “小明,我想实现一个简单的消息队列?”“Redis缓存啊!”
“小明,分布式锁这玩意有什么方案?”“Redis缓存啊!” “小明,公司系统响应如蜗牛,咋整?”“Redis缓存啊!”
本着研究的精神,我们来分析下小明的第四个问题。
个人原创文章,谢绝一切转载!
准备:
Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/
Dubbo2.7.5/Druid1.2.21/Zookeeper3.5.5/Mysql8.0.11/Vue2.5/Redis3.2
难度: 新手--战士--老兵--大师
目标:
- Spring优雅整合Redis做数据库缓存
步骤:
为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。源码地址:https://github.com/xiexiaobiao/vehicle-shop-admin
1 先说结论
Redis缓存不是金弹,若系统DB毫无压力,系统性能瓶颈不在DB上,不建议强加缓存层!
- 增加业务复杂度:同一缓存必须被全部相关方法所覆盖,如订单缓存,只要涉及到订单数据更新的方法都要进行缓存逻辑处理。
同时,KV存储时,因各方法返回的类型不同,这样就需要多个缓存池,但各方法后台的数据又存在关联,往往导致一个方法需
要处理关联的多个缓存,从而形成网状处理逻辑。
2. 存在并发问题:缓存没有锁机制,B线程进行DB更新,同时A线程请求数据,缓存中存在即返回,但B线程还未更新到缓存,导
致缓存与DB不一致;或者A线程B线程都进行DB更新,但写入缓存的顺序发生颠倒,也会导致缓存与DB不一致,请看官君想想如何解决;
3.内存消耗:小数据量可直接全部进内存,但海量数据不可能全部直接进入Redis,机器吃不消!可考虑只缓存DB数据索引,然后配合
“布隆过滤器”拦截无效请求,有效请求再去DB查询;
4. 缓存位置:缓存注解的方法,执行时序上应尽量靠近DB,远离前端,如放dao层,请看官君思考下为啥。
适用场景:1.确认DB为系统性能瓶颈,2.数据内容稳定,低频更新,高频查询,如历史订单数据;3.热点数据,如新上市商品;
2 步骤
2.1 原理
这里我说的是注解模式,有四个注解,SpringCache缓存原理即注解+拦截器 org.springframework.cache.interceptor.CacheInterceptor 对方法进行拦截处理:
@Cacheable:可标记在类或方法上。标记在类上则缓存该类所有方法的返回值。请求方法时,先在缓存进行key匹配,存在则直接取缓存数据并返回。主要参数表:

@CacheEvict:从缓存中移除相应数据。主要参数表:

@CachePut:方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,
而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。主要参数表:

@Caching: 多个Cache注解组合使用,比如新增用户时,同时要删除其他缓存,并更新用户信息缓存,即以上三个注解的集合。

2.2 编码
项目有五个微服务,我仅改造了customer服务模块:
引入依赖,build.gradle文件:

Redis配置项,resources/config/application-dev.yml文件:

文件: com.biao.shop.customer.conf.RedisConf
@Configuration
@EnableCaching
public class RedisConf { @Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
return RedisCacheManager.create(redisConnectionFactory);
} @Bean
public CacheManager cacheManager() {
// configure and return an implementation of Spring's CacheManager SPI
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
return cacheManager;
} @Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 设置key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化器,使用Jackson 2,将对象序列化为JSON
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
// json转对象类,不设置,默认的会将json转成hashmap
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
return redisTemplate;
}
}
以上代码解析:1.声明缓存管理器CacheManager,会创建一个切面(aspect)并触发Spring缓存注解的切点,根据类或者方法所使用的注解以及缓存的状态,
这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值 2. RedisTemplate即为Redis连接器,实际上即为jedis客户端。
文件: com.biao.shop.customer.impl.ShopClientServiceImpl
@org.springframework.stereotype.Service
@Slf4j
public class ShopClientServiceImpl extends ServiceImpl<ShopClientDao, ShopClientEntity> implements ShopClientService { private final Logger logger = LoggerFactory.getLogger(ShopClientServiceImpl.class); private ShopClientDao shopClientDao; @Autowired
public ShopClientServiceImpl(ShopClientDao shopClientDao){
this.shopClientDao = shopClientDao;
} @Override
public String getMaxClientUuId() {
return shopClientDao.selectList(new LambdaQueryWrapper<ShopClientEntity>()
.isNotNull(ShopClientEntity::getClientUuid).orderByDesc(ShopClientEntity::getClientUuid))
.stream().limit(1).collect(Collectors.toList())
.get(0).getClientUuid();
} @Override
@Caching(put = @CachePut(cacheNames = {"shopClient"},key = "#root.args[0].clientUuid"),
evict = @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true))
public int createClient(ShopClientEntity clientEntity) {
clientEntity.setGenerateDate(LocalDateTime.now());
return shopClientDao.insert(clientEntity);
} /** */
@Override
@CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
public int deleteBatchById(Collection<Integer> ids) {
logger.info("deleteBatchById 删除Redis缓存");
return shopClientDao.deleteBatchIds(ids);
} @Override
@CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
public int deleteById(int id) {
logger.info("deleteById 删除Redis缓存");
return shopClientDao.deleteById(id);
} @Override
@Caching(evict = {@CacheEvict(cacheNames = "shopClient",key = "#root.args[0]"),
@CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)})
public int deleteByUUid(String uuid) {
logger.info("deleteByUUid 删除Redis缓存");
QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
qw.eq(true,"uuid",uuid);
return shopClientDao.delete(qw);
} @Override
@Caching(put = @CachePut(cacheNames = "shopClient",key = "#root.args[0].clientUuid"),
evict = @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true))
public int updateClient(ShopClientEntity clientEntity) {
logger.info("updateClient 更新Redis缓存");
clientEntity.setModifyDate(LocalDateTime.now());
return shopClientDao.updateById(clientEntity);
} @Override
@CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
public int addPoint(String uuid,int pointToAdd) {
ShopClientEntity clientEntity = this.queryByUuId(uuid);
log.debug(clientEntity.toString());
clientEntity.setPoint(Objects.isNull(clientEntity.getPoint()) ? 0 : clientEntity.getPoint() + pointToAdd);
return shopClientDao.updateById(clientEntity);
} @Override
@Cacheable(cacheNames = "shopClient",key = "#root.args[0]")
public ShopClientEntity queryByUuId(String uuid) {
logger.info("queryByUuId 未使用Redis缓存");
QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
qw.eq(true,"client_uuid",uuid);
return shopClientDao.selectOne(qw);
} @Override
@Cacheable(cacheNames = "shopClientById",key = "#root.args[0]")
public ShopClientEntity queryById(int id) {
logger.info("queryById 未使用Redis缓存");
return shopClientDao.selectById(id);
} @Override
@Cacheable(cacheNames = "shopClientPage")
public PageInfo<ShopClientEntity> listClient(Integer current, Integer size, String clientUuid, String name,
String vehiclePlate, String phone) {
logger.info("listClient 未使用Redis缓存");
QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
Map<String,Object> map = new HashMap<>(4);
map.put("client_uuid",clientUuid);
map.put("vehicle_plate",vehiclePlate);
map.put("phone",phone);
// "name" 模糊匹配
boolean valid = Objects.isNull(name);
qw.allEq(true,map,false).like(!valid,"client_name",name);
PageHelper.startPage(current,size);
List<ShopClientEntity> clientEntities = shopClientDao.selectList(qw);
return PageInfo.of(clientEntities);
} // java Stream
@Override
@Cacheable(cacheNames = "shopClientPlateList")
public List<String> listPlate() {
logger.info("listPlate 未使用Redis缓存");
List<ShopClientEntity> clientEntities =
shopClientDao.selectList(new LambdaQueryWrapper<ShopClientEntity>().isNotNull(ShopClientEntity::getVehiclePlate));
return clientEntities.stream().map(ShopClientEntity::getVehiclePlate).collect(Collectors.toList());
} @Override
@Cacheable(cacheNames = "shopClientList",key = "#root.args[0].toString()")
public List<ShopClientEntity> listByClientDto(ClientQueryDTO clientQueryDTO) {
logger.info("listByClientDto 未使用Redis缓存");
QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
boolean phoneFlag = Objects.isNull(clientQueryDTO.getPhone());
boolean clientNameFlag = Objects.isNull(clientQueryDTO.getClientName());
boolean vehicleSeriesFlag = Objects.isNull(clientQueryDTO.getVehicleSeries());
boolean vehiclePlateFlag = Objects.isNull(clientQueryDTO.getVehiclePlate());
//如有null的条件直接不参与查询
qw.eq(!phoneFlag,"phone",clientQueryDTO.getPhone())
.like(!clientNameFlag,"client_name",clientQueryDTO.getClientName())
.like(!vehicleSeriesFlag,"vehicle_plate",clientQueryDTO.getVehiclePlate())
.like(!vehiclePlateFlag,"vehicle_series",clientQueryDTO.getVehicleSeries());
return shopClientDao.selectList(qw);
}
}
以上代码解析:
1. 因方法返回类型不同,故建立了5个缓存 2. 使用SpEL表达式#root.args[0]取得方法第一个参数,使用#result取得返回对象,
用于构造key 3. 对于@Cacheable不能使用#result返回对象做key值,如queryById(int id)方法,会导致NPE,,因为此注解将在方法执行前先
进入缓存匹配,而#result则是在方法执行后计算 4. @Caching注解可一次集合多个注解,如deleteByUUid(String uuid)方法,删除一个用户记录,
需同时进行更新shopClient,并清空其他几个缓存。
2.3 测试
运行起来整个项目,启动顺序:souladmin -> soulbootstrap -> zookeeper -> authority -> customer -> stock -> order -> business -> vue前端 ,
进入后端管理页: 按页浏览客户信息,分别点击页签:

可以看到缓存shopClientPage缓存了4项数据,key值即为方法的参数组合,再去点击页签,则系统后台无DB请求记录输出,说明直接使用了缓存:

编辑客户信息,我随意打开了两个:

可以看到缓存shopClientById增加了两个对象,再去点击编辑,则系统后台无DB查询记录输出,说明直接使用了缓存:

按条件查询客户:

可以看到缓存shopClientPage增加一项,因为key值不一样,故独立为一项缓存数据,多次点查询,则系统后台无DB查询SQL输出,说明直接使用了缓存:

新增客户:

可以看到shopClientPage缓存将会被清空,同时增加一个shopClient缓存的对象,即同时进行了多个缓存池操作:

问题解答:
前面说到的两个问题:
1.多线程问题,可配合DB事务机制,进行缓存延时双删,每次DB更新前,先删除缓存中对象,更新后,再去删除一次缓存中对象,
2.缓存方法位置问题,按照前端到后端的“倒金字塔模型”,越靠近前端,缓存数据对象被其他业务逻辑更新的可能性越大,靠近DB,能尽量保证每次DB的更新都能被缓存逻辑感知。
全文完!
我的其他文章:
只写原创,敬请关注

Spring优雅整合Redis缓存的更多相关文章
- SpringBoot入门系列(七)Spring Boot整合Redis缓存
前面介绍了Spring Boot 中的整合Mybatis并实现增删改查,.不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/category/ ...
- SpringBoot缓存管理(二) 整合Redis缓存实现
SpringBoot支持的缓存组件 在SpringBoot中,数据的缓存管理存储依赖于Spring框架中cache相关的org.springframework.cache.Cache和org.spri ...
- (转)spring boot整合redis
一篇写的更清晰的文章,包括redis序列化:http://makaidong.com/ncjava/330749_5285125.html 1.项目目录结构 2.引入所需jar包 <!-- Sp ...
- Spring Boot 结合 Redis 缓存
Redis官网: 中:http://www.redis.cn/ 外:https://redis.io/ redis下载和安装 Redis官方并没有提供Redis的Windows版本,这里使用微软提供的 ...
- Spring Boot自定义Redis缓存配置,保存value格式JSON字符串
Spring Boot自定义Redis缓存,保存格式JSON字符串 部分内容转自 https://blog.csdn.net/caojidasabi/article/details/83059642 ...
- Spring Boot 整合 Redis 和 JavaMailSender 实现邮箱注册功能
Spring Boot 整合 Redis 和 JavaMailSender 实现邮箱注册功能 开篇 现在的网站基本都有邮件注册功能,毕竟可以通过邮件定期的给用户发送一些 垃圾邮件 精选推荐
- Spring Boot 整合 Redis 实现缓存操作
摘要: 原创出处 www.bysocket.com 「泥瓦匠BYSocket 」欢迎转载,保留摘要,谢谢! 『 产品没有价值,开发团队再优秀也无济于事 – <启示录> 』 本文提纲 ...
- 【Spring】17、spring cache 与redis缓存整合
spring cache,基本能够满足一般应用对缓存的需求,但现实总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持 ...
- spring整合redis缓存,以注解(@Cacheable、@CachePut、@CacheEvict)形式使用
maven项目中在pom.xml中依赖2个jar包,其他的spring的jar包省略: <dependency> <groupId>redis.clients</grou ...
随机推荐
- IT培训的背后,是“韭菜”的躺赚梦!
作者:九歌 本文转自公众号:Hack 本文只谈"骗局",不谈其他,绝不引战,如有错误,希望指出我会及时改正 导语 为什么要写这篇文章呢,近些年培训这个话题也比较火,很多在看这篇文章 ...
- React Native 学习笔记--进阶(二)--动画
React Native 进阶(二)–动画 动画 流畅.有意义的动画对于移动应用用户体验来说是非常必要的.我们可以联合使用两个互补的系统:用于全局的布局动画LayoutAnimation,和用于创建更 ...
- RHEL系统下安装atlassian-jira-5
操作系统:RHEL 6.4 x86_64 Jira版本:atlassian-jira-5.2.11-x64.bin 安装路径:/opt/atlassian/jira/ 数据保存路径:/opt/atla ...
- Python2.7错误处理FileNotFoundError报错NameError: name 'FileNotFoundError' is not defined
错误信息如下: 原因是FileNotFoundError是python3.0中的写法,而Python2.7中应写为IOError.
- CSS——NO.6(盒模型)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- 读书笔记-《Mysql技术内幕》
MYSQL 技术内幕 Mysql体系 连接池组件 管理服务和工具 SQL接口 查询分析器 优化器 缓冲 插件式存储引擎 物理文件 存储引擎 InnoDB(默认引擎) 支持事务 行锁设计 多版本并发控制 ...
- iMX287A嵌入式Qt环境搭建
目录 1.嵌入式Qt简介 2.查看开发板Qt库的版本 3.第一个嵌入式Qt程序--Hello World 4.Linux桌面版本Qt环境的搭建 5.注意 @ 1.嵌入式Qt简介 Qt 是一个跨平台的应 ...
- C# 客户端内存优化分析
背景概述 C# 开发客户端系统的时候,.net 框架本身就比较消耗内存资源,特别是xp 这种老爷机内存配置不是很高的电脑上运行,所以就需要进行内存上的优化,才能流畅的在哪些低端电脑上运行. 想要对C# ...
- [面试专题]前端需要知道的web安全知识
前端需要知道的web安全知识 标签(空格分隔): 未分类 安全 [Doc] Crypto (加密) [Doc] TLS/SSL [Doc] HTTPS [Point] XSS [Point] CSRF ...
- 组件(4):使用slot进行内容分发
组件的作用域(一) 父组件模板的内容在父组件作用域内编译:子组件模板的内容在子组件作用域内编译. 父子组件的编译相互独立,编译时只能使用各自作用域中的属性和方法,例如,你不可以在父组件模板内,将一个指 ...