了解一下三种分布式锁:关系型数据库分布式锁、redis缓存分布式锁、zookeeper分布式锁

基于数据库实现分布式锁
要实现分布式锁,最简单的方式就是创建一张锁表,然后通过操作该表中的数据来实现。
当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保证只有一个操作可以成功,操作成功的那个线程就获得了访问共享资源的锁,可以进行操作。
基于数据库实现的分布式锁,是最容易理解的。但是,因为数据库需要落到硬盘上,频繁读取数据库会导致IO开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。对于双11、双12等需求量激增的场景,数据库锁是无法满足其性能要求的。而在平日的购物中,我们可以在局部场景中使用数据库锁实现对资源的互斥访问。
下面,以电商售卖吹风机的场景为例。吹风机库存是2个,有5个来自不同地区的用户{A,B,C,D,E}想要购买,其中用户A想买1个,用户B想买2个,用户C想买1个。
用户A和用户B几乎同时下单,但用户A的下单请求最先到达服务器。因此,该商家的产品数据库中增加了一条关于用户A的记录,用户A获得了锁,他的订单请求被处理,服务器修改吹风机库存数,减去1后还剩下1个。
当用户A的订单请求处理完成后,有关用户A的记录被删除,服务器开始处理用户B的订单请求。这时,库存只有1个了,无法满足用户B的订单需求,因此用户B购买失败。
从数据库中,删除用户B的记录,服务器开始处理用户C的订单请求,库存中1个吹风机满足用户C的订单需求。所以,数据库中增加了一条关于用户C的记录,用户C获得了锁,他的订单请求被处理,服务器修改吹风机数量,减去1后还剩下0个。

 

 

可以看出,基于数据库实现分布式锁比较简单,绝招在于创建一张锁表,为申请者在锁表里建立一条记录,记录建立成功则获得锁,消除记录则释放锁。该方法依赖于数据库,主要有两个缺点:
单点故障问题。一旦数据库不可用,会导致整个系统崩溃。
死锁问题。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。一旦已获得锁的进程挂掉或者解锁操作失败,会导致锁记录一直存在数据库中,其他进程无法获得锁。

 

基于缓存实现分布式锁
数据库的性能限制了业务的并发量,那么对于双11、双12等需求量激增的场景是否有解决方法呢?
基于缓存实现分布式锁的方式,非常适合解决这种场景下的问题。所谓基于缓存,也就是说把数据存放在计算机内存中,不需要写入磁盘,减少了IO读写。接下来,我以Redis为例与你展开这部分内容。
Redis通常可以使用setnx(key, value)函数来实现分布式锁。key和value就是基于缓存的分布式锁的两个属性,其中key表示锁id,value = currentTime + timeOut,表示当前时间+超时时间。也就是说,某个进程获得key这把锁后,如果在value的时间内未释放锁,系统就会主动释放锁。
setnx函数的返回值有0和1:
返回1,说明该服务器获得锁,setnx将key对应的value设置为当前时间 + 锁的有效时间。
返回0,说明其他服务器已经获得了锁,进程不能进入临界区。该服务器可以不断尝试setnx操作,以获得锁。

是以电商售卖吹风机的场景为例,和你说明基于缓存实现的分布式锁,假设现在库存数量是足够的。
用户A的请求因为网速快,最先到达Server2,setnx操作返回1,并获取到购买吹风机的锁;用户B和用户C的请求,几乎同时到达了Server1和Server3,但因为这时Server2获取到了吹风机数据的锁,所以只能加入等待队列。
Server2获取到锁后,负责管理吹风机的服务器执行业务逻辑,只用了1s就完成了订单。订单请求完成后,删除锁的key,从而释放锁。此时,排在第二顺位的Server1获得了锁,可以访问吹风机的数据资源。但不巧的是,Server1在完成订单后发生了故障,无法主动释放锁。
于是,排在第三顺位的Server3只能等设定的有效时间(比如30分钟)到期,锁自动释放后,才能访问吹风机的数据资源,也就是说用户C只能到00:30:01以后才能继续抢购。

 

总结来说,Redis通过队列来维持进程访问共享资源的先后顺序。Redis锁主要基于setnx函数实现分布式锁,当进程通过setnx<key,value>函数返回1时,表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁,或者等到时间超时才能获得锁。
相对于基于数据库实现分布式锁的方案来说,基于缓存实现的分布式锁的优势表现在以下几个方面:
性能更好。数据被存放在内存,而不是磁盘,避免了频繁的IO操作。
很多缓存可以跨集群部署,避免了单点故障问题。

