Springboot redis 入门示例
1. 概述
在快速入门 Spring Boot 整合 Redis 之前,我们先来做个简单的了解。在 Spring 的生态中,我们使用 Spring Data Redis 来实现对 Redis 的数据访问。
市面上已经有 Redis、Redisson、Lettuce 等优秀的 Java Redis 工具库,为什么还要有 Spring Data Redis 呢?不要慌,我们先来看一张图:
- 对于下层,Spring Data Redis 提供了统一的操作模板(后文中,我们会看到是 RedisTemplate 类),封装了 Jedis、Lettuce 的 API 操作,访问 Redis 数据。所以,实际上,Spring Data Redis 内置真正访问的实际是 Jedis、Lettuce 等 API 操作。
- 对于上层,开发者学习如何使用 Spring Data Redis 即可,而无需关心 Jedis、Lettuce 的 API 操作。甚至,未来如果我们想将 Redis 访问从 Jedis 迁移成 Lettuce 来,无需做任何的变动。
- 目前,Spring Data Redis 暂时只支持 Jedis、Lettuce 的内部封装,而 Redisson 是由 redisson-spring-data 来提供。
- springboot 2.X 版本使用
Lettce
作为默认连接池。至于使用jedis
还是lettce
是仁者见仁智者见智了。按照项目考察进行使用。
SkyWalking 中间件,暂时只支持 Jedis 的自动化的追踪。
我们在例子中使用非默认配置jedis
连接池。
2. 开始入门
2.1 导入依赖
在pom
文件导入相关依赖
<!-- spring 对redis的整合 提供模板的访问方法 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 去掉对 Lettuce 的依赖,因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 Jedis 的依赖,这样 Spring Boot 实现对 Jedis 的自动化配置 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 阿里开源的 json序列化工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.2 配置文件
在 application.propertites
中添加 redis的相关配置
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接超时时间(毫秒)60秒
spring.redis.timeout=600000
# Redis 数据库号,默认为 0
spring.redis.database=0
# 连接池最大连接数(使用负值表示没有限制)
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=2
2.3 redis 配置类
新建 RedisConfig.java 使用json序列化消息 ,默认使用jdk序列化,但是这种方式不能跨语言,一般生产会使用json
这种交互格式。
## 公众号 C位程序员
@Configuration
public class RedisConfig {
/**
* 替换自带的redisTemplate 修改序列化方式
*
* @param factory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
RedisSerializer jackson2JsonRedisSerializer = getJacksonSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* redis的json序列化
*
* @return
*/
private RedisSerializer getJacksonSerializer() {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
return new GenericJackson2JsonRedisSerializer(om);
}
}
实际上,Redis Client
传递给 Redis Server
是传递的 KEY
和 VALUE
都是二进制值数组。为了方便查看我们需要序列化。
RedisSerializer
的实现类,如下图
2.4 RedisTemplate
实际上RedisTemplate还提供了 StringRedisTemplate
,感兴趣的大家去使用一下就可以了。
org.springframework.data.redis.core.RedisTemplate<K, V> 类,从类名上,我们就明明白白知道,提供 Redis 操作模板 API 。核心属性如下:
// RedisTemplate.java
// 省略了一些不重要的属性。
// <1> 序列化相关属性
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
// <2> Lua 脚本执行器
private @Nullable ScriptExecutor<K> scriptExecutor;
// <3> 常见数据结构操作类
// cache singleton objects (where possible)
private @Nullable ValueOperations<K, V> valueOps;
private @Nullable ListOperations<K, V> listOps;
private @Nullable SetOperations<K, V> setOps;
private @Nullable ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;
那么 Pub/Sub、Transaction、Pipeline、Keys、Cluster、Connection 等相关的 API 操作呢?它在 RedisTemplate 自身提供,因为它们不属于具体每一种数据结构,所以没有封装在对应的 Operations 类中。哈哈哈,打开 RedisTemplate 类,去瞅瞅,妥妥的明白。
2.5 RedisUtil
封装的一些redis常用操作,篇幅过长我就贴代码了,感兴趣的去 RedisUtil.java 下载。
3. 示例
3.1 简单 set 和 get
我们通过模板方法访问redis的api基本是一致的。我们可以通过封装的 ValueOperations
进行操作。
3.1.1 GetSetTest.java
@Test
void contextLoads() {
redisTemplate.opsForValue().set("key","hello redis");
Object key = redisTemplate.opsForValue().get("key");
System.out.println(key);
}
}
##运行结果
hello redis
3.1.2 源码讲解 execute
我们想避开 ValueOperations
使用操作,就要用到 <T> T execute(RedisCallback<T> action)
方法了。
<T> T execute(RedisCallback<T> action); 我们只讲这个
<T> T execute(SessionCallback<T> session); 这个感兴趣的朋友可以自己了解下
下面我们展开讲讲这个方法,这是 RedisTemplate 的核心逻辑。
这个方法有很多重载方法,我们只讲核心逻辑。
我们主要关注 RedisCallback<T> action
这个对象,这是我们的执行逻辑。这是个接口类。
public interface RedisCallback<T> {
@Nullable
T doInRedis(RedisConnection connection) throws DataAccessException;
}
# RedisTemplate.java 公众号 C位程序员
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
// 省略无关
RedisConnectionFactory factory = getRequiredConnectionFactory();
RedisConnection conn = null;
try {
// 判断是否开启事务 不是重点
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
// <1> 获取一个redis连接
conn = RedisConnectionUtils.getConnection(factory);
}
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
boolean pipelineStatus = connToUse.isPipelined();
// 是否是 pipeline 命令 后面我们会提到
if (pipeline && !pipelineStatus) {
connToUse.openPipeline();
}
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
// *** <2> 核心逻辑 得到 RedisConnection 后会调用接口中的 doInRedis() 方法执行命令
T result = action.doInRedis(connToExpose);
// close pipeline
if (pipeline && !pipelineStatus) {
connToUse.closePipeline();
}
// <3> 返回结果
return postProcessResult(result, connToUse, existingConnection);
} finally {
RedisConnectionUtils.releaseConnection(conn, factory, enableTransactionSupport);
}
}
@Nullable
protected <T> T postProcessResult(@Nullable T result, RedisConnection conn, boolean existingConnection) {
return result;
}
可以看到 大体逻辑
- 获取连接
- **执行命令 **
- 返回结果值(一般会进行序列化处理)
我们分析一下我们上面简单例子中的 redisTemplate``.opsForValue().get(``"key"``)``;
## DefaultValueOperations.java
public V get(Object key) {
return execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);
}
## AbstractOperations.java
abstract class ValueDeserializingRedisCallback implements RedisCallback<V> {
private Object key;
public ValueDeserializingRedisCallback(Object key) {
this.key = key;
}
public final V doInRedis(RedisConnection connection) {
byte[] result = inRedis(rawKey(key), connection);
// 将结果进行序列化 我们上面配置的是json
return deserializeValue(result);
}
@Nullable
protected abstract byte[] inRedis(byte[] rawKey, RedisConnection connection);
}
相信看完上面的代码,大家可以明白了。其实底层还是 doInRedis
。我们也可以直接执行execute()
方法,实现逻辑即可。不过redisTemplate 已经将常用api进行封装
了。
3.2 pipeline
如果朋友们没有了解过 Redis 的 Pipeline 机制,可以看看 《Redis 文档 —— Pipeline》 文章,批量操作,提升性能必备神器。
3.2.1 PipelineTest.java
@SpringBootTest
class PipelineTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test01() {
List<Object> results = stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
// set 写入
for (int i = 0; i < 3; i++) {
connection.set(("key"+i).getBytes(), ("C位程序员"+i).getBytes());
}
// get
for (int i = 0; i < 3; i++) {
connection.get(("key"+i).getBytes());
}
// 返回 null 即可
return null;
});
// 打印结果
System.out.println(results);
}
}
## 运行结果
[true, true, true, C位程序员0, C位程序员1, C位程序员2]
3.2.2 源码讲解 executePipelined
如果你理解了 我上面说的 execute
方法,那这里也很简单。
// <1> 基于 Session 执行 Pipeline
@Override
public List<Object> executePipelined(SessionCallback<?> session) {
return executePipelined(session, valueSerializer);
}
@Override
public List<Object> executePipelined(SessionCallback<?> session, @Nullable RedisSerializer<?> resultSerializer) {
// ... 省略代码
}
// <2> 直接执行 Pipeline
@Override
public List<Object> executePipelined(RedisCallback<?> action) {
return executePipelined(action, valueSerializer);
}
@Override
public List<Object> executePipelined(RedisCallback<?> action, @Nullable RedisSerializer<?> resultSerializer) {
// ... 省略代码
}
在 RedisTemplate 类中,提供了 2 组四个方法,用于执行 Redis Pipeline 操作。代码如下:
- 两组方法的差异,在于是否是 Session 中执行。我们只讲 Pipeline + RedisCallback 的组合的方法。
- 每组方法里,差别在于是否传入 RedisSerializer 参数。如果不传,则使用 RedisTemplate 自己的序列化相关的属性。
// RedisTemplate.java
@Override
public List<Object> executePipelined(RedisCallback<?> action, @Nullable RedisSerializer<?> resultSerializer) {
// <1> 执行 Redis 方法
return execute((RedisCallback<List<Object>>) connection -> {
// <2> 打开 pipeline
connection.openPipeline();
boolean pipelinedClosed = false; // 标记 pipeline 是否关闭
try {
// <3> 执行
Object result = action.doInRedis(connection);
// <4> 不要返回结果
if (result != null) {
throw new InvalidDataAccessApiUsageException(
"Callback cannot return a non-null value as it gets overwritten by the pipeline");
}
// <5> 提交 pipeline 执行
List<Object> closePipeline = connection.closePipeline();
pipelinedClosed = true;
// <6> 反序列化结果,并返回
return deserializeMixedResults(closePipeline, resultSerializer, hashKeySerializer, hashValueSerializer);
} finally {
if (!pipelinedClosed) {
connection.closePipeline();
}
}
});
}
- <1> 处,调用
#execute(RedisCallback<T> action)
方法,执行 Redis 方法。注意,此处传入的 action 参数,不是我们传入的 RedisCallback 参数。我们的会在该 action 中被执行。 - <2> 处,调用
RedisConnection#openPipeline()
方法,自动打开Pipeline
模式。这样,我们就不需要手动去打开了。 - <3> 处,调用我们传入的实现的
RedisCallback#doInRedis(RedisConnection connection)
方法,执行在 Pipeline 中,想要执行的 Redis 操作。 - <4> 处,不要返回结果。因为 RedisCallback 是统一定义的接口,所以可以返回一个结果。但是在 Pipeline 中,未提交执行时,显然是没有结果,返回也没有意思。简单来说,就是我们在实现
RedisCallback#doInRedis(RedisConnection connection)
方法时,返回 null 即可。 - <5> 处,调用
RedisConnection#closePipeline()
方法,自动提交 Pipeline 执行,并返回执行结果。 - <6> 处,反序列化结果,并返回 Pipeline 结果。
3.3 Pub/Sub
Redis 提供了 Pub/Sub 功能,实现简单的订阅功能,不了解的,可以看看 「Redis 文档 —— Pub/Sub」 。
3.3.1 源码解析
暂时不提供,感兴趣的胖友,可以自己看看最核心的 org.springframework.data.redis.listener.RedisMessageListenerContainer 类,Redis 消息监听器容器,基于 Pub/Sub 的 SUBSCRIBE、PSUBSCRIBE 命令实现,我们只需要添加相应的 org.springframework.data.redis.connection.MessageListener 即可。不算复杂,1000 多行,只要调试下核心的功能即可。
3.3.2 PubSubTest.java
Spring Data Redis 实现 Pub/Sub 的示例,主要分成两部分:
- 配置 RedisMessageListenerContainer Bean 对象,并添加我们自己实现的 MessageListener 对象,用于监听处理相应的消息。
- 使用 RedisTemplate 发布消息。
Topic
**
org.springframework.data.redis.listener.Topic 接口,表示 Redis 消息的 Topic 。它有两个子类实现:
- ChannelTopic :对应 SUBSCRIBE 订阅命令。
- PatternTopic :对应 PSUBSCRIBE 订阅命令。
生产者
**
PubSubTest
@SpringBootTest
class PubSubTest {
public static final String TOPIC = "PUB_SUB_TEST";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test01() throws InterruptedException {
for (int i = 0; i < 10; i++) {
stringRedisTemplate.convertAndSend(TOPIC, "C位程序员" + i);
Thread.sleep(1000L);
}
}
}
**消费者
需要实现 MessageListener
@Component
public class SimpleMsgListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
System.out.println("收到 Topic 消息:");
System.out.println("线程编号:" + Thread.currentThread().getName());
System.out.println("message:" + message);
System.out.println("pattern:" + new String(pattern));
}
}
然后在RedisConfig 中添加逻辑,将我们的自定义监听器添加到容器中
// RedisConfig.java
@Bean
public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory factory) {
// 创建 RedisMessageListenerContainer 对象
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 设置 RedisConnection 工厂。 它就是实现多种 Java Redis 客户端接入的秘密工厂。
container.setConnectionFactory(factory);
// 添加监听器 这里只能添加同一种类型的 Topic ChannelTopic和 PatternTopic 不能混合
container.addMessageListener(new SimpleMsgListener(), new ChannelTopic("PUB_SUB_TEST"));
//container.addMessageListener(new TestChannelTopicMessageListener(), new ChannelTopic("AOTEMAN"));
//container.addMessageListener(new TestPatternTopicMessageListener(), new PatternTopic("TEST"));
return container;
}
运行 **PubSubTest **中的测试方法
运行结果,只截取了部分
收到 Topic 消息:
线程编号:listenerContainer-2
message:C位程序员0
pattern:PUB_SUB_TEST
收到 Topic 消息:
线程编号:listenerContainer-3
message:C位程序员1
pattern:PUB_SUB_TEST
收到 Topic 消息:
线程编号:listenerContainer-4
message:C位程序员2
pattern:PUB_SUB_TEST
收到 Topic 消息:
线程编号:listenerContainer-5
message:C位程序员3
pattern:PUB_SUB_TEST
有一点要注意,默认的 RedisMessageListenerContainer 情况下,MessageListener 是并发消费,在线程池中执行(具体见传送门代码)。所以如果想相同 MessageListener 串行消费,可以在方法上加 synchronized 修饰,来实现同步。
3.3.3 总结
Redis 提供了 PUB/SUB 订阅功能,实际我们在使用时,一定要注意,它提供的不是一个可靠的订阅系统。例如说,有消息 PUBLISH 了,Redis Client 因为网络异常断开,无法订阅到这条消息。等到网络恢复后,Redis Client 重连上后,是无法获得到该消息的。相比来说,成熟的消息队列提供的订阅功能,因为消息会进行持久化(Redis 是不持久化 Publish 的消息的),并且有客户端的 ACK 机制做保障,所以即使网络断开重连,消息一样不会丢失。
Redis 5.0 版本后,正式发布 Stream 功能,相信是有可能可以替代掉 Redis Pub/Sub 功能,提供可靠的消息订阅功能。
上述的场景,艿艿自己在使用 PUB/SUB 功能的时候,确实被这么坑过。当时我们的管理后台的权限,是缓存在 Java 进程当中,通过 Redis Pub/Sub 实现缓存的刷新。结果,当时某个 Java 节点网络出问题,恰好那个时候,有一条刷新权限缓存的消息 PUBLISH 出来,结果没刷新到。结果呢,运营在访问某个功能的时候,一会有权限(因为其他 Java 节点缓存刷新了),一会没有权限。
最近,艿艿又去找了几个朋友请教了下,问问他们在生产环境下,是否使用 Redis Pub/Sub 功能,他们说使用 Kafka、或者 RocketMQ 的广播消费功能,更加可靠有保障。
对了,我们有个管理系统里面有 Websocket 需要实时推送管理员消息,因为不知道管理员当前连接的是哪个 Websocket 服务节点,所以我们是通过 Redis Pub/Sub 功能,广播给所有 Websocket 节点,然后每个 Websocket 节点判断当前管理员是否连接的是它,如果是,则进行 Websocket 推送。因为之前网络偶尔出故障,会存在消息丢失,所以近期我们替换成了 RocketMQ 的广播消费,替代 Redis Pub/Sub 功能。
当然,不能说 Redis Pub/Sub 毫无使用的场景,以下艿艿来列举几个:
- 1、在使用 Redis Sentinel 做高可用时,Jedis 通过 Redis Pub/Sub 功能,实现对 Redis 主节点的故障切换,刷新 Jedis 客户端的主节点的缓存。如果出现 Redis Connection 订阅的异常断开,会重新主动去 Redis Sentinel 的最新主节点信息,从而解决 Redis Pub/Sub 可能因为网络问题,丢失消息。
- 2、Redis Sentinel 节点之间的部分信息同步,通过 Redis Pub/Sub 订阅发布。
- 3、在我们实现 Redis 分布式锁时,如果获取不到锁,可以通过 Redis 的 Pub/Sub 订阅锁释放消息,从而实现其它获得不到锁的线程,快速抢占锁。当然,Redis Client 释放锁时,需要 PUBLISH 一条释放锁的消息。在 Redisson 实现分布式锁的源码中,我们可以看到。
- 4、Dubbo 使用 Redis 作为注册中心时,使用 Redis Pub/Sub 实现注册信息的同步。
也就是说,如果想要有保障的使用 Redis Pub/Sub 功能,需要处理下发起订阅的 Redis Connection 的异常,例如说网络异常。然后,重新主动去查询最新的数据的状态
3.4 lua script
Redis 提供 Lua 脚本,满足我们希望组合排列使用 Redis 的命令,保证串行执行的过程中,不存在并发的问题。同时,通过将多个命令组合在同一个 Lua 脚本中,一次请求,直接处理,也是一个提升性能的手段。不了解的,可以看看 「Redis 文档 —— Lua 脚本」 。
第一步,编写 Lua 脚本
创建 lua
脚本,实现 CAS 功能。代码如下:
if redis.call('GET', KEYS[1]) ~= ARGV[1] then
return 0
end
redis.call('SET', KEYS[1], ARGV[2])
return 1
- 第 1 到 3 行:判断
KEYS[1]
对应的 VALUE 是否为ARGV[1]
值。如果不是(Lua 中不等于使用~=
),则直接返回 0 表示失败。 - 第 4 到 5 行:设置
KEYS[1]
对应的 VALUE 为新值ARGV[2]
,并返回 1 表示成功。
第二步,调用 Lua 脚本
创建 ScriptTest 测试类,编写代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ScriptTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test01() throws IOException {
// <1.1> lua 脚本
String scriptContents = "if redis.call('GET', KEYS[1]) ~= ARGV[1] then\n" +
" return 0\n" +
"end\n" +
"redis.call('SET', KEYS[1], ARGV[2])\n" +
"return 1";
// <1.2> 创建 RedisScript 对象
RedisScript<Long> script = new DefaultRedisScript<>(scriptContents, Long.class);
stringRedisTemplate.opsForValue().set("luakey","我是原始值");
// <2> 执行 LUA 脚本
Long result = stringRedisTemplate.execute(script, Collections.singletonList("luakey"), "我是原始值", "我是替换值");
System.out.println(result);
}
}
## 运行结果
1
- <1.1> 行,读取 lua 脚本。
- <1.2> 行,创建 DefaultRedisScript 对象。第一个参数是脚本内容( scriptSource ),第二个是脚本执行返回值( resultType )。
- <2> 处,调用 RedisTemplate#execute(RedisScript script, List keys, Object… args)方法,发送 Redis 执行 LUA 脚本。
3.5 分布式锁
可能朋友们不是很了解 Redisson 这个库,胖友可以跳转 Redis 客户端 Redisson ,看看对它的介绍。简单来说,这是 Java 最强的 Redis 客户端
!除了提供了 Redis 客户端的常见操作之外,还提供了 Redis 分布式锁、BloomFilter 布隆过滤器等强大的功能。在 redisson-examples 中,Redisson 官方提供了大量的示例。
4. 总结
redis的功能很强大,应用场景非常广。熟练掌握redis的一些特性对我们开发业务中会极大的提高效率。
今天的你多努力一点,明天的C位就是你!
一起学习成为 C位程序员。💋
微信公众号已开启,【C位程序员】,没关注的同学们记得关注哦!