(五)Redis分布式锁的问题和实现

关于分布式的锁,大概有几个热点问题

关键点一:原子命令加锁。

对于 Redis 的加锁操作先set key,再设置 key 的过期时间,这样的话不是原子性操作。不是原子操作会带来什么问题,就不用我说了吧?

而在 2.6.12 版本后,可以通过向 Redis 发送下面的命令,实现原子性的加锁操作:

SET key random_value NX PX 30000

我们常用的redis客户端,Jedis、Lettuce都实现了这一命令方法,比如jedis的setnx(),Lettuce的setIfAbsent()。

关键点二:设置值的时候,放的是random_value。而不是你随便扔个“OK”进去。

先解释一下上面的命令中的几个参数的含义:

random_value:是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。 比如我们使用UUID。

NX:表示只有当要设置的 key 值不存在的时候才能 set 成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。

PX 30000:表示这个锁有一个 30 秒的自动过期时间。当然,这里 30 秒只是一个例子,客户端可以选择合适的过期时间。

那么为什么要使用 PX 30000 去设置一个超时时间?是怕进程 A 不讲道理啊,锁没等释放呢,万一崩了,直接原地把锁带走了,导致系统中谁也拿不到锁。

就算这样,还是不能保证万无一失。如果进程 A 又不讲道理,操作锁内资源超过笔者设置的超时时间,那么就会导致其他进程拿到锁,等进程 A 回来了,回手就是把其他进程的锁删了。这就引出了下一个关键点。

再解释一下为什么 value 需要设置为一个随机字符串。这也是第三个关键点。

关键点三:value 的值设置为随机数主要是为了更安全的释放锁,释放锁的时候需要检查 key 是否存在,且 key 对应的值是否和我指定的值一样,是一样的才能释放锁。所以可以看到这里有获取、判断、删除三个操作。

释放锁伪代码

String uuid = xxxx;
// 伪代码,具体实现看项目中用的连接工具
// 有的提供的方法名为set 有的叫setIfAbsent
set Test uuid NX PX 3000
try{
// biz handle....
} finally {
    // unlock
    if(uuid.equals(redisTool.get('key')){
        redisTool.del('key');
    }
}

这回看起来是不是稳了?相反,这回的问题更明显了,在 Finally 代码块中,Get 和 Del 并非原子操作,还是有进程安全问题。

进程安全问题会在什么场景下出现?我就先不回答了,感兴趣的可以在留言中交流下

为了保障原子性,我们需要用 lua 脚本。
那么删除锁的正确姿势之一,就是可以使用 Lua 脚本,通过 Redis 的 eval/evalsha 命令来。

下面简单看下一个释放锁有问题的代,相信也是很多人使用最多的方法

/**
     * 获取锁
     * @param key
     * @param expireSecond
     * @return
     */
    public boolean lock(String key, int expireSecond) {

        Jedis Jedis = null;
        try {
            Jedis = jedisPool.getResource();
            Long result = Jedis.setnx(key, "1");
            if(result == 1){
                Jedis.expire(key, expireSecond);
                return true;
            }
            return false;
        } catch (Exception ex) {
            logger.error("lock-setnx error:", ex);
            returnBrokenResource(Jedis);
            throw new TraweServiceException("获取锁出现异常:" + ex.getMessage(), ex);
        } finally {
            returnResource(Jedis);
        }
    }

    /**
     * 获取锁,如果没有获取到会再去尝试几次
     *
     * @param key 锁键值
     * @param expireSecond 锁过期时间
     * @param tryCount 尝试次数
     * @return 是否获得锁
     */
    @SuppressWarnings("static-access")
    public boolean tryLock(String key, int expireSecond, int tryCount) {
        if (lock(key, expireSecond)) {
            return true;
        }

        for (int i=0; i<tryCount; i++) {
            int sleepMills = RandomUtils.nextInt(20, 200);
            try {
                Thread.currentThread().sleep(sleepMills);
            } catch (InterruptedException e) {
                logger.error(e, e);
            }

            if (lock(key, expireSecond)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 释放锁
     * @param key
     */
    public void unlock(String key) {
        del(key);
    }

实现Redis锁

网上大佬用jedis写的锁简单修改下使用lettuce实现

@Component
public class RedisDistributedLock {
    
    @Autowired
    @Resource(name="redisTemplateMaster")
    private RedisTemplate<Object,Object> redisTemplate;
    
    private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
    
    private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
    
    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        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();
    }


    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, expire);
        // 如果获取锁失败,按照传入的重试次数进行重试
        while((!result) && retryTimes-- > 0){
            try {
                logger.debug("lock failed, retrying..." + retryTimes);
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                return false;
            }
            result = setRedis(key , expire);
        }
        return result;
    }
    
    private boolean setRedis(final String key, final long expire ) {
        try{
            
            RedisCallback<Boolean> callback = (connection) -> {
                String uuid = UUID.randomUUID().toString();
                lockFlag.set(uuid);
                return connection.set(key.getBytes(Charset.forName("UTF-8")), uuid.getBytes(Charset.forName("UTF-8")), Expiration.milliseconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
            };
            return (Boolean)redisTemplate.execute(callback);
        } catch (Exception e) {
            logger.error("redis lock error.", e);
        }
        return false;
    }
    
    
    public boolean releaseLock(String key) {
        // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
        try {
             RedisCallback<Boolean> callback = (connection) -> {
                String value = lockFlag.get();
                return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN ,1, key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")));
            };
             return (Boolean)redisTemplate.execute(callback);
        } catch (Exception e) {
            logger.error("release lock occured an exception", e);
        } finally {
            // 清除掉ThreadLocal中的数据,避免内存溢出
            lockFlag.remove();
        }
        return false;
    }
    
}

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