spring boot redis分布式锁

一. Redis 分布式锁的实现以及存在的问题

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

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

redisTemplate.opsForValue().setIfAbsent("key","value");

设置过期时间

redisTemplate.expire("key", 30000,TimeUnit.MILLISECONDS);

释放锁

redisTemplate.delete("key");

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

Stringresult = redisTemplate.execute(newRedisCallback() {    @OverridepublicStringdoInRedis(RedisConnection connection)throwsDataAccessException {        JedisCommands commands = (JedisCommands) connection.getNativeConnection();returncommands.set(key,"锁定的资源","NX","PX", expire);    }});

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

NX:表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。

PX:expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定

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

线程T1获取锁

线程T1执行业务操作,由于某些原因阻塞了较长时间

锁自动过期,即锁自动释放了

线程T2获取锁

线程T1业务操作完毕,释放锁(其实是释放的线程T2的锁)

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

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

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

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

ifredis.call("get",KEYS[1]) == ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end

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

使用 RedisTemplate 执行的代码如下:

// 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本Longresult = redisTemplate.execute(newRedisCallback() {publicLongdoInRedis(RedisConnection connection) throws DataAccessException {        Object nativeConnection = connection.getNativeConnection();// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行// 集群模式if(nativeConnectioninstanceofJedisCluster) {return(Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);        }// 单机模式elseif(nativeConnectioninstanceofJedis) {return(Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);        }return0L;    }});

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

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

# JedisClusterConnection.java/** * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisScriptingCommands#eval(byte[], org.springframework.data.redis.connection.ReturnType, int, byte[][]) */@OverridepublicT eval(byte[] script, ReturnType returnType,intnumKeys, byte[]... keysAndArgs) {thrownewInvalidDataAccessApiUsageException("Eval is not supported in cluster environment.");}

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

线程T1获取锁成功

Redis 的master节点挂掉,slave自动顶上

线程T2获取锁,会从slave节点上去判断锁是否存在,由于Redis的master slave复制是异步的,所以此时线程T2可能成功获取到锁

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

# DistributedLock.java 顶级接口/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:11:05

* @version 1.0.0

*/publicinterface DistributedLock {publicstaticfinallongTIMEOUT_MILLIS =30000;publicstaticfinalintRETRY_TIMES = Integer.MAX_VALUE;publicstaticfinallongSLEEP_MILLIS =500;publicbooleanlock(Stringkey);publicbooleanlock(Stringkey,intretryTimes);publicbooleanlock(Stringkey,intretryTimes,longsleepMillis);publicbooleanlock(Stringkey,longexpire);publicbooleanlock(Stringkey,longexpire,intretryTimes);publicbooleanlock(Stringkey,longexpire,intretryTimes,longsleepMillis);publicbooleanreleaseLock(Stringkey);}

# AbstractDistributedLock.java 抽象类,实现基本的方法,关键方法由子类去实现/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:10:57

* @version 1.0.0

*/publicabstractclass AbstractDistributedLock implements DistributedLock {    @Overridepublicbooleanlock(Stringkey) {returnlock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);    }    @Overridepublicbooleanlock(Stringkey,intretryTimes) {returnlock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);    }    @Overridepublicbooleanlock(Stringkey,intretryTimes,longsleepMillis) {returnlock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);    }    @Overridepublicbooleanlock(Stringkey,longexpire) {returnlock(key, expire, RETRY_TIMES, SLEEP_MILLIS);    }    @Overridepublicbooleanlock(Stringkey,longexpire,intretryTimes) {returnlock(key, expire, retryTimes, SLEEP_MILLIS);    }}

# RedisDistributedLock.java Redis分布式锁的实现importjava.util.ArrayList;importjava.util.List;importjava.util.UUID;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.dao.DataAccessException;importorg.springframework.data.redis.connection.RedisConnection;importorg.springframework.data.redis.core.RedisCallback;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.util.StringUtils;importredis.clients.jedis.Jedis;importredis.clients.jedis.JedisCluster;importredis.clients.jedis.JedisCommands;/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:11:14

* @version 1.0.0

