{"id":22327589,"url":"https://github.com/sillyhatxu/distributed-lock","last_synced_at":"2025-07-22T13:05:14.895Z","repository":{"id":121860443,"uuid":"224136479","full_name":"sillyhatxu/distributed-lock","owner":"sillyhatxu","description":"Distributed locks are a very useful primitive in many environments where different processes must operate with shared resources in a mutually exclusive way.","archived":false,"fork":false,"pushed_at":"2020-03-11T08:10:39.000Z","size":51,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-26T06:27:28.426Z","etag":null,"topics":["distributed-lock","golang","redis-lock","simple"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sillyhatxu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-11-26T08:08:25.000Z","updated_at":"2020-07-25T15:53:25.000Z","dependencies_parsed_at":null,"dependency_job_id":"80b68527-52e8-409a-b53a-17e26787e45e","html_url":"https://github.com/sillyhatxu/distributed-lock","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/sillyhatxu/distributed-lock","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sillyhatxu%2Fdistributed-lock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sillyhatxu%2Fdistributed-lock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sillyhatxu%2Fdistributed-lock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sillyhatxu%2Fdistributed-lock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sillyhatxu","download_url":"https://codeload.github.com/sillyhatxu/distributed-lock/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sillyhatxu%2Fdistributed-lock/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266499045,"owners_count":23938779,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["distributed-lock","golang","redis-lock","simple"],"created_at":"2024-12-04T03:09:50.191Z","updated_at":"2025-07-22T13:05:14.867Z","avatar_url":"https://github.com/sillyhatxu.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# distributed-lock\n\n[![Build Status](https://travis-ci.org/go-redsync/redsync.svg?branch=master)](https://travis-ci.org/go-redsync/redsync)\n\n\nDistributed locks are a very useful primitive in many environments where different processes must operate with shared resources in a mutually exclusive way. `distributed-lock` provides a Redis-based distributed mutual exclusion lock implementation for Go as described.\n\n\n\n## Installation\n\nhttps://blog.kowalczyk.info/article/JyRZ/generating-good-unique-ids-in-go.html\n\n\nhttps://crazyfzw.github.io/2019/04/15/distributed-locks-with-redis/\n\n\n### 实现一\n\n1. 加锁代码分析\n\n首先，set()加入了NX参数，可以保证如果已有key存在，则函数不会调用成功，也就是只有一个客户端能持有锁，满足互斥性。\n\n其次，由于我们对锁设置了过期时间，即使锁的持有者后续发生崩溃而没有解锁，锁也会因为到了过期时间而自动解锁（即key被删除），不会发生死锁。\n\n最后，因为我们将value赋值为requestId，用来标识这把锁是属于哪个请求加的，那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。\n\n2. 解锁代码分析\n\n将Lua代码传到jedis.eval()方法里，并使参数KEYS[1]赋值为lockKey，ARGV[1]赋值为requestId。\n\n在执行的时候，首先会获取锁对应的value值，检查是否与requestId相等，如果相等则解锁（删除key）。\n\n3. 存在单点风险\n\n以上实现在 Redis 正常运行情况下是没问题的，但如果存储锁对应key的那个节点挂了的话，就可能存在丢失锁的风险，导致出现多个客户端持有锁的情况，这样就不能实现资源的独享了。\n\n客户端A从master获取到锁\n\n在master将锁同步到slave之前，master宕掉了（Redis的主从同步通常是异步的）。\n\n主从切换，slave节点被晋级为master节点\n\n客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。导致存在同一时刻存不止一个线程获取到锁的情况。\n\n所以在这种实现之下，不论Redis的部署架构是单机模式、主从模式、哨兵模式还是集群模式，都存在这种风险。\n\n因为Redis的主从同步是异步的。 运行的是，Redis 之父 antirez 提出了 redlock算法 可以解决这个问题。\n\n\n\nInstall Redsync using the go get command:\n\n    $ go get github.com/go-redsync/redsync\n\nThe only dependencies are the Go distribution and [Redigo](https://github.com/gomodule/redigo).\n\n## Documentation\n\n- [Reference](https://godoc.org/github.com/go-redsync/redsync)\n\n## Contributing\n\nContributions are welcome.\n\n## License\n\nRedsync is available under the [BSD (3-Clause) License](https://opensource.org/licenses/BSD-3-Clause).\n\n## Disclaimer\n\nThis code implements an algorithm which is currently a proposal, it was not formally analyzed. Make sure to understand how it works before using it in production environments.\n\n\n\n## 一、实现分布式锁的要求\n\n1. 互斥性。在任何时候，当且仅有一个客户端能够持有锁。\n2. 不能有死锁。持有锁的客户端崩溃后，后续客户端能够加锁。\n3. 容错性。大部分Redis或者ZooKeeper节点能够正常运行。\n4. 加锁解锁相同。加锁的客户端和解锁的客户端必须为同一个客户端，不能让其他的解锁了。\n\n## 二、Redis实现分布式锁的常用命令\n\n1.SETNX key val\n\n当且仅当key不存在时，set一个key为val的字符串，返回1；若key存在，则什么都不做，返回0。\n\n2.expire key timeout\n\n为key设置一个超时时间，单位为second，超过这个时间锁会自动释放，避免死锁。\n\n3.delete key\n\n删除key，此处用来解锁使用。\n\n4.HEXISTS key field\n\n当key 中存储着field的时候返回1，如果key或者field至少有一个不存在返回0。\n\n5.HINCRBY key field increment\n\n将存储在 key 中的哈希（Hash）对象中的指定字段 field 的值加上增量 increment。如果键 key 不存在，一个保存了哈希对象的新建将被创建。如果字段 field 不存在，在进行当前操作前，其将被创建，且对应的值被置为 0。返回值是增量之后的值。\n\n## 三、常见写法\n\n由上面三个命令，我们可以很快的写一个分布式锁出来：\n\n```\nif (conn.setnx(\"lock\",\"1\").equals(1L)) { \n    //do something\n    return true; \n} \n```\n\nreturn false; \n\n但是这样也会存在问题，如果获取该锁的客户端挂掉了怎么办？一般而言，我们可以通过设置expire的过期时间来防止客户端挂掉所带来的影响，可以降低应用挂掉所带来的影响，不过当时间失效的时候，要保证其他客户端只有一台能够获取。\n\n## 四、Redisson\n\nRedisson在基于NIO的Netty框架上，充分的利用了Redis键值数据库提供的一系列优势，在Java实用工具包中常用接口的基础上，为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力，大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务，更进一步简化了分布式环境中程序相互之间的协作。——摘自百度百科\n\n### 4.1 测试例子\n先在pom引入Redssion\n\n```\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.redisson\u003c/groupId\u003e\n    \u003cartifactId\u003eredisson\u003c/artifactId\u003e\n    \u003cversion\u003e3.6.1\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n起100个线程，同时对count进行操作，每次操作减1，加锁的时候能够保持顺序输出，不加的话为随机。\n\n```\npublic class RedissonTest implements Runnable {\n    private static RedissonClient redisson;\n    private static int count = 10000;\n\n    private static void init() {\n        Config config = new Config();\n        config.useSingleServer()\n                .setAddress(\"redis://119.23.46.71:6340\")\n                .setPassword(\"root\")\n                .setDatabase(10);\n        redisson = Redisson.create(config);\n    }\n\n    @Override\n    public void run() {\n        RLock lock = redisson.getLock(\"anyLock\");\n        lock.lock();\n        count--;\n        System.out.println(count);\n        lock.unlock();\n    }\n\n    public static void main(String[] args) {\n        init();\n        for (int i = 0; i \u003c 100; i++) {\n            new Thread(new RedissonTest()).start();\n        }\n    }\n}\n```\n输出结果（部分结果）：\n\n```\n...\n9930\n9929\n9928\n9927\n9926\n9925\n9924\n9923\n9922\n9921\n\n...\n```\n\n去掉lock.lock()和lock.unlock()之后（部分结果）：\n\n```\n...\n9930\n9931\n9933\n9935\n9938\n9937\n9940\n9941\n9942\n9944\n9947\n9946\n9914\n...\n```\n\n## 五、RedissonLock源码分析\n\n最新版的Redisson要求redis能够支持eval的命令，否则无法实现，即Redis要求2.6版本以上。在lua脚本中可以调用大部分的Redis命令，使用脚本的好处如下：\n\n(1)减少网络开销:在Redis操作需求需要向Redis发送5次请求，而使用脚本功能完成同样的操作只需要发送一个请求即可，减少了网络往返时延。\n\n(2)原子操作:Redis会将整个脚本作为一个整体执行，中间不会被其他命令插入。换句话说在编写脚本的过程中无需担心会出现竞态条件，也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。\n\n(3)复用:客户端发送的脚本会永久存储在Redis中，这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。\n\n\n### 5.1 使用到的全局变量\n\n全局变量：\n\nexpirationRenewalMap：存储entryName和其过期时间，底层用的netty的PlatformDependent.newConcurrentHashMap()\ninternalLockLeaseTime：锁默认释放的时间：30 * 1000，即30秒\nid：UUID，用作客户端的唯一标识\nPUBSUB：订阅者模式，当释放锁的时候，其他客户端能够知道锁已经被释放的消息，并让队列中的第一个消费者获取锁。使用PUB/SUB消息机制的优点：减少申请锁时的等待时间、安全、 锁带有超时时间、锁的标识唯一，防止死锁 锁设计为可重入，避免死锁。\ncommandExecutor：命令执行器，异步执行器\n\n### 5.2 加锁\n\n以lock.lock()为例，调用lock之后，底层使用的是lockInterruptibly，之后调用lockInterruptibly(-1, null);\n\n（1）我们来看一下lockInterruptibly的源码，如果别的客户端没有加锁，则当前客户端进行加锁并且订阅，其他客户端尝试加锁，并且获取ttl，然后等待已经加了锁的客户端解锁。\n\n```\n//leaseTime默认为-1\npublic void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {\n    long threadId = Thread.currentThread().getId();//获取当前线程ID\n    Long ttl = tryAcquire(leaseTime, unit, threadId);//尝试加锁\n    // 如果为空，当前线程获取锁成功，否则已经被其他客户端加锁\n    if (ttl == null) {\n        return;\n    }\n    //等待释放，并订阅锁\n    RFuture\u003cRedissonLockEntry\u003e future = subscribe(threadId);\n    commandExecutor.syncSubscription(future);\n    try {\n        while (true) {\n            // 重新尝试获取锁\n            ttl = tryAcquire(leaseTime, unit, threadId);\n            // 成功获取锁\n            if (ttl == null) {\n                break;\n            }\n            // 等待锁释放\n            if (ttl \u003e= 0) {\n                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);\n            } else {\n                getEntry(threadId).getLatch().acquire();\n            }\n        }\n    } finally {\n        // 取消订阅\n        unsubscribe(future, threadId);\n    }\n}\n```\n（2）下面是tryAcquire的实现，调用的是tryAcquireAsync\n\n    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {\n        return get(tryAcquireAsync(leaseTime, unit, threadId));\n    }\n（3）下面是tryAcquireAsync的实现，异步尝试进行加锁，尝试加锁的时候leaseTime为-1。通常如果客户端没有加锁成功，则会进行阻塞，leaseTime为锁释放的时间。\n\n```\nprivate \u003cT\u003e RFuture\u003cLong\u003e tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {\n    if (leaseTime != -1) {   //在lock.lock()的时候，已经声明了leaseTime为-1，尝试加锁\n        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);\n    }\n    RFuture\u003cLong\u003e ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);\n    //监听事件，订阅消息\n    ttlRemainingFuture.addListener(new FutureListener\u003cLong\u003e() {\n        @Override\n        public void operationComplete(Future\u003cLong\u003e future) throws Exception {\n            if (!future.isSuccess()) {\n                return;\n            }\n            Long ttlRemaining = future.getNow();\n            // lock acquired\n            if (ttlRemaining == null) {\n                //获取新的超时时间\n                scheduleExpirationRenewal(threadId);\n            }\n        }\n    });\n    return ttlRemainingFuture;  //返回ttl时间\n}\n```\n\n（4）下面是tryLockInnerAsyncy异步加锁，使用lua能够保证操作是原子性的\n\n```\n\u003cT\u003e RFuture\u003cT\u003e tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand\u003cT\u003e command) {\n    internalLockLeaseTime = unit.toMillis(leaseTime);\n    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,\n              \"if (redis.call('exists', KEYS[1]) == 0) then \" +\n                  \"redis.call('hset', KEYS[1], ARGV[2], 1); \" +\n                  \"redis.call('pexpire', KEYS[1], ARGV[1]); \" +\n                  \"return nil; \" +\n              \"end; \" +\n              \"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then \" +\n                  \"redis.call('hincrby', KEYS[1], ARGV[2], 1); \" +\n                  \"redis.call('pexpire', KEYS[1], ARGV[1]); \" +\n                  \"return nil; \" +\n              \"end; \" +\n              \"return redis.call('pttl', KEYS[1]);\",\n                Collections.\u003cObject\u003esingletonList(getName()), internalLockLeaseTime, getLockName(threadId));\n}\n```\n\n参数\nKEYS[1](getName()) ：需要加锁的key，这里需要是字符串类型。\nARGV[1](internalLockLeaseTime) ：锁的超时时间，防止死锁\nARGV[2](getLockName(threadId)) ：锁的唯一标识，也就是刚才介绍的 id（UUID.randomUUID()） + “:” + threadId\n\n### lua脚本解释\n\n--检查key是否被占用了，如果没有则设置超时时间和唯一标识，初始化value=1\n\n```\nif (redis.call('exists', KEYS[1]) == 0) then\n  redis.call('hset', KEYS[1], ARGV[2], 1);\n  redis.call('pexpire', KEYS[1], ARGV[1]);\n  return nil; \nend; \n```\n--如果锁重入,需要判断锁的key field 都一致情况下 value 加一 \n\n```\nif (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then \n  redis.call('hincrby', KEYS[1], ARGV[2], 1);\n  --锁重入重新设置超时时间  \n  redis.call('pexpire', KEYS[1], ARGV[1]); \n  return nil; \nend;\n```\n--返回剩余的过期时间\n\n```\nreturn redis.call('pttl', KEYS[1]);\n```\n（5）流程图\n\n![process.png](assets/process.png)\n\n### 5.3 解锁\n\n解锁的代码很简单，大意是将该节点删除，并发布消息。\n\n（1）unlock源码\n\n    public void unlock() {\n        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));\n        if (opStatus == null) {\n            throw new IllegalMonitorStateException(\"attempt to unlock lock, not locked by current thread by node id: \"\n                    + id + \" thread-id: \" + Thread.currentThread().getId());\n        }\n        if (opStatus) {\n            cancelExpirationRenewal();\n        }\n（2）异步解锁，并返回是否成功\n```\nprotected RFuture\u003cBoolean\u003e unlockInnerAsync(long threadId) {\n    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,\n            \"if (redis.call('exists', KEYS[1]) == 0) then \" +\n                \"redis.call('publish', KEYS[2], ARGV[1]); \" +\n                \"return 1; \" +\n            \"end;\" +\n            \"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then \" +\n                \"return nil;\" +\n            \"end; \" +\n            \"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); \" +\n            \"if (counter \u003e 0) then \" +\n                \"redis.call('pexpire', KEYS[1], ARGV[2]); \" +\n                \"return 0; \" +\n            \"else \" +\n                \"redis.call('del', KEYS[1]); \" +\n                \"redis.call('publish', KEYS[2], ARGV[1]); \" +\n                \"return 1; \"+\n            \"end; \" +\n            \"return nil;\",\n            Arrays.\u003cObject\u003easList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));\n\n    }\n```\n\n##### 输入的参数有：\n参数：\n\nKEYS[1](getName())：需要加锁的key，这里需要是字符串类型。\n\nKEYS[2](getChannelName())：redis消息的ChannelName,一个分布式锁对应唯一的一个 channelName:“redisson_lock__channel__{” + getName() + “}”\n\nARGV[1](LockPubSub.unlockMessage)：redis消息体，这里只需要一个字节的标记就可以，主要标记redis的key已经解锁，再结合redis的Subscribe，能唤醒其他订阅解锁消息的客户端线程申请锁。\n\nARGV[2](internalLockLeaseTime)：锁的超时时间，防止死锁\n\nARGV[3](getLockName(threadId)) ：锁的唯一标识，也就是刚才介绍的 id（UUID.randomUUID()） + “:” + threadId\n\n此处lua脚本的作用：\n\n* 如果keys[1]不存在，则发布消息，说明已经被解锁了\n\n```\nif (redis.call('exists', KEYS[1]) == 0) then\n    redis.call('publish', KEYS[2], ARGV[1]);\n    return 1;\nend;\n```\n\n* key和field不匹配，说明当前客户端线程没有持有锁，不能主动解锁。\n\n```\nif (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then\n    return nil;\nend;\n```\n\n* 将value减1，这里主要用在重入锁\n\n```\nlocal counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); \nif (counter \u003e 0) then \n    redis.call('pexpire', KEYS[1], ARGV[2]); \n    return 0; \nelse \n```\n\n* 删除key并消息\n\n```\n    redis.call('del', KEYS[1]); \n    redis.call('publish', KEYS[2], ARGV[1]); \n    return 1;\nend; \nreturn nil;   \n```     \n\n（3）删除过期信息\n\n```\nvoid cancelExpirationRenewal() {\n    Timeout task = expirationRenewalMap.remove(getEntryName());\n    if (task != null) {\n        task.cancel();\n    }\n}\n```\n\n## 总结\n\nRedis2.6版本之后引入了eval，能够支持lua脚本，更好的保证了redis的原子性，而且redisson采用了大量异步的写法来避免性能所带来的影响。本文只是讲解了下redisson的重入锁，其还有公平锁、联锁、红锁、读写锁等，有兴趣的可以看下。感觉这篇文章写得也不是很好，毕竟netty还没开始学，有些api也不太清楚，希望各位大佬能够建议建议~~\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsillyhatxu%2Fdistributed-lock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsillyhatxu%2Fdistributed-lock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsillyhatxu%2Fdistributed-lock/lists"}