Redis 实现简单的分布式锁

借助 SETNX(不完全正确)

Redis 中 SETNE 只有在 key 不存在时设置 key 的值,因此非常容易就实现了锁功能。只需要客户端对指定 KEY 成功设置一个随机值,借助这个值来防止其他的进程取得锁。

127.0.0.1:6379> get simpleLock
(nil)
127.0.0.1:6379> setnx simpleLock Locked 
(integer) 1    //成功得到锁返回1
127.0.0.1:6379> get simpleLock 
"Locked"
127.0.0.1:6379> setnx simpleLock Release 
(integer) 0   // 这里重新设置锁的值,返回0代表其他客户端获得锁
127.0.0.1:6379> get simpleLock
"Locked"
127.0.0.1:6379> 
127.0.0.1:6379> del simpleLock  // 删除键,释放锁
(integer) 1 
127.0.0.1:6379> setnx simpleLock Release //再重新获取锁
(integer) 1
127.0.0.1:6379> 

通过 SETNX 基本能实现一个不完全正确的锁,Java代码如下:

package me.touch.redis;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@RunWith(JUnit4.class)
public class SimpleLock {

    private Jedis jedis;
    private JedisPool pool;

    @Before
    public void setUp() {
        pool = new JedisPool(new JedisPoolConfig(), "localhost");
        jedis = pool.getResource();
    }

    @After
    public void after() {
        jedis.close();
        pool.destroy();
    }
    
    /**
     * 获得简单锁
     * @return
     */
    public boolean acquireSimpleLock(String lockName){
        return jedis.setnx(lockName, "Locked") == 1 ;
    }
    
    /**
     * 释放锁
     * @return
     */
    public boolean releaseSimpleLock(String lockName){
        return jedis.del(lockName, "Locked") == 1 ;
    }
    
    @Test
    public void test(){
        if(acquireSimpleLock("simpleLock")){
            System.out.println("获取锁成功 ·····");
            
            // Do something ........
            
            if(releaseSimpleLock("simpleLock")){
                System.out.println("释放锁成功 ·····");
            }
            
        }
    }
}   

运行结果:

获取锁成功 ·····
释放锁成功 ·····

但是这个锁是不完全正确的,缺少超时机制,缺少重试机制,释放锁的时候没有验证当前锁是否由当前进程拥有等。
一个不完全正确的锁会导致一些不正确的行为,如:

  • 当缺少超时机制时,当持有锁的进程死掉后,锁得不释放,造成死锁。
  • 当持有锁的进程操作时间过长导致锁自动释放,但是很进程本身不知道,使得逻辑完成后错误的释放其他进程的锁(需要验证锁是否是当前进程持有)。
  • 当持有锁的进程崩溃后,其他进程无法检测到,只能浪费时间等待锁达到超时时候被释放。
  • 当一个进程持有锁过期后,其他多个进程同时尝试去获取锁,并且都获取了锁,而且都认为自己是唯一一个获取到锁的进程(需要验证锁是否是当前进程持有)

使用 Luna 脚本 (基本正确)

Redis 中的命令是原子执行,的所以我们可以在 Lua 脚本中组合多个命令来完成我们的的逻辑。

lua 脚本获取锁

--  EXISTS 判断是否存在 KEY ,如果存在,说明其他进程已经获得锁,不存在这,设置KEY
if redis.call('EXISTS', KEYS[1]) == 0 then
    return redis.call('SETEX', KEYS[1], unpack(ARGV))
end

lua 脚本释放锁

-- GET 获取 KEY 值,判断是否与指定的值相等,相等则删除KEY
if redis.call('GET', KEYS[1]) == ARGV[1] then 
  return redis.call('DEL', KEYS[1]) or true 
end

Java 源码

     /**
     * 获得锁
     * @param keyName 锁的名称
     * @param keyVlaue 锁的值,建议使用UUID
     * @param expire 锁的过期时间
     * @param timeout 获取所得超时时间,毫秒
     * @return
     */
    public boolean  acquireLockWhithTimeOut(String keyName, String keyVlaue, 
                  String expire, long timeout){
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call('EXISTS', KEYS[1]) == 0 then \n")
          .append(" return redis.call('SETEX', KEYS[1], unpack(ARGV)) \n")
          .append("end");
        
        long now = System.currentTimeMillis();
        do{
            if("OK".equals(jedis.eval(sb.toString(), 1, keyName, expire,  keyVlaue))){
                return true;
            }
        }while( System.currentTimeMillis() < (now + timeout));
        
        return false;
    }
    
    
    /**
     * 释放锁
     * @param keyName 锁名称
     * @param keyVlaue 锁的值
     * @return
     */
    public boolean   releaseLock(String keyName, String keyVlaue){
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call('GET', KEYS[1]) == ARGV[1] then \n")
          .append(" return redis.call('DEL', KEYS[1]) or true \n")
          .append("end");
        return ((Long) jedis.eval(sb.toString(), 1, keyName,  keyVlaue))  == 1 ;
    }

     @Test
    public void test() throws InterruptedException{
        //使用 uuid 作为锁的值
        String  uuid = UUID.randomUUID().toString();
        if(acquireLockWhithTimeOut("simpleLock",  uuid, "60", 60*1000)){
            System.out.println("获取锁成功 ·····");
            // Do something ........
            TimeUnit.SECONDS.sleep(1); // 线程睡上30秒
            if(releaseLock("simpleLock", uuid)){
                System.out.println("释放锁成功 ·····");
            }
        }
    }

运行结果:

b308b026-8b01-4cf0-b145-b9061bf617f6
获取锁成功 ·····
释放锁成功 ·····

在这个例子中通过传入 timeout 设置获取锁的超时时间实现了锁获取的重试机制;同时,通过 expire 指定了 key 的过期时间,避免照成了死锁。在获取锁时指定的值为UUID,保证了锁的唯一性。此外,在释放锁时比较 UUID 成功避免错误释放其他进程锁的问题,因此也不会出现多个进程多获取到锁的情况。当前实现已经是基本正确的锁实现了,能用于绝大部分应用场景,但是依然没有解决因为持有锁的进程崩溃造成其他进程浪费时间等待锁过期的问题。

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

推荐阅读更多精彩内容