php并发加锁

图片
0 220
lvphp 11月前发布
签名:LVEVEN创始人

CleverCode在工作项目中,会遇到一些php并发访问去修改一个数据问题,如果这个数据不加锁,就会造成数据的错误。下面CleverCode将分析一个财务支付锁的问题。

1 没有应用锁机制

1.1 财务支付简化版本代码

  1. <?php  
  2.   
  3. /**  
  4.  * pay.php  
  5.  *  
  6.  * 支付没有应用锁 
  7.  *  
  8.  * Copy right (c) 2016 http://blog.csdn.net/CleverCode  
  9.  *  
  10.  * modification history:  
  11.  * --------------------  
  12.  * 2016/9/10, by CleverCode, Create  
  13.  *  
  14.  */    
  15.   
  16. //用户支付  
  17. function pay($userId,$money)  
  18. {  
  19.       
  20.     if(false == is_int($userId) || false == is_int($money))  
  21.     {  
  22.         return false;  
  23.     }    
  24.       
  25.     //取出总额  
  26.     $total = getUserLeftMoney($userId);  
  27.       
  28.     //花费大于剩余  
  29.     if($money > $total)  
  30.     {  
  31.         return false;      
  32.     }  
  33.       
  34.     //余额  
  35.     $left = $total - $money;  
  36.       
  37.     //更新余额  
  38.     return setUserLeftMoney($userId,$left);  
  39.   
  40. }  
  41.   
  42. //取出用户的余额  
  43. function getUserLeftMoney($userId)  
  44. {  
  45.     if(false == is_int($userId))  
  46.     {  
  47.         return 0;  
  48.     }  
  49.     $sql = "select account form user_account where userid = ${userId}";  
  50.       
  51.     //$mysql = new mysql();//mysql数据库  
  52.     return $mysql->query($sql);  
  53. }  
  54.   
  55. //更新用户余额  
  56. function setUserLeftMoney($userId,$money)  
  57. {  
  58.     if(false == is_int($userId) || false == is_int($money))  
  59.     {  
  60.         return false;  
  61.     }          
  62.       
  63.     $sql = "update user_account set account = ${money} where userid = ${userId}";  
  64.       
  65.     //$mysql = new mysql();//mysql数据库  
  66.     return $mysql->execute($sql);  
  67. }  
  68.   
  69. ?>  


1.2  问题分析

如果有两个操作人(p和m),都用用户编号100账户,分别在pc和手机端同时登陆,100账户总余额有1000,p操作人花200,m操作人花300。并发过程如下。

p操作人:

     1 取出用户的余额1000。

     2 支付后剩余 800 = 1000 - 200。

     3 更新后账户余额800。

m操作人:

       1 取出用户余额1000。

       2 支付后剩余700 = 1000 - 300。

       3 支付后账户余额700。

两次支付后,账户的余额居然还有700,应该的情况是花费了500,账户余额500才对。造成这个现象的根本原因,是并发的时候,p和m同时操作取到的余额数据都是1000。


2 加锁设计

锁的操作一般只有两步,一 获取锁(getLock);二是释放锁(releaseLock)。但现实锁的方式有很多种,可以是文件方式实现;sql实现;Memcache实现;根据这种场景我们考虑使用策略模式。


2.1 类图设计如下



2.2 php源码设计如下