很多缓存服务都提供了可以用来实现分布式锁的方法,比如Redis的setnx方法等。
可以直接设置超时时间来控制锁的释放,因为这些缓存服务器一般支持自动删除过期数据。
这个方案的不足是,通过超时时间来控制锁的失效时间,并不是十分靠谱,因为一个进程执行时间可能比较长,或受系统进程做内存回收等影响,导致时间超时,从而不正确地释放了锁。

单机redis锁看看下面的文章

来了解一下分布式锁以及Redis分布式锁demo(单机)

redis集群分布式锁(大部分大厂的首选)

redis多主集群分布式锁需要就算容错率,要保证数据ok。比如网络中死了一台机器,要求还是ok的,那么请问需要部署多少台机器。

 

N=2*1+1=3

redis死了两台机器,要求还是ok的,那么需要N=2*2+1 = 5,那么是5台机器;

容错率计算规则:N(部署台数)=2*(宕机数)+1

设计理念

该方案也是基于(set加锁、lua脚本解锁)进行改良的,所以redis之父antirez只描述了差异的地方,大致方案如下:

假设我们有N个redis主节点,例如N=5,这些节点是完全独立的,我们不使用复制或者任何其他隐式协调系统,为了取到锁客户端执行以下操作

客户端只有在满足下面两个条件时,才能认为是加锁成功

  1. 客户端从超过半数(大于等于N/2+1)的redis实例上获取到了锁;
  2. 客户端获取锁的总耗时没有超过锁的有效时间;

基于ZooKeeper实现分布式锁
ZooKeeper基于树形数据存储结构实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题。ZooKeeper的树形数据存储结构主要由4种节点构成:
持久节点。这是默认的节点类型,一直存在于ZooKeeper中。
持久顺序节点。也就是说,在创建节点时,ZooKeeper根据节点创建的时间顺序对节点进行编号。
临时节点。与持久节点不同,当客户端与ZooKeeper断开连接后,该进程创建的临时节点就会被删除。
临时顺序节点,就是按时间顺序编号的临时节点。
根据它们的特征,ZooKeeper基于临时顺序节点实现了分布锁。
还是以电商售卖吹风机的场景为例。假设用户A、B、C同时在11月11日的零点整提交了购买吹风机的请求,ZooKeeper会采用如下方法来实现分布式锁:

  1. 在与该方法对应的持久节点shared_lock的目录下,为每个进程创建一个临时顺序节点。如下图所示,吹风机就是一个拥有shared_lock的目录,当有人买吹风机时,会为他创建一个临时顺序节点。
  2. 每个进程获取shared_lock目录下的所有临时节点列表,注册子节点变更的Watcher,并监听节点。
  3. 每个节点确定自己的编号是否是shared_lock下所有子节点中最小的,若最小,则获得锁。例如,用户A的订单最先到服务器,因此创建了编号为1的临时顺序节点LockNode1。该节点的编号是持久节点目录下最小的,因此获取到分布式锁,可以访问临界资源,从而可以购买吹风机。
  4. 若本进程对应的临时节点编号不是最小的,则分为两种情况:

    a. 本进程为读请求,如果比自己序号小的节点中有写请求,则等待;
    b. 本进程为写请求,如果比自己序号小的节点中有读请求,则等待。
例如,用户B也想要买吹风机,但在他之前,用户C想看看吹风机的库存量。因此,用户B只能等用户A买完吹风机、用户C查询完库存量后,才能购买吹风机。

 

可以看到,使用ZooKeeper可以完美解决设计分布式锁时遇到的各种问题,比如单点故障、不可重入、死锁等问题。虽然ZooKeeper实现的分布式锁,几乎能涵盖所有分布式锁的特性,且易于实现,但需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。

很多大厂不选择zookeeper
kafka2.8把zookeeper干掉了,zookeeper是cp的,保证数据一致性,不兼顾api高性能

 

三种实现方式对比

我通过一张表格来对比一下这三种方式的特点,以方便你理解、记忆。

总结来说,ZooKeeper分布式锁的可靠性最高,有封装好的框架,很容易实现分布式锁的功能,并且几乎解决了数据库锁和缓存式锁的不足,因此是实现分布式锁的首选方法。
从上述分析可看出,为了确保分布式锁的可用性,我们在设计时应考虑到以下几点:

  • 互斥性,即在分布式系统环境下,分布式锁应该能保证一个资源或一个方法在同一时间只能被一个机器的一个线程或进程操作。
  • 具备锁失效机制,防止死锁。即使有一个进程在持有锁的期间因为崩溃而没有主动解锁,也能保证后续其他进程可以获得锁。
  • 可重入性,即进程未释放锁时,可以多次访问临界资源。
  • 有高可用的获取锁和释放锁的功能,且性能要好

 

