Spring Cloud集成Spring Data Redis

Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。

Redis 是一个高性能的 key-value 数据库,同时支持多种存储类型,包括 String(字符串)、List(链表)、Set(集合)、Zset(sorted set——有序集合)和 Hash(哈希类型)。

用 Redistemplate 操作 Redis

Java 中操作 Redis 我们可以用 Jedis,也可以用 Spring Data Redis。

本节我们基于 Spring Data Redis 操作 Redis,Spring Data Redis 也是基于 Jedis 来实现的,它在 Jedis 上面封装了一层,让我们操作 Redis 更加简单。

关于在 Spring Boot 中集成 Spring Data Redis 不做过多讲解,本节主要讲怎么用 Redistemplate 操作 Redis。

Redistemplate 是一个泛型类,可以指定 Key 和 Value 的类型,我们以字符串操作来讲解,可以直接用 StringRedisTemplate 来操作。

在使用的类中直接注入 StringRedisTemplate 即可,如下代码所示。

@Autowired
private StringRedisTemplate stringRedisTemplate;

StringRedisTemplate 中提供了很多方法来操作数据,主要有以下几种:
  • opsForValue:操作 Key Value 类型
  • opsForHash:操作 Hash 类型
  • opsForList:操作 List 类型
  • opsForSet:操作 Set 类型
  • opsForZSet:操作 opsForZSet 类型

下面我们以 Key Value 类型来讲解,设置一个缓存,缓存时间为 1 小时,如下代码所示。

stringRedisTemplate.opsForValue().set("key", "C语言中文网", 1, TimeUnit.HOURS);

获取缓存,如下代码所示。

String value = stringRedisTemplate.opsForValue().get("key");

删除缓存,如下代码所示。

stringRedisTemplate.delete("key");

判断一个 key 是否存在,如下代码所示。

boolean exists = stringRedisTemplate.hasKey("key");

如果你不喜欢用这些封装好的方法,想要用最底层的方法来操作也是可以的。通过 StringRedisTemplate 可以拿到 RedisConnection,如下代码所示。

RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();

用 Repository 操作 Redis

凡是 Spring Data 系列的框架,都是一种风格,我们都可以用 Repository 方式来操作数据。下面我们看下怎么使用 Repository。

定义一个数据存储的实体类,@Id 类似于数据库中的主键,能够自动生成,RedisHash 是 Hash 的名称,相当于数据库的表名,如下代码所示。
@Data
@RedisHash("persons")
public class Person {
    @Id
    String id;
    String firstname;
    String lastname;
}
定义 Repository 接口,代码如下所示。
public interface PersonRepository extends CrudRepository<Person, String> {

}
使用接口对数据进行增删改查操作,代码如下所示。
@Autowired
PersonRepository repo;
public void basicCrudOperations() {
    Person person = new Person("张三", "zhangsan");
    repo.save(person);
    repo.findOne(person.getId());
    repo.count();
    repo.delete(person);
}
数据保存到 Redis 中会变成两部分,一部分是一个 set,里面存储的是所有数据的 ID 值,另一部分是一个 Hash,存储的是具体每条数据。

Spring Cache缓存数据

一般的缓存逻辑都是下面代码这样的方式,首先判断缓存中是否有数据,有就获取数据返回,没有就从数据库中查数据,然后缓存进去,再返回。
public Person get(String id) {
    Person person = repo.findOne(id);
    if (person != null) {
        return person;
    }
    person = dao.findById(id);
    repo.save(person);
    return person;
}
首先这种方式在逻辑上是肯定没有问题的,大部分人也都是这么用的,不过当这种代码充满整个项目的时候,看起来就非常别扭了,感觉有点多余,不过通过 Spring Cache 就能解决这个问题。我们不需要关心缓存的逻辑,只需要关注从数据库中查询数据,将缓存的逻辑交给框架来实现。

Spring Cache 利用注解方式来实现数据的缓存,还具备相当的灵活性,能够使用 SpEL(Spring Expression Language)来定义缓存的 key,还能定义多种条件判断。

Spring Cache 的注解定义在 spring-context 包中,如图 1 所示。

Spring Cache注解
图 1  Spring Cache注解

常用的注解有 @Cacheable、@CachePut、@CacheEvict。
  • @Cacheable:用于查询的时候缓存数据。
  • @CachePut:用于对数据修改的时候修改缓存中的数据。
  • @CacheEvict:用于对数据删除的时候清除缓存中的数据。

