美文网首页DatabaseSpringBoot
SpringBoot整合Redis与Cache与实现

SpringBoot整合Redis与Cache与实现

作者: maxzhao_ | 来源:发表于2019-06-19 11:44 被阅读5次

    Redis 简介

    GitHub 地址:https://github.com/antirez/redis

    GitHub 介绍:Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, HyperLogLogs, Bitmaps.

    对于缓存

    • 内存的速度远远大于硬盘的速度
    • 缓存主要是在获取资源方便性能优化的关键方面
    • Redis 是缓存数据库
    • 缓存未命中解决与防止缓存击穿

    缓存更新策略

    1. Cache aside :

      • 思路:先更新数据库,在更新缓存。

      • 问题:一个读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放到缓存,所以,会造成脏数据。

      • 出现此问题的前提:读缓存时缓存失效,而且并发着有一个写操作。

      • 而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

    2. Read through

    • 思路:在查询操作中更新缓存
    1. Write through
      • 思路:有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)
    2. Write behind caching
      • 思路:只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。
      • 实现有点复杂,具体参考《缓存更新的套路》

    Redis 实践(复杂缓存)

    配置application.yml

    spring:
      cache:
        type: REDIS
        redis:
          cache-null-values: false
          time-to-live: 600000ms
          use-key-prefix: true
          #缓存名称列表
        cache-names: userCache,allUsersCache
      redis:
        host: 127.0.0.1
        port: 6379
        database: 0
        # 单通道
        lettuce:
          shutdown-timeout: 200ms
          pool:
            max-active: 7
            max-idle: 7
            min-idle: 2
            max-wait: -1ms
        timeout: 1000
    
    

    对应的配置类:org.springframework.boot.autoconfigure.data.redis.RedisProperties

    添加配置类

    这里自定义RedisTemplate的配置类,主要是想使用Jackson替换默认的序列化机制:

    @Configuration
    public class RedisConfig {
        /**
         * redisTemplate 默认使用JDK的序列化机制, 存储二进制字节码, 所以自定义序列化类
         * @param redisConnectionFactory redis连接工厂类
         * @return RedisTemplate
         */
        @Bean
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
    
            // 使用Jackson2JsonRedisSerialize 替换默认序列化
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    
            // 设置value的序列化规则和 key的序列化规则
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    }
    

    使用Cache aside策略的实例

    这里只展示使用服务

    @Service(value = "appUserService")
    public class AppUserServiceImpl implements AppUserService {
    
        @Resource(name = "appUserRepository")
        private AppUserRepository appUserRepository;
        
        @Resource
        private RedisTemplate<String, User> redisTemplate;
        
        /**
        * 不做任何操作
        * @param appUser 用户
        **/
        @Override
        public AppUser saveOne(AppUser appUser) {
            return appUserRepository.save(appUser);
        }
        /**
         * 获取用户信息
         * 如果缓存存在,从缓存中获取城市信息
         * 如果缓存不存在,从 DB 中获取城市信息,然后插入缓存
         *
         * @param loginName 用户登录名
         * @return 用户
         */
        @Override
        public AppUser findByLoginName(String loginName) {
            ogger.info("获取用户start...");
            // 从缓存中获取用户信息
            String key = "AppUser:" + loginName;
            ValueOperations<String, User> operations = redisTemplate.opsForValue();
    
            // 缓存存在
            boolean hasKey = redisTemplate.hasKey(key);
            if (hasKey) {
                AppUser user = operations.get(key);
                logger.info("从缓存中获取了用户 AppUser = " + loginName);
                return user;
            }
            // 缓存不存在,从 DB 中获取
            List<AppUser> appUserList = appUserRepository.findByLoginNameEquals(loginName); 
            // 插入缓存
            if(appUserList.size() > 0){
                operations.set(key, appUserList.get(0), 10, TimeUnit.SECONDS);
            }
            return appUserList.size() > 0 ? appUserList.get(0) : null;
        }
        /**
         * 更新用户
         * 如果缓存存在,删除
         * 如果缓存不存在,不操作
         *
         * @param user 用户
         */
        public void updateUser(AppUser user) {
            logger.info("更新用户start...");
            appUserRepository.save(user);
            // 缓存存在,删除缓存
            String key = "AppUser:" + user.getLoginName();
            boolean hasKey = redisTemplate.hasKey(key);
            if (hasKey) {
                redisTemplate.delete(key);
                logger.info("更新用户时候,从缓存中删除用户 >> " + user.getLoginName());
            }
        }
        /**
         * 删除用户
         * 如果缓存中存在,删除
         */
        public void deleteById(Long id) {
            logger.info("删除用户start...");
            AppUser user = appUserRepository.get(id);
            appUserRepository.deleteById(id);
    
            // 缓存存在,删除缓存
            String key = "AppUser:" + user.getLoginName();
            boolean hasKey = redisTemplate.hasKey(key);
            if (hasKey) {
                redisTemplate.delete(key);
                logger.info("删除用户时候,从缓存中删除用户 >> " + user.getLoginName());
            }
        }
    }
    

    Redis + Cache 实践(简单缓存)

    Spring缓存支持

    Spring定义了org.springframework.cache.CacheManagerorg.springframework.cache.Cache 接口来统一不同缓存技术。 其中CacheManager是Spring提供的各种缓存技术抽象接口,内部使用Cache接口进行缓存的增删改查操作,我们一般不会直接和Cache打交道。

    针对不同的缓存技术,Spring有不同的CacheManager实现类,定义如下表:

    CacheManager 描述
    SimpleCacheManager 使用简单的Collection存储缓存数据,用来做测试用
    ConcurrentMapCacheManager 使用ConcurrentMap存储缓存数据
    EhCacheCacheManager 使用EhCache作为缓存技术
    GuavaCacheManager 使用Google Guava的GuavaCache作为缓存技术
    JCacheCacheManager 使用JCache(JSR-107)标准的实现作为缓存技术,比如Apache Commons JCS
    RedisCacheManager 使用Redis作为缓存技术

    在我们使用任意一个实现的CacheManager的时候,需要注册实现Bean:

    /**
     * EhCache的配置
     */
    @Bean
    public EhCacheCacheManager cacheManager(CacheManager cacheManager) {
        return new EhCacheCacheManager(cacheManager);
    }
    

    声明式缓存注解

    Spring提供4个注解来声明缓存规则,如下表所示:

    注解 说明
    @Cacheable 方法执行前先看缓存中是否有数据,如果有直接返回。如果没有就调用方法,并将方法返回值放入缓存
    @CachePut 无论怎样都会执行方法,并将方法返回值放入缓存
    @CacheEvict 将数据从缓存中删除
    @Caching 可通过此注解组合多个注解策略在一个方法上面

    @Cacheable 、@CachePut 、@CacheEvict都有value属性,指定要使用的缓存名称,而key属性指定缓存中存储的键。

    @EnableCaching 开启缓存。

    @Cacheable

    这个注解含义是方法结果会被放入缓存,并且一旦缓存后,下一次调用此方法,会通过key去查找缓存是否存在,如果存在就直接取缓存值,不再执行方法。

    这个注解有几个参数值,定义如下

    参数 解释
    cacheNames 缓存名称
    value 缓存名称的别名
    condition Spring SpEL 表达式,用来确定是否缓存
    key SpEL 表达式,用来动态计算key
    keyGenerator Bean 名字,用来自定义key生成算法,跟key不能同时用
    unless SpEL 表达式,用来否决缓存,作用跟condition相反
    sync 多线程同时访问时候进行同步

    在计算key、condition或者unless的值得时候,可以使用到以下的特有的SpEL表达式

    表达式 解释
    #result 表示方法的返回结果
    #root.method 当前方法
    #root.target 目标对象
    #root.caches 被影响到的缓存列表
    #root.methodName 方法名称简称
    #root.targetClass 目标类
    #root.args[x] 方法的第x个参数

    @CachePut

    该注解在执行完方法后会触发一次缓存put操作,参数跟@Cacheable一致

    @CacheEvict

    该注解在执行完方法后会触发一次缓存evict操作,参数除了@Cacheable里的外,还有个特殊的allEntries, 表示将清空缓存中所有的值。

    缓存注解使用

    在service中定义增删改的几个常见方法,通过注解实现缓存:

    @Service
    @Transactional
    public class UserService {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
        @Resource
        private AppuserRepository appuserRepository;
    
        /**
         * cacheNames 设置缓存的值
         * key:指定缓存的key,这是指参数id值。key可以使用spEl表达式
         *
         * @param id
         * @return
         */
        @Cacheable(value = "userCache", key = "#id", unless="#result == null")
        public AppUser getById(int id) {
            logger.info("获取用户start...");
            return appuserRepository.selectById(id);
        }
    
        @Cacheable(value = "allUsersCache", unless = "#result.size() == 0")
        public List<User> getAllUsers() {
            logger.info("获取所有用户列表");
            return appuserRepository.findByLoginNameEquals(null);
        }
    
        /**
         * 创建用户,同时使用新的返回值的替换缓存中的值
         * 创建用户后会将allUsersCache缓存全部清空
         */
        @Caching(
                put = {@CachePut(value = "userCache", key = "#user.id")},
                evict = {@CacheEvict(value = "allUsersCache", allEntries = true)}
        )
        public AppUser createUser(AppUser user) {
            logger.info("创建用户start..., user.id=" + user.getId());
            appuserRepository.save(user);
            return user;
        }
    
        /**
         * 更新用户,同时使用新的返回值的替换缓存中的值
         * 更新用户后会将allUsersCache缓存全部清空
         */
        @Caching(
                put = {@CachePut(value = "userCache", key = "#user.id")},
                evict = {@CacheEvict(value = "allUsersCache", allEntries = true)}
        )
        public AppUser updateUser(Appuser user) {
            logger.info("更新用户start...");
            appuserRepository.save(user);
            return user;
        }
    
        /**
         * 对符合key条件的记录从缓存中移除
         * 删除用户后会将allUsersCache缓存全部清空
         */
        @Caching(
                evict = {
                        @CacheEvict(value = "userCache", key = "#id"),
                        @CacheEvict(value = "allUsersCache", allEntries = true)
                }
        )
        public void deleteById(int id) {
            logger.info("删除用户start...");
            appuserRepository.deleteById(id);
        }
    
    }
    

    缓存配置类

    @Configuration
    @EnableCaching
    public class RedisCacheConfig {
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Autowired
        private Environment env;
    
        @Bean
        public LettuceConnectionFactory redisConnectionFactory() {
            RedisStandaloneConfiguration redisConf = new RedisStandaloneConfiguration();
            redisConf.setHostName(env.getProperty("spring.redis.host"));
            redisConf.setPort(Integer.parseInt(env.getProperty("spring.redis.port")));
            redisConf.setPassword(RedisPassword.of(env.getProperty("spring.redis.password")));
            return new LettuceConnectionFactory(redisConf);
        }
    
        @Bean
        public RedisCacheConfiguration cacheConfiguration() {
            RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(600))
                    .disableCachingNullValues();
            return cacheConfig;
        }
    
        @Bean
        public RedisCacheManager cacheManager() {
            RedisCacheManager rcm = RedisCacheManager.builder(redisConnectionFactory())
                    .cacheDefaults(cacheConfiguration())
                    .transactionAware()
                    .build();
            return rcm;
        }
    }
    

    keyGenerator 自定义key

    一般来讲我们使用key属性就可以满足大部分要求,但是如果你还想更好的自定义key,可以实现keyGenerator。

    这个属性为定义key生成的类,和key属性不能同时存在。

    RedisCacheConfig配置类中添加我自定义的KeyGenerator:

    /**
     * 自定义缓存key的生成类实现
     */
    @Bean(name = "myKeyGenerator")
    public KeyGenerator myKeyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object o, Method method, Object... params) {
                logger.info("自定义缓存,使用第一参数作为缓存key,params = " + Arrays.toString(params));
                // 仅仅用于测试,实际不可能这么写
                return params[0];
            }
        };
    }
    

    切换缓存技术

    得益于SpringBoot的自动配置机制,切换缓存技术除了替换相关maven依赖包和配置Bean外,使用方式和实例中一样, 不需要修改业务代码。如果你要切换到其他缓存技术非常简单。

    EhCache

    当我们需要使用EhCache作为缓存技术的时候,只需要在pom.xml中添加EhCache的依赖:

    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcahe</artifactId>
    </dependency>
    

    EhCache的配置文件ehcache.xml只需要放到类路径下面,SpringBoot会自动扫描,例如:

    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
             updateCheck="false" monitoring="autodetect"
             dynamicConfig="true">
    
        <diskStore path="java.io.tmpdir/ehcache"/>
    
        <defaultCache
                maxElementsInMemory="50000"
                eternal="false"
                timeToIdleSeconds="3600"
                timeToLiveSeconds="3600"
                overflowToDisk="true"
                diskPersistent="false"
                diskExpiryThreadIntervalSeconds="120"
        />
    
        <cache name="authorizationCache"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="3600"
               timeToLiveSeconds="3600"
               overflowToDisk="false"
               statistics="true">
        </cache>
    </ehcache>
    

    SpringBoot会为我们自动配置EhCacheCacheManager这个Bean,不过你也可以自己定义。

    Guava

    当我们需要Guava作为缓存技术的时候,只需要在pom.xml中增加Guava的依赖即可:

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>18.0</version>
    </dependency>
    

    SpringBoot会为我们自动配置GuavaCacheManager这个Bean。

    Redis

    最后还提一点,本篇采用Redis作为缓存技术,添加了依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    SpringBoot会为我们自动配置RedisCacheManager这个Bean,同时还会配置RedisTemplate这个Bean。 后面这个Bean就是下一篇要讲解的操作Redis数据库用,这个就比单纯注解缓存强大和灵活的多了。

    参考文章

    Spring Boot Redis Cache

    SpringBoot系列 - 缓存

    本文地址:

    SpringBoot整合Redis与Cache与实现

    推荐

    SpringBoot整合Redis及Redis简介和操作

    相关文章

      网友评论

        本文标题:SpringBoot整合Redis与Cache与实现

        本文链接:https://www.haomeiwen.com/subject/zuvhqctx.html