Redis实现分布式锁

2020-07-06 00:00:00 命令 线程 时间 获取 进程

分布式锁的实现方式有很多,本篇文章讲述一下使用Redis实现分布式锁。网上有很多使用Redis实现分布式锁的代码,但是这些代码或多或少都有问题。这篇文章会写一个实现,同时标明一些注意点。

场景

为了便于阐述,这里假设一个游戏场景,用户A有开山斧一把,价值500元宝,用户B有800元宝,想买A的开山斧,这些数据都存在Redis中。需要编写代码成功的实现该笔交易。

问题

Redis实现分布式锁,需要考虑如下问题:

  • 持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至还可能会错误地释放掉了其他进程持有的锁。
  • 一个持有锁并打算执行长时间操作的进程已经崩溃,但其他想要获取锁的进程不知道哪个进程持有着锁,也无法检测出持有锁的进程已经崩溃,只能白白地浪费时间等待锁被释放。
  • 在一个进程持有的锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁。

三个特性

实现一个低保障的分布式锁,需要具备三个特性

  1. 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
  2. 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
  3. 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.

命令

使用Redis实现分布式锁,一般使用SETNX或者SET命令,SETNX不能同时设置过期时间,如果使用的版本大于等于2.6.12,可以使用SET命令,可以使用这个命令原子性的实现SETNX和EXPIRE的功能,下面是两个命令的简介

SETNX

命令格式:SETNX key value

时间复杂度:O(1)

说明:将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。

返回值

  • 1 如果key被设置了
  • 如果key没有被设置

SET

命令格式:SET key value [EX seconds] [PX milliseconds] [NX|XX]

时间复杂度:O(1)

说明:将键key设定为指定的“字符串”值。如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。当set命令执行成功之后,之前设置的过期时间都将失效。

选项

从2.6.12版本开始,redis为SET命令增加了一系列选项:

  • EX seconds – 设置键key的过期时间,单位时秒
  • PX milliseconds – 设置键key的过期时间,单位是毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值

实现

此处使用SETNX实现,毕竟有的公司Redis版本可能较低,使用SETNX可以实现,SET更加没有问题。

代码如下:

<?php

function uuid($prefix = '')
{
    $chars = md5(uniqid(mt_rand(), true));
    $uuid  = substr($chars, , 8) . '-';
    $uuid .= substr($chars, 8, 4) . '-';
    $uuid .= substr($chars, 12, 4) . '-';
    $uuid .= substr($chars, 16, 4) . '-';
    $uuid .= substr($chars, 20, 12);
    $ret = $prefix . $uuid;
    return strtoupper($ret);
}

function acquireLock($redis,$lockName, $acquireTime = 10, $lockTime = 10)
{
    $lockKey    = 'lock:' + $lockName;
    $identifier = uuid('identify');
    $end        = time() + $acquireTime;
    while (time() < $end) {
        if ($redis->setnx($lockKey, $identifier)) {
            $redis->expire($lockKey, $lockTime);
            return $identifier;
        } elseif ($redis->ttl($lockKey) == -1) {
            $redis->expire($lockKey, $lockTime);
        }
        usleep(1000);
    }
    return false;
}


function process(){
    $redis      = new Redis();
    $lockName = 'market';
    //1.获取锁
    $locked = acquireLock($redis,$lockName);
    if($locked === false){
        return false;
    }
    //2.进行交易
    //判断A和B是否满足交易条件
    //使用管道,对A和B进行操作

    //3.释放锁
    $releaseRes = releaseLock($redis,$lockName,$locked);
    if($releaseRes === false){
        return false;
    }
}

function releaseLock($redis,$lockName,$identifier){
    $lockKey    = 'lock:' + $lockName;
    $redis->watch($lockKey);
    if($redis->get($lockKey) === $identifier){
        $redis->multi();
        $redis->del($lockKey);
        $redis->exec();
        return true;
    }
    $redis->unwatch();
    return false;
}

相关文章