背景
需要使用
setIfAbsent(Object key, Object value, long timeout, TimeUnit unit)
方法,然后当前版本不支持,所以自己定义一个这样的方法
思路: 使用redis
的事务操作,把保存key和设置过期时间放到一个事务执行
- 切换事务模式
- 事务队列
- 执行事务
redis客户端执行multi后返回ok,表明redis进入事务状态。进入事务状态以后redis并不会立即执行命令,会将redis客户端发送的命令存入队列,暂不执行,此时返回queued。最后调用exec,将命令从队列中取出来,然后一次性执行,这些,命令同时成功同时失败,最后将命令执行结果一次性返回,并且将事务状态标志复位。
在执行这些命令的过程中,使用同一客户端,并且不会被其它客户端中断
试一下
redisTemplate.multi();
redisTemplate.opsForValue().setIfAbsent(key,JSON.toJSONString(value));
redisTemplate.expire(key,timeout, unit);
redisTemplate.exec();
报错了
org.springframework.dao.InvalidDataAccessApiUsageException: No ongoing transaction. Did you forget to call multi?
at org.springframework.data.redis.connection.jedis.JedisConnection.exec(JedisConnection.java:785) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at org.springframework.data.redis.connection.DefaultStringRedisConnection.exec(DefaultStringRedisConnection.java:252) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_131]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_131]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_131]
at org.springframework.data.redis.core.CloseSuppressingInvocationHandler.invoke(CloseSuppressingInvocationHandler.java:57) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at com.sun.proxy.$Proxy197.exec(Unknown Source) ~[?:?]
at org.springframework.data.redis.core.RedisTemplate$3.doInRedis(RedisTemplate.java:622) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at org.springframework.data.redis.core.RedisTemplate$3.doInRedis(RedisTemplate.java:620) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:207) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:157) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at org.springframework.data.redis.core.RedisTemplate.execRaw(RedisTemplate.java:620) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
at org.springframework.data.redis.core.RedisTemplate.exec(RedisTemplate.java:607) ~[spring-data-redis-1.8.9.RELEASE.jar!/:?]
异常的意思很明显
没有开启的事务,你是不是忘了执行
multi()
?
不是明明开启了吗?
如果是jedis的话这样确实就可以了,但是springboot-data-redis对jedis封装后就不一样了,出现上述异常是因为在执行jedis在执行exec的时候transaction属性为空
@Override
public List<Object> exec() {
try {
if (isPipelined()) {
pipeline(newJedisResult(getRequiredPipeline().exec(),
new TransactionResultConverter<>(new LinkedList<>(txResults), JedisConverters.exceptionConverter())));
return null;
}
if (transaction == null) {
throw new InvalidDataAccessApiUsageException("No ongoing transaction. Did you forget to call multi?");
}
List<Object> results = transaction.exec();
return !CollectionUtils.isEmpty(results)
? new TransactionResultConverter<>(txResults, JedisConverters.exceptionConverter()).convert(results)
: results;
} catch (Exception ex) {
throw convertJedisAccessException(ex);
} finally {
txResults.clear();
transaction = null;
}
}
既然这里的transaction
是null
,那么在那里赋值呢 接着看
private static void potentiallyRegisterTransactionSynchronisation(RedisConnectionHolder connHolder,
final RedisConnectionFactory factory) {
if (isActualNonReadonlyTransactionActive()) {
if (!connHolder.isTransactionSyncronisationActive()) {
connHolder.setTransactionSyncronisationActive(true);
RedisConnection conn = connHolder.getConnection();
conn.multi();//jedis开启事务
TransactionSynchronizationManager
.registerSynchronization(new RedisTransactionSynchronizer(connHolder, conn, factory));
}
}
}
@Override
public void multi() {
//定义transaction
this.transaction = jedis.multi();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
可以看到
potentiallyRegisterTransactionSynchronisation()时会调用jedis的multi从而开启redis的事务,jedis的multi()里完成了transaction的定义
继续看
@Nullable
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
RedisConnectionFactory factory = getRequiredConnectionFactory();
RedisConnection conn = null;
try {
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
T result = action.doInRedis(connToExpose);
// close pipeline
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
在执行redis excute()时都会去判断enableTransactionSupport
这个属性.决定开启一个链接还是获取一个链接(从池里)
但是两个方法最后都会追踪到这个方法
public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
boolean enableTransactionSupport) {
RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
if (connHolder != null) {
if (enableTransactionSupport) {
potentiallyRegisterTransactionSynchronisation(connHolder, factory);
}
return connHolder.getConnection();
}
RedisConnection conn = factory.getConnection();
if (bind) {
RedisConnection connectionToBind = conn;
if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
connectionToBind = createConnectionProxy(conn, factory);
}
connHolder = new RedisConnectionHolder(connectionToBind);
TransactionSynchronizationManager.bindResource(factory, connHolder);
if (enableTransactionSupport) {
potentiallyRegisterTransactionSynchronisation(connHolder, factory);
}
return connHolder.getConnection();
}
return conn;
}
会根据enableTransactionSupport
这个属性来决定是否调用potentiallyRegisterTransactionSynchronisation()
这个方法,而这个就是我们前面找到的定义transaction
的地方
解决
那我们这样不就可以了
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
redisTemplate.opsForValue().setIfAbsent(key,JSON.toJSONString(value));
redisTemplate.expire(key,timeout, unit);
redisTemplate.exec();
但是这样写有个很严重的问题,redisTemplate.exec()
这个方法并不会去关闭连接,不信你自己去看源码
public List<Object> exec() {
//看源码的顺序1
List<Object> results = this.execRaw();
return this.getConnectionFactory().getConvertPipelineAndTxResults() ? this.deserializeMixedResults(results, this.valueSerializer, this.hashKeySerializer, this.hashValueSerializer) : results;
}
protected List<Object> execRaw() {
return (List)this.execute(new RedisCallback<List<Object>>() {
public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
//看源码的顺序2
return connection.exec();
}
});
}
//看源码的顺序3,去掉不关心的,
@Nullable
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
RedisConnectionFactory factory = getRequiredConnectionFactory();
RedisConnection conn = null;
try {
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}
T result = action.doInRedis(connToExpose);
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
//这里关闭
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
虽然最终会调用releaseConnection();
public static void releaseConnection(@Nullable RedisConnection conn, RedisConnectionFactory factory) {
if (conn == null) {
return;
}
RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
if (connHolder != null && connHolder.isTransactionSyncronisationActive()) {
if (log.isDebugEnabled()) {
log.debug("Redis Connection will be closed when transaction finished.");
}
return;
}
// release transactional/read-only and non-transactional/non-bound connections.
// transactional connections for read-only transactions get no synchronizer registered
if (isConnectionTransactional(conn, factory) && TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
unbindConnection(factory);
} else if (!isConnectionTransactional(conn, factory)) {
if (log.isDebugEnabled()) {
log.debug("Closing Redis Connection");
}
conn.close();
}
}
但是你并没有设置isTransactionSyncronisationActive
为false
,所以并不能关闭连接。这样的话将会导致连接池泄露。
那么正确的姿势是什么呢?
sessionCallback
public Boolean setIfAbsent(String key, Object value, long timeout, TimeUnit unit) {
// 使用sessionCallBack处理
SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
redisTemplate.opsForValue().setIfAbsent(key,JSON.toJSONString(value));
redisTemplate.expire(key,timeout, unit);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
};
return redisTemplate.execute(sessionCallback);
}
而且sessionCallback
的源码可以看到,
- operations.multi()直接使用jedis的multi会给transaction复制
- 最后会将连接释放
public <T> T execute(SessionCallback<T> session) {
RedisConnectionFactory factory = this.getConnectionFactory();
RedisConnectionUtils.bindConnection(factory, this.enableTransactionSupport);
Object var3;
try {
var3 = session.execute(this);
} finally {
RedisConnectionUtils.unbindConnection(factory);
}
return var3;
}
先就这样吧,吃饭去了