没有不值得去解决的问题,也没有不值得去学习的技术!

在 Yii2.0 下实现 Redis 的锁定机制的流程

将获取锁定与释放锁定抽象为一个类文件,\common\models\redis\Lock.php
1、设置锁定的过期时间:当前的 Unix 时间戳 + Redis锁定超时时间,单位为秒(3),编辑文件:\common\config\params.php,如图1


    'lock' => [
        'keyPrefix' => 'lock:', //Redis锁定 key 前缀
        'timeOut' => 3, //Redis锁定超时时间,单位为秒
    ],


设置锁定的过期时间:当前的 Unix 时间戳 + Redis锁定超时时间,单位为秒(3)
图1
2、获取相关的设置参数,编辑文件:\api\models\redis\GameCategory.php,如图2


            // 设置锁定的过期时间
            $time = time();
            $lockKey = Yii::$app->params['lock']['keyPrefix'] . 'game_category';
            $lockExpire = $time + Yii::$app->params['lock']['timeOut'];


获取相关的设置参数
图2
3、获取 Redis 连接,以执行相关命令,编辑文件:\api\models\redis\GameCategory.php,如图3


            // 获取 Redis 连接,以执行相关命令
            $redis = Yii::$app->redis;


获取 Redis 连接,以执行相关命令
图3
4、获取锁定,如图4


            // 获取锁定
            $executeCommandResult = $redis->setnx($lockKey, $lockExpire);


注:SETNX key value 将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。
获取锁定
图4
5、返回0,表示已经被其他客户端锁定,如图5、6、7


            // 返回0,表示已经被其他客户端锁定
            if ($executeCommandResult == 0) {
                // $fileName = microtime(true);
                // file_put_contents('./../runtime/0-' . $fileName . '.txt', '1');
                // 防止死锁,获取当前锁的过期时间
                $lockCurrentExpire = $redis->get($lockKey);
                // $fileName = microtime(true);
                // file_put_contents('./../runtime/6-' . $fileName . $lockCurrentExpire . '.txt', '1');
                // 判断锁是否过期,如果已经过期
                if ($time > $lockCurrentExpire) {
                    // $fileName = microtime(true);
                    // file_put_contents('./../runtime/1-' . $fileName . '.txt', '1');
                    // 释放锁定
                    // $redis->del($lockKey);
                    // 获取锁定
                    // $executeCommandResult = $redis->setnx($lockKey, $lockExpire);
                    // 防止并发锁定,检查存储在 key 的旧值是否仍然是过期的时间戳,如果是,则获取锁定,否则返回假
                    $executeCommandResult = $redis->getset($lockKey, $lockExpire);
                    if ($lockCurrentExpire != $executeCommandResult) {
                        // $fileName = microtime(true);
                        // file_put_contents('./../runtime/2-' . $fileName . '.txt', '1');
                        return ['status' => false, 'code' => 0, 'message' => ''];
                    }
                    // $fileName = microtime(true);
                    // file_put_contents('./../runtime/3-' . $fileName . '.txt', '1');
                }
                
                // 返回0,表示已经被其他客户端锁定,且不存在死锁,返回假
                if ($executeCommandResult == 0) {
                    // $fileName = microtime(true);
                    // file_put_contents('./../runtime/4-' . $fileName . '.txt', '1');
                    return ['status' => false, 'code' => 0, 'message' => ''];
                }

            }
            
            // $fileName = microtime(true);
            // file_put_contents('./../runtime/5-' . $fileName . '.txt', '1');