首先我们配置一下 Redistemplate,设置下序列化方式为 JSON,这样存在于 Redis 中的数据查看起来就比较方便了,如下代码所示。
@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.afterPropertiesSet();
        setSerializer(redisTemplate);
        return redisTemplate;
    }

    private void setSerializer(RedisTemplate<String, String> template) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(jackson2JsonRedisSerializer);
    }
}
除了 Json 序列化,还有很多其他的序列化方式,读者可以根据自己的需求来设置,序列化的类在 Spring-Data-Redis 包中,如图 2 所示。

Spring Cache序列化
图 2  Spring Cache序列化

除了配置序列化方式,我们还可以配置 CacheManager 来设置缓存的过期时间,代码如下所示。
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofDays(1)).disableCachingNullValues()
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
}
还可以配置缓存 Key 的自动生成方式,这里是用类名+方法名+参数来生成缓存的 Key,只有这样才能让 Key 具有唯一性,当然你也可以使用默认的 org.springframework.cache.interceptor.SimpleKeyGenerator,代码如下所示。
@Bean
public KeyGenerator keyGenerator() {
    return new KeyGenerator() {
        @Override
        public Object generate(Object target, Method method, Object... params) {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(":" + method.getName());
            for (Object obj : params) {
                sb.append(":" + obj.toString());
            }
            return sb.toString();
        }
    };
}
接下来,我们改造一下上面定义的get方法,用注解的方式来使用缓存,具体代码如下所示。
@Cacheable(value="get", key="#id")
public Person get(String id) {
    return findById(id);
}
@Cacheable 中的 value 我们可以定义成与方法名称一样。标识这个方法的缓存 key,会在 Redis 中存储一个 Zset,Zset 的 key 就是我们定义的 value 的值,Zset 中会存储具体的每个缓存的 key,也就是当调用 get("1001") 的时候,Zset 中就会存储 1001,同时 Redis 中会有一个单独的 String 类型的数据,Key 是 1001。

#id 是 SpEL 的语法,通过参数名来定义缓存 key,上面的 key 如果直接用参数 id 来定义的话会出问题的,比如当另一个缓存方法的参数也是 1001 的时候,这个 key 就会冲突。所以我们最好还是定义一个唯一的前缀,然后再加上参数,这样就不会有冲突了,比如:key="'get'+#id"。

除了用 SpEL 来自定义key,我们刚刚其实配置了一个 keyGenerator,它就是用来生成 key 的,使用方法代码如下所示。
@Cacheable(value = "get", keyGenerator = "keyGenerator")
public Person get(String id) {
    return findById(id);
}
指定 keyGenerator 后,就可以不用配置 key,通过 keyGenerator 会自动生成缓存的 key,生成的规则就是我们配置中自己定义的,我们可以看到使用我们自定义的 keyGenerator 之后,存储在 Redis 中的数据的 key 是很长的一个字符串,规则也就是我们自定义的类名+方法名+参数。

@CachePut、@CacheEvict 的用法和 @Cacheable 一样,本节不做演示。

缓存异常处理

在用注解进行自动缓存的过程中,包括 Redis 的链接都是框架自动完成的,在缓存过程中如果 Redis 连接不上出现了异常,这时候整个请求都将失败。

缓存是一种辅助的手段,就算不能用,也不能影响正常的业务逻辑,如果我们直接用 Redis 的连接或者 Redistemplate 来操作的话可以通过异常捕获来解决这个问题,在用注解进行自动缓存的时候我们需要定义异常处理类来对异常进行处理。
@Configuration
public class CacheAutoConfiguration extends CachingConfigurerSupport {
    private Logger logger = LoggerFactory.getLogger(CacheAutoConfiguration.class);

    /**
     * redis 数据操作异常处理, 这里的处理:在日志中打印出错误信息, 但是放行
     * 保证 redis 服务器出现连接等问题的时候不影响程序的正常运行, 使得能够出问题时不用缓存 , 继续执行业务逻辑去查询 DB
     *
     * @return
     */
    @Bean
    public CacheErrorHandler errorHandler() {
        CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key, Object value) {
                logger.error("redis 异常:key=[{}]", key, e);
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
                logger.error("redis 异常:key=[{}]", key, e);
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                logger.error("redis 异常:", e);
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                logger.error("redis 异常:", e);
            };
            return cacheErrorHandler;
        }
    }
}
通过上面的处理,即使 Redis 挂掉了,程序连接不上时也不会影响业务功能,而是会继续执行查询数据库的操作。