知识扩展:如何解决分布式锁的羊群效应问题?
在分布式锁问题中,会经常遇到羊群效应。所谓羊群效应,就是在整个分布式锁的竞争过程中,大量的“Watcher通知”和“子节点列表的获取”操作重复运行,并且大多数节点的运行结果都是判断出自己当前并不是编号最小的节点,继续等待下一次通知,而不是执行业务逻辑。
这,就会对ZooKeeper服务器造成巨大的性能影响和网络冲击。更甚的是,如果同一时间多个节点对应的客户端完成事务或事务中断引起节点消失,ZooKeeper服务器就会在短时间内向其他客户端发送大量的事件通知。
那如何解决这个问题呢?具体方法可以分为以下三步。
在与该方法对应的持久节点的目录下,为每个进程创建一个临时顺序节点。
每个进程获取所有临时节点列表,对比自己的编号是否最小,若最小,则获得锁。
若本进程对应的临时节点编号不是最小的,则继续判断:
若本进程为读请求,则向比自己序号小的最后一个写请求节点注册watch监听,当监听到该节点释放锁后,则获取锁;
若本进程为写请求,则向比自己序号小的最后一个读请求节点注册watch监听,当监听到该节点释放锁后,获取锁。 

巩固补充

知识点问题

redis分布式锁的实现?其他方式了解吗?对比数据库、redis、zookeeper实现分布式锁这三种实现方式上合锁竞争上有什么不同? 

  • redis分布式锁按照一个key是否过期+lua脚本实现,官网推荐redlock算法。
  • zookeeper按照一个zk里面只可以有且有一个znode节点。加锁成功就是简历一个节点,到期使用完了自己删除。
  • 两个为了避免单点故障,一般3台机器,zk是全体同步才返回消息,redis异步通知,容易出现master宕机后slave上位但锁丢失的情况。

php redlock分布式锁demo 

 

<?php
 
class RedLock
{
    private $retryDelay;
    private $retryCount;
    private $clockDriftFactor = 0.01;
 
    private $quorum;
 
    private $servers = array();
    private $instances = array();
 
    function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
    {
        $this->servers = $servers;
 
        $this->retryDelay = $retryDelay;
        $this->retryCount = $retryCount;
 
        $this->quorum  = min(count($servers), (count($servers) / 2 + 1));
    }
 
    public function lock($resource, $ttl)
    {
        $this->initInstances();
 
        $token = uniqid();
        $retry = $this->retryCount;
 
        do {
            $n = 0;
            $startTime = microtime(true) * 1000;
 
            foreach ($this->instances as $instance) {
                if ($this->lockInstance($instance, $resource, $token, $ttl)) {
                    $n++;
                }
            }
 
            # Add 2 milliseconds to the drift to account for Redis expires
            # precision, which is 1 millisecond, plus 1 millisecond min drift
            # for small TTLs.
            $drift = ($ttl * $this->clockDriftFactor) + 2;
 
            $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
            if ($n >= $this->quorum && $validityTime > 0) {
                return [
                    'validity' => $validityTime,
                    'resource' => $resource,
                    'token'    => $token,
                ];
 
            } else {
                foreach ($this->instances as $instance) {
                    $this->unlockInstance($instance, $resource, $token);
                }
            }
 
            // Wait a random delay before to retry
            $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
            usleep($delay * 1000);
 
            $retry--;
 
        } while ($retry > 0);
 
        return false;
    }
 
    public function unlock(array $lock)
    {
        $this->initInstances();
        $resource = $lock['resource'];
        $token    = $lock['token'];
 
        foreach ($this->instances as $instance) {
            $this->unlockInstance($instance, $resource, $token);
        }
    }
 
    private function initInstances()
    {
        if (empty($this->instances)) {
            foreach ($this->servers as $server) {
                list($host, $port, $timeout) = $server;
                $redis = new \Redis();
                $redis->connect($host, $port, $timeout);
                $redis->auth('111111');//这里请换上redis的密码
                $this->instances[] = $redis;
            }
        }
    }
 
    private function lockInstance($instance, $resource, $token, $ttl)
    {
        return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
    }
 
    private function unlockInstance($instance, $resource, $token)
    {
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$resource, $token], 1);
    }
}

 

//使用
public static function connectRedLock(){
    require_once('./redisLock/RedLock.php');
       $servers = [
           ['111.111.111.111', 6379, 0.01],
       ];
       $redLock = new RedLock($servers);
       $lock = $redLock->lock('redisLock', 20000);
       if(!$lock){
           self::connectRedLock();
       }
       return $lock;
}

https://github.com/ronnylt/redlock-php

 

posted @ 2021-01-25 10:05  温柔的风  阅读(531)  评论(0编辑  收藏  举报