LockSystem.php

  1. <?php  
  2.   
  3. /**  
  4.  * LockSystem.php  
  5.  *  
  6.  * php锁机制 
  7.  *  
  8.  * Copy right (c) 2016 http://blog.csdn.net/CleverCode  
  9.  *  
  10.  * modification history:  
  11.  * --------------------  
  12.  * 2016/9/10, by CleverCode, Create  
  13.  *  
  14.  */   
  15.   
  16. class LockSystem  
  17. {  
  18.     const LOCK_TYPE_DB = 'SQLLock';  
  19.     const LOCK_TYPE_FILE = 'FileLock';  
  20.     const LOCK_TYPE_MEMCACHE = 'MemcacheLock';  
  21.       
  22.     private $_lock = null;  
  23.     private static $_supportLocks = array('FileLock''SQLLock''MemcacheLock');    
  24.       
  25.     public function __construct($type$options = array())   
  26.     {  
  27.         if(false == empty($type))  
  28.         {  
  29.             $this->createLock($type$options);  
  30.         }  
  31.     }     
  32.   
  33.     public function createLock($type$options=array())  
  34.     {  
  35.         if (false == in_array($type, self::$_supportLocks))  
  36.         {  
  37.             throw new Exception("not support lock of ${type}");  
  38.         }  
  39.         $this->_lock = new $type($options);  
  40.     }  
  41.       
  42.     public function getLock($key$timeout = ILock::EXPIRE)  
  43.     {  
  44.         if (false == $this->_lock instanceof ILock)    
  45.         {  
  46.             throw new Exception('false == $this->_lock instanceof ILock');            
  47.         }    
  48.         $this->_lock->getLock($key$timeout);     
  49.     }  
  50.       
  51.     public function releaseLock($key)  
  52.     {  
  53.         if (false == $this->_lock instanceof ILock)    
  54.         {  
  55.             throw new Exception('false == $this->_lock instanceof ILock');            
  56.         }    
  57.         $this->_lock->releaseLock($key);           
  58.     }     
  59. }  
  60.   
  61. interface ILock  
  62. {  
  63.     const EXPIRE = 5;  
  64.     public function getLock($key$timeout=self::EXPIRE);  
  65.     public function releaseLock($key);  
  66. }  
  67.   
  68. class FileLock implements ILock  
  69. {  
  70.     private $_fp;  
  71.     private $_single;  
  72.   
  73.     public function __construct($options)  
  74.     {  
  75.         if (isset($options['path']) && is_dir($options['path']))  
  76.         {  
  77.             $this->_lockPath = $options['path'].'/';  
  78.         }  
  79.         else  
  80.         {  
  81.             $this->_lockPath = '/tmp/';  
  82.         }  
  83.          
  84.         $this->_single = isset($options['single'])?$options['single']:false;  
  85.     }  
  86.   
  87.     public function getLock($key$timeout=self::EXPIRE)  
  88.     {  
  89.         $startTime = Timer::getTimeStamp();  
  90.   
  91.         $file = md5(__FILE__.$key);  
  92.         $this->fp = fopen($this->_lockPath.$file.'.lock'"w+");  
  93.         if (true || $this->_single)  
  94.         {  
  95.             $op = LOCK_EX + LOCK_NB;  
  96.         }  
  97.         else  
  98.         {  
  99.             $op = LOCK_EX;  
  100.         }  
  101.         if (false == flock($this->fp, $op$a))  
  102.         {  
  103.             throw new Exception('failed');  
  104.         }  
  105.          
  106.         return true;  
  107.     }  
  108.   
  109.     public function releaseLock($key)  
  110.     {  
  111.         flock($this->fp, LOCK_UN);  
  112.         fclose($this->fp);  
  113.     }  
  114. }  
  115.   
  116. class SQLLock implements ILock  
  117. {  
  118.     public function __construct($options)  
  119.     {  
  120.         $this->_db = new mysql();   
  121.     }  
  122.   
  123.     public function getLock($key$timeout=self::EXPIRE)  
  124.     {         
  125.         $sql = "SELECT GET_LOCK('".$key."', '".$timeout."')";  
  126.         $res =  $this->_db->query($sql);  
  127.         return $res;  
  128.     }  
  129.   
  130.     public function releaseLock($key)  
  131.     {  
  132.         $sql = "SELECT RELEASE_LOCK('".$key."')";  
  133.         return $this->_db->query($sql);  
  134.     }  
  135. }  
  136.   
  137. class MemcacheLock implements ILock  
  138. {  
  139.     public function __construct($options)  
  140.     {  
  141.           
  142.         $this->memcache = new Memcache();  
  143.     }  
  144.   
  145.     public function getLock($key$timeout=self::EXPIRE)  
  146.     {       
  147.         $waitime = 20000;  
  148.         $totalWaitime = 0;  
  149.         $time = $timeout*1000000;  
  150.         while ($totalWaitime < $time && false == $this->memcache->add($key, 1, $timeout))   
  151.         {  
  152.             usleep($waitime);  
  153.             $totalWaitime += $waitime;  
  154.         }  
  155.         if ($totalWaitime >= $time)  
  156.             throw new Exception('can not get lock for waiting '.$timeout.'s.');  
  157.   
  158.     }  
  159.   
  160.     public function releaseLock($key)  
  161.     {  
  162.         $this->memcache->delete($key);  
  163.     }  
  164. }  

