PHP利用Redis锁解决并发访问

  • 并发访问限制问题

对于一些需要限制同一个用户并发访问的场景,如果用户并发请求多次,而服务器处理没有加锁限制,用户则可以多次请求成功。例如换领优惠券,如果用户同一时间并发提交换领码,在没有加锁限制的情况下,用户则可以使用同一个换领码同时兑换到多张优惠券。

常见的业务逻辑代码如下:

if A(可以换领)
   B(执行换领)
   C(更新为已换领)
D(结束)

如果用户并发提交换领码,都能通过可以换领(A)的判断,因为必须有一个执行换领(B)后,才会更新为已换领(C)。因此如果用户在有一个更新为已换领之前,有多少次请求,这些请求都可以执行成功。

  • 并发访问限制方法

使用文件锁可以实现并发访问限制,但对于分布式架构的环境,使用文件锁不能保证多台服务器的并发访问限制。

具体的 redis 加锁类和示例代码如下:

<?php

/**
 *  Redis 操作类
 *  Func:
 *  public  lock    获取锁
 *  public  unlock  释放锁
 *  private connect 连接
 */
class RedisLock
{ // class start

    private $_config;
    private $_redis;

    /**
     * RedisLock constructor.
     * @param array $config
     * @throws Exception
     */
    public function __construct($config = [])
    {
        $this->_config = $config;
        $this->_redis = $this->connect();
    }

    /**
     * 获取锁
     * @param  String $key 锁标识
     * @param  Int $expire 锁过期时间
     * @return Boolean
     */
    public function lock($key, $expire = 5)
    {
        $is_lock = $this->_redis->setnx($key, time() + $expire);

        // 不能获取锁
        if (!$is_lock) {

            // 判断锁是否过期
            $lock_time = $this->_redis->get($key);

            // 锁已过期,删除锁,重新获取
            if (time() > $lock_time) {
                $this->unlock($key);
                $is_lock = $this->_redis->setnx($key, time() + $expire);
            }
        }

        return $is_lock ? true : false;
    }

    /**
     * 释放锁
     * @param  String $key 锁标识
     * @return Boolean
     */
    public function unlock($key)
    {
        return $this->_redis->del($key);
    }

    /**
     * 链接redis
     * @return bool|Redis
     * @throws Exception
     */
    private function connect()
    {
        try {
            $redis = new \Redis();
            $redis->connect($this->_config['host'], $this->_config['port'], $this->_config['timeout'], $this->_config['reserved'], $this->_config['retry_interval']);
            if (empty($this->_config['auth'])) {
                $redis->auth($this->_config['auth']);
            }
            $redis->select($this->_config['index']);
        } catch (RedisException  $e) {
            throw new Exception($e->getMessage());
            return false;
        }
        return $redis;
    }
}

$config = array(
    'host' => 'localhost',
    'port' => 6379,
    'index' => 0,
    'auth' => '',
    'timeout' => 1,
    'reserved' => NULL,
    'retry_interval' => 100,
);

// 创建redislock对象
$oRedisLock = new RedisLock($config);

// 定义锁标识
$key = 'mylock';

// 获取锁
$is_lock = $oRedisLock->lock($key, 10);

if ($is_lock) {
    echo 'get lock success<br>';
    echo 'do sth..<br>';
    sleep(5);
    echo 'success<br>';
    //释放锁
    $oRedisLock->unlock($key);

// 获取锁失败
} else {
    echo 'request too frequently<br>';
}

保证同一时间只有一个访问有效,有效限制并发访问。

本作品采用《CC 协议》,转载必须注明作者和本文链接
CleverBilly
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 18

我司用 redis 的 Incrby 来解决这个问题. 大概如下:

if ($redis->incr($key) == 1) {
  // 执行
} else{
  // 不执行
}
2年前 评论

time() + $expire ,这得多久

2年前 评论
CleverBilly (楼主) 2年前
王大牛 (作者) 2年前
王大牛 (作者) 2年前
CleverBilly (楼主) 2年前
王大牛 (作者) 2年前
陈先生

如果出现了 第一个锁执行时间很长到redis过期, 第二个锁 和第一个锁同key 那么当第一把锁要unlock的时候就会释放掉第二把锁

2年前 评论
vinhson 2年前
陈先生 (作者) 2年前

看到过一篇文章的写法是这样的

$ok = $redis->set($key, $random, ['nx', 'ex' => $ttl]);
if ($ok) {
    // 获取到锁
    ... do something ...
    if ($redis->get($key) == $random) {
        $redis->del($key);
    }
}
2年前 评论
陈先生 2年前
Su (作者) 2年前
陈先生 2年前

大佬,你这个锁和laravel自带的缓存锁哪个好一点?

2年前 评论

应该要考虑锁的续命,假如有A和B两个进程需要用到这个锁,A获取到锁了,如果A的代码执行时间超过锁释放的时间了,这个时候B也能获取到这个锁了

1年前 评论

感觉应该用队列啥的,写一个锁续命的服务,不然只能设置一个超长的过期时间,然后祈祷服务器不会宕机,成功加锁的都能正常释放

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!