自定义缓存工具类

在上面代码中,我们首先判断缓存中是否有数据,如果没有就从数据库中查询,然后再缓存进行,这样操作比较麻烦,还得写判断。使用注解的方式相对来说就简单多了,如果你不想用注解的方式,还是想在代码层面自己做缓存控制,那么我们可以自己封装一个工具类来避免写大量的判断操作。笔者以字符串操作为例,不涉及反序列化为对象的封装,封装缓存的基本操作,读者可以自行扩展。

首先定义一个缓存操作的接口,提供获取缓存,删除缓存操作,具体代码如下所示。
public interface CacheService {
    /**
     * 设置缓存
     *
     * @param key      缓存 KEY
     * @param value    缓存值
     * @param timeout  缓存过期时间
     * @param timeUnit 缓存过期时间单位
     */
    public void setCache(String key, String value, long timeout, TimeUnit timeUnit);

    /**
     * 获取缓存
     *
     * @param key 缓存KEY
     * @return
     */
    public String getCache(String key);

    public <V, K> String getCache(K key, Closure<V, K> closure);

    public <V, K> String getCache(K key, Closure<V, K> closure, long timeout, TimeUnit timeUnit);

    /**
     * 删除缓存
     *
     * @param key缓存KEY
     */
    public void deleteCache(String key);
}
实现类代码如下所示。
@Service
public class CacheServiceImpl implements CacheService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private long timeout = 1L;

    private TimeUnit timeUnit = TimeUnit.HOURS;

    @Override
    public void setCache(String key, String value, long timeout, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    @Override
    public String getCache(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    @Override
    public void deleteCache(String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    public <V, K> String getCache(K key, Closure<V, K> closure) {
        return doGetCache(key, closure, this.timeout, this.timeUnit);
    }

    @Override
    public <V, K> String getCache(K key, Closure<V, K> closure, long timeout, TimeUnit timeUnit) {
        return doGetCache(key, closure, timeout, timeUnit);
    }

    private <K, V> String doGetCache(K key, Closure<V, K> closure, long timeout, TimeUnit timeUnit) {
        String ret = getCache(key.toString());
        if (ret == null) {
            Object r = closure.execute(key);
            setCache(key.toString(), r.toString(), timeout, timeUnit);
            return r.toString();
        }
        return ret;
    }
}
定义一个方法回调的接口,用于执行回调的业务逻辑。代码如下所示。

public interface Closure<O, I> {
    public O execute(I input);
}

下面我们对这个缓存操作类的代码进行讲解。简单的 set 和 get 缓存不做过多讲解,因为和直接使用 RedisTemplate 没什么区别,本节主要讲的是基于方法的回调实现缓存。

我们的目的也很简单,就是先去判断缓存中是否存在,不存在的话则从数据库查询,然后将插入到缓存中的逻辑统一,不用每个方法中都去写这个判断的逻辑。

方法的回调实现是基于 Closure 接口来做的,使用方法代码如下所示。
@Autowired
private CacheService cacheService;

public String get() {
    String cacheKey = "1001";
    return cacheService.getCache(cacheKey, new Closure<String, String>() {
        @Override
        public String execute(String id) {
            // 执行你的业务逻辑
            return userService.getById(id);
        }
    });
}
通过这样的封装,我们就不用在每个缓存的地方都去判断了,当缓存中有值的时候,getCache 方法会根据缓存的 key 去缓存中获取,然后返回;如果没有值的话会执行 execute 中的逻辑获取数据,然后缓存起来再返回。

推荐文章
PHP GD库是什么

PHP在Web开发领域被广泛应用的原因在于,PHP不仅可以生成HTML页面,还可以创建和操作二进制形式的数据,例如图像、文件等等。其中,使用PHP处理图像就需要GD库的支持,本节我们就来介绍一下GD库

网站地图(sitemap)的作用和格式

网站地图又称“站点地图”,它就是一个页面,是存放网站所有重要链接的容器。一般该页面会放置网站上所有希望搜索引擎重点抓取的页面链接(如图1所示)。 图1:迈锐光电网站地图 多数网站的链接层次比较

UML中的类图及类图之间的关系

统一建模语言简介 统一建模语言(UnifiedModelingLanguage,UML)是用来设计软件蓝图的可视化建模语言,1997年被国际对象管理组织(OMG)采纳为面向对象的建模语言的国际标准。

WP表示什么?

WindowsPhone(简称为WP)是微软于2010年10月21日正式发布的一款手机操作系统,初始版本命名为WindowsPhone7.0。基于WindowsCE内核,采用了一种称为Metro的用户

system32是什么文件夹?

system32是什么文件夹?System32是Windows操作系统的系统文件夹,是操作系统的神经中枢,文件夹中包含了大量的用于Windows操作系统的文件。System32文件夹里主要用于存储DL

syms在matlab中的作用是什么?

syms在matlab中的作用是:定义符号变量。1、定义一个符号变量xsymsx2、创建符号变量x和ysymsxy3、列出所有符号变量、函数和数组创建一些符号变量、函数和数组如:symsaf(x);A

python数据类型有哪几种?

数据类型是每种编程语言必备属性,只有给数据赋予明确的数据类型,计算机才能对数据进行处理运算,因此,正确使用数据类型是十分必要的,不同的语言,数据类型类似,但具体表示方法有所不同,以下是Python编程

JS执行上下文和活动对象

在《JS变量》一节中我们曾介绍过变量的作用域,JavaScript支持全局作用域和局部作用域。这个局部作用域也就是函数作用域,局部变量在函数内可见,也称为私有变量。 作用域 作用域(Scope)表示

HMaster是什么?

HMaster是HBase集群中的主服务器,负责监控集群中的所有RegionServer,并且是所有元数据更改的接口。 在分布式集群中,HMaster服务器通常运行在HDFS的NameNode上,H

printf函数和scanf函数,C语言printf函数和scanf函数详解

对于printf函数,相信大家并不陌生。之所以称它为格式化输出函数,关键就是该函数可以按用户指定的格式,把指定的数据显示到显示器屏幕上。该函数原型的一般格式如下: intprintf(constch

Struts2的非表单标签

Struts2的非表单标签主要用于在页面中生成非表单的可视化元素,以及输出在Action中封装的信息,如输出错误提示信息等。 常用的非表单标签有、和标签,它们分别用于显示动作信息、动作错误信息和字段

1)产品销售系统 2)网站多用户系统 3)产品互动系统 图1:天猫评价系统 4)推广营销系统

