文章开头先介绍一下为什么我使用的事基于redis的分布式锁,首先项目对于那种极限场景下发生的那种不可靠的情况比较有限,并发量也达不到那么高,然后后端服务的技术栈里不包括zookeeper以及接入的话成本比较大,服务器都是平台组在负责,AWS的个人几乎碰不到所以要基于公司技术架构的情况下去实现最优方案只能是redis了。各种方案其实都有利弊,看你各自的技术架构了。
redis锁一般使用setnx,但是单纯地操作会有很多问题,下边罗列一下。
- 首先要给锁加入过期时间不至于造成程序中断后所无法释放问题所以是使用setnx ex的形式保证原子性
- 加锁解决了,解锁又出现一些问题:过期时间设置多久?如果程序执行的时间比较久没有结束则锁过期,然后等程序执行完后解锁操作会把非当前获取的给解锁了......所以解锁的时候加个判断value值是否一致,但是如果此时正要删除锁的时候,锁已经过期,其他线程已经设置了新值,那我们删除的是别人持有的锁。所以还是原子性问题,此时需要redis命令+Lua脚本
- 最后一个问题就是如果锁的过期时间设置的短了,此时业务逻辑还没有执行完,怎么办那就需要一个对锁的自动延时,Redisson里的watchDog可以解决
由于我们的项目使用的是spring-data-redis包的RedisTemplate而非Redisson,所以对业务逻辑做了评估之后锁的过期时间我不会设置太短也不会过长,大约在30s~60s左右足够接口调用的执行时间了,并且还设置了服务接口的超时时间配置,所以不至于持有锁的时间过长。
先说一个实际应用的问题就是:我们项目使用RedisTemplate需要进行Bean的注入配置,并进行了Jackson2JsonRedisSerializer序列化的配置,但是因为解锁操作中使用的是LUA脚本所以我重新进行一个bean的配置
@Bean("redisTemplateObjects")
public RedisTemplate redisTemplateObjects(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplateObjects = new RedisTemplate<>();
redisTemplateObjects.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
redisTemplateObjects.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplateObjects.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplateObjects.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplateObjects.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplateObjects.setStringSerializer(jackson2JsonRedisSerializer);
redisTemplateObjects.afterPropertiesSet();
return redisTemplateObjects;
}
/**
* 指定StringRedisSerializer的序列化以便用于LUA脚本的执行
*/
@Bean("redisTemplateStr")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
RedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
return redisTemplate;
}
在redisUtil中提供加解锁的方法,如下
@Resource(name = "redisTemplateStr")
public RedisTemplate stringRedisTemplate;
/**
* 加锁
* @param key 锁的key
* @param val 锁的val
* @param timeOut 获取锁的超时时间
* @param expreTime 锁过期时间
* @param unit 过期单位
* @return
*/
public boolean tryLock(String key, String val, long timeOut,long expreTime,TimeUnit unit){
long start = System.currentTimeMillis();
for (;;) {
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, val, expreTime, unit);
if (aBoolean){
return true;
}
long time = System.currentTimeMillis() - start;
if (time > timeOut){
return false;
}
}
}
/**
* 解锁
* @param key
* @param val
* @return
*/
public boolean unLock(String key, String val){
Long SUCCESS = 1L;
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Object execute = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), val);
if(SUCCESS.equals(execute)) {
return true;
}
return false;
}
实际在业务层使用的时候增加必要的try catch finally,解锁操作需要在finally代码块中使用。回顾一下实际踩过的坑吧,一个是 RedisTemplate的序列化问题,因为原先的存量的数据都是序列化存储过的,所以单独进行bean注入的新的stringRedisTemplate,另一个是解锁操作力的LUA脚本执行错误和语法问题。最终版本是可以执行的。
写在最后:任何技术方案和架构都需要以具体的业务场景和需求来确认是否是最适合的,具体需求具体分析。redis的大神还出过redlock的方案,网络上有关于各路大神针对redis锁是否可靠的讨论,我觉得以咱这个体量和业务场景根本碰不到,所以仅仅看看就好,任何系统和工具都是不断地发现问题解决问题升级迭代才能更好地服务于人的。