spring-data-redis中的坑和误区

来源:互联网 发布:c语言输出整数 编辑:程序博客网 时间:2024/06/06 02:32

spring的redis模板中有问题的地方

在项目开发过程中,想要进行redis的并发控制,这时候,想当然地使用了spring-data-redis库中template里面提供的multi()和exec()方法,如下:

redis.multi() // do something hereredis.exec()

使用了之后,就出现了如下异常:

org.springframework.dao.InvalidDataAccessApiUsageException: ERR EXEC without MULTI; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR EXEC without MULTI

在google搜了一下,才知道,原来是目前该库的redistemplate的multi和exec方法,都是新产生连接,而非使用本来的连接,这个异常,也是由于这个原因所以才导致的。(因为新连接中,直接执行退出同步,系统肯定会去找是从哪儿开始同步的,这一找,发现没有开始同步的命令,就会抛出异常了)

这个时候,只能自己调用底层的RedisCallBack和Jedis去实现底层redis语句了。类似这样:

Jedis jedis = new Jedis("localhost",6379);  new RedisCallback<Object>() {      public Object doInRedis(RedisConnection connection) throws DataAccessException {          connection.multi();          return null;      }  }.doInRedis(new JedisConnection(jedis));  

这时候肯定会有个疑问,既然这个template每次都会生成新连接,那这个multi和exec命令还有什么用?官方是这么回答的:

The methods are exposed in case the connection is shared across methods. Currently we don’t provide any out of the box support for connection binding but the RedisTemplate supports it - just like with the rest of the templates, one connection could be bound to the running thread and the RT will use it without creating new ones.

大致意思是这些方法目前是没有用的。但实际上这个模板是能够支持的。可以看到RedisTemplate中有这么一个方法:

<T> T execute(SessionCallback<T> session);

其参数类型是SessionCallBack而不是RedisCallback。我们平时在persist、或者Ofs的时候,模板默认调用的是参数类型为RedisCallback的函数:

<T> T execute(RedisCallback<T> action);

我们看一下他的实现:

public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {    Assert.notNull(action, "Callback object must not be null");    RedisConnectionFactory factory = getConnectionFactory();    RedisConnection conn = RedisConnectionUtils.getConnection(factory);    boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);    preProcessConnection(conn, existingConnection);    boolean pipelineStatus = conn.isPipelined();    if (pipeline && !pipelineStatus) {        conn.openPipeline();    }    try {        RedisConnection connToExpose = (exposeConnection ? conn : createRedisConnectionProxy(conn));        T result = action.doInRedis(connToExpose);        // close pipeline        if (pipeline && !pipelineStatus) {            conn.closePipeline();        }        // TODO: any other connection processing?        return postProcessResult(result, conn, existingConnection);    } finally {        RedisConnectionUtils.releaseConnection(conn, factory);    }}

虽然是从工厂拿出来的,但是可以预见其与并无法将事务与该连接绑定。而我们再来看下参数类型为SessionCallback的execute方法源码:

public <T> T execute(SessionCallback<T> session) {    RedisConnectionFactory factory = getConnectionFactory();    // bind connection    RedisConnectionUtils.bindConnection(factory);    try {        return session.execute(this);    } finally {        RedisConnectionUtils.unbindConnection(factory);    }}

可以看出来,其会对连接进行绑定和解绑。因而其从功能上应该是能够实现对事务的支持的。在CollectionUtils源码中,可以找到对该方法的运用,可以很明显看到。其通过如下代码实现了监听+事务管理。

static <K> void rename(final K key, final K newKey, RedisOperations<K, ?> operations) {    operations.execute(new SessionCallback<Object>() {        @SuppressWarnings("unchecked")        public Object execute(RedisOperations operations) throws DataAccessException {            do {                operations.watch(key);                if (operations.hasKey(key)) {                    operations.multi();                    operations.rename(key, newKey);                }                else {                    operations.multi();                }            } while (operations.exec() == null);            return null;        }    });}

除此之外,iteye上还有人指出了这个模板的其他问题,可参考点击我

使用redis事务时需要注意的地方

说到了事务,不得不再提醒大家一个很容易忽视的问题。拿stackoverflow网站上的一个经典问答来做例子:

redis.multi() current = redis.get('powerlevel') redis.set('powerlevel', current + 1) redis.exec()

其事务法是无法生效的,是因为redis在MULTI/EXEC代码块中,命令都会被delay,放入Queue中,而不会直接返回对应的值。即例子中的get将返回一个字符串“QUEUED”,从而set中无法实现自增。要完成这一切,请使用:

redis.watch('powerlevel') current = redis.get('powerlevel') redis.multi() redis.set('powerlevel', current + 1) redis.exec()

通过监听powerlevel key来实现数据的一致性。一旦该key被其他会话修改,这个事务即会失败。同时由于将get操作从MULTI块中提出,从而避免了无法获取数据的情况。

0 0
原创粉丝点击