likes
comments
collection
share

通俗易懂!带你入门 Springboot redis 示例

作者站长头像
站长
· 阅读数 28

Springboot redis 入门示例

1. 概述

在快速入门 Spring Boot 整合 Redis 之前,我们先来做个简单的了解。在 Spring 的生态中,我们使用 Spring Data Redis 来实现对 Redis 的数据访问。市面上已经有 Redis、Redisson、Lettuce 等优秀的 Java Redis 工具库,为什么还要有 Spring Data Redis 呢?不要慌,我们先来看一张图:通俗易懂!带你入门 Springboot 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 是传递的 KEYVALUE 都是二进制值数组。为了方便查看我们需要序列化。 RedisSerializer 的实现类,如下图通俗易懂!带你入门 Springboot redis 示例

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;
	}

可以看到 大体逻辑

  1. 获取连接
  2. **执行命令 **
  3. 返回结果值(一般会进行序列化处理)

我们分析一下我们上面简单例子中的 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 的 SUBSCRIBEPSUBSCRIBE 命令实现,我们只需要添加相应的 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

3.5 分布式锁

可能朋友们不是很了解 Redisson 这个库,胖友可以跳转 Redis 客户端 Redisson ,看看对它的介绍。简单来说,这是 Java 最强的 Redis 客户端!除了提供了 Redis 客户端的常见操作之外,还提供了 Redis 分布式锁、BloomFilter 布隆过滤器等强大的功能。在 redisson-examples 中,Redisson 官方提供了大量的示例。

4. 总结

redis的功能很强大,应用场景非常广。熟练掌握redis的一些特性对我们开发业务中会极大的提高效率。

今天的你多努力一点,明天的C位就是你!

一起学习成为 C位程序员。💋 微信公众号已开启,【C位程序员】,没关注的同学们记得关注哦!

部分参考 :www.iocoder.cn/Spring-Boot…

转载自:https://juejin.cn/post/6881541490649071624
评论
请登录