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
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 15

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

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

time() + $expire ,这得多久

6个月前 评论
CleverBilly (楼主) 6个月前
王大牛 (作者) 6个月前
王大牛 (作者) 6个月前
CleverBilly (楼主) 6个月前
王大牛 (作者) 6个月前
陈先生

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

6个月前 评论
vinhson 6个月前
陈先生 (作者) 6个月前

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

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

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

2个月前 评论

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