*/publicclass RedisDistributedLock extends AbstractDistributedLock {privatefinalLogger logger = LoggerFactory.getLogger(RedisDistributedLock.class);privateRedisTemplate redisTemplate;privateThreadLocal lockFlag =newThreadLocal();publicstaticfinalStringUNLOCK_LUA;static{        StringBuilder sb =newStringBuilder();        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");        sb.append("then ");        sb.append("    return redis.call(\"del\",KEYS[1]) ");        sb.append("else ");        sb.append("    return 0 ");        sb.append("end ");        UNLOCK_LUA = sb.toString();    }publicRedisDistributedLock(RedisTemplate redisTemplate) {super();this.redisTemplate = redisTemplate;    }    @Overridepublicbooleanlock(Stringkey,longexpire,intretryTimes,longsleepMillis) {booleanresult = setRedis(key, expire);// 如果获取锁失败,按照传入的重试次数进行重试while((!result) && retryTimes-- >0){try{                logger.debug("lock failed, retrying..."+ retryTimes);                Thread.sleep(sleepMillis);            }catch(InterruptedException e) {returnfalse;            }            result = setRedis(key, expire);        }returnresult;    }privatebooleansetRedis(Stringkey,longexpire) {try{Stringresult = redisTemplate.execute(newRedisCallback() {                @OverridepublicStringdoInRedis(RedisConnection connection)throwsDataAccessException {                    JedisCommands commands = (JedisCommands) connection.getNativeConnection();Stringuuid = UUID.randomUUID().toString();                    lockFlag.set(uuid);returncommands.set(key, uuid,"NX","PX", expire);                }            });return!StringUtils.isEmpty(result);        }catch(Exception e) {            logger.error("set redis occured an exception", e);        }returnfalse;    }        @OverridepublicbooleanreleaseLock(Stringkey) {// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除try{            List keys =newArrayList();            keys.add(key);            List args =newArrayList();            args.add(lockFlag.get());// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本Long result = redisTemplate.execute(newRedisCallback() {publicLong doInRedis(RedisConnection connection)throwsDataAccessException {ObjectnativeConnection = connection.getNativeConnection();// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行// 集群模式if(nativeConnectioninstanceofJedisCluster) {return(Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);                    }// 单机模式elseif(nativeConnectioninstanceofJedis) {return(Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);                    }return0L;                }            });returnresult !=null&& result >0;        }catch(Exception e) {            logger.error("release lock occured an exception", e);        }returnfalse;    }    }

二. 基于 AOP 的 Redis 分布式锁

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

首先定义个注解:

importjava.lang.annotation.ElementType;importjava.lang.annotation.Inherited;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:10:36

* @version 1.0.0

*/@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic@interfaceRedisLock {/** 锁的资源,redis的key*/Stringvalue()default"default";/** 持锁时间,单位毫秒*/long keepMills()default30000;/** 当获取失败时候动作*/LockFailAction action()defaultLockFailAction.CONTINUE;        publicenumLockFailAction{/** 放弃 */GIVEUP,/** 继续 */CONTINUE;    }/** 重试的间隔时间,设置GIVEUP忽略此项*/long sleepMills()default200;/** 重试次数*/intretryTimes()default5;}

装配分布式锁的bean

importorg.springframework.boot.autoconfigure.AutoConfigureAfter;importorg.springframework.boot.autoconfigure.condition.ConditionalOnBean;importorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.core.RedisTemplate;importcom.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock;importcom.itopener.lock.redis.spring.boot.autoconfigure.lock.RedisDistributedLock;/**

* @author fuwei.deng

* @date 2017年6月14日 下午3:11:31

* @version 1.0.0

*/@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)public class DistributedLockAutoConfiguration {        @Bean@ConditionalOnBean(RedisTemplate.class)    public DistributedLock redisDistributedLock(RedisTemplateredisTemplate){returnnewRedisDistributedLock(redisTemplate);    }    }

定义切面(spring boot配置方式)

importjava.lang.reflect.Method;importjava.util.Arrays;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.aspectj.lang.reflect.MethodSignature;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.autoconfigure.AutoConfigureAfter;importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importcom.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock;importcom.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock.LockFailAction;importcom.itopener.lock.redis.spring.boot.autoconfigure.lock.DistributedLock;/** *@authorfuwei.deng *@date2017年6月14日 下午3:11:22 *@version1.0.0 */@Aspect@Configuration@ConditionalOnClass(DistributedLock.class)@AutoConfigureAfter(DistributedLockAutoConfiguration.class)publicclassDistributedLockAspectConfiguration{privatefinalLogger logger = LoggerFactory.getLogger(DistributedLockAspectConfiguration.class);@AutowiredprivateDistributedLock distributedLock;@Pointcut("@annotation(com.itopener.lock.redis.spring.boot.autoconfigure.annotations.RedisLock)")privatevoid lockPoint(){            }@Around("lockPoint()")publicObject around(ProceedingJoinPoint pjp) throws Throwable{        Method method = ((MethodSignature) pjp.getSignature()).getMethod();        RedisLock redisLock = method.getAnnotation(RedisLock.class);        String key = redisLock.value();if(StringUtils.isEmpty(key)){            Object[] args = pjp.getArgs();            key = Arrays.toString(args);        }        int retryTimes = redisLock.action().equals(LockFailAction.CONTINUE) ? redisLock.retryTimes() :0;        boolean lock = distributedLock.lock(key, redisLock.keepMills(), retryTimes, redisLock.sleepMills());if(!lock) {            logger.debug("get lock failed : "+ key);returnnull;        }//得到锁,执行方法,释放锁logger.debug("get lock success : "+ key);try{returnpjp.proceed();        }catch(Exception e) {            logger.error("execute locked method occured an exception", e);        }finally{            boolean releaseResult = distributedLock.releaseLock(key);            logger.debug("release lock : "+ key + (releaseResult ?" success":" failed"));        }returnnull;    }}

spring boot starter还需要在 resources/META-INF 中添加 spring.factories 文件

# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAutoConfiguration,\com.itopener.lock.redis.spring.boot.autoconfigure.DistributedLockAspectConfiguration

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

@AutowiredprivateDistributedLock distributedLock;

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

欢迎欢迎学Java的朋友们加入java架构交流: 855835163

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容