Springboot redis 入门示例

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是传递的 KEYVALUE 都是二进制值数组。为了方便查看我们需要序列化。


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

可以看到 大体逻辑

  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位程序员】,没关注的同学们记得关注哦!
在这里插入图片描述

部分参考 :http://www.iocoder.cn/Spring-Boot/Redis/?self

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值