Spring优雅整合Redis缓存

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介:

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存储时,因各方法返回的类型不同,这样就需要多个缓存池,但各方法后台的数据又存在关联,往往导致一个方法需

要处理关联的多个缓存,从而形成网状处理逻辑。

  1. 存在并发问题:缓存没有锁机制,B线程进行DB更新,同时A线程请求数据,缓存中存在即返回,但B线程还未更新到缓存,导

致缓存与DB不一致;或者A线程B线程都进行DB更新,但写入缓存的顺序发生颠倒,也会导致缓存与DB不一致,请看官君想想如何解决;

3.内存消耗:小数据量可直接全部进内存,但海量数据不可能全部直接进入Redis,机器吃不消!可考虑只缓存DB数据索引,然后配合

“布隆过滤器”拦截无效请求,有效请求再去DB查询;

  1. 缓存位置:缓存注解的方法,执行时序上应尽量靠近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 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的更新都能被缓存逻辑感知。

全文完!

原文地址https://www.cnblogs.com/xxbiao/p/12593525.html

相关实践学习
基于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
相关文章
|
26天前
|
存储 XML 缓存
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache功能的开发实战指南(一)
【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache功能的开发实战指南
60 0
|
1天前
|
存储 缓存 运维
软件体系结构 - 缓存技术(5)Redis Cluster
【4月更文挑战第20天】软件体系结构 - 缓存技术(5)Redis Cluster
29 10
|
8天前
|
缓存 NoSQL Java
使用Redis进行Java缓存策略设计
【4月更文挑战第16天】在高并发Java应用中,Redis作为缓存中间件提升性能。本文探讨如何使用Redis设计缓存策略。Redis是开源内存数据结构存储系统,支持多种数据结构。Java中常用Redis客户端有Jedis和Lettuce。缓存设计遵循一致性、失效、雪崩、穿透和预热原则。常见缓存模式包括Cache-Aside、Read-Through、Write-Through和Write-Behind。示例展示了使用Jedis实现Cache-Aside模式。优化策略包括分布式锁、缓存预热、随机过期时间、限流和降级,以应对缓存挑战。
|
16天前
|
存储 缓存 NoSQL
使用redis进行缓存加速
使用redis进行缓存加速
26 0
|
17天前
|
存储 缓存 NoSQL
Java手撸一个缓存类似Redis
`LocalExpiringCache`是Java实现的一个本地缓存类,使用ConcurrentHashMap存储键值对,并通过ScheduledExecutorService定时清理过期的缓存项。类中包含`put`、`get`、`remove`等方法操作缓存,并有`clearCache`方法来清除过期的缓存条目。初始化时,会注册一个定时任务,每500毫秒检查并清理一次过期缓存。单例模式确保了类的唯一实例。
13 0
|
1月前
|
缓存 NoSQL Java
spring cache整合redis实现springboot项目中的缓存功能
spring cache整合redis实现springboot项目中的缓存功能
46 1
|
1月前
|
缓存 NoSQL 安全
【Redis】缓存穿透
【Redis】缓存穿透
30 0
|
1月前
|
缓存 监控 NoSQL
解析Redis缓存雪崩及应对策略
解析Redis缓存雪崩及应对策略
|
1月前
|
存储 缓存 NoSQL
Redis高效缓存:加速应用性能的利器
Redis高效缓存:加速应用性能的利器
|
1月前
|
存储 缓存 Java
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践
【Spring原理高级进阶】有Redis为啥不用?深入剖析 Spring Cache:缓存的工作原理、缓存注解的使用方法与最佳实践