概述
Spring Boot 1.4之前,Redis依赖的名称为:spring-boot-starter-redis
,1.4后改名为spring-boot-starter-data-redis
,成为Spring Data一员,而Spring Data项目定位为spring提供统一的数据仓库接口。
本文的源码基于如下3.2.4
版本:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.2.4</version>
</dependency>
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:
java
@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);
}
}
默认提供两个Bean,RedisTemplate和StringRedisTemplate,后者是前者的子类。两者方法基本一致,不同在于操作的数据类型不同,前者是一个泛型类,两个泛型都是Object,Key和Value都可以是对象。后者指明父类的两个泛型都是String,即Key和Value都只能是字符串。
入门使用
- 引入依赖
- 配置Redis连接
resources/application.yml
yml
spring:
redis:
host: 127.0.0.1
database: 0
port: 6379
password:
本机启动单点Redis即可,使用Redis的0号库作为默认库(默认有16个库)。
使用非常简单:
java
@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"));
}
}
在生产环境下,则需搭建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事务相关命令:
java
void watch(K key);
void watch(Collection<K> keys);//重载方法
void unwatch();
void multi();
void discard();
List<Object> exec();
List<Object> exec(RedisSerializer<?> valueSerializer);//重载方法
Redis Server
相关命令:
java
List<RedisClientInfo> getClientList();
void killClient(String host, int port);
void replicaOf(String host, int port);
void replicaOfNoOne();
Long convertAndSend(String channel, Object message);
Operations
提供各种Operations操作,这些最终转化为RedisCallback来执行的。也就是说通过使用RedisCallback可以实现更强的功能。通常不直接操作键值,而是通过opsForXxx()
访问;实现RedisOperations接口,这个接口定义一系列与Redis相关的基础数据操作接口,数据类型分别与下列API对应:
java
// 非绑定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);
若以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脚本完成原子性操作。
java
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
return scriptExecutor.execute(script, keys, args);
}
完成对Lua脚本的调用。Redis+Lua脚本实现分布式的应用限流。
序列化
RedisTemplate类里声明的一系列序列化器:
java
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();
在afterPropertiesSet
方法中可看到默认的序列化方案:
java
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;
}
默认的方案是使用JdkSerializationRedisSerializer,使用Redis Desktop Manager等工具查看时不太友好。字符串和使用JDK序列化之后的字符串是两个概念。
查看set方法的源码:
java
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);
}
最终与Redis交互使用的是原生connection,键值则全部是字节数组,意味着所有的序列化都依赖于应用层完成,Redis只认字节!
StringRedisSerializer
StringRedisTemplate继承自RedisTemplate,提供StringRedisSerializer的实现:
java
public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
setKeySerializer(RedisSerializer.string());
setValueSerializer(RedisSerializer.string());
setHashKeySerializer(RedisSerializer.string());
setHashValueSerializer(RedisSerializer.string());
}
}
即只能存取字符串。使用什么样的序列化器序列化,就必须使用同样的序列化器反序列化。
RedisSerializer
接口源码如下:
java
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;
}
}
其实现类有:
- JdkSerializationRedisSerializer:默认使用的序列化方案
- StringRedisSerializer:StringRedisTemplate使用
- GenericToStringSerializer:依赖于内部的ConversionService,将所有的类型转存为字符串
- GenericJackson2JsonRedisSerializer:以JSON的形式序列化对象
- Jackson2JsonRedisSerializer:以JSON的形式序列化对象
- OxmSerializer:以XML的形式序列化对象
可以将全局的RedisTemplate覆盖,也可以在使用时在局部实例化一个RedisTemplate替换(不依赖于IOC容器)需要根据实际的情况选择替换的方式,以Jackson2JsonRedisSerializer为例:
java
@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;
}
Kryo
也可以考虑根据自己项目和需求的特点,扩展序列化器。如为了追求性能,可能考虑使用Kryo序列化器替换缓慢的JDK序列化器:
java
@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
}
}
}
}
Kyro线程不安全,使用一个ThreadLocal来维护,也可以挑选其他高性能的序列化方案如Hessian,Protobuf。
属性配置
项目开发里,存在个性化的属性配置,参考RedisProperties源码。
RedisCallback
TODO
SessionCallback
TODO