乐观锁和悲观锁在php中应用

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

linux系统自带信号量是属于悲观锁,超过并发量会阻塞。这种阻塞对用户体验比乐观锁有好些。乐观锁直接返回失败

php下信号量使用

$sem_id = sem_get($key, $max_acquire);获取系统信号量,设置最大并发值(最大请求通道数),开辟内存空间
sem_acquire($sem_id)捕获系统信号(semaphore),若超过限度max_acquire,则行程在捕捉是会block锁住信号
紧接着可以执行业务代码,可以将该信号中的被占用的请求通道数量保存起来。执行玩业务代码后,当请求数量大于等于$max_acquire则sem_release($sem_id)释放一个请求通道;当请求数量=0时,记录请求数为1,执行完业务代码后sem_release($sem_id)释放该请求通道;当请求数大于0小于$mac_acquire
时,执行完业务代码即可sem_release()释放请求通道
要实现代码的完全解决并发问题,sem_get()时,$max_acquire直接设置成1即可


下面是一段根据信号量做的流量限制代码,如某个接口只能抗200的并发,用下面方法可以用来做并发限制,理论上$act_config['flow_control']设置成200,但我们服务大都是集群,$act_config['flow_control']该设置成200除以集群服务器数目

信号量是系统级别的并发控制
/**
* 基于信号量锁的限量
* 计数通过cmem存储,按天过期
* @param $actid 活动号
* @param $num 单机限量大小
* @param $end_time 活动结束时间
* @return boolean
*/
public static function flow_lock($actid, $act_config){
global $_TMEM_CFG;
require_once(GAME_DOC_ROOT . 'Api/tphp_tmem.class.php');//这就是一个memcache

$num = intval($act_config['flow_control']); //最大并发数$max_acquire
if ($num <= 0) { //设置错误记录日志
$errInfo = sprintf("FILE:%s|FUNC:%s|LINE:%s|MSG:%s|RETCODE:%s|",
basename(__FILE__),__FUNCTION__,__LINE__, "flow_lock config error : actid=" . $actid, '');
Util::writeV5Log( $errInfo, 'err');
return TRUE;
}

$expire = 86400;

$cmem = new tphp_tmem($_TMEM_CFG['t_reflux']);
$serverip = Util::getRealServerIP();
$key = 'ams_flow_lock_'.$actid . "_" . $serverip . '_' . $_SERVER['REQUEST_TIME'];
fb($key, 'flow_lokc_key');

$sem_id = sem_get($actid, 3); //获取系统信号量
if ($sem_id === false){
$errInfo = sprintf("FILE:%s|FUNC:%s|LINE:%s|MSG:%s|RETCODE:%s|",
basename(__FILE__),__FUNCTION__,__LINE__, "flow_lock Failed to create semaphore: actid=" . $actid , '');
Util::writeV5Log( $errInfo, 'err');
return FALSE;
}

sem_acquire($sem_id); //检测信号量请求数是否大于$max_acquire
$json = $cmem->get($key);

fb($json,'flow_lock_get');

if($cmem->errCode == 0){
$cnt = $json; //$cnt为保存的信号量占有数
if ($cnt >= $num) {
sem_release($sem_id);
self::ams_sem_remove($sem_id, $act_config);
return FALSE;
}

$cnt++;
$result = $cmem->set($key, $cnt);

}else if($cmem->errCode == -13200){
$result = $cmem->set($key, 1);
}else{
$errInfo = sprintf("FILE:%s|FUNC:%s|LINE:%s|MSG:%s|RETCODE:%s|",
basename(__FILE__),__FUNCTION__,__LINE__, "flow_lock read cmem error: actid=" . $actid . ";key=" . $key, $json['ret']);
Util::writeV5Log( $errInfo, 'err');
}
sem_release($sem_id);
self::ams_sem_remove($sem_id, $act_config);

return TRUE;
}


文件op_increace_num 为uin产出序列号(递增)
再如cmem提供的incr_init()初始化,incr_get()获取值,incr_value()设置值,都为原子操作,属于悲观锁,有阻塞性质。memcache也有
$res = $this->cmem->incr_get($totalKey);
if($res['ret'] == 0){   //有数据
$result = $this->cmem->incr_value($totalKey, 1); //原子加操作 集群访问时,操作的是同一个key,命中的是同一台cmemcache,单机原子加解决了并发问题
if($result == 0){
return $this->setUinData($res['data'] + 1); //由于incre_value原子操作,确保了$totalKey在并发情况下每个进程增加1。进程1运行到这行代码时$res['data']=1,进程2由于incr_value()使得$res['data']原子加1,此时进程2运行时$res['data']结果为2
}else{
return Code::CMEM_SAVE_DATA_ERROR;
}

}elseif($res['ret'] == -13200){//无数据
$result = $this->cmem->incr_init($totalKey, 1); //也是原子操作,但判断结果是没数据时,同样存在并发情况,会多次调用incr_init()初始化接口,因此可以直接手动先初始一个值,在再发布接口,并发情况下,该健肯定存在值
if($result == 0){
return $this->setUinData(1);
}else{
return Code::CMEM_SAVE_DATA_ERROR;
}
}else{
return Code::CMEM_GET_DATA_ERROR;
}



乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

乐观锁php的应用 一般是版本号 cas

防止并发的问题,memcached 提供了CAS (chech and save) 方式,在get 时候获取对应值的同时还获取当前key 对应的token(或者叫版本号),在更新操作时候需要带上token,会比较当前的token,是否和get时的一直,如果不一致就更新失败。
如果由于其他人对当前key操作,token值就会发生变化。 php的参考代码如下:
$cas = 0.0;

do {
    $cnt = $m->get('cnt_key', null, $cas);

    if ($m->getResultCode() == Memcached::RES_NOTFOUND) {
        $m->add('cnt_key', 1);
    } else { 
        $m->cas($cas, 'cnt_key', $cnt + 1);
    }   
} while ($m->getResultCode() != Memcached::RES_SUCCESS);

注意上面代码中的 get 方法中有一个$cas 参数,这个就是当前cnt_key对应的token,一旦 cnt_key 被操作后,其值也发生变化。在cas方法中需要传递$cas值,如果token发生变化,那么cas 将会执行失败,也即 $m->getResultCode() != Memcached::RES_SUCCESS。所以将会再次执行循环体。

这个是在memcached 客户端做的。对于incr,保证原子性,是在memcached服务端完成的,原理应该类似上述的cas操作,由于没有查看memcached的源代码,不敢妄下结论。

参考链接:https://segmentfault.com/q/1010000002902575

      http://blog.csdn.net/hongchangfirst/article/details/26004335 一分钟理解乐观锁和悲观锁




posted @ 2016-11-03 22:01  虽不能至,心向往之。  阅读(441)  评论(0)    收藏  举报