spring优雅整合redis缓存

甲由崽      2022-05-07     248

关键词:

 

“小明,多系统的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

难度: 新手--战士--老兵--大师

目标:

  1. Spring优雅整合Redis做数据库缓存

步骤:

为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。源码地址:https://github.com/xiexiaobiao/vehicle-shop-admin

1 先说结论

Redis缓存不是金弹,若系统DB毫无压力,系统性能瓶颈不在DB上,不建议强加缓存层!

  1. 增加业务复杂度:同一缓存必须被全部相关方法所覆盖,如订单缓存,只要涉及到订单数据更新的方法都要进行缓存逻辑处理。

    同时,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的更新都能被缓存逻辑感知。

全文完!


我的其他文章:

1 SOFARPC模式下的Consul注册中心

八种控制线程顺序的方法

移动应用APP购物车(店铺系列二)

H5开发移动应用APP(店铺系列一)

阿里云平台OSS对象存储

 

只写原创,敬请关注 

 

redis整合spring结合使用缓存实例(转)

...net/evankaka     摘要:本文介绍了如何在Spring中配置redis,并通过Spring中AOP的思想,将缓存的方法切入到有需要进入缓存的类或方法前面。一、Redis介绍什么是Redis?&nb 查看详情

redis入门很简单之五jedis和spring的整合

Redis入门很简单之五【Jedis和Spring的整合】博客分类: NoSQL/Redis/MongoDBredisnosql缓存jedisspring 在上一篇文章中,简单介绍了Jedis的连接池使用方式。如果和Spring进行整合的话,我们将获得更好的简洁性、灵活性,显然是一种... 查看详情

Spring Cache with Redis - 如果与 Redis 的连接失败,如何优雅地处理甚至跳过缓存

】SpringCachewithRedis-如果与Redis的连接失败,如何优雅地处理甚至跳过缓存【英文标题】:SpringCachewithRedis-HowtogracefullyhandleorevenskipCachingincaseofConnectionFailuretoRedis【发布时间】:2015-02-2618:14:39【问题描述】:我已在我的Spring应用程序... 查看详情

springboot整合spring@cache和redis(代码片段)

转载请注明出处:https://www.cnblogs.com/wenjunwei/p/10779450.htmlspring基于注解的缓存对于缓存声明,spring的缓存提供了一组java注解:@Cacheable:触发缓存写入。@CacheEvict:触发缓存清除。@CachePut:更新缓存(不会影响到方法的运行)。@Caching:重新... 查看详情

spring整合redis做数据缓存(windows环境)

当我们一个项目的数据量很大的时候,就需要做一些缓存机制来减轻数据库的压力,提升应用程序的性能,对于java项目来说,最常用的缓存组件有Redis、Ehcache和Memcached。Ehcache是用java开发的缓存组件,和java结合良好,直接在jvm... 查看详情

spring整合redis客户端及缓存接口设计

一、写在前面缓存作为系统性能优化的一大杀手锏,几乎在每个系统或多或少的用到缓存。有的使用本地内存作为缓存,有的使用本地硬盘作为缓存,有的使用缓存服务器。但是无论使用哪种缓存,接口中的方法... 查看详情

spring整合redis简单使用

1.简单介绍redis是基于C语言开发。redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sortedset--有序集合)和hash(哈希类型)。redis是一个缓存数据库(片面的... 查看详情

精通springboot--整合redis实现缓存

今天我们来讲讲怎么在springboot中整合redis实现对数据库查询结果的缓存。首先第一步要做的就是在pom.xml文件添加spring-boot-starter-data-redis。要整合缓存,必不可少的就是我们要继承一个父类CachingConfigurerSupport。我们先看看这个类... 查看详情

springboot整合springseesion实现redis缓存

参考技术A使用SpringBoot开发项目时我们经常需要存储Session,因为Session中会存一些用户信息或者登录信息。传统的web服务是将session存储在内存中的,一旦服务挂了,session也就消失了,这时候我们就需要将session存储起来,而Redis就... 查看详情

springboot整合redis缓存

使用springBoot添加redis缓存需要在POM文件里引入<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency><dependency 查看详情

sping整合redis,以及做mybatis的第三方缓存

一、spring整合redisRedis作为一个时下非常流行的NOSQL语言,不学一下有点过意不去。背景:学习Redis用到的框架是maven+spring+mybatis(框架如何搭建这边就不叙述了)首先在pom里面添加当前所需要的jar包,有下面几个:………………... 查看详情

guava和spring整合使用(这里只用到了缓存)

...e.CacheBuilder;2importcom.google.common.cache.CacheBuilderSpec;34importorg.springframework.cache 查看详情

32springboot——缓存之整合redis

springboot缓存默认使用ConcurrentMapCacheManager 将数据保存在下面的Map中1、docker中开启Redis 2、添加Redis相关依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-st 查看详情

springboot整合redis实现缓存

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>1.5.2.RELEASE</version></dependency& 查看详情

springboot整合redis做简单缓存(代码片段)

...依赖<!--引入redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--使用fastjson做序列化和反序列化--><dependency><groupId>com.alibaba</gr... 查看详情

springboot整合mybatis,redis,代码

一说明这是spring整合redis注解开发的系类:二正文在注解开发时候,会有这几个注解需要注意:具体含义:  [email protected]可以标记在方法上,也可以标记在类上。当标记在方法上时表示该方法是支持缓存的,当标记在类上时则... 查看详情

spring整合redis后怎么更改db

...化到磁盘中!本人有通过redis的hash数据类型来做过购物车spring配置文件spring-redis.xml<beanid="jedisPoolConfig"class="redis.clients.jedis.JedisPoolConfig"><propertyname="maxIdle"value="6"></property><propertyname=&qu... 查看详情

springboot整合redis,一篇解决缓存的所有问题(代码片段)

前言上一篇博文,我们重点介绍了SpringBoot如何整合Mybatis,JPA等技术,访问我们的关系型数据库,这篇博文我们介绍SpringBoot如何整合Redis来访问非关系型数据库,带你深入了解Redis的自动原理,并结合具体... 查看详情