记一次springboot 中使用redis分布式锁引发的问题

记一次springboot 中使用redis分布式锁引发的问题

我们知道spring redis为我们提供了两个非常有用的模板:RedisTemplate,StringRedisTemplate。
1.主要分析一下RedisTemplate,我们主要看一下key和value使用的是什么序列化。

public void afterPropertiesSet() {

		super.afterPropertiesSet();

		boolean defaultUsed = false;

		if (defaultSerializer == null) {
			// 很明显,默认的序列化使用的是JDK的序列化。
			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}

		if (enableDefaultSerializer) {
			// 没有指定的情况下,key 和value都是使用默认的序列化
			if (keySerializer == null) {
				keySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (valueSerializer == null) {
				valueSerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashKeySerializer == null) {
				hashKeySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashValueSerializer == null) {
				hashValueSerializer = defaultSerializer;
				defaultUsed = true;
			}
		}

		if (enableDefaultSerializer && defaultUsed) {
			Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
		}

		if (scriptExecutor == null) {
			this.scriptExecutor = new DefaultScriptExecutor<K>(this);
		}

		initialized = true;
	}

由上面的代码,我们可以知道当我们使用RedisTemplate时,如果没有指定序列化,会使用默认的序列化(jdk提供的序列化,这里不说这个效率和码流大小的问题)。
那么问题来了,经过我们封装的redis工具类的SetNX代码是这样的:

/**
	 * 实现分布式锁
	 * <p>
	 * 只有key不存在的情况下才会操作,否则不做任何操作
	 * 
	 * @param key
	 * @param value
	 * @param expireSecond
	 *            过期时间
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public  boolean setNX(final String key, final Object value, long expireSecond) {
		Object obj = null;
		try {
			obj = redisTemplate.execute(new RedisCallback<Object>() {
				@Override
				public Object doInRedis(RedisConnection connection) throws DataAccessException {
					// 注意:这里key使用String的org.springframework.data.redis.serializer.StringRedisSerializer这个序列化
					// value使用JdkSerializationRedisSerializer(jdk的序列化)
					StringRedisSerializer serializer = new StringRedisSerializer();
					JdkSerializationRedisSerializer js = new JdkSerializationRedisSerializer();
					Boolean success = connection.setNX(serializer.serialize(key), js.serialize(value));
					connection.close();
					return success;
				}
			});
			// 设置过期
			if (expireSecond > 0 && obj != null && (Boolean) obj) {
				redisTemplate.expire(key, expireSecond, TimeUnit.SECONDS);
			}
		} catch (Exception e) {
			log.error("setNX redis error, key : {}" + key + ",e:" + e.getMessage());
		}
		return obj != null ? (Boolean) obj : false;
	}

从上面的配置,我们可以看出了问题,SetNX方法中key使用的序列化跟模板中默认的序列化不一致。这样会导致设置的key无法过期。从而导致某个key的锁一直有效。试想一下,如果我们为了锁定一个订单是否有重复提交或者多个请求只处理一个请求的时候,当第一个请求处理,锁了该订单号,然后去保存数据库(假如参数格式不对,导致保存数据库失败)。那么修改好参数后,想再次重新保存,但是由于锁一直有效,设置不了过期时间。所以第二次保存的时候一直保存不了。
说白了就是因为redis中序列化不一致。导致redis根据key序列化出来的值查找不到。导致设置过期时间失败导致。当做了永远不过期的key处理。解决这个问题可以重写一下RedisTemplate的key序列化。或者修改一下SetNX方法中key的序列化为jdk序列化即可。
方法1:增加一个redis配置类

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    @Primary
    public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
        return new RedisCacheManager(redisTemplate);
    }

    /**
     * 模板key的序列化使用StringRedisSerializer,value使用默认
     *
     * @param factory 链接工厂
     * @return 新的RedisTemplate bean
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        return template;
    }
}

方法2:修改setNX的key序列化,保证和RedisTemplate模板中的一致

public  boolean setNX(final String key, final Object value, long expireSecond) {
		Object obj = null;
		try {
			obj = redisTemplate.execute(new RedisCallback<Object>() {
				@Override
				public Object doInRedis(RedisConnection connection) throws DataAccessException {
					JdkSerializationRedisSerializer js = new JdkSerializationRedisSerializer();
					Boolean success = connection.setNX(js.serialize(key), js.serialize(value));
					connection.close();
					return success;
				}
			});
			// 设置过期
			if (expireSecond > 0 && obj != null && (Boolean) obj) {
				redisTemplate.expire(key, expireSecond, TimeUnit.SECONDS);
			}
		} catch (Exception e) {
			log.error("setNX redis error, key : {}" + key + ",e:" + e.getMessage());
		}
		return obj != null ? (Boolean) obj : false;
	}

猜你喜欢

转载自blog.csdn.net/chen846262292/article/details/85259603