1. Set the lock expiration time: current Unix timestamp + Redis lock timeout, in seconds (3). Edit file: \common\config\params.php, as shown in Figure 1
'lock' => [
'keyPrefix' => 'lock:', //Redis锁定 key 前缀
'timeOut' => 3, //Redis锁定超时时间,单位为秒
],

2. Get the relevant configuration parameters. Edit file: \api\models\redis\GameCategory.php, as shown in Figure 2
// 设置锁定的过期时间
$time = time();
$lockKey = Yii::$app->params['lock']['keyPrefix'] . 'game_category';
$lockExpire = $time + Yii::$app->params['lock']['timeOut'];

3. Get the Redis connection to execute relevant commands. Edit file: \api\models\redis\GameCategory.php, as shown in Figure 3
// 获取 Redis 连接,以执行相关命令
$redis = Yii::$app->redis;

4. Acquire the lock, as shown in Figure 4
// 获取锁定
$executeCommandResult = $redis->setnx($lockKey, $lockExpire);
Note: SETNX key value
Sets key to value if key does not exist; in this case, it is equivalent to the SET command. When key already exists, it does nothing. SETNX stands for “SET if Not eXists”.

5. Returning 0 indicates that the resource has already been locked by another client, as shown in Figures 5, 6, and 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');



6. Release the lock, as shown in Figure 8
Note: DEL key [key …]
If a deleted key does not exist, it is simply ignored.

7. Regarding step 5, to determine whether the lock has expired: if it has already expired, comment out the lock release and lock acquisition. To prevent concurrent locking, you can perform the following test procedure for verification
8. First, comment out the lock release to simulate the problem: what happens if the client fails, crashes, or cannot release the lock? As shown in Figure 9

9. In the code section that determines whether the lock has expired, if it has already expired, adopt the first algorithm and uncomment all file_put_contents statements, as shown in Figure 10

10. In Redis, execute the command: FLUSHDB to clear all keys, as shown in Figure 11

11. Execute the concurrent request test and set the number of threads to 10, as shown in Figures 12 and 13


12. Check the files generated in the \api\runtime directory: the number of files starting with 0, 4, and 6 is 9 respectively, and the number of files starting with 5 is 1, which is normal, as shown in Figure 14

13. In Redis, delete all business-related keys except lock:game_category, i.e., keys starting with game_category, to simulate that the lock has already expired, as shown in Figure 15

14. Delete the files generated in the \api\runtime directory, as shown in Figure 16

15. Execute the concurrent request test and set the number of threads to 10, as shown in Figures 17 and 18


16. Check the files generated in the \api\runtime directory: the number of files starting with 0 and 6 is 10, the number of files starting with 1 and 4 is 8 respectively, and the number of files starting with 5 is 2, as shown in Figure 19
Note 1: The number of files starting with 5 is 2, but it should only be 1. A value greater than 1 indicates that the locking was unsuccessful
Note 2: When determining whether the lock has expired, if it has already expired, you cannot simply call DEL to release the lock and then acquire the lock based on SETNX, because there is a race condition here. When multiple clients detect an expired lock, release it, and then acquire it, they will all acquire the lock.

17. In the code section that determines whether the lock has expired, if it has already expired, adopt the second algorithm and uncomment all file_put_contents statements, as shown in Figure 20

18. Repeat steps 13, 14, and 15. Check the files generated in the \api\runtime directory: the number of files starting with 0 and 6 is 10, the number of files starting with 1 is 7, the number of files starting with 2 is 6, the number of files starting with 3 is 1, the number of files starting with 4 is 3, and the number of files starting with 5 is 1, which is normal, as shown in Figure 21
Note 1: The number of files starting with 1 is 7, the number of files starting with 2 is 6, and the number of files starting with 3 is 1. The sum of the latter two exactly equals the former, indicating that among the 7 expired threads, only one acquired the lock. Ultimately, the number of files starting with 5 is also 1
Note 2: Due to the characteristics of GETSET, you can check whether the old value stored in the key is still the expired timestamp. If it is, the lock is acquired; otherwise, it returns false.

19. Clean up all code that facilitated testing during development, such as file_put_contents, as shown in Figure 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' => ''];
}
}

20. Abstract the lock acquisition and release into a class file, \common\models\redis\Lock.php, as shown in Figures 23 and 24
<?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. Edit file: \api\models\redis\GameCategory.php, as shown in Figures 25, 26, and 27
/* Redis模型的锁定实现 */
$lockKeyName = 'game_category';
$lock = new Lock();
$lockResult = $lock->lock($lockKeyName);
// 返回 false,表示已经被其他客户端锁定
if ($lockResult['status'] === false) {
return ['status' => false, 'code' => 0, 'message' => ''];
}



