在工作项目中,会遇到一些php并发访问去修改一个数据问题,如果这个数据不加锁,就会造成数据的错误。下面我将分析一个财务支付锁的问题。希望对大家有所帮助。
1 没有应用锁机制
1.1 财务支付简化版本代码
? 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 <!--?php /** * pay.php * * 支付没有应用锁 * * Copy right (c) 2016 * * modification history: * -------------------- * 2016/9/10, by CleverCode, Create * */ //用户支付 function pay($userId,$money) { if(false == is_int($userId) || false == is_int($money)) { return false; } //取出总额 $total = getUserLeftMoney($userId); //花费大于剩余 if($money --> $total) { return false; } //余额 $left = $total - $money; //更新余额 return setUserLeftMoney($userId,$left); } //取出用户的余额 function getUserLeftMoney($userId) { if(false == is_int($userId)) { return 0; } $sql = "select account form user_account where userid = ${userId}"; //$mysql = new mysql();//mysql数据库 return $mysql->query($sql); } //更新用户余额 function setUserLeftMoney($userId,$money) { if(false == is_int($userId) || false == is_int($money)) { return false; } $sql = "update user_account set account = ${money} where userid = ${userId}"; //$mysql = new mysql();//mysql数据库 return $mysql->execute($sql); } ?>1.2 问题分析
如果有两个操作人(p和m),都用用户编号100账户,分别在pc和手机端同时登陆,100账户总余额有1000,p操作人花200,m操作人花300。并发过程如下。
p操作人:
m操作人:
两次支付后,账户的余额居然还有700,应该的情况是花费了500,账户余额500才对。造成这个现象的根本原因,是并发的时候,p和m同时操作取到的余额数据都是1000。
2 加锁设计
锁的操作一般只有两步,一 获取锁(getLock);二是释放锁(releaseLock)。但现实锁的方式有很多种,可以是文件方式实现;sql实现;Memcache实现;根据这种场景我们考虑使用策略模式。
2.1 类图设计如下
2.2 php源码设计如下
LockSystem.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 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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 <!--?php /** * LockSystem.php * * php锁机制 * * Copy right (c) 2016 * * modification history: * -------------------- * 2016/9/10, by CleverCode, Create * */ class LockSystem { const LOCK_TYPE_DB = 'SQLLock'; const LOCK_TYPE_FILE = 'FileLock'; const LOCK_TYPE_MEMCACHE = 'MemcacheLock'; private $_lock = null; private static $_supportLocks = array('FileLock', 'SQLLock', 'MemcacheLock'); public function __construct($type, $options = array()) { if(false == empty($type)) { $this--->createLock($type, $options); } } public function createLock($type, $options=array()) { if (false == in_array($type, self::$_supportLocks)) { throw new Exception("not support lock of ${type}"); } $this->_lock = new $type($options); } public function getLock($key, $timeout = ILock::EXPIRE) { if (false == $this->_lock instanceof ILock) { throw new Exception('false == $this->_lock instanceof ILock'); } $this->_lock->getLock($key, $timeout); } public function releaseLock($key) { if (false == $this->_lock instanceof ILock) { throw new Exception('false == $this->_lock instanceof ILock'); } $this->_lock->releaseLock($key); } } interface ILock { const EXPIRE = 5; public function getLock($key, $timeout=self::EXPIRE); public function releaseLock($key); } class FileLock implements ILock { private $_fp; private $_single; public function __construct($options) { if (isset($options['path']) && is_dir($options['path'])) { $this->_lockPath = $options['path'].'/'; } else { $this->_lockPath = '/tmp/'; } $this->_single = isset($options['single'])?$options['single']:false; } public function getLock($key, $timeout=self::EXPIRE) { $startTime = Timer::getTimeStamp(); $file = md5(__FILE__.$key); $this->fp = fopen($this->_lockPath.$file.'.lock', "w+"); if (true || $this->_single) { $op = LOCK_EX + LOCK_NB; } else { $op = LOCK_EX; } if (false == flock($this->fp, $op, $a)) { throw new Exception('failed'); } return true; } public function releaseLock($key) { flock($this->fp, LOCK_UN); fclose($this->fp); } } class SQLLock implements ILock { public function __construct($options) { $this->_db = new mysql(); } public function getLock($key, $timeout=self::EXPIRE) { $sql = "SELECT GET_LOCK('".$key."', '".$timeout."')"; $res = $this->_db->query($sql); return $res; } public function releaseLock($key) { $sql = "SELECT RELEASE_LOCK('".$key."')"; return $this->_db->query($sql); } } class MemcacheLock implements ILock { public function __construct($options) { $this->memcache = new Memcache(); } public function getLock($key, $timeout=self::EXPIRE) { $waitime = 20000; $totalWaitime = 0; $time = $timeout*1000000; while ($totalWaitime < $time && false == $this->memcache->add($key, 1, $timeout)) { usleep($waitime); $totalWaitime += $waitime; } if ($totalWaitime >= $time) throw new Exception('can not get lock for waiting '.$timeout.'s.'); } public function releaseLock($key) { $this->memcache->delete($key); } }3 应用锁机制
3.1 支付系统应用锁
? 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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 <!--?php /** * pay.php * * 支付应用锁 * * Copy right (c) 2016 * * modification history: * -------------------- * 2016/9/10, by CleverCode, Create * */ //用户支付 function pay($userId,$money) { if(false == is_int($userId) || false == is_int($money)) { return false; } try { //创建锁(推荐使用MemcacheLock) $lockSystem = new LockSystem(LockSystem::LOCK_TYPE_MEMCACHE); //获取锁 $lockKey = 'pay'.$userId; $lockSystem--->getLock($lockKey,8); //取出总额 $total = getUserLeftMoney($userId); //花费大于剩余 if($money > $total) { $ret = false; } else { //余额 $left = $total - $money; //更新余额 $ret = setUserLeftMoney($userId,$left); } //释放锁 $lockSystem->releaseLock($lockKey); } catch (Exception $e) { //释放锁 $lockSystem->releaseLock($lockKey); } } //取出用户的余额 function getUserLeftMoney($userId) { if(false == is_int($userId)) { return 0; } $sql = "select account form user_account where userid = ${userId}"; //$mysql = new mysql();//mysql数据库 return $mysql->query($sql); } //更新用户余额 function setUserLeftMoney($userId,$money) { if(false == is_int($userId) || false == is_int($money)) { return false; } $sql = "update user_account set account = ${money} where userid = ${userId}"; //$mysql = new mysql();//mysql数据库 return $mysql->execute($sql); } ?>3.2 锁分析
p操作人:
m操作人:
两次支付后,余额500。非常完美了解决了并发造成的临界区资源的访问问题。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!