redis锁,集群redis锁

本文深入探讨了基于Redis实现分布式锁的原理与实践,包括锁的互斥性、防止死锁及确保同一客户端加解锁的机制。通过Lua脚本优化setnx和expire操作,避免死锁风险,同时提供了一种线程监控锁过期时间的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的锁;3. 基于ZooKeeper的锁。

首先安装redis并启动服务 redis-3.0.6 :https://blog.youkuaiyun.com/raoxiaoya/article/details/92797864

首先,为了确保锁可用,我们至少要确保锁的实现同时满足以下四个条件:

1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

redis服务器使用单线程来执行命令的,并且是原子性的,所不存在同时执行多个命令的情况,都是 one by one 的执行。

主要用到 setnx expire eval

代码如下:

<?php

class RedisCli {
	protected static $redis;

	public static function getRedisInstance($conf){
		if(isset(self::$redis)){
			return self::$redis;
		}
		try{
			$redis = new \Redis();
			$re = $redis->connect($conf['host'], $conf['port']);
			if(!$re){
				throw new \Exception('connect redis error');
			}

			if(isset($conf['auth'])){
				$re = $redis->auth($conf['auth']);
				if(!$re){
					throw new \Exception('redis auth error');
				}
			}

			self::$redis = $redis;
			return $redis;
		}catch(\Exception $e){
			throw new \Exception($e->getMessage());
		}
	}

	/*
		获取锁
		1、setnx + expire + lua
		防止setnx后程序崩溃,导致expire没有执行,形成死锁,所以需要使用Lua脚本来优化,
		$script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', 	
		KEYS[1], ARGV[2]) else return 0 end";
		
		2、set
		当 redis >= 2.6.12 时set命令得到扩展
		SET key value [EX seconds] [PX milliseconds] [NX|XX]
		因此可以写成 $redis->set($lockKey, $value, 'EX' , $expire, 'NX'),但是遗憾的是phpredis
		扩展提供的set方法是这样的:
		public function set( $key, $value, $timeout = 0 ) {}
		可以Lua来执行set了。或者使用rawCommand()来执行原生的set命令。
	*/
	public static function tryGetDistributedLock($redis, $lockKey, $value, $expire){
		$script = "return redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX')";
		if($redis->eval($script, [$lockKey, $value, $expire], 1)){
			return true;
		}else{
			return false;
		}
	}
	
	/*
	 	释放锁
	 	释放锁最要考虑的问题是不能释放了别人的锁,当我要del的时候,此时expire过期,锁被别
	 	人拿到,如果我不加思索就delete,就会删掉别人的锁。
	 	
		get + del + lua
	*/
	public static function releaseDistributedLock($redis, $lockKey, $value){
		$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		if($redis->eval($script, [$lockKey, $value], 1)){
			return true;
		}
		return false;
	}
}


function redisLock($resourceId){
	$conf = ['host'=>'127.0.0.1', 'port'=>6379];

	$client = RedisCli::getRedisInstance($conf);

	$lockKey = 'lockKey_' . $resourceId;
	$value = bin2hex(random_bytes(10));
	$expire = 10;

	$re = RedisCli::tryGetDistributedLock($client, $lockKey, $value, $expire);

	if($re){
		echo 'get lock success'.PHP_EOL;
	}else{
		echo 'get lock fail'.PHP_EOL;
		return ;
	}

	try {

		doSomething();

	} catch(\Exception $e) {

		echo $e->getMessage() . PHP_EOL;

	} finally {

		$re = RedisCli::releaseDistributedLock($client, $lockKey, $value);
		if($re){
			echo 'release lock success'.PHP_EOL;
		}else{
			echo 'release lock fail'.PHP_EOL;
		}

		return ;
	}
}

function doSomething(){
	$n = rand(1, 20);
	switch($n){
		case 1: 
			sleep(15);// 模拟超时
			break;
		case 2:
			throw new \Exception('system throw message...');// 模拟程序中止
			break;
		case 3:
			die('system crashed...');// 模拟程序崩溃
			break;
		default:
			sleep(1);// 正常处理过程
	}
}

$count = 100;
while(--$count > 0){
	redisLock(0);
	usleep(100000);// 0.5s
}

由于多线程和多进程都不方便查看打印,于是新开几个命令窗口来执行脚本,观看打印结果。

考虑到分布式的系统,每台服务器的时间可能不一样,所以不能用服务器生成时间来填充value值。

在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

expire的设置也是个问题,过长的话,如果我这台机器出问题,导致锁没有释放,会导致阻塞;过短会导致我程序还没执行完锁已释放并被别人拿去了,我再来释放锁是失败的(我在del时做了校验),这样其实就丧失了锁的意义了,万一我后续的业务需要在锁着的情况下来完成呢?

一个解决的方式:在获取锁成功后开启一个线程,此线程一直阻塞,直到key过期的前3秒去访问一下这个key,如果key还在且value也对的上,证明过期时间设置的短了,那么此线程就调用expire命令去延长过期时间。

为了减小锁的颗粒度,我们可以为每个资源设置不同的 lockKey ,提高并行效率,比如,要秒杀一些商品,应该为每个商品设置锁,而不是只有一个锁。

由于没有通知机制,每一个尝试获取锁的请求,当获取失败时,应该多给予一定次数的尝试机会,多次失败之后才返回 false。

以上情况是redis为单台服务器的时候,当redis为集群的时候,意味着我们的web服务器可能连到不同的redis,此时要上锁则需要把全部的redis都上锁,释放锁也是同样的道理,如果使用的是Laravel框架可直接使用插件 Laravel-redlock 当然其实现起来也不复杂,比如 redlock-php ,也可以自行整合到自己的项目中。

注意Laravel中的Redis类的eval方法的参数有点变化:

use Illuminate\Support\Facades\Redis;

// 获取锁
public static function tryGetDistributedLock($key, $val = 1, $expire = 60)
{
    $script = "return redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX')";
    if (Redis::eval($script, 1, $key, $val, $expire)) {
        return true;
    } else {
        return false;
    }
}

// 释放锁
public static function releaseDistributedLock($key, $val = 1)
{
    $script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    if (Redis::eval($script, 1, $key, $val)) {
        return true;
    }
    return false;
}

public static function tryGetDistributedLockBlock($key, $value, $timeout = 10, $expire = 60)
{
    if (!is_int($timeout) || $timeout <= 0) {
        return false;
    }
    if ($timeout > 60) {
        $timeout = 60;
    }
    $flag = false;
    $start = time();
    while (true) {
        $flag = self::tryGetDistributedLock($key, $value, $expire);
        if (!$flag && (time() - $start) < $timeout) {
            usleep(10000); //10ms
            continue;
        }
        break;
    }

    return $flag;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值