文章目录
一、概述
这里关于为什么要使用redis不做赘述,主要介绍SpringBoot集成redis做缓存,采用Spring Boot 框架提供的一个快速集成 Redis 数据库的启动器spring-boot-starter-data-redis
spring-boot-starter-data-redis
主要引入了三个依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.7.5</version>
<scope>compile</scope>
</dependency>
<!--引入的lettuce实现,这就是为啥默认是lettuce的原因-->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.10.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
1.spring-boot-starter
:自动装配的核心逻辑,SpringBoot
中默认可以自动装配的starter
也定义在其中的spring-boot-autoconfigure
中的spring.factories
文件中,本文要讲的RedisAutoConfiguration
就定义在其中。有了这个包后,即拥有了自动装配的能力了,例如新建一个springboot
项目,pom
中只引入spring-boot-starter-data-redis
包,其就能自动装配了。
2.spring-data-redis
:spring
对redis
相关操作的人性化封装,使得redis
的操作只需简单的调用接口即可,redis
的操作的实现过程则有lettuce
或jedis
驱动(客户端)实现,spring-data-redis
只是在接口层对他们做了统一。spring-data-redis
包依赖了jedis
和lettuce-core
这两个驱动,但是optional
为true
,即这两个依赖不会传递到spring-boot-starter-data-redis
中。
lettuce-core
就是spring-boot-starter-data-redis
选择的默然驱动
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>${lettuce}</version>
<optional>true</optional>
</dependency>
二、快速入门
2.1 pom文件中引入Redis依赖
<!-- spring-boot 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>
<!-- 阿里JSON解析器 Redis序列化使用 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.25</version>
</dependency>
2.2 application.yml中添加Redis配置
spring:
# 对应 RedisProperties 类
redis:
host: 192.168.10.110
port: 6379
database: 0
# Redis 连接超时时间,单位:毫秒。
timeout: 0
# 对应 RedisProperties.Jedis 内部类
jedis:
pool:
# 连接池最大连接数,默认为 8 。使用负数表示没有限制。
max-active: 8
# 默认连接数最小空闲的连接数,默认为 8 。使用负数表示没有限制。
max-idle: 8
# 默认连接池最小空闲的连接数,默认为 0 。允许设置 0 和 正数。
min-idle: 0
# 连接池最大阻塞等待时间,单位:毫秒。默认为 -1 ,表示不限制。
max-wait: -1
2.3 序列化
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
@Bean
public DefaultRedisScript<Long> limitScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(limitScriptText());
redisScript.setResultType(Long.class);
return redisScript;
}
/**
* 限流脚本
*/
private String limitScriptText() {
return "local key = KEYS[1]\n" +
"local count = tonumber(ARGV[1])\n" +
"local time = tonumber(ARGV[2])\n" +
"local current = redis.call('get', key);\n" +
"if current and tonumber(current) > count then\n" +
" return tonumber(current);\n" +
"end\n" +
"current = redis.call('incr', key)\n" +
"if tonumber(current) == 1 then\n" +
" redis.call('expire', key, time)\n" +
"end\n" +
"return tonumber(current);";
}
}
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
public FastJson2JsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
}
}
2.4 进一步封装RedisTemplate
package com.xmc.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @Title: RedisCache
* @Package: com.xmc.cache
* @Description: spring redis 工具类
* @Author: 李建奎
* @Date: 创建时间 2023-06-21
*/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Resource
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
2.5 测试
@SpringBootTest(classes = SpringBootCacheRedisApplication.class)
public class SpringBootCacheRedisApplicationTest {
@Resource
private RedisCache redisCache;
@Test
public void test() {
redisCache.setCacheObject("name", "小木虫");
final Object name = redisCache.getCacheObject("name");
System.out.println(name);
final ArrayList<User> users = ListUtil.toList(User.builder().name("张三").age(12).build()
, User.builder().name("里斯").age(16).build());
redisCache.setCacheList("users", users);
final List<Object> users1 = redisCache.getCacheList("users");
System.out.println(users1);
final HashMap<String, Object> maps = MapUtil.newHashMap();
maps.put("key1",users);
maps.put("key2",users);
redisCache.setCacheMap("maps", maps);
final Map<String, Object> maps1 = redisCache.getCacheMap("maps");
System.out.println(maps1);
}
}
三、理论进阶
3.1 Jedis和Lettuce的区别
-
Lettuce 是一个可伸缩的线程安全的 Redis 客户端,支持同步、异步和响应式模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题。它基于优秀 Netty NIO 框架构建,支持 Redis 的高级功能,如 Sentinel,集群,流水线,自动重新连接和 Redis 数据模型
-
Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接 Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
-
从 Spring Boot 2.x 开始 Lettuce 已取代 Jedis 成为首选 Redis 的客户端。当然 Spring Boot 2.x 仍然支持 Jedis,并且你可以任意切换客户端,只需排除io.lettuce:lettuce-core并添加redis.clients:jedis即可。就稳定性而言,个人建议使用Jedis
总之,Lettuce 和 Jedis 都有其优缺点,具体选择哪个取决于具体的应用场景和需求。如果需要处理高并发、多线程、事务等高级功能,或者需要支持 Sentinel 和 Cluster 集群模式,建议使用 Lettuce;如果对性能要求不太高,API 设计要求简单易用,可以考虑使用 Jedis。
3.2 Redis序列化
主要分成四类:
- JDK 序列化方式
- String 序列化方式
- JSON 序列化方式
- XML 序列化方式
默认使用的JdkSerializationRedisSerializer
序列化,key和value都会拼接一连串16进制字符(实际就是标志位 + 字符串长度 + 字符串内容),这样势必会对读取造成很大不方便,实际的使用场景还是JSON序列化便捷很多
实际使用场景中我们一般key采用StringRedisSerializer
进行序列化,value采用JSON序列化
四、项目中遇到的问题
主要记录个人使用过程中发现的问题,但具体问题需要具体分析
4.1 读取数据的时候提示缓存区不足
nested exception is io.lettuce.core.RedisException: java.lang.IndexOutOfB
oundsException: writerIndex(2147419686) + minWritableBytes(65536) exceeds maxCapacity(2147483647): PooledUnsafeDirectByteBuf(ridx: 2147419583,
widx: 2147419686, cap: 2147483647)
这个异常通常是由于 Redis 返回的数据超过了 Lettuce 的缓冲区容量所导致的。具体来说,Lettuce 在处理 Redis 响应时,会将响应数据暂存到 ByteBuf 缓冲区中,当数据量超过缓冲区容量时就会抛出 IndexOutOfBoundsException 异常。
我在项目中开启了2000多个任务,其中都会多次不断的访问redis,频繁的访问redis导致Lettuce 的缓冲区容量爆满所致
科普一下Lettuce的缓存区
在 Lettuce 中,缓冲区是一个 ByteBuf 对象,用于暂存从 Redis 服务器返回的响应数据。Lettuce 使用 Netty 作为底层网络通信框架,Netty 的 ByteBuf 是一个可扩展的、读写分离的缓冲区实现,可以高效地进行数据读写操作。
在 Lettuce 的缓冲区中,有以下几个重要的属性:
- readerIndex:表示当前缓冲区的读取位置。
- writerIndex:表示当前缓冲区的写入位置。
- capacity:表示当前缓冲区的容量大小。
当从 Redis 服务器返回的响应数据量较大时,如果超过了 Lettuce 缓冲区的容量大小,就会抛出 IndexOutOfBoundsException 异常。
4.2 redis命令超时
Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeOut
RedisCommandTimeoutException
异常表示 Redis 命令执行超时,通常出现在 Redis 服务器负载过高、网络状况不佳或者 Redis 命令执行时间过长等情况下。当 Redis 命令执行超时时,Lettuce 客户端会抛出该异常,以提示开发人员需要采取相应的措施来优化 Redis 操作
以下是一些可能导致 Redis 命令执行超时的原因和解决方法:
-
Redis 命令执行时间过长
如果 Redis 命令的执行时间过长,可能会导致连接池中的连接被占用太久,从而影响后续请求的处理。可以通过优化 Redis 查询语句、增加 Redis 实例的内存和 CPU 资源、分片等方式来减少 Redis 命令的执行时间。 -
网络状况不佳
如果网络延迟或带宽不足,可能会导致 Redis 命令的执行时间变长,甚至超时。可以尝试优化网络拓扑、调整网络参数、使用更高带宽的网络服务商等方式来改善网络状况。 -
Redis 服务器负载过高
如果 Redis 服务器负载过高,可能会导致 Redis 命令的执行时间变长,甚至超时。可以考虑扩容 Redis 集群、增加 Redis 实例的内存和 CPU 资源、优化 Redis 配置文件等方式来降低 Redis 服务器的负载。 -
Lettuce 客户端超时设置过短
Lettuce 客户端提供了多种超时设置,如连接超时、命令执行超时等。如果超时时间设置过短,可能会导致 Redis 命令执行超时。可以根据实际情况适当延长 Lettuce 的超时时间。
总之,RedisCommandTimeoutException 异常通常是由于 Redis 命令执行超时所导致的,可以通过优化 Redis 查询语句、改善网络状况、增加 Redis 实例的资源、优化 Redis 配置文件等方式来减少 Redis 命令的执行时间,从而避免此类异常的出现。
网上很多解决方案是采用Jedis客户端解决的,我这里也是直接切换的Jedis
4.3 java文件GBK写入redis中文乱码
idea中Java文件编码是GBK,将项目的编码方式修改程UTF-8