HBase创建表(create命令)

与关系型数据库不同,在HBase中,基本组成为表,不存在多个数据库。因此,在HBase中存储数据先要创建表,创建表的同时需要设置列族的数量和属性。 示例:Student数据表 行键 列族

MySQL LEFT/RIGHT JOIN:外连接

在《MySQL内连接》一节我们了解了MySQL的内连接。内连接的查询结果都是符合连接条件的记录,而外连接会先将连接的表分为基表和参考表,再以基表为依据返回满足和不满足条件的记录。 外连接可以分为左外

C语言枚举类型(C语言enum用法)详解

在实际编程中,有些数据的取值往往是有限的,只能是非常少量的整数,并且最好为每个值都取一个名字,以方便在后续代码中使用,比如一个星期只有七天,一年只有十二个月,一个班每周有六门课程等。 以每周七天为例

pytorch是什么?

PyTorch是一个开源的Python机器学习库,基于Torch,用于自然语言处理等应用程序。2017年1月,由Facebook人工智能研究院(FAIR)基于Torch推出了PyTorch。它是一个基

raid1和raid5的区别是什么?

RAID5和RAID1的区别1、读写方面:RAID1读和单个磁盘没有区别,写则需要两边都写;RAID5读性能最好,写性能小于对单个磁盘进行写入操作;所以RAID1适合读操作多的情景而RAID5适合写操

网站导航该如何设置?

网站导航用来连接到网站的重要页面,一般分为以下几种类型。 主导航 网站主导航一般就是放在网站最上面,是网站的栏目或主要内容的导入链接。一般情况下,导航上的栏目或内容是这个网站最主要的内容,也是除首页

Maven配置远程仓库

虽然用户可以从中央仓库中找到绝大部分流行的构件,但是毕竟不能找到所有构件。对那些在中央仓库中没有的构件,又要怎么办呢?可以在pom.xml中添加另外一个远程仓库。比如,将jbossMaven远程仓库添

什么是冯诺依曼计算机(5大部件和特点)

提到计算机,就不得不提及在计算机的发展史上做出杰出贡献的著名应用数学家冯·诺依曼(VonNeumann),他带领专家提出了一个全新的存储程序的通用电子计算机方案(如图1所示)。 图1计算机的组成