Redis 分布式锁的实现以及存在的问题(Spring Cloud)

一. Redis 分布式锁

这里是列表文本 锁是针对某个资源,保证其访问的互斥性,在实际使用当中,这个资源一般是一个字符串。使用 Redis 实现锁,主要是将资源放到 Redis 当中,利用其原子性,当其他线程访问时,如果 Redis 中已经存在这个资源,就不允许之后的一些操作。spring boot使用 Redis 的操作主要是通过 RedisTemplate 来实现,一般步骤如下:


  • 1、将锁资源放入 Redis (注意是当key不存在时才能放成功,所以使用 setIfAbsent 方法):

    redisTemplate.opsForValue().setIfAbsent("key", "value");
    
  • 2、设置过期时间

    redisTemplate.expire("key", 30000, TimeUnit.MILLISECONDS);
    
  • 3、释放锁

    redisTemplate.delete("key");
    

一般情况下,这样的实现就能够满足锁的需求了,但是如果在调用 setIfAbsent 方法之后线程挂掉了,即没有给锁定的资源设置过期时间,默认是永不过期,那么这个锁就会一直存在。所以需要保证设置锁及其过期时间两个操作的原子性,spring data的 RedisTemplate 当中并没有这样的方法。但是在jedis当中是有这种原子操作的方法的,需要通过 RedisTemplate 的 execute 方法获取到jedis里操作命令的对象,代码如下:

