Redis 提供事务(Transaction)和管道(Pipeline),两个概念都不复杂,可以对比来看,管道可以广泛用于提升 Redis 读写效率。
Redis Transactions
Redis 原本通过 multi
exec
和 discard
指令执行事务。RedisTemplate<>
本身提供三个同名方法来支持事务,但是 RedisTemplate
本身封装程度高,不能保证这种事务定义下的所有操作在同一次 Redis 连接中被执行。
因此,作为一种封装度更低的替代,Spring Data Redis 提供 SessionCallback
来确保多个操作在同一次 Redis 链接中进行。
//execute a transaction
List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForSet().add("key", "value1");
// This will contain the results of all operations in the transaction
return operations.exec();
}
});
System.out.println("Number of items added to set: " + txResults.get(0));
SessionCallback
的 execute
返回值仍会经过 RedisTemplate
的反序列化操作 ,然后作为 RedisTemplate
的 execute
结果返回。
1.1 后对于连接返回值的转换与否发生了变化
As of version 1.1, an important change has been made to the
exec
methods ofRedisConnection
andRedisTemplate
. Previously, these methods returned the results of transactions directly from the connectors. This means that the data types often differed from those returned from the methods ofRedisConnection
. For example,zAdd
returns a boolean indicating whether the element has been added to the sorted set. Most connectors return this value as a long, and Spring Data Redis performs the conversion. Another common difference is that most connectors return a status reply (usually the string,OK
) for operations such asset
. These replies are typically discarded by Spring Data Redis. Prior to 1.1, these conversions were not performed on the results ofexec
. Also, results were not deserialized inRedisTemplate
, so they often included raw byte arrays. If this change breaks your application, setconvertPipelineAndTxResults
tofalse
on yourRedisConnectionFactory
to disable this behavior.
@Transactional
RedisTemplate
默认不能加 @Transactional
标签,但可以通过 template.setEnableTransactionSupport(true)
来开启。
Enabling transaction support binds
RedisConnection
to the current transaction backed by aThreadLocal
. If the transaction finishes without errors, the Redis transaction gets commited withEXEC
, otherwise rolled back withDISCARD
.
但这样做存在一些限制
// must be performed on thread-bound connection
template.opsForValue().set("thing1", "thing2");
// read operation must be run on a free (not transaction-aware) connection
template.keys("*");
// returns null as values set within a transaction are not visible
template.opsForValue().get("thing1");
Redis Pipelining
Redis 管道允许一次性发送多条指令而不单独等待每条的回复(即不用等待 RTT),所有恢复将在最后一次性读取。这样做会提升指令发送速度,尤其在向一个 List 中添加大量元素时。
如果完全不关注返回结果,可以使用 RedisTemplate
的 execute
方法,传递 true
的 pipeline 参数。executePipelined
方法以 RedisCallback
或 SessionCallback
为输入,在一个 pipeline 中完成运行并返回所有结果。
以下是一个通过 pipeline 从队列右端 pop
出一堆元素的方法:
List<Object> results = stringRedisTemplate.executePipelined(
new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisConnection stringRedisConn = (StringRedisConnection)connection;
for(int i=0; i< batchSize; i++) {
stringRedisConn.rPop("myqueue");
}
return null;
}
});
两者区别
首先,Pipline 和 Transaction 是完全不同的两种机制,互相不能够替代——
-
Pipeline 本质是客户端对指令进行打包发送的行为,服务端是透明的;Transaction 本质是服务端执行指令时进行的打包,由客户端指令指挥
因此,Pipeline 最终要的作用是减少客户端等待时间,从而提升客户端程序性能,不能保证原子性(服务端只会看作一系列普通指令,当然不会保证原子性的)。而 Transaction 因为使用 MULTI/EXEC 事务机制,虽然从 SQL 事务 ACID 角度来看不满足原子性,但如果全部指令都能被正确执行,程序上是满足原子性的,执行过程中不会被打断。
-
Pipeline 不会阻塞服务端,但 Transaction 会阻塞服务端
实际应用中使用 LUA 脚本来满足原子性要比 Transaction 高效得多。考虑网络吞吐问题,需要高频执行的脚本甚至可以预加载到服务端,需要调用时直接传脚本 SHA1(在预加载时会由服务端发回) + 参数 来进行调用。
LUA
值得注意的一点是启动程序时可以将 LUA 脚本预加载到 Redis 服务器
@PostConstruct
public void loadScript() {
String execute = strRedisTemplate.execute((RedisCallback<String>) connection ->
connection.scriptLoad(flashSaleIfExistScript.getScriptAsString().getBytes()));
}
scriptLoad
的返回结果是脚本对应的 SHA1,但是我们其实并不需要保存,因为RedisScript
本身在加载脚本时会计算一次 SHA1 ,可以直接通过 getSha1()
获得。
需要调用时直接传脚本 SHA1(在预加载时会由服务端发回) + 参数 来进行调用。Redis 支持的脚本执行指令包括 eval
和 evalsha
两个,Redis Template 将这两条指令都封装在了 org.springframework.data.redis.connection
包中 RedisScriptingCommands
接口下。
ScriptExecutor
在执行时会首先通过evalsha
来执行脚本,如果 Redis 脚本缓存中没有对应脚本再退回到eval
方法。
根据以上官方文档的描述,我们不需要主动调用 evalsha
,ScriptExecutor
会自动替我们完成。那我们来看看 Spring Data Redis 源码怎么写的,先找到 DefaultScriptExecutor
。
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
return this.execute(script, this.template.getValueSerializer(), this.template.getValueSerializer(), keys, args);
}
public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) {
return this.template.execute((connection) -> {
ReturnType returnType = ReturnType.fromJavaType(script.getResultType());
byte[][] keysAndArgs = this.keysAndArgs(argsSerializer, keys, args);
int keySize = keys != null ? keys.size() : 0;
if (!connection.isPipelined() && !connection.isQueueing()) {
return this.eval(connection, script, returnType, keySize, keysAndArgs, resultSerializer);
} else {
connection.eval(this.scriptBytes(script), returnType, keySize, keysAndArgs);
return null;
}
});
}
可以看到,除非在管道或队列操作中,否则所有的 execute
最终都会落到 eval
这个方法上。
protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType, int numKeys, byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {
Object result;
try {
result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);
} catch (Exception var9) {
if (!ScriptUtils.exceptionContainsNoScriptError(var9)) {
throw var9 instanceof RuntimeException ? (RuntimeException)var9 : new RedisSystemException(var9.getMessage(), var9);
}
result = connection.eval(this.scriptBytes(script), returnType, numKeys, keysAndArgs);
}
return script.getResultType() == null ? null : this.deserializeResult(resultSerializer, result);
}
确实,evalSha
如果不能得到正确结果就会抛异常,转而用 eval
方法了。这对于不预载脚本的场景是不是低效了点?