概述

Spring Boot 1.4之前,Redis依赖的名称为:spring-boot-starter-redis,1.4后改名为spring-boot-starter-data-redis,成为Spring Data一员,而Spring Data项目定位为spring提供统一的数据仓库接口。

本文的源码基于如下3.2.4版本:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<version>3.2.4</version>
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

Spring Data Redis底层还是使用Jedis来间接操作Redis,摒弃Jedis中不好的设计,对Jedis中大量API进行归类封装,将同一类型操作封装为Operations接口;而连接池由Spring Data Redis自动管理(基于Apache Commons Pool2),提供高度封装的RedisTemplate类。

在Spring Boot 2.0后不再使用Jedis,因为Jedis采用直连方式存在流阻塞(BIO);改为Lettuce驱动Redis,同时引入Netty、Reactor等技术栈。

自动配置

即RedisAutoConfiguration:

@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
	@Bean
	@ConditionalOnMissingBean(RedisConnectionDetails.class)
	PropertiesRedisConnectionDetails redisConnectionDetails(RedisProperties properties) {
		return new PropertiesRedisConnectionDetails(properties);
	}

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		return new StringRedisTemplate(redisConnectionFactory);
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

默认提供两个Bean,RedisTemplate和StringRedisTemplate,后者是前者的子类。两者方法基本一致,不同在于操作的数据类型不同,前者是一个泛型类,两个泛型都是Object,Key和Value都可以是对象。后者指明父类的两个泛型都是String,即Key和Value都只能是字符串。

入门使用

  1. 引入依赖
  2. 配置Redis连接resources/application.yml
spring:
 redis:
   host: 127.0.0.1
   database: 0
   port: 6379
   password:
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

本机启动单点Redis即可,使用Redis的0号库作为默认库(默认有16个库)。

使用非常简单:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisDemoApplication.class)
public class TestRedis {
	@Resource
	private StringRedisTemplate stringRedisTemplate;
	@Resource
	private RedisTemplate<String, User> redisTemplate;
	