返回0,表示已经被其他客户端锁定
图5
返回0,表示已经被其他客户端锁定
图6
返回0,表示已经被其他客户端锁定
图7
6、释放锁定,如图8 注:DEL key [key …] 如果删除的key不存在,则直接忽略。
释放锁定
图8
7、对于在第5点,判断锁是否过期,如果已经过期,注释掉释放锁定与获取锁定,为了防止并发锁定,可做以下测试流程以验证 8、先注释掉释放锁定,以模拟:如果客户端失败,崩溃或者无法释放锁,会发生什么?的问题,如图9
先注释掉释放锁定,以模拟:如果客户端失败,崩溃或者无法释放锁,会发生什么?的问题
图9
9、在判断锁是否过期,如果已经过期,这处代码段中,采用第一种算法,且将file_put_contents全部取消注释,如图10
在判断锁是否过期,如果已经过期,这处代码段中,采用第一种算法,且将file_put_contents全部取消注释
图10
10、在Redis中,执行命令:FLUSHDB,清空所有key,如图11
在Redis中,执行命令:FLUSHDB,清空所有key
图11
11、执行并发请求测试,设置线程数为10,如图12、13
执行并发请求测试,设置线程数为10
图12
执行并发请求测试,设置线程数为10
图13
12、查看\api\runtime目录下所生成的文件,以0、4、6开头的文件数量皆为9,以5开头的文件数量为1,正常,如图14
查看\api\runtime目录下所生成的文件,以0、4、6开头的文件数量皆为9,以5开头的文件数量为1,正常
图14
13、在Redis中,删除除了lock:game_category外的所有业务相关key,即以game_category开头的key,以模拟:锁定已经过期,如图15
在Redis中,删除除了lock:game_category外的所有业务相关key,即以game_category开头的key,以模拟:锁定已经过期
图15
14、删除\api\runtime目录下所生成的文件,如图16
删除\api\runtime目录下所生成的文件
图16
15、执行并发请求测试,设置线程数为10,如图17、18
执行并发请求测试,设置线程数为10
图17
执行并发请求测试,设置线程数为10
图18
16、查看\api\runtime目录下所生成的文件,以0、6开头的文件数量为10,以1、4开头的文件数量皆为8,以5开头的文件数量为2,如图19 注1:以5开头的文件数量为2,只能够为1,大于1的话,表示锁定未成功 注2:判断锁是否过期,如果已经过期,当这种情况发生时,不能只是调用DEL来释放锁,然后基于SETNX获取锁定,因为这里有一个竞争关系,当多个客户端检测到一个过期的锁,并均释放锁,然后获取,则都获得了锁定。
查看\api\runtime目录下所生成的文件,以0、6开头的文件数量为10,以1、4开头的文件数量皆为8,以5开头的文件数量为2
图19
17、在判断锁是否过期,如果已经过期,这处代码段中,采用第二种算法,且将file_put_contents全部取消注释,如图20
在判断锁是否过期,如果已经过期,这处代码段中,采用第二种算法,且将file_put_contents全部取消注释
图20
18、重复第13、14、15等3个步骤,查看\api\runtime目录下所生成的文件,以0、6开头的文件数量为10,以1开头的文件数量为7,以2开头的文件数量为6,以3开头的文件数量为1,以4开头的文件数量为3,以5开头的文件数量为1,正常,如图21 注1:以1开头的文件数量为7,以2开头的文件数量为6,以3开头的文件数量为1,后两者相加正好等于前者,表示在已经过期的7个线程中,只有一个获得了锁定,最终以5开头的文件数量也为1 注2:由于GETSET的特性,可以检查存储在 key 的旧值是否仍然是过期的时间戳,如果是,则获取锁,否则返回假
重复第13、14、15等3个步骤,查看\api\runtime目录下所生成的文件,以0、6开头的文件数量为10,以1开头的文件数量为7,以2开头的文件数量为6,以3开头的文件数量为1,以4开头的文件数量为3,以5开头的文件数量为1,正常
图21
19、清理掉所有方便于开发期间测试的代码,如file_put_contents等,如图22


            // 设置锁定的过期时间,获取相关锁定参数
            $time = time();
            $lockKey = Yii::$app->params['lock']['keyPrefix'] . 'game_category';
            $lockExpire = $time + Yii::$app->params['lock']['timeOut'];
            // 获取 Redis 连接,以执行相关命令
            $redis = Yii::$app->redis;
            // 获取锁定
            $executeCommandResult = $redis->setnx($lockKey, $lockExpire);
            // 返回0,表示已经被其他客户端锁定
            if ($executeCommandResult == 0) {
                // 防止死锁,获取当前锁的过期时间
                $lockCurrentExpire = $redis->get($lockKey);
                // 判断锁是否过期,如果已经过期
                if ($time > $lockCurrentExpire) {
                    // 防止并发锁定,检查存储在 key 的旧值是否仍然是过期的时间戳,如果是,则获取锁定,否则返回假
                    $executeCommandResult = $redis->getset($lockKey, $lockExpire);
                    if ($lockCurrentExpire != $executeCommandResult) {
                        return ['status' => false, 'code' => 0, 'message' => ''];
                    }
                }
                
                // 返回0,表示已经被其他客户端锁定,且不存在死锁,返回假
                if ($executeCommandResult == 0) {
                    return ['status' => false, 'code' => 0, 'message' => ''];
                }

            }


清理掉所有方便于开发期间测试的代码,如file_put_contents等
图22
20、将获取锁定与释放锁定抽象为一个类文件,\common\models\redis\Lock.php,如图23、24  
<pre class="wp-block-syntaxhighlighter-code">

<?php namespace common\models\redis; use Yii; /** * This is the model class for table "{{%lock}}". * */ class Lock extends \yii\redis\ActiveRecord { /** * Redis模型的锁定实现 * @param string $lockKeyName 锁定键名 * 格式如下: * * 'game_category' //锁定键名,如比赛分类 * * @return integer 成功返回对象数组/失败返回错误信息 * 格式如下: * * [ * 'status' => true //状态
     * ]
     * 
     * 或者
     * 
     * [
     *     'status' => false, //状态
     *     'code' => 0, //返回码
     *     'message' => '', //说明
     * ]
     *
     */
    public function lock($lockKeyName)
    {
        // 设置锁定的过期时间,获取相关锁定参数
        $time = time();
        $lockKey = Yii::$app->params['lock']['keyPrefix'] . $lockKeyName;
        $lockExpire = $time + Yii::$app->params['lock']['timeOut'];
        // 获取 Redis 连接,以执行相关命令
        $redis = Yii::$app->redis;
        // 获取锁定
        $executeCommandResult = $redis->setnx($lockKey, $lockExpire);
        // 返回0,表示已经被其他客户端锁定
        if ($executeCommandResult == 0) {
            // 防止死锁,获取当前锁的过期时间
            $lockCurrentExpire = $redis->get($lockKey);
            // 判断锁是否过期,如果已经过期
            if ($time > $lockCurrentExpire) {
                // 防止并发锁定,检查存储在 key 的旧值是否仍然是过期的时间戳,如果是,则获取锁定,否则返回假
                $executeCommandResult = $redis->getset($lockKey, $lockExpire);
                if ($lockCurrentExpire != $executeCommandResult) {
                    return ['status' => false, 'code' => 0, 'message' => ''];
                }
            }

            // 返回0,表示已经被其他客户端锁定,且不存在死锁,返回假
            if ($executeCommandResult == 0) {
                return ['status' => false, 'code' => 0, 'message' => ''];
            }

        }
    }

    /**
     * Redis模型的释放锁定实现
     * @param string $lockKeyName 锁定键名
     * 格式如下:
     *
     * 'game_category' //锁定键名,如比赛分类
     *
     * @return integer 被删除的keys的数量
     * 格式如下:
     *
     * 1 //被删除的keys的数量
     * 
     * 或者
     * 
     * 0 //被删除的keys的数量
     *
     */
    public function unlock($lockKeyName)
    {
        // 获取相关锁定参数
        $lockKey = Yii::$app->params['lock']['keyPrefix'] . $lockKeyName;
        // 获取 Redis 连接,以执行相关命令
        $redis = Yii::$app->redis;
        // 释放锁定
        return $redis->del($lockKey);
    }
}

</pre>
将获取锁定与释放锁定抽象为一个类文件,\common\models\redis\Lock.php
图23
将获取锁定与释放锁定抽象为一个类文件,\common\models\redis\Lock.php
图24
21、编辑文件:\api\models\redis\GameCategory.php,如图25、26、27  


            /* Redis模型的锁定实现 */
            $lockKeyName = 'game_category';
            $lock = new Lock();
            $lockResult = $lock->lock($lockKeyName);
            // 返回 false,表示已经被其他客户端锁定
            if ($lockResult['status'] === false) {
                return ['status' => false, 'code' => 0, 'message' => ''];
            }


编辑文件:\api\models\redis\GameCategory.php
图25
编辑文件:\api\models\redis\GameCategory.php
图25
编辑文件:\api\models\redis\GameCategory.php
图27

PHP / Laravel / Yii2 老项目维护与长期技术支持

如果你的 PHP / Laravel / Yii2 项目已经上线,但遇到原开发离职、Bug 长期无人修复、接口不稳定、性能下降、代码难以接手等问题,可以联系我做一次远程技术排查。

适合以下情况:
✅ 老旧 PHP 系统无人维护
✅ Laravel / Yii2 项目 Bug 修复
✅ 后台管理系统小功能迭代
✅ RESTful API 接口排查
✅ MySQL / Redis / Nginx 性能问题
✅ 长期远程兼职维护

可先从一次小问题开始:
✅ 线上报错排查
✅ 接口异常分析
✅ 慢查询与性能瓶颈定位
✅ 代码结构初步评估
✅ 部署环境与日志检查

如需咨询,请联系我,并注明:PHP 维护咨询

联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理