需求描述
应用中一个第三方接口回调会产生并发请求,单次同时推送很多条信息,出现重复入库情况,需要在入库前拦截。
解决方案
- 使用
laravel
队列不在此文章讨论范围; - 使用
Redis
锁
实现方法
1.请求处理开始前,先尝试获取锁,如果获取成功则继续执行,否则,终止执行。加锁时,需要考虑如果后续任务执行失败,能定时清理掉该锁,以防出现死锁。代码示例如下:
代码语言:javascript复制/**
* 尝试获取锁
* @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.该请求执行完成后,解除锁。示例代码如下:
代码语言:javascript复制/**
* 解除锁
* @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;
}
完整代码
代码语言:javascript复制use IlluminateSupportFacadesRedis;
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
使用的是predis
,Reis::set()
方法不支持这种写法。
3.请求id$requestId
是做什么的?怎么保证唯一?
· requestId是区分本次请求与其他请求的标识。在加锁区间的业务执行完成后,需要解锁,requestId保证了解锁的是当前请求的锁,而不是其他锁。·