	@Test
	public void test() {
		stringRedisTemplate.opsForValue().set("aaa", "111");
		redisTemplate.opsForValue().set("bbb", new User("cc@qq.com", "dd"));
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

在生产环境下,则需搭建Redis Sentinel或Redis Cluster两种模式保证Redis集群的高可用。

RedisTemplate

RedisTemplate,模板方法设计模式,源码是这样分类的:

  • execute类:execute有6个重载方法,executePipelined有4个,还有1个executeWithStickyConnection
  • Redis键:支持原生Redis指令的命令,如:
  • hasKey:判断是否存在某个Key
  • delete:删除Key
  • type:查询Key的类型
  • unlink:将键从键空间中解除链接。与delete不同,此命令的内存回收是异步发生的
  • randomKey:随机返回一个Key
  • rename:重命名Key
  • expire/expireAt:为给定Key设置TTL时间,Time To Live
  • getExpire:返回给定Key的TTL时间
  • 排序:有5个重载方法
  • 事务
  • Redis Server
  • Operations
execute

execute方法有2类共6个:

  • 用于提交并执行Lua脚本的2个;
  • 用于指定RedisCallback、SessionCallback的4个;

RedisCallback:让RedisTemplate进行回调,通过他们可以在同一条连接中执行多个Redis命令;

SessionCallback:相比RedisCallback的优势在于SessionCallback提供良好的封装。

RedisCallback和SessionCallback都是在一个连接里,防止每执行一条命令创建一次连接。

事务

Redis事务相关命令:

void watch(K key);
void watch(Collection<K> keys);//重载方法
void unwatch();
void multi();
void discard();
List<Object> exec();
List<Object> exec(RedisSerializer<?> valueSerializer);//重载方法
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
Redis Server

相关命令:

List<RedisClientInfo> getClientList();
void killClient(String host, int port);
void replicaOf(String host, int port);
void replicaOfNoOne();
Long convertAndSend(String channel, Object message);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
Operations

提供各种Operations操作,这些最终转化为RedisCallback来执行的。也就是说通过使用RedisCallback可以实现更强的功能。通常不直接操作键值,而是通过opsForXxx()访问;实现RedisOperations接口,这个接口定义一系列与Redis相关的基础数据操作接口,数据类型分别与下列API对应:

// 非绑定key操作有9类共10个
ClusterOperations<K, V> opsForCluster();
GeoOperations<K, V> opsForGeo();//Geo地理空间
<HK, HV> HashOperations<K, HK, HV> opsForHash();
HyperLogLogOperations<K, V> opsForHyperLogLog();
ListOperations<K, V> opsForList();
SetOperations<K, V> opsForSet();
<HK, HV> StreamOperations<K, HK, HV> opsForStream();
<HK, HV> StreamOperations<K, HK, HV> opsForStream(HashMapper<? super K, ? super HK, ? super HV> hashMapper);
ValueOperations<K, V> opsForValue();
ZSetOperations<K, V> opsForZSet();
// 绑定key操作有7个
BoundGeoOperations<K, V> boundGeoOps(K key);
<HK, HV> BoundHashOperations<K, HK, HV> boundHashOps(K key);
BoundListOperations<K, V> boundListOps(K key);
BoundSetOperations<K, V> boundSetOps(K key);
<HK, HV> BoundStreamOperations<K, HK, HV> boundStreamOps(K key);
BoundValueOperations<K, V> boundValueOps(K key);
BoundZSetOperations<K, V> boundZSetOps(K key);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

若以bound开头,则意味着在操作之初就会绑定一个Key,后续的所有操作便默认是对该Key的操作。

CAS操作

CAS,Compare and Set,通常有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。CAS也通常与并发,乐观锁,非阻塞,机器指令等关键词放到一起讲解。

通过redisTemplate.opsForValue()redisTemplate.boundValueOps()可得到一个ValueOperations或BoundValueOperations接口(以值为字符串的操作接口为例)。这些xxxOperations都是接口,提供基础操作外,还提供一系列CAS操作,几乎都有重载方法:

  • setIfAbsent:将Key的值设为Value,当且仅当Key不存在时,设置成功返回1,设置失败返回0;
  • getAndSet:将给定Key的值设为Value,并返回旧值(Old Value);
  • increment:将Key所储存的值加上增量delta(如果方法没有delta,则加1)。如果Key不存在,则Key的值会先被初始化为0再执行

发布订阅

Redis内置channel机制,可以用于实现分布式的队列和广播。RedisTemplate.convertAndSend()用于发送消息,与RedisMessageListenerContainer配合接收,可以实现一个简易的发布订阅。

Lua脚本

Redis内置Lua的解析器,RedisTemplate中包含一个Lua执行器ScriptExecutor,可执行Lua脚本完成原子性操作。

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
	return scriptExecutor.execute(script, keys, args);
}
  • 1.
  • 2.
  • 3.

完成对Lua脚本的调用。Redis+Lua脚本实现分布式的应用限流。

序列化

RedisTemplate类里声明的一系列序列化器:

private boolean enableDefaultSerializer = true;// 配置默认序列化器
private @Nullable RedisSerializer<?> defaultSerializer;
private @Nullable ClassLoader classLoader;
private @Nullable RedisSerializer keySerializer = null;
private @Nullable RedisSerializer valueSerializer = null;
private @Nullable RedisSerializer hashKeySerializer = null;
private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

afterPropertiesSet方法中可看到默认的序列化方案:

public void afterPropertiesSet() {
	super.afterPropertiesSet();
	if (defaultSerializer == null) {
		defaultSerializer = new JdkSerializationRedisSerializer(classLoader != null ? classLoader : this.getClass().getClassLoader());
	}
	if (enableDefaultSerializer) {
		if (keySerializer == null) {
			keySerializer = defaultSerializer;
		}
		if (valueSerializer == null) {
			valueSerializer = defaultSerializer;
		}
		if (hashKeySerializer == null) {
			hashKeySerializer = defaultSerializer;
		}
		if (hashValueSerializer == null) {
			hashValueSerializer = defaultSerializer;
		}
	}
	if (scriptExecutor == null) {
		this.scriptExecutor = new DefaultScriptExecutor<>(this);
	}
	initialized = true;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

默认的方案是使用JdkSerializationRedisSerializer,使用Redis Desktop Manager等工具查看时不太友好。字符串和使用JDK序列化之后的字符串是两个概念。

查看set方法的源码:

public void set(K key, V value) {
	byte[] rawValue = rawValue(value);
	execute(new ValueDeserializingRedisCallback(key) {
		protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
			connection.set(rawKey, rawValue);
			return null;
		}
	}, true);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

最终与Redis交互使用的是原生connection,键值则全部是字节数组,意味着所有的序列化都依赖于应用层完成,Redis只认字节!

StringRedisSerializer

StringRedisTemplate继承自RedisTemplate,提供StringRedisSerializer的实现:

public class StringRedisTemplate extends RedisTemplate<String, String> {
	public StringRedisTemplate() {
		setKeySerializer(RedisSerializer.string());
		setValueSerializer(RedisSerializer.string());
		setHashKeySerializer(RedisSerializer.string());
		setHashValueSerializer(RedisSerializer.string());
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

即只能存取字符串。使用什么样的序列化器序列化,就必须使用同样的序列化器反序列化。

RedisSerializer

接口源码如下:

public interface RedisSerializer<T> {
	static RedisSerializer<Object> java() {
		return java((ClassLoader)null);
	}
	
	static RedisSerializer<Object> java(@Nullable ClassLoader classLoader) {
		return new JdkSerializationRedisSerializer(classLoader);
	}
	
	static RedisSerializer<Object> json() {
		return new GenericJackson2JsonRedisSerializer();
	}
	
	static RedisSerializer<String> string() {
		return StringRedisSerializer.UTF_8;
	}
	
	static RedisSerializer<byte[]> byteArray() {
		return ByteArrayRedisSerializer.INSTANCE;
	}
	
	@Nullable
	byte[] serialize(@Nullable T value) throws SerializationException;
	
	@Nullable
	T deserialize(@Nullable byte[] bytes) throws SerializationException;
	
	default boolean canSerialize(Class<?> type) {
		return ClassUtils.isAssignable(this.getTargetType(), type);
	}
	
	default Class<?> getTargetType() {
		return Object.class;
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

其实现类有:

  • JdkSerializationRedisSerializer:默认使用的序列化方案
  • StringRedisSerializer:StringRedisTemplate使用
  • GenericToStringSerializer:依赖于内部的ConversionService,将所有的类型转存为字符串
  • GenericJackson2JsonRedisSerializer:以JSON的形式序列化对象
  • Jackson2JsonRedisSerializer:以JSON的形式序列化对象
  • OxmSerializer:以XML的形式序列化对象

可以将全局的RedisTemplate覆盖,也可以在使用时在局部实例化一个RedisTemplate替换(不依赖于IOC容器)需要根据实际的情况选择替换的方式,以Jackson2JsonRedisSerializer为例:

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
	RedisTemplate template = new RedisTemplate();
	template.setConnectionFactory(factory);
	Jackson2JsonRedisSerializer jackson = new Jackson2JsonRedisSerializer(Object.class);
	// 修改Jackson序列化默认行为
	ObjectMapper mapper = new ObjectMapper();
	mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
	mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
	jackson.setObjectMapper(mapper);
	// 指定RedisTemplate的Key和Value的序列化器
	template.setKeySerializer(new StringRedisSerializer());
	template.setValueSerializer(jackson);
	template.afterPropertiesSet();
	return template;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
Kryo

也可以考虑根据自己项目和需求的特点,扩展序列化器。如为了追求性能,可能考虑使用Kryo序列化器替换缓慢的JDK序列化器:

@Slf4j
public class KryoRedisSerializer<T> implements RedisSerializer<T> {
	private static final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
		protected Kryo initialValue() {
			Kryo kryo = new Kryo();
			return kryo;
		};
	};
	
	@Override
	public byte[] serialize(Object obj) throws SerializationException {
		if (obj == null) {
			throw new RuntimeException("serialize param must not be null");
		}
		Kryo kryo = kryos.get();
		Output output = new Output(64, -1);
		try {
			kryo.writeClassAndObject(output, obj);
			return output.toBytes();
		} finally {
			closeOutputStream(output);
		}
	}
	
	@Override
	public T deserialize(byte[] bytes) throws SerializationException {
		if (bytes == null) {
			return null;
		}
		Kryo kryo = kryos.get();
		Input input = null;
		try {
			input = new Input(bytes);
			return (T) kryo.readClassAndObject(input);
		} finally {
			closeInputStream(input);
		}
	}
	
	private static void closeOutputStream(OutputStream output) {
		if (output != null) {
			try {
				output.flush();
				output.close();
			} catch (Exception e) {
				// logging
			}
		}
	}
	
	private static void closeInputStream(InputStream input) {
		if (input != null) {
			try {
				input.close();
			} catch (Exception e) {
				// logging
			}
		}
	}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

Kyro线程不安全,使用一个ThreadLocal来维护,也可以挑选其他高性能的序列化方案如Hessian,Protobuf。

属性配置

项目开发里,存在个性化的属性配置,参考RedisProperties源码。

RedisCallback

TODO

SessionCallback

TODO