分布式的几件小事(十)分布式锁是啥

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 介绍分布式锁相关知识

1.布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

2.实现方式

1.基于数据库实现

①基于数据库表实现。

实现方式 : 这是最简单的方法,可以在数据库中建立一张表,然后通过操作该表中的数据来实现。


当我们想要锁住某个方法或资源的时候,就在表中增加一条记录,想要释放锁的时候就删除这条记录。


比如创建如下的表结构:

CREATETABLE myLock(  id INT(11)NOTNULL AUTO_INCREMENT COMMENT '自增主键id',  method_name VARCHAR(256)NOTNULL DEFAULT '' COMMENT '要加锁的方法名',  create_time TIMESTAMPNOTNULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据创建时间',  PRIMARY key(id),  UNIQUE key `idx_method_name`(method_name)) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='分布式锁';

想要锁住某个方法的时间,就去插入一条数据。

insertinto myLock(method_name)values("method_name")

方法执行完毕之后,就删除这条数据,释放锁。

deletefrom myLock where method_name ="method_name"

存在的问题

A: 强依赖数据库,如果数据是单点,一旦数据库挂掉就不可用。

B: 没有失效时间,一旦解锁失败,或者加锁服务挂掉,锁将一直存在。

C: 锁是非阻塞的,引入插入如果失败叫报错了,不会等待去获取锁,想要重新获取锁就要去重新触发该操作。

D: 锁是非重入的,同一个线程在释放锁之前无法再次获取锁。


解决方案

A: 数据库使用主从架构,进行数据的主从双向同步,主库挂掉里面切换从库。

B: 可以在系统里面做一个定时任务,每隔一定时间就去清理一下超时的锁。

C: 在进行获取锁的时候,在执行insert操作时搞一个while循环不断去尝试,知道成功。

D: 可以在表中再增加信息,比如当前获得锁的机器信息和线程信息,判断如果是同一个,直接获取锁。


②基于数据库排它锁实现

还是使用刚才创建的那张表,可以使用数据库的排它锁来实现,基于InnoDB引擎,可以在获取锁时关闭自动提交,然后在查询语句中加上 for update来获取数据库的排它锁(注意:where后面的字段一定要加索引,只有这样才会使用行锁),当获取排它锁成功后,可以进行操作,操作完毕提交事物来释放该锁。


优势

A: 天然阻塞,在获取排它锁失败时会一直阻塞,知道成功。

B: 服务宕机之后会释放锁。


缺点

A: 数据库单点问题。

B: 锁的可重入问题。

C: mysql可能会进行查询优化,可能不使用索引,而去全表扫描,这样将加表锁。

D: 如果一个排它锁一直不去提交,就会占用数据库的连接,一旦变多就有可能撑爆数据库连接池。


2.基于redis实现。

①最普通的锁实现

在redis里面通过下面的指令创建一个key,

// NX   表示只有key不存的时候,才会成功,存在就失败     
// PX 30000  表示30秒后锁自动释放
SET mylock 随机值 NX PX 30000

如果创建成功就表示获取到了锁,否则就表示没有获取到锁。


释放锁就是删除key,但是一般可以用lua脚本删除,这样可以去判断value一样才会删除。


为啥要用随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除key的话就会删除本来不属于你自己的锁,所以得用随机值加上面的lua脚本来释放锁。

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

存在的问题

A: redis单点问题,如果redis挂了那么就会无法获取锁。

B: redis就算是主从架构也会出现问题,比如key还没来的级复制给从节点,主节点就挂了。


②RedLock算法

获取锁原理

假设有一个redis cluster集群,有5个redis master实例,然后执行如下步骤去获取锁。

A: 获取当前时间戳,单位是毫秒。

B: 轮流尝试在每个master节点上面创建锁,过程比较短,一般就十几毫秒。

C: 尝试在大多数节点上建立锁,比如5个节点就要求最少3个节点创建成功。

D: 客户端计算建立好锁的时间,如果建立锁的时机小于超时时间,就算建立成功了。

E: 如果所建立失败了,那么就依次去删除已经建立成功的锁。

F: 如果别人已经建立了一把分布式错,自己就不断轮询去尝试获取锁。


存在问题

A: 获取锁失败时需要不断去重试,比较消耗性能。

B: 如果redis获取锁的客户端挂掉了,需要等待超时才会释放锁。

C: 上锁需要遍历,计算时间等,过程比较复杂。

D: 不是十分的可靠,可参考这两篇文章:

How to do distributed locking

[Is Redlock safe?](http://antirez.com/news/101)


3.使用zookeeper实现

①基于零时节点实现

zk分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。


简易代码如下:

/*** ZooKeeperSession* @author Administrator**/publicclassZooKeeperSession {
privatestaticCountDownLatchconnectedSemaphore=newCountDownLatch(1);
privateZooKeeperzookeeper;
privateCountDownLatchlatch;
publicZooKeeperSession() {
try {
this.zookeeper=newZooKeeper(
"192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 
50000, 
newZooKeeperWatcher());            
try {
connectedSemaphore.await();
            } catch(InterruptedExceptione) {
e.printStackTrace();
            }
System.out.println("ZooKeeper session established......");
        } catch (Exceptione) {
e.printStackTrace();
        }
    }
/*** 获取分布式锁* @param productId*/publicBooleanacquireDistributedLock(LongproductId) {
Stringpath="/product-lock-"+productId;
try {
zookeeper.create(path, "".getBytes(), 
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
returntrue;
        } catch (Exceptione) {
while(true) {
try {
Statstat=zk.exists(path, true); // 相当于是给node注册一个监听器,去看看这个监听器是否存在if(stat!=null) {
this.latch=newCountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch=null;
                        }
zookeeper.create(path, "".getBytes(), 
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
returntrue;
                     } catch(Exceptione) {
continue;
                     }
            }
// 很不优雅,我呢就是给大家来演示这么一个思路,比较通用的.      }
returntrue;
    }
/*** 释放掉一个分布式锁* @param productId*/publicvoidreleaseDistributedLock(LongproductId) {
Stringpath="/product-lock-"+productId;
try {
zookeeper.delete(path, -1); 
System.out.println("release the lock for product[id="+productId+"]......");  
        } catch (Exceptione) {
e.printStackTrace();
        }
    }
/*** 建立zk session的watcher* @author Administrator**/privateclassZooKeeperWatcherimplementsWatcher {
publicvoidprocess(WatchedEventevent) {
System.out.println("Receive watched event: "+event.getState());
if(KeeperState.SyncConnected==event.getState()) {
connectedSemaphore.countDown();
            } 
if(this.latch!=null) {  
this.latch.countDown();  
             }
        }
    }
/*** 封装单例的静态内部类* @author Administrator**/privatestaticclassSingleton {
privatestaticZooKeeperSessioninstance;
static {
instance=newZooKeeperSession();
        }
publicstaticZooKeeperSessiongetInstance() {
returninstance;
        }
    }
/*** 获取单例* @return*/publicstaticZooKeeperSessiongetInstance() {
returnSingleton.getInstance();
    }
/*** 初始化单例的便捷方法*/publicstaticvoidinit() {
getInstance();
    }
}

存在的问题

A: 需要动态的去创建、销毁节点,性能没有基于redis的高。

B: 使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试。多次重试之后还不行的话才会删除临时节点。

C: 尝试获取锁,锁销毁之后需要获取锁的线程要去竞争锁,复杂。


②就行零时顺序节点实现。

相比如前面的方式来说。零时顺序节点就是在创建零时节点的时候不再是创建失败,而是给每个节点添加了一个编号,每次只有编号最小的可以获取到锁,非最小的节点去监听离他最近的那个次小的节点,实现获取锁的有序性。这样每个节点只需要去监听一个节点,比他小的那个节点释放锁他会成功获取锁。

简易代码实例:

publicclassZooKeeperDistributedLockimplementsWatcher{
privateZooKeeperzk;
privateStringlocksRoot="/locks";
privateStringproductId;
privateStringwaitNode;
privateStringlockNode;
privateCountDownLatchlatch;
privateCountDownLatchconnectedLatch=newCountDownLatch(1);
privateintsessionTimeout=30000; 
publicZooKeeperDistributedLock(StringproductId){
this.productId=productId;
try {
Stringaddress="192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";
zk=newZooKeeper(address, sessionTimeout, this);
connectedLatch.await();
        } catch (IOExceptione) {
thrownewLockException(e);
        } catch (KeeperExceptione) {
thrownewLockException(e);
        } catch (InterruptedExceptione) {
thrownewLockException(e);
        }
    }
publicvoidprocess(WatchedEventevent) {
if(event.getState()==KeeperState.SyncConnected){
connectedLatch.countDown();
return;
        }
if(this.latch!=null) {  
this.latch.countDown(); 
        }
    }
publicvoidacquireDistributedLock() {   
try {
if(this.tryLock()){
return;
            }
else{
waitForLock(waitNode, sessionTimeout);
            }
        } catch (KeeperExceptione) {
thrownewLockException(e);
        } catch (InterruptedExceptione) {
thrownewLockException(e);
        } 
    }
publicbooleantryLock() {
try {
// 传入进去的locksRoot + “/” + productId// 假设productId代表了一个商品id,比如说1// locksRoot = locks// /locks/10000000000,/locks/10000000001,/locks/10000000002lockNode=zk.create(locksRoot+"/"+productId, newbyte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 看看刚创建的节点是不是最小的节点// locks:10000000000,10000000001,10000000002List<String>locks=zk.getChildren(locksRoot, false);
Collections.sort(locks);
if(lockNode.equals(locksRoot+"/"+locks.get(0))){
//如果是最小的节点,则表示取得锁returntrue;
            }
//如果不是最小的节点,找到比自己小1的节点intpreviousLockIndex=-1;
for(inti=0; i<locks.size(); i++) {
if(lockNode.equals(locksRoot+/+locks.get(i))) {
previousLockIndex=i-1;
break;
                 }
            }
this.waitNode=locks.get(previousLockIndex);
        } catch (KeeperExceptione) {
thrownewLockException(e);
        } catch (InterruptedExceptione) {
thrownewLockException(e);
        }
returnfalse;
    }
privatebooleanwaitForLock(StringwaitNode, longwaitTime) throwsInterruptedException, KeeperException {
Statstat=zk.exists(locksRoot+"/"+waitNode, true);
if(stat!=null){
this.latch=newCountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);                 this.latch=null;
        }
returntrue;
   }
publicvoidunlock() {
try {
// 删除/locks/10000000000节点// 删除/locks/10000000001节点System.out.println("unlock "+lockNode);
zk.delete(lockNode,-1);
lockNode=null;
zk.close();
        } catch (InterruptedExceptione) {
e.printStackTrace();
        } catch (KeeperExceptione) {
e.printStackTrace();
        }
    }
publicclassLockExceptionextendsRuntimeException {
privatestaticfinallongserialVersionUID=1L;
publicLockException(Stringe){
super(e);
        }
publicLockException(Exceptione){
super(e);
        }
    }
}

3.比较

三种方案的比较 上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。


从理解的难易程度角度(从低到高) 数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高) Zookeeper >= 缓存 > 数据库

从性能角度(从高到低) 缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低) Zookeeper > 缓存 > 数据库

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
NoSQL Java Redis
【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的分布式锁的功能组件(一)
【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的分布式锁的功能组件
91 0
|
4月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
4月前
|
存储 调度
分布式锁设计问题之云存储的最佳实践中保障分布式锁的容错能力如何解决
分布式锁设计问题之云存储的最佳实践中保障分布式锁的容错能力如何解决
|
2月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
64 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
5月前
|
NoSQL Java Redis
分布式锁实现原理问题之使用Redis的setNx命令来实现分布式锁问题如何解决
分布式锁实现原理问题之使用Redis的setNx命令来实现分布式锁问题如何解决
|
2月前
|
分布式计算 NoSQL Java
Hadoop-32 ZooKeeper 分布式锁问题 分布式锁Java实现 附带案例和实现思路代码
Hadoop-32 ZooKeeper 分布式锁问题 分布式锁Java实现 附带案例和实现思路代码
46 2
|
2月前
|
SQL NoSQL 安全
分布式环境的分布式锁 - Redlock方案
【10月更文挑战第2天】Redlock方案是一种分布式锁实现,通过在多个独立的Redis实例上加锁来提高容错性和可靠性。客户端需从大多数节点成功加锁且总耗时小于锁的过期时间,才能视为加锁成功。然而,该方案受到分布式专家Martin的质疑,指出其在特定异常情况下(如网络延迟、进程暂停、时钟偏移)可能导致锁失效,影响系统的正确性。Martin建议采用fencing token方案,以确保分布式锁的正确性和安全性。
52 0
|
4月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
这篇文章介绍了如何在SpringBoot项目中整合Redis,并探讨了缓存穿透、缓存雪崩和缓存击穿的问题以及解决方法。文章还提供了解决缓存击穿问题的加锁示例代码,包括存在问题和问题解决后的版本,并指出了本地锁在分布式情况下的局限性,引出了分布式锁的概念。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
|
3月前
|
Java
分布式-Zookeeper-分布式锁
分布式-Zookeeper-分布式锁
|
7月前
|
NoSQL Java Redis
【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的分布式锁的功能组件(二)
【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的分布式锁的功能组件
46 0