Redis系列之Spring Data Redis

概述

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都只能是字符串。

入门使用

  1. 引入依赖
  2. 配置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 SentinelRedis 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

参考

相关推荐
虾球xz1 分钟前
游戏引擎学习第64天
redis·学习·游戏引擎
Hello.Reader7 小时前
Redis大Key问题全解析
数据库·redis·bootstrap
B1nna10 小时前
Redis学习(三)缓存
redis·学习·缓存
A227416 小时前
Redis——缓存雪崩
java·redis·缓存
weisian15116 小时前
Redis篇--应用篇3--数据统计(排行榜,计数器)
数据库·redis·缓存
言之。16 小时前
Redis单线程快的原因
数据库·redis·缓存
LYX369320 小时前
Docker 安装mysql ,redis,nacos
redis·mysql·docker
计算机毕设定制辅导-无忧学长20 小时前
Redis 持久化机制详解
redis
奋斗的老史1 天前
Spring Retry + Redis Watch实现高并发乐观锁
java·redis·spring
loop lee1 天前
Redis - Token & JWT 概念解析及双token实现分布式session存储实战
java·redis