来源: redis实现分布式锁 |
以下为测试分布式锁的实现方法。
场景:
比如50个人向B转账,B的余额当前为0,转50次,每次1元,理论来说B的余额应该为50元。
当在高并发的情况下,那么最终结果就不一定是50了。
现在有张表名为balance,3个字段id(自增),name(姓名),balance(余额)
现在用php模拟一下简单的转账操作。
|
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
|
<?php header("Content-type => text/html; charset=utf-8");// session startdefine("SESSION_ON", true);// define project's configdefine("CONFIG", '/conf/web.php');// Debug switchdefine("Debug", true);// include framework entrance fileinclude('../common.php');use pt\framework\Debug\console as debug;use pt\framework\template as template;use pt\tool\page as page;use pt\framework\db as db;include(COMMON_PATH.'web_func.php');$db = db::init();// 查询B用户余额$balance = "select balance from balance where id = 1;";$balance = $db -> prepare($balance) -> execute();$balance = $balance[0]['balance'];// B余额加1$SQL = "update balance set balance = :balance + 1 where id = 1;";$rs = $db -> prepare($SQL) -> execute(array(':balance' => $balance)); |
使用jmeter并发50测试且50个请求全部成功。去查看数据库。

可以看到数据库中balance字段值为44,不是理论的50元。
原因就是在高并发情况下出现了问题。
比如有2个请求同时从数据库中获得了一样的余额,比如1,第一个请求先进行了update操作但是还没结束,
第二个请求发现第一个请求正在更新数据库,第一个修改请求会将balance表的id=1这条数据处于行锁状态(innodb引擎+索引才能实现行锁)。
这样第二个请求就处于等待状态,等待第一个请求update完成并释放行锁后,再更新id=1的数据,问题来了。
第一个请求已经将余额修改为2,第二个请求也把余额改为2了。(其实并没有真正更新,发现一样就不改了,在SQLyog中会提示(0 row(s) affected),影响了0行)
(完整的转账应该至少3条记录要通过事务更新,from减,to加,插入资金流水表)
现在为了解决这个问题,引入redis,通过setnx这个方法实现,具体解释请百度。
当redis中存在to(我这里把to的用户id做为key)这个key时,则停止转账。
|
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
67
68
69
70
|
<?php header("Content-type => text/html; charset=utf-8");// session startdefine("SESSION_ON", true);// define project's configdefine("CONFIG", '/conf/web.php');// debug switchdefine("DEBUG", true);// include framework entrance fileinclude('../common.php');use pt\framework\debug\console as debug;use pt\framework\template as template;use pt\tool\page as page;use pt\framework\db as db;include(COMMON_PATH.'web_func.php');$db = db::init();$to = 1; // 用户id做为keytry{ $redis = new Redis(); $redis -> connect('127.0.0.1', 6379); $redis -> auth('111111'); $connected = $redis -> ping();}catch(Exception $e){ echo '连接失败:' . $e -> getMessage(); exit;}// 获取用户余额$balance = "select balance from balance where id = 1;";$balance = $db -> prepare($balance) -> execute();$balance = $balance[0]['balance'];$expire = 60; // key超时时间$key = $to;$content = 'running';$rs = $redis -> setnx($key, $content);if ($rs === true) // 加锁{ $redis -> expire($key, $expire); // 设置key超时时间,防止某个机器加锁以后挂掉造成key死锁(一直存在)。 $sql = "update balance set balance = :balance + 1 where id = 1;"; $rs = $db -> prepare($sql) -> execute(array(':balance' => $balance)); if ($rs === false) { echo 'add balance failed'; } else { echo 'add balance success'; } $redis -> delete($to); // 删除锁}else{ //echo 'lock false , lock by others' . PHP_EOL; //echo 'after ' . $redis -> ttl($key) . 's release'; header('HTTP/1.1 403 Forbidden'); // 这里输出403错误方便jmeter中查看。实际中可能返回个友好的错误提示,比如请重试。} |
再用jmeter并发50测试。

发现有34个请求成功,16个失败,总共50个请求。
再去看数据库中balance字段。余额为34。

这样就通过redis实现了分布式锁,当有多个服务器做负载均衡时,每次转账都会先请求redis,查这个用户是否存在key,防止出现同一时间转账导致结果错误。
如有错误,请指正。
PS:
另外我觉得也可以通过消息队列实现,排队转账。
原创文章,转载请注明。本文链接地址: https://www.rootop.org/pages/4144.html
Mikel