Spring Boot 2.x整合Redis

目录

最近在学习 Spring Boot 2.x 整合 Redis,在这里和大家分享一下,希望对大家有帮助。

返回目录
返回目录

Redis 是什么

Redis 是开源免费高性能的 key-value 数据库。有以下的优势(源于 Redis 官网http://www.redis.net.cn/):

  • 性能极高 – Redis 能读的速度是 110000 次 /s, 写的速度是 81000 次 /s 。

  • 丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。

  • 原子 – Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来。

  • 丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。

如果想了解更多关于 Redis 是什么,可以参考:Redis 初识

返回目录

为什么要使用 Redis

为什么要使用 Redis?21 世纪以来信息技术的飞速发展,我们已经迎来了大数据时代,数据较之前爆炸增长,并发访问量激增;由之带来的就是系统设计由单体架构向分布式架构的转变和演进。对于缓存这个问题,如果系统是单体的系统,可以在单个的 JVM 里面进行本地缓存,例如可以用 HashMap 来缓存一些数据。但是到了分布式系统里面,一个大系统里面有很多个子系统,那他们之间如何共享缓存?这就需要 Redis,用 Redis 来实现缓存系统。都说“时势造英雄”,其实我想说 Redis 的大规模使用也是这个道理,用 Redis 做缓存系统从性能和并发两方面考虑都是值得肯定的。

  1. 性能:如果碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。
  2. 并发: 在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用 redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。

更多关于“为什么要使用 Redis”,博客园【原创】分布式之 redis 复习精讲 ,这篇博文关于 Redis 的讲解我觉得超赞,谢谢作者的耐心分享。

Spring Boot 2.x 如何整合 Redis

我使用的 Spring Boot 版本是 2.1.0,根据网上的一些旧的教程进行整合 Redis 3.2 的时候,会有许多地方有错误提示。这是因为 Spring Boot 2.x 做了一些修改,这些修改对使用而有没有影响呢?我们改怎么整合呢?下面就进入正式的整合过程。

1. pom.xnl 引入依赖

这是之前的依赖:
        <dependency>
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-redis</artifactId>
        </dependency>  

现在会提示 Project build error: 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-redis:jar is missing. 提示 spring-boot-starter-redis:jar 找不到。

这是因为 Spring Boot 1.4 之后不再支持spring-boot-starter-redis,更换spring-boot-starter-data-redis之后就可以了。如果你的 pom 文件报错,请检查是否将 spring-boot-starter-redis 改成了 spring-boot-starter-data-redis。

Spring Boot 2.x 要使用下面的依赖:

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

2. application.properties 添加配置文件

这是之前版本的配置文件:

# REDIS (RedisProperties)
# Redis 数据库索引(默认为 0)
spring.redis.database=0  
# Redis 服务器地址
spring.redis.host=127.0.0.1
# Redis 服务器连接端口
spring.redis.port=6379  
# Redis 服务器连接密码(默认为空)
spring.redis.password=  
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8  
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1 
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0  
# 连接超时时间(毫秒)
spring.redis.timeout=0  

如果 Spring Boot 2.x 这么配置,有错误提示 Property 'spring.redis.pool.max-active' is Deprecated: Use 'spring.redis.jedis.pool.max-idle' instead. 'spring.redis.pool.max-active'已经被弃用了,推荐使用'spring.redis.jedis.pool.max-idle' 来代替。

这是因为在 2.x 中配置 redis 的连接池信息时,不再使用 spring.redis.pool 的属性,而是直接使用 redis 的 lettuce 或 jedis 客户端来配置。现在的配置如下:

# REDIS (RedisProperties)
# Redis 数据库索引(默认为 0)
spring.redis.database=0  
# Redis 服务器地址
spring.redis.host=127.0.0.1
# Redis 服务器连接端口
spring.redis.port=6379  
# Redis 服务器连接密码(默认为空)
spring.redis.password=  
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8  
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0  
# 连接超时时间(毫秒)
spring.redis.timeout=0  

3. 配置 CacheManager

这是之前的 RedisConfig 配置类:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public CacheManager cacheManager(RedisTemplate<?, ?>  redisTemplate) {
        CacheManager cacheManager = new  RedisCacheManager(redisTemplate);
        return cacheManager;
    }
@Bean
</span><span style="color: rgba(0, 0, 255, 1)">public</span> RedisTemplate&lt;String, String&gt;<span style="color: rgba(0, 0, 0, 1)">  redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate</span>&lt;String, String&gt; redisTemplate = <span style="color: rgba(0, 0, 255, 1)">new</span>  RedisTemplate&lt;String, String&gt;<span style="color: rgba(0, 0, 0, 1)">();
    redisTemplate.setConnectionFactory(factory);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> redisTemplate;
}

}

