通常我们使用redisTemplate 的setIfAbsent()方法进行加锁,在到使用expire()方法进行设置超时时间的是时候,两个操作时使用两个链接不在一个事务中,当存在客户端setIfAbsent()加锁成功后服务中断,expire()无法进行超时设置,导致死锁的情况。
针对以上情况可以采取两种解决方案:
方案一:
将锁的超时间放在锁(key)的值(里面),及redis客户端在获取锁的时将客户端获取锁的时间加上超时时间形成失效时间点形成一个key-value的数据存在redis中,当客户端再次获取锁时候先判断锁的key是否存在,当存在时判断value的值和客户端的时间进行比对,看锁是否过期。
缺点:锁的过期机制受客户端时间限制,当存在加锁成功后,客户端时间发生大时间后移的请求,锁便存在长时间的死锁问题
方案二:
由于在redisTemplate 低版本的api中加锁和超时设置是两步存在死锁的问题(redisTemplate 2.1.*版本可以setIfAbsent方法可以设置锁同时设置时间),而redisTemplate 的api操作其实也是封装redis的指令,并发送给服务端进行执行一系列操作,因此我们可以直接通过redisTemplate 发送指令进行加锁操作解决事务问题,通过发送lua脚本给服务端执行业务操作
import com.alibaba.dubbo.common.utils.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class RedisUtil {
// @Qualifier("stringRedisTemplate")
@Autowired
private RedisTemplate redisTemplate;
/**
* redisUtil.setIfAbsent 新加的带有超时的setIfAbsent 脚本
*/
String newSetIfAbsentScriptStr = " if (redis.call('setnx', KEYS[1], ARGV[2]) == 1) then" +
" redis.call('expire', KEYS[1], ARGV[2])" +
" return 1;" +
" else" +
" return 0;" +
" end;";
public RedisScript<Boolean> newSetIfAbsentScript = new DefaultRedisScript<Boolean>(newSetIfAbsentScriptStr, Boolean.class);
/**
* @Description: setIfAbsent升级版,加了超时时间
* @Author: Gong Yongwei
* @Date: 2018/12/12 9:21
* @param key
* @param value
* @param seconds 超时时间,秒为单位
* @return: boolean
*/
public boolean setIfAbsent(String key, String value, Long seconds) {
List<Object> keys = new ArrayList<Object>();
keys.add(key);
Object[] args = { value, seconds.toString() };
return (boolean) redisTemplate.<Boolean> execute(newSetIfAbsentScript, keys, args);
}
}
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import com.alibaba.dubbo.common.utils.CollectionUtils;
import com.wangyong.rooster.BaseTest;
import com.wangyong.redis.RedisKeyTimeout;
import com.wangyong.redis.RedisUtil;
import org.junit.Test;
@Slf4j
public class RedisTest extends BaseTest {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisUtil redisUtil;
@Test
public void test() {
String key = "lock_generator_calender_00001";
// redisTemplate.opsForValue().set(key, key);
redisTemplate.delete(key);
Object result = redisUtil.setIfAbsent(key,key,10L);
System.out.println("加锁结果:"+result);
System.out.println("锁时间"+redisTemplate.getExpire(key, TimeUnit.MILLISECONDS));
result = redisUtil.setIfAbsent(key,key,20L);
System.out.println("加锁结果2:"+result);
System.out.println("锁时间2:"+redisTemplate.getExpire(key, TimeUnit.MILLISECONDS));
redisTemplate.delete(key);
}
}
在这过程中可能出现错误
这个错误大致看起来时数组越界问题,其实时在问题是设置锁的时候,value没有进行序列化的问题,而redisTemplate 默认的序列化方式是jdk的序列化方式,redis的服务端是无法处理的,因此我们需要制定redisTemplate 的value的序列化方式
@Bean
public RedisTemplate<Object, Object> stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
//key都用String 序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
template.setHashKeySerializer(stringSerializer);
template.setKeySerializer(stringSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setConnectionFactory(redisConnectionFactory);
return template;
}
因此结果成功了:
另外网上也有:使用redisTemplate.execute(new RedisCallback<Boolean>() {});的方式,但是会存在,获取锁失败是仍然修改锁超时时间的问题,因此不采取
public boolean setIfAbsent(final String key, final Serializable value, final long exptime) {
Boolean b = (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
RedisSerializer keySerializer = redisTemplate.getKeySerializer();
Object obj = connection.execute("setnx", keySerializer.serialize(key),
valueSerializer.serialize(value),
SafeEncoder.encode("NX"),
SafeEncoder.encode("EX"),
Protocol.toByteArray(exptime));
return obj != null;
}
});
return b;
}