3 应用锁机制

3.1 支付系统应用锁

  1. <?php  
  2.   
  3. /**  
  4.  * pay.php  
  5.  *  
  6.  * 支付应用锁 
  7.  *  
  8.  * Copy right (c) 2016 http://blog.csdn.net/CleverCode  
  9.  *  
  10.  * modification history:  
  11.  * --------------------  
  12.  * 2016/9/10, by CleverCode, Create  
  13.  *  
  14.  */    
  15.   
  16. //用户支付  
  17. function pay($userId,$money)  
  18. {  
  19.       
  20.     if(false == is_int($userId) || false == is_int($money))  
  21.     {  
  22.         return false;  
  23.     }    
  24.       
  25.     try  
  26.     {  
  27.         //创建锁(推荐使用MemcacheLock)  
  28.         $lockSystem = new LockSystem(LockSystem::LOCK_TYPE_MEMCACHE);               
  29.           
  30.         //获取锁  
  31.         $lockKey = 'pay'.$userId;  
  32.         $lockSystem->getLock($lockKey,8);  
  33.           
  34.         //取出总额  
  35.         $total = getUserLeftMoney($userId);  
  36.           
  37.         //花费大于剩余  
  38.         if($money > $total)  
  39.         {  
  40.             $ret = false;      
  41.         }  
  42.         else  
  43.         {   
  44.             //余额  
  45.             $left = $total - $money;  
  46.               
  47.             //更新余额  
  48.             $ret = setUserLeftMoney($userId,$left);  
  49.         }  
  50.           
  51.         //释放锁  
  52.         $lockSystem->releaseLock($lockKey);   
  53.     }  
  54.     catch (Exception $e)  
  55.     {  
  56.         //释放锁  
  57.         $lockSystem->releaseLock($lockKey);       
  58.     }  
  59.   
  60. }  
  61.   
  62. //取出用户的余额  
  63. function getUserLeftMoney($userId)  
  64. {  
  65.     if(false == is_int($userId))  
  66.     {  
  67.         return 0;  
  68.     }  
  69.     $sql = "select account form user_account where userid = ${userId}";  
  70.       
  71.     //$mysql = new mysql();//mysql数据库  
  72.     return $mysql->query($sql);  
  73. }  
  74.   
  75. //更新用户余额  
  76. function setUserLeftMoney($userId,$money)  
  77. {  
  78.     if(false == is_int($userId) || false == is_int($money))  
  79.     {  
  80.         return false;  
  81.     }          
  82.       
  83.     $sql = "update user_account set account = ${money} where userid = ${userId}";  
  84.       
  85.     //$mysql = new mysql();//mysql数据库  
  86.     return $mysql->execute($sql);  
  87. }  
  88.   
  89. ?>  


3.2  锁分析

p操作人:

     1 获取锁:pay100

     2 取出用户的余额1000。

     3 支付后剩余 800 = 1000 - 200。

     4 更新后账户余额800。

     5 释放锁:pay100

m操作人:

       1 等待锁:pay100

       2 获取锁:pay100

       3 获取余额:800

       4 支付后剩余500 = 800 - 300。

       5 支付后账户余额500。

       6 释放锁:pay100

两次支付后,余额500。非常完美了解决了并发造成的临界区资源的访问问题。


打赏我,让我更有动力~

收藏   0 | Support  0 | Against  0
Login | Register Can Publish Content

精美音乐推荐

最近热帖
window + php 安装redis扩展 0
返回顶部