现在这么写会有报错 The constructor RedisCacheManager(RedisTemplate<capture#1-of ?,capture#2-of ?>) is undefined。

这是因为 Spring Boot 2.x 版本删除了 RedisCacheManager 这个构造器, 也不可以通过之前的 setDefaultExpiration 方法设置默认的缓存过期时间等。

那么要怎样构造一个 RedisCacheManager?看看官方文档中怎么说?文档地址:https://docs.spring.io/spring-data/data-redis/docs/2.1.x/reference/html/#new-in-2.1.0

官方文档 5.13.1. Support for the Spring Cache Abstraction(对 Spring Cache Abstraction 的支持)是关于怎么配置缓存的说明,我尝试着翻译了一下(蓝色部分),英文水平有限,各位轻喷。

Spring Redis provides an implementation for the Spring cache abstraction through the org.springframework.data.redis.cache package. To use Redis as a backing implementation, add RedisCacheManager to your configuration, as follows:

Spring Redis 在 org.springframework.data.redis.cache 包中为 Spring 缓存抽象提供了一个实现方案。要使用 Redis 作为支持实现,需要将 RedisCacheManager 添加到配置中,如下所示:

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    return RedisCacheManager.create(connectionFactory);
}

RedisCacheManager behavior can be configured with RedisCacheManagerBuilder, letting you set the default RedisCacheConfiguration, transaction behavior, and predefined caches.

RedisCacheManager 可以用 RedisCacheManagerBuilder 进行配置,允许自定义设置默认的 RedisCacheConfiguration、事务行为和预定义的缓存。

RedisCacheManager cm = RedisCacheManager.builder(connectionFactory)
    .cacheDefaults(defaultCacheConfig())
    .initialCacheConfigurations(singletonMap("predefined", defaultCacheConfig().disableCachingNullValues()))
    .transactionAware().build();

As shown in the preceding example, RedisCacheManager allows definition of configurations on a per-cache basis.

正如上面的例子所示,RedisCacheManager 允许基于每个缓存进行配置。

The behavior of RedisCache created with RedisCacheManager is defined with RedisCacheConfiguration. The configuration lets you set key expiration times, prefixes, and RedisSerializer implementations for converting to and from the binary storage format, as shown in the following example:

RedisCacheManager 创建 RedisCache 的行为被定义为 RedisCacheConfiguration。该配置允许设置键值过期时间、前缀和 RedisSerializer 实现等属性,以便与二进制存储格式进行转换,如下所示:

RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(1)).disableCachingNullValues();

RedisCacheManager defaults to a lock-free RedisCacheWriter for reading and writing binary values. Lock-free caching improves throughput. The lack of entry locking can lead to overlapping, non-atomic commands for the putIfAbsent and clean methods, as those require multiple commands to be sent to Redis. The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key, which leads to additional requests and potential command wait times.

RedisCacheManager 默认是无锁的,用于读写二进制值的 RedisCacheWriter。无锁缓存可提高吞吐量。缺少锁定可能导致 putIfAbsent 和 clean 方法的非原子命令重叠,因为这些方法需要向 Redis 发送多条命令。锁对应程序通过设置显式锁定的 key 并检查是否存在该 key 来防止命令重叠,这会导致额外的请求和潜在的命令等待时间。

It is possible to opt in to the locking behavior as follows:

也可以选择锁定行为,如下所示:

RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.lockingRedisCacheWriter())
    .cacheDefaults(defaultCacheConfig())
    ...

By default, any key for a cache entry gets prefixed with the actual cache name followed by two colons. This behavior can be changed to a static as well as a computed prefix.

默认情况下,缓存条目的任何 key 目以实际缓存名称为前缀,后跟两个冒号。此行为可以更改为静态前缀和计算前缀。

The following example shows how to set a static prefix:

以下示例显示如何设置静态前缀:

// static key prefix
RedisCacheConfiguration.defaultCacheConfig().prefixKeysWith("( ͡° ᴥ ͡°)");

The following example shows how to set a computed prefix:

// computed key prefix
RedisCacheConfiguration.defaultCacheConfig().computePrefixWith(cacheName -> "¯_( ツ)_/¯" + cacheName);

The following table lists the default settings for RedisCacheManager:

下表列出了 RedisCacheManager 的默认设置:

Setting
Value
Cache Writer
Non-locking
Cache Configuration
RedisCacheConfiguration#defaultConfiguration
Initial Caches
None
Trasaction Aware
No