1String result = redisTemplate.execute(new RedisCallback<String>() { 2 [@Override](https://my.oschina.net/u/1162528) 3 public String doInRedis(RedisConnection connection) throws DataAccessException { 4 JedisCommands commands = (JedisCommands) connection.getNativeConnection(); 5 return commands.set(key, "锁定的资源", "NX", "PX", expire); 6 } 7});

注意: Redis 从2.6.12版本开始 set 命令支持 NX 、 PX 这些参数来达到 setnx 、 setex 、 psetex 命令的效果,文档参见: http://doc.redisfans.com/string/set.html

1* NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。 2 3* PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定

这样在获取锁的时候就能够保证设置 Redis 值和过期时间的原子性,避免前面提到的两次 Redis 操作期间出现意外而导致的锁不能释放的问题。但是这样还是可能会存在一个问题,考虑如下的场景顺序:

1* 线程T1获取锁 2* 线程T1执行业务操作,由于某些原因阻塞了较长时间 3* 锁自动过期,即锁自动释放了 4* 线程T2获取锁 5* 线程T1业务操作完毕,释放锁(其实是释放的线程T2的锁)

按照这样的场景顺序,线程T2的业务操作实际上就没有锁提供保护机制了。所以,每个线程释放锁的时候只能释放自己的锁,即锁必须要有一个拥有者的标记,并且也需要保证释放锁的原子性操作。

因此在获取锁的时候,可以生成一个随机不唯一的串放入当前线程中,然后再放入 Redis 。释放锁的时候先判断锁对应的值是否与线程中的值相同,相同时才做删除操作。

Redis 从2.6.0开始通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值,文档参见: http://doc.redisfans.com/script/eval.html

因此我们可以通过 Lua 脚本来达到释放锁的原子操作,定义 Lua 脚本如下:

1if redis.call("get",KEYS[1]) == ARGV[1] then 2 return redis.call("del",KEYS[1]) 3else 4 return 0 5end

具体意思可以参考上面提供的文档地址

使用 RedisTemplate 执行的代码如下:

1// 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 2// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本 3Long result = redisTemplate.execute(new RedisCallback<Long>() { 4 public Long doInRedis(RedisConnection connection) throws DataAccessException { 5 Object nativeConnection = connection.getNativeConnection(); 6 // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 7 // 集群模式 8 if (nativeConnection instanceof JedisCluster) { 9 return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args); 10 } 11 12 // 单机模式 13 else if (nativeConnection instanceof Jedis) { 14 return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args); 15 } 16 return 0L; 17 } 18});

代码中分为集群模式单机模式,并且两者的方法、参数都一样,原因是spring封装的执行脚本的方法中( RedisConnection 接口继承于 RedisScriptingCommands 接口的 eval 方法),集群模式的方法直接抛出了不支持执行脚本的异常(虽然实际是支持的),所以只能拿到 Redis 的connection来执行脚本,而 JedisCluster 和 Jedis 中的方法又没有实现共同的接口,所以只能分开调用。

spring封装的集群模式执行脚本方法源码:

1# JedisClusterConnection.java 2/** 3 * (non-Javadoc) 4 * [@see](https://my.oschina.net/weimingwei) org.springframework.data.redis.connection.RedisScriptingCommands#eval(byte[], org.springframework.data.redis.connection.ReturnType, int, byte[][]) 5 */ 6[@Override](https://my.oschina.net/u/1162528) 7public <T> T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { 8 throw new InvalidDataAccessApiUsageException("Eval is not supported in cluster environment."); 9}

至此,我们就完成了一个相对可靠的 Redis 分布式锁,但是,在集群模式的极端情况下,还是可能会存在一些问题,比如如下的场景顺序( 暂时不深入开展 ):

1* 线程T1获取锁成功 2* Redis 的master节点挂掉,slave自动顶上 3* 线程T2获取锁,会从slave节点上去判断锁是否存在,由于Redis的master slave复制是异步的,所以此时线程T2可能成功获取到锁

为了可以以后扩展为使用其他方式来实现分布式锁,定义了接口和抽象类,所有的源码如下:

1# DistributedLock.java 顶级接口 2/** 3 * [@author](https://my.oschina.net/arthor) fuwei.deng 4 * [@date](https://my.oschina.net/u/2504391) 2017年6月14日 下午3:11:05 5 * @version 1.0.0 6 */ 7public interface DistributedLock { 8 9 public static final long TIMEOUT_MILLIS = 30000; 10 11 public static final int RETRY_TIMES = Integer.MAX_VALUE; 12 13 public static final long SLEEP_MILLIS = 500; 14 15 public boolean lock(String key); 16 17 public boolean lock(String key, int retryTimes); 18 19 public boolean lock(String key, int retryTimes, long sleepMillis); 20 21 public boolean lock(String key, long expire); 22 23 public boolean lock(String key, long expire, int retryTimes); 24 25 public boolean lock(String key, long expire, int retryTimes, long sleepMillis); 26 27 public boolean releaseLock(String key); 28}

1# AbstractDistributedLock.java 抽象类,实现基本的方法,关键方法由子类去实现 2/** 3 * @author fuwei.deng 4 * @date 2017年6月14日 下午3:10:57 5 * @version 1.0.0 6 */ 7public abstract class AbstractDistributedLock implements DistributedLock { 8 9 @Override 10 public boolean lock(String key) { 11 return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS); 12 } 13 14 @Override 15 public boolean lock(String key, int retryTimes) { 16 return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS); 17 } 18 19 @Override 20 public boolean lock(String key, int retryTimes, long sleepMillis) { 21 return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis); 22 } 23 24 @Override 25 public boolean lock(String key, long expire) { 26 return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS); 27 } 28 29 @Override 30 public boolean lock(String key, long expire, int retryTimes) { 31 return lock(key, expire, retryTimes, SLEEP_MILLIS); 32 } 33 34}

1# RedisDistributedLock.java Redis分布式锁的实现 2import java.util.ArrayList; 3import java.util.List; 4import java.util.UUID; 5 6import org.slf4j.Logger; 7import org.slf4j.LoggerFactory; 8import org.springframework.dao.DataAccessException; 9import org.springframework.data.redis.connection.RedisConnection; 10import org.springframework.data.redis.core.RedisCallback; 11import org.springframework.data.redis.core.RedisTemplate; 12import org.springframework.util.StringUtils; 13 14import redis.clients.jedis.Jedis; 15import redis.clients.jedis.JedisCluster; 16import redis.clients.jedis.JedisCommands; 17 18/** 19 * @author fuwei.deng 20 * @date 2017年6月14日 下午3:11:14 21 * @version 1.0.0 22 */ 23public class RedisDistributedLock extends AbstractDistributedLock { 24 25 private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class); 26 27 private RedisTemplate<Object, Object> redisTemplate; 28 29 private ThreadLocal<String> lockFlag = new ThreadLocal<String>(); 30 31 public static final String UNLOCK_LUA; 32 33 static { 34 StringBuilder sb = new StringBuilder(); 35 sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] "); 36 sb.append("then "); 37 sb.append(" return redis.call(\"del\",KEYS[1]) "); 38 sb.append("else "); 39 sb.append(" return 0 "); 40 sb.append("end "); 41 UNLOCK_LUA = sb.toString(); 42 } 43 44 public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) { 45 super(); 46 this.redisTemplate = redisTemplate; 47 } 48 49 @Override 50 public boolean lock(String key, long expire, int retryTimes, long sleepMillis) { 51 boolean result = setRedis(key, expire); 52 // 如果获取锁失败,按照传入的重试次数进行重试 53 while((!result) && retryTimes-- > 0){ 54 try { 55 logger.debug("lock failed, retrying..." + retryTimes); 56 Thread.sleep(sleepMillis); 57 } catch (InterruptedException e) { 58 return false; 59 } 60 result = setRedis(key, expire); 61 } 62 return result; 63 } 64 65 private boolean setRedis(String key, long expire) { 66 try { 67 String result = redisTemplate.execute(new RedisCallback<String>() { 68 @Override 69 public String doInRedis(RedisConnection connection) throws DataAccessException { 70 JedisCommands commands = (JedisCommands) connection.getNativeConnection(); 71 String uuid = UUID.randomUUID().toString(); 72 lockFlag.set(uuid); 73 return commands.set(key, uuid, "NX", "PX", expire); 74 } 75 }); 76 return !StringUtils.isEmpty(result); 77 } catch (Exception e) { 78 logger.error("set redis occured an exception", e); 79 } 80 return false; 81 } 82 83 @Override 84 public boolean releaseLock(String key) { 85 // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除 86 try { 87 List<String> keys = new ArrayList<String>(); 88 keys.add(key); 89 List<String> args = new ArrayList<String>(); 90 args.add(lockFlag.get()); 91 92 // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 93 // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本 94 95 Long result = redisTemplate.execute(new RedisCallback<Long>() { 96 public Long doInRedis(RedisConnection connection) throws DataAccessException { 97 Object nativeConnection = connection.getNativeConnection(); 98 // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 99 // 集群模式 100 if (nativeConnection instanceof JedisCluster) { 101 return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args); 102 } 103 104 // 单机模式 105 else if (nativeConnection instanceof Jedis) { 106 return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args); 107 } 108 return 0L; 109 } 110 }); 111 112 return result != null && result > 0; 113 } catch (Exception e) { 114 logger.error("release lock occured an exception", e); 115 } 116 return false; 117 } 118}

二. 基于 AOP 的 Redis 分布式锁

在实际的使用过程中,分布式锁可以封装好后使用在方法级别,这样就不用每个地方都去获取锁和释放锁,使用起来更加方便。

  • 首先定义个注解:

    1import java.lang.annotation.ElementType; 2import java.lang.annotation.Inherited; 3import java.lang.annotation.Retention; 4import java.lang.annotation.RetentionPolicy; 5import java.lang.annotation.Target; 6 7/** 8 * @author fuwei.deng 9 * @date 2017年6月14日 下午3:10:36 10 * @version 1.0.0 11 */ 12@Target({ElementType.METHOD}) 13@Retention(RetentionPolicy.RUNTIME) 14@Inherited 15public @interface RedisLock { 16 17 /** 锁的资源,redis的key*/ 18 String value() default "default"; 19 20 /** 持锁时间,单位毫秒*/ 21 long keepMills() default 30000; 22 23 /** 当获取失败时候动作*/ 24 LockFailAction action() default LockFailAction.CONTINUE; 25 26 public enum LockFailAction{ 27 /** 放弃 */ 28 GIVEUP, 29 /** 继续 */ 30 CONTINUE; 31 } 32 33 /** 重试的间隔时间,设置GIVEUP忽略此项*/ 34 long sleepMills() default 200; 35 36 /** 重试次数*/ 37 int retryTimes() default 5; 38}
  • 装配分布式锁的bean

    1import org.springframework.boot.autoconfigure.AutoConfigureAfter; 2import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 3import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; 4import org.springframework.context.annotation.Bean; 5import org.springframework.context.annotation.Configuration; 6import org.springframework.data.redis.core.RedisTemplate; 7 8import com.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock; 9import com.itopener.lock.redis.spring.boot.autoconfigure.lock.RedisDistributedLock; 10 11/** 12 * @author fuwei.deng 13 * @date 2017年6月14日 下午3:11:31 14 * @version 1.0.0 15 */ 16@Configuration 17@AutoConfigureAfter(RedisAutoConfiguration.class) 18public class DistributedLockAutoConfiguration { 19 20 @Bean 21 @ConditionalOnBean(RedisTemplate.class) 22 public DistributedLock redisDistributedLock(RedisTemplate<Object, Object> redisTemplate){ 23 return new RedisDistributedLock(redisTemplate); 24 } 25 26}
  • 定义切面(spring boot配置方式)

    1import java.lang.reflect.Method; 2import java.util.Arrays; 3 4import org.aspectj.lang.ProceedingJoinPoint; 5import org.aspectj.lang.annotation.Around; 6import org.aspectj.lang.annotation.Aspect; 7import org.aspectj.lang.annotation.Pointcut; 8import org.aspectj.lang.reflect.MethodSignature; 9import org.slf4j.Logger; 10import org.slf4j.LoggerFactory; 11import org.springframework.beans.factory.annotation.Autowired; 12import org.springframework.boot.autoconfigure.AutoConfigureAfter; 13import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 14import org.springframework.context.annotation.Configuration; 15import org.springframework.util.StringUtils; 16 17import com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock; 18import com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock.LockFailAction; 19import com.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock; 20 21/** 22 * @author fuwei.deng 23 * @date 2017年6月14日 下午3:11:22 24 * @version 1.0.0 25 */ 26@Aspect 27@Configuration 28@ConditionalOnClass(DistributedLock.class) 29@AutoConfigureAfter(DistributedLockAutoConfiguration.class) 30public class DistributedLockAspectConfiguration { 31 32 private final Logger logger = LoggerFactory.getLogger(DistributedLockAspectConfiguration.class); 33 34 @Autowired 35 private DistributedLock distributedLock; 36 37 @Pointcut("@annotation(com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock)") 38 private void lockPoint(){ 39 40 } 41 42 @Around("lockPoint()") 43 public Object around(ProceedingJoinPoint pjp) throws Throwable{ 44 Method method = ((MethodSignature) pjp.getSignature()).getMethod(); 45 RedisLock redisLock = method.getAnnotation(RedisLock.class); 46 String key = redisLock.value(); 47 if(StringUtils.isEmpty(key)){ 48 Object[] args = pjp.getArgs(); 49 key = Arrays.toString(args); 50 } 51 int retryTimes = redisLock.action().equals(LockFailAction.CONTINUE) ? redisLock.retryTimes() : 0; 52 boolean lock = distributedLock.lock(key, redisLock.keepMills(), retryTimes, redisLock.sleepMills()); 53 if(!lock) { 54 logger.debug("get lock failed : " + key); 55 return null; 56 } 57 58 //得到锁,执行方法,释放锁 59 logger.debug("get lock success : " + key); 60 try { 61 return pjp.proceed(); 62 } catch (Exception e) { 63 logger.error("execute locked method occured an exception", e); 64 } finally { 65 boolean releaseResult = distributedLock.releaseLock(key); 66 logger.debug("release lock : " + key + (releaseResult ? " success" : " failed")); 67 } 68 return null; 69 } 70}
  • spring boot starter还需要在 resources/META-INF 中添加 spring.factories 文件

    1# Auto Configure 2org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 3com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAutoConfiguration,\ 4com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAspectConfiguration

这样封装之后,使用spring boot开发的项目,直接依赖这个starter,就可以在方法上加 RedisLock 注解来实现分布式锁的功能了,当然如果需要自己控制,直接注入分布式锁的bean即可

1@Autowired 2private DistributedLock distributedLock;

如果需要使用其他的分布式锁实现,继承 AbstractDistributedLock 后实现获取锁和释放锁的方法即可

参考资料 :

1* http://zhangtielei.com/posts/blog-redlock-reasoning.html 2* http://doc.redisfans.com/index.html 3* https://www.jianshu.com/p/d72e8526bea1 4* https://www.jianshu.com/p/8cc44d008177

源码地址 :

https://gitee.com/itopener/springboot (目录:itopener-parent / spring-boot-starters-parent / lock-redis-spring-boot-starter-parent)

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )