在 Yii2.0 下实现 Redis 的锁定机制的流程
1、设置锁定的过期时间:当前的 Unix 时间戳 + Redis锁定超时时间,单位为秒(3),编辑文件:\common\config\params.php,如图1
1 2 3 4 | 'lock' => [ 'keyPrefix' => 'lock:' , //Redis锁定 key 前缀 'timeOut' => 3, //Redis锁定超时时间,单位为秒 ], |
2、获取相关的设置参数,编辑文件:\api\models\redis\GameCategory.php,如图2
1 2 3 4 | // 设置锁定的过期时间 $time = time(); $lockKey = Yii:: $app ->params[ 'lock' ][ 'keyPrefix' ] . 'game_category' ; $lockExpire = $time + Yii:: $app ->params[ 'lock' ][ 'timeOut' ]; |
3、获取 Redis 连接,以执行相关命令,编辑文件:\api\models\redis\GameCategory.php,如图3
1 2 | // 获取 Redis 连接,以执行相关命令 $redis = Yii:: $app ->redis; |
4、获取锁定,如图4
1 2 | // 获取锁定 $executeCommandResult = $redis ->setnx( $lockKey , $lockExpire ); |
注:SETNX key value
将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。
5、返回0,表示已经被其他客户端锁定,如图5、6、7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // 返回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'); |
6、释放锁定,如图8
注:DEL key [key …]
如果删除的key不存在,则直接忽略。
7、对于在第5点,判断锁是否过期,如果已经过期,注释掉释放锁定与获取锁定,为了防止并发锁定,可做以下测试流程以验证
8、先注释掉释放锁定,以模拟:如果客户端失败,崩溃或者无法释放锁,会发生什么?的问题,如图9
9、在判断锁是否过期,如果已经过期,这处代码段中,采用第一种算法,且将file_put_contents全部取消注释,如图10
10、在Redis中,执行命令:FLUSHDB,清空所有key,如图11
11、执行并发请求测试,设置线程数为10,如图12、13
12、查看\api\runtime目录下所生成的文件,以0、4、6开头的文件数量皆为9,以5开头的文件数量为1,正常,如图14
13、在Redis中,删除除了lock:game_category外的所有业务相关key,即以game_category开头的key,以模拟:锁定已经过期,如图15
14、删除\api\runtime目录下所生成的文件,如图16
15、执行并发请求测试,设置线程数为10,如图17、18
16、查看\api\runtime目录下所生成的文件,以0、6开头的文件数量为10,以1、4开头的文件数量皆为8,以5开头的文件数量为2,如图19
注1:以5开头的文件数量为2,只能够为1,大于1的话,表示锁定未成功
注2:判断锁是否过期,如果已经过期,当这种情况发生时,不能只是调用DEL来释放锁,然后基于SETNX获取锁定,因为这里有一个竞争关系,当多个客户端检测到一个过期的锁,并均释放锁,然后获取,则都获得了锁定。
17、在判断锁是否过期,如果已经过期,这处代码段中,采用第二种算法,且将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 的旧值是否仍然是过期的时间戳,如果是,则获取锁,否则返回假
19、清理掉所有方便于开发期间测试的代码,如file_put_contents等,如图22
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 设置锁定的过期时间,获取相关锁定参数 $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' => '' ]; } } |
20、将获取锁定与释放锁定抽象为一个类文件,\common\models\redis\Lock.php,如图23、24
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | <?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 ); } } |
21、编辑文件:\api\models\redis\GameCategory.php,如图25、26、27
1 2 3 4 5 6 7 8 | /* Redis模型的锁定实现 */ $lockKeyName = 'game_category' ; $lock = new Lock(); $lockResult = $lock ->lock( $lockKeyName ); // 返回 false,表示已经被其他客户端锁定 if ( $lockResult [ 'status' ] === false) { return [ 'status' => false, 'code' => 0, 'message' => '' ]; } |
近期评论