首页 > 技术文章 > redis通过lua脚本实现分布式锁

javayida 2018-12-04 20:06 原文

项目中有使用分布式锁的需求,项目中通过lua脚本通过redistemplate进行调用,
下面总结转自:https://blog.csdn.net/u014495560/article/details/82531046
我们不得不要考虑分布式锁的实现需要具备的几点要求:

互斥性 在任意时刻,只有一个线程能够获得锁
不会死锁 一个线程获得锁后,不会一直持有不释放,导致其他线程无法获得锁而影响业务
加解锁是同一线程 试想如果加锁的线程还没有执行完业务,被另一线程解锁,那分布式锁必定是无法解决问题的
健壮性 在使用集群的情况下,如果增加、删除节点,要尽量避免key的miss,如果命中率太低,势必会在某些极端情况下影响到业务,这里可以考虑一致性哈希等算法,一般由运维同学来实施;如果连接Redis时,发生jedis的超时异常,业务该如何处理

加锁方式
加锁的方法我们要考虑这么几个问题:

选择合适的变量作为key值,这个变量可以允许同一时间只有一个线程执行某段业务代码
相对于key的value如何定义?这里我们设定一个requestId作为value值,这个值可以在实际编程中使用UUID来生成,这样做的好处是可以保证加锁和解锁的是同一线程
如何防止死锁?考虑到Redis的命令可以设置生命周期,我们最好的办法就是为每个加锁的业务都要求使用者根据业务设定合理的过期时间,业务处理完之后尽可能快的释放锁
在高并发的场景下,同一时间只能有一个线程成功加锁,如何实现?自然我们想到Redis的setNx命令

使用lua脚本,不知道为啥碰到一个小问题,一直获取不到ttl就是传进去的锁时间,然后lua里面获取到的一直是nil,然后跟0进行比较,就会爆如下异常:


org.springframework.dao.InvalidDataAccessApiUsageException: ERR Error running script (call to f_d98ab356b04aa005f75e549ab6691030cfde35f4): @user_script:7: user_script:7: attempt to compare number with nil ; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR Error running script (call to f_d98ab356b04aa005f75e549ab6691030cfde35f4): @user_script:7: user_script:7: attempt to compare number with nil 

	at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:64)
	at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:41)
	at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
	at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
	at org.springframework.data.redis.connection.jedis.JedisConnection.convertJedisAccessException(JedisConnection.java:181)
	at org.springframework.data.redis.connection.jedis.JedisScriptingCommands.convertJedisAccessException(JedisScriptingCommands.java:175)
	at org.springframework.data.redis.connection.jedis.JedisScriptingCommands.eval(JedisScriptingCommands.java:131)
	at org.springframework.data.redis.connection.DefaultedRedisConnection.eval(DefaultedRedisConnection.java:1233)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.data.redis.core.CloseSuppressingInvocationHandler.invoke(CloseSuppressingInvocationHandler.java:61)
	at com.sun.proxy.$Proxy77.eval(Unknown Source)
	at org.springframework.data.redis.core.script.DefaultScriptExecutor.eval(DefaultScriptExecutor.java:84)
	at org.springframework.data.redis.core.script.DefaultScriptExecutor.lambda$execute$0(DefaultScriptExecutor.java:68)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:171)
	at org.springframework.data.redis.core.script.DefaultScriptExecutor.execute(DefaultScriptExecutor.java:58)
	at org.springframework.data.redis.core.script.DefaultScriptExecutor.execute(DefaultScriptExecutor.java:52)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:346)
	at net.rdd.util.RedisLockScriptUtil.tryLock(RedisLockScriptUtil.java:36)
	at net.rdd.test.redis.RedisScriptTest.test01(RedisScriptTest.java:38)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR Error running script (call to f_d98ab356b04aa005f75e549ab6691030cfde35f4): @user_script:7: user_script:7: attempt to compare number with nil 
	at redis.clients.jedis.Protocol.processError(Protocol.java:127)
	at redis.clients.jedis.Protocol.process(Protocol.java:161)
	at redis.clients.jedis.Protocol.read(Protocol.java:215)
	at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
	at redis.clients.jedis.Connection.getOne(Connection.java:322)
	at redis.clients.jedis.BinaryJedis.eval(BinaryJedis.java:3116)
	at org.springframework.data.redis.connection.jedis.JedisScriptingCommands.eval(JedisScriptingCommands.java:129)
	... 47 more

原来灵活的lua脚本:

local key = KEYS[1];
local value = ARGV[1];
local ttl = tonumber(ARGV[2]);

local lock = redis.call('setnx', key, value);
 
if lock == 1 and ttl > 0 then
	redis.call('expire', key, ttl);
end;

return lock

后面将ttl写死了如图

找好久不知道什么原因,只能将lua脚本写死,然后执行是可以的,我怀疑是window的redis执行lua会有部分问题,以后有时间可以在linux上面redis尝试一下.

在这里插入图片描述

这下面是锁工具类,就是获取锁,和释放锁两个方法,获取锁一般有重试时间,还有最大时间,要是超时一般都直接抛出异常就好了.一般项目中,要是没有获取到锁,代表活动太火爆啥的,一般都是直接抛出异常好了,前端给予对应展示就好了.然后获取到锁的就可以进行其他操作,比如送红包啥的,根据业务进行处理就好了,测试类,我也直接贴下面,

package net.rdd.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.UUID;

/**
 * Created by rdd on 2018/12/18.
 */
@Component
public class RedisLockScriptUtil {

    private Integer acquireTimeout = 10;//资源占有锁的时间 秒s
    private Integer acquireInterval = 1000;//尝试获取锁的时限 ms

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    @Qualifier("lockScript")
    private RedisScript<Boolean> lockScript;
    @Autowired
    @Qualifier("unlockScript")
    private RedisScript<Boolean> unlockScript;


    public String tryLock(String lockKey) {
        String lockValue = UUID.randomUUID().toString();
        Long endTime = System.currentTimeMillis() + acquireTimeout;
        while (System.currentTimeMillis() < endTime) {
            Boolean lockResult = (Boolean) redisTemplate.execute(lockScript, Collections.singletonList(lockKey), lockValue, 7);
            if (lockResult) {
                //加锁成功
                return lockValue;
            } else {
                //等待,准备再次获取锁
                try {
                    Thread.sleep(acquireInterval);
                } catch (InterruptedException ex) {
                    continue;
                }
            }
        }
        //获取锁超时,可以直接抛异常

        return "";
    }

    public boolean releaseLock(String lockKey, String lockValue) {
        Boolean releaseResult = (Boolean) redisTemplate.execute(unlockScript, Collections.singletonList(lockKey), lockValue);
        return releaseResult;
    }

}

测试代码,一般在项目中使用

@Test
    public void test01() {

        String sss = SpringContextUtil.getBean(RedisLockScriptUtil.class).tryLock("aaaa");

        if (Strings.isNullOrEmpty(sss)) {
            //未获取到锁,且超时
//            throw new Exception("活动太火爆请稍后再试");
        }
        try {
            //todo doSomething
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean dd = SpringContextUtil.getBean(RedisLockScriptUtil.class).releaseLock("aaaa",sss);
        if (dd) {
           //释放锁成功
        }
        //释放锁失败

    }

源码参考地址:https://github.com/raodongdong/SpringBootRedis

推荐阅读