SpringBoot整合Redis与Cache与实现

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简介和操作

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258