需求描述

应用中一个第三方接口回调会产生并发请求,单次同时推送很多条信息,出现重复入库情况,需要在入库前拦截。

解决方案

  1. 使用laravel队列(不在此文章讨论范围);
  2. 使用Redis

实现方法

1.请求处理开始前,先尝试获取锁,如果获取成功则继续执行,否则,终止执行。加锁时,需要考虑如果后续任务执行失败,能定时清理掉该锁,以防出现死锁。代码示例如下:

/**
     * 尝试获取锁
     * @param String $key               锁
     * @param String $requestId         请求id
     * @param int $expireTime           过期时间
     * @return bool                     是否获取成功
     */
    public static function tryGetLock(String $key, int $expireTime, String $requestId) {

        $lua ="return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])";

        $result = Redis::eval($lua, 1,$key, $requestId, $expireTime);
        return self::LOCK_SUCCESS === (String)$result;
    }

2.该请求执行完成后,解除锁。示例代码如下:

/**
     * 解除锁
     * @param String $key               锁
     * @param String $requestId         请求id
     */
    public static function releaseLock( String $key, String $requestId) {
        $lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        $result = Redis::eval($lua, 1, $key, $requestId);
        return self::RELEASE_SUCCESS === $result;
    }

完整代码

use Illuminate\Support\Facades\Redis;

class RedisTool
{
    const LOCK_SUCCESS = 'OK';
    const RELEASE_SUCCESS = 1;

    /**
     * 尝试获取锁
     * @param String $key               锁
     * @param String $requestId         请求id
     * @param int $expireTime           过期时间
     * @return bool                     是否获取成功
     */
    public static function tryGetLock(String $key, int $expireTime, String $requestId) {

        $lua ="return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])";

        $result = Redis::eval($lua, 1,$key, $requestId, $expireTime);
        return self::LOCK_SUCCESS === (String)$result;
    }


    /**
     * 解除锁
     * @param String $key               锁
     * @param String $requestId         请求id
     */
    public static function releaseLock( String $key, String $requestId) {
        $lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        $result = Redis::eval($lua, 1, $key, $requestId);
        return self::RELEASE_SUCCESS === $result;
    }
}

总结备注

1.为什么要用Lua脚本来实现?

· 加锁时,先通过setnx加锁,然后在通过expire设置过期时间,无法保证redis原子性,在setnx执行后,程序可能挂掉,造成死锁;
· 解锁时,如果通过Redis::del($key),可能解除的是其他请求的锁;
总结:执行单个redis时,是可以保证原子性,如果是两个操作,则无法保证原子性。

2.加锁时为什么不直接用Redis::set($Key, $requestId, ['nx', 'ex' => $expireTime])?

· 这里我的laravel使用的是predisReis::set()方法不支持这种写法。

3.请求id$requestId是做什么的?怎么保证唯一?

· $requestId是区分本次请求与其他请求的标识。在加锁区间的业务执行完成后,需要解锁,$requestId保证了解锁的是当前请求的锁,而不是其他锁。
· $requestId要保证全局唯一才安全,可以使用Str::uuid()生成。


参考文章:

浅谈 Redis 分布式锁实现
Redis 使用 Lua 脚本替代 SETNX / DECR 保证原子性
Last modification:December 25th, 2020 at 01:28 pm