以下是我在工作中用到的类,redis加锁两种方式,解锁为了保证原子性所以只用lua+redis的方式
缺陷:虽然死锁问题解决了,但业务执行时间超过锁有效期还是存在多客户端加锁问题。
不过,这个类已经满足了我现在的业务需求
更优的解决方案可以参考以下两篇文章:
https://redis.io/topics/distlock (Redlock的算法描述)
https://mp.weixin.qq.com/s/1bPLk_VZhZ0QYNZS8LkviA
Redlock-php 实现了分布式锁
主从复制 用红锁解决
代码实现:
class RedisLock { /** * @var 当前锁标识,用于解锁 */ private $_lockFlag; private $_redis; public function __construct($host = '127.0.0.1', $port = '6379', $passwd = '') { $this->_redis = new Redis(); $this->_redis->connect($host, $port); if ($passwd) { $this->_redis->auth($passwd); } } public function lock($key, $expire = 5) { $now= time(); $expireTime = $expire + $now; if ($this->_redis->setnx($key, $expireTime)) { $this->_lockFlag = $expireTime; return true; } // 获取上一个锁的到期时间 $currentLockTime = $this->_redis->get($key); if ($currentLockTime < $now) { /* 用于解决 C0超时了,还持有锁,加入C1/C2/...同时请求进入了方法里面 C1/C2都执行了getset方法(由于getset方法的原子性, 所以两个请求返回的值必定不相等保证了C1/C2只有一个获取了锁) */ $oldLockTime = $this->_redis->getset($key, $expireTime); if ($currentLockTime == $oldLockTime) { $this->_lockFlag = $expireTime; return true; } } return false; } public function lockByLua($key, $expire = 5) { $script = <<<EOF local key = KEYS[1] local value = ARGV[1] local ttl = ARGV[2] if (redis.call('setnx', key, value) == 1) then return redis.call('expire', key, ttl) elseif (redis.call('ttl', key) == -1) then return redis.call('expire', key, ttl) end return 0 EOF; $this->_lockFlag = md5(microtime(true)); return $this->_eval($script, [$key, $this->_lockFlag, $expire]); } public function unlock($key) { $script = <<<EOF local key = KEYS[1] local value = ARGV[1] if (redis.call('exists', key) == 1 and redis.call('get', key) == value) then return redis.call('del', key) end return 0 EOF; if ($this->_lockFlag) { return $this->_eval($script, [$key, $this->_lockFlag]); } } private function _eval($script, array $params, $keyNum = 1) { $hash = $this->_redis->script('load', $script); return $this->_redis->evalSha($hash, $params, $keyNum); } } $redisLock = new RedisLock(); $key = 'lock'; if ($redisLock->lockByLua($key)) { // to do... $redisLock->unlock($key); }