并发访问场景可能产生的问题

对于某些数据而言,用户的并发访问将导致数据出错。例如银行账户的金额。

假设现在某张银行卡账户余额为 1000RMB,此时用户A请求消费 500RMB:

1
银行余额 = 1000RMB - 500RMB = 500RMB

但与此同时,用户B也发起了一个消费请求,请求消费 300RMB,在用户A的消费请求处理前,B读到的账户余额仍是 1000RMB。因此在处理B的消费请求后:

1
账户余额 = 1000RMB - 300RMB = 700RMB

原本在两次消费过后,银行账户余额应为 1000RMB - 500RMB - 300RMB = 200RMB,但在用户B请求过后账户却还有 700RMB

因此,对于此类会影响数据准确性的并发请求,应当要进行加锁限制。

为什么使用 Redis 构造锁

PHP 构造锁的方法有很多,有文件锁、SQL锁、Redis/Memcache锁。

但由于采用分布式设计,业务代码同时在多台服务器上运行,无法使用文件实现锁的功能。又考虑到 SQL 锁的资源消耗,最终选择使用 Redis 来构造锁。

构造锁时需要注意的问题

  1. 预防处理持有锁在执行操作的时候进程奔溃,导致死锁,其他进程一直得不到此锁(加锁后未解锁)
  2. 持有锁进程因为操作时间长而导致锁自动释放,但本身进程并不知道,最后错误的释放其他进程的锁(误删他锁)
  3. 一个进程锁过期后,其他多个进程同时尝试获取锁,并且都成功获得锁

实现思路

  1. 加锁 —— 使用 setnx() 操作
    • 加锁时同时设置过期时间,防止死锁
    • 加锁时同时设置锁标志作为 value 值,防止误删他锁
  2. 解锁 —— 使用 del() 操作
    • 判断锁标志再执行解锁操作,防止误删他锁

Talk is cheap

最后 Show 一下 Code。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class RedisLock extends Redis
{
/**
* Redis锁前缀
*/
const PREFIX = 'TEST_LOCK';
/**
* @var \Redis
*/
private $_redis;
/**
* 构造函数
* RedisLock constructor.
*/
public function __construct()
{
$this->_redis = self::getConnection();
}
/**
* 加锁
* @param $lockName
* @param int $timeout
* @return bool|string
*/
public function lock($lockName, $timeout = 5)
{
$lockName = self::getKey($lockName);
$identifier = uniqid(); //获取锁唯一标示符
$timeout = ceil($timeout);
$end = time() + $timeout;
while (time() < $end) { //循环获取锁
if ($this->_redis->setnx($lockName, $identifier)) { //查看localName是否被上锁
$this->_redis->expire($lockName, $timeout); //设置锁的过期时间,防止死锁
return $identifier; //返回唯一标志符
} elseif ($this->_redis->ttl($lockName) === -1) { //返回lockName剩余生存时间
$this->_redis->expire($lockName, $timeout);
}
usleep(0.001);
}
return false;
}
/**
* 释放锁
* @param $lockName
* @param $identifier
* @return bool
*/
public function unlock($lockName, $identifier)
{
$lockName = self::getKey($lockName);
if ($this->_redis->get($lockName) == $identifier) { //判断是否被修改
$this->_redis->multi();
$this->_redis->del($lockName); //锁没有被修改,释放自己的锁
$this->_redis->exec();
return true;
} else {
return false; //锁被修改,不能释放他人锁
}
}
}

参考资料