The following table lists the default settings for RedisCacheConfiguration:

下表列出了 RedisCacheConfiguration 的默认设置:

Key Expiration
None
Cache null
Yes
Prefix Keys
Yes
Default Prefix
The actual cache name
Key Serializer
StringRedisSerializer
Value Serializer
JdkSerializationRedisSerializer
Conversion Service
DefaultFormattingConversionService with default cache key converters

对于 Spring Boot 2.x,这里我使用RedisCacheConfiguration简单配置一下缓存时间,完成 RedisCacheManager 的配置,代码如下:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> CacheManager cacheManager(RedisConnectionFactory  factory) {
    RedisCacheConfiguration config </span>=  RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(1<span style="color: rgba(0, 0, 0, 1)">));
    RedisCacheManager cacheManager </span>=<span style="color: rgba(0, 0, 0, 1)">  RedisCacheManager.builder(factory).cacheDefaults(config).build();
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> cacheManager;
}

}

4. Service 层

使用缓存的情况一般是这样的:第一次访问的时候先从数据库读取数据,然后将数据写入到缓存,再次访问同一内容的时候就从缓存中读取,如果缓存中没有则从数据库中读取。
添加缓存逻辑的时候,从数据库中将内容读取出来之后,先 set 入缓存,然后再从缓存中添加读取行为,如果缓存为空则从数据库中进行读取。

在这里仅创建一个简单的 RedisService,来进行存取缓存数据。

@Service
public class RedisService {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    public void set(String key, Object value) {
        //更改在 redis 里面查看 key 编码问题
        RedisSerializer redisSerializer = new  StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        ValueOperations<String, Object> vo =  redisTemplate.opsForValue();
        vo.set(key, value);
    }
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Object get(String key) {
    ValueOperations</span>&lt;String, Object&gt; vo =<span style="color: rgba(0, 0, 0, 1)">  redisTemplate.opsForValue();
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> vo.get(key);
}

}

5. Model 层

实体类没有修改,和之前文章里面用的一样:

@Entity
@Table(name = "user")
public class User implements Serializable {
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">long</span> serialVersionUID = 1L<span style="color: rgba(0, 0, 0, 1)">;

@Id
@GeneratedValue
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Long id;
@Column(name </span>= "username"<span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String userName;
@Column(name </span>= "password"<span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String passWord;

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> User() {
    </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">();
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> User(String userName, String passWord) {
    </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">();
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.userName =<span style="color: rgba(0, 0, 0, 1)"> userName;
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.passWord =<span style="color: rgba(0, 0, 0, 1)"> passWord;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> User(Long id, String userName, String passWord) {
    </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">();
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.id =<span style="color: rgba(0, 0, 0, 1)"> id;
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.userName =<span style="color: rgba(0, 0, 0, 1)"> userName;
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.passWord =<span style="color: rgba(0, 0, 0, 1)"> passWord;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Long getId() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setId(Long id) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.id =<span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getUserName() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> userName;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setUserName(String userName) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.userName =<span style="color: rgba(0, 0, 0, 1)"> userName;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getPassWord() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> passWord;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setPassWord(String passWord) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.passWord =<span style="color: rgba(0, 0, 0, 1)"> passWord;
}

}

 6. Controller 层

在 user.Controller 添加两个测试方法,一个写入 Redis,一个从 Redis 读取:
@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserRepository userRepository;
@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> RedisService redisService;

@RequestMapping(</span>"/saveUser"<span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String saveUser(Long id, String userName, String  passWord) {
    User user </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> User(id, userName, passWord);
    redisService.set(id </span>+ ""<span style="color: rgba(0, 0, 0, 1)">, user);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> "success"<span style="color: rgba(0, 0, 0, 1)">;
}

@RequestMapping(</span>"/getUserById"<span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> User getUserById(Long id) {
    User res </span>= (User) redisService.get(id + ""<span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> res;
}

}

7. 测试

使用 Postman 进行测试,访问http://localhost:8080//user/saveUser?id=12&userName=Howard&passWord=magician,添加了一个 User。

看看 Redis 数据库,使用 get key 查看,得到一个对象:

http://localhost:8080//user/getUserById?id=12,通过 id 获取 User。

这是过滤器和拦截器的日志信息,可以看到没有进行 MySQL 数据库的操作,直接从缓存中读取,说明 Redis 配置生效了:

如果访问之前的接口,是有操作数据库的:

总结

以上就是 Spring Boot 2.x 整合 Redis 的全过程,和 Spring Boot 之前的版本在使用上有些细微的差别。
由于水平有限,文中或多或少有欠妥之处,还望各位大佬指正!