PHP并发锁的使用
开发中时常会遇到并发,当前遇到一个使用场景,需要定时去拉取对账单,但是可能框架原本的问题,原本定时在10点整开始拉取一次的时候,导致10点钟同时多次请求,导致程序上没有拦截成功,数据同时插入了多条相同数据。其实在当前项目种已经做了判断是否存在方法,当前对账单如果存在的话,会直接进行更新操作,但是因为并发的存在,当时情况下,同时多条相同的数据进行了判断是否存在的,发现没有,同时进行了插入操作,导致数据,出现了大量重复数据,也就是说就算你做了是否存在的判断,在并发下,其实是不可靠的,这个时候我们就需要在程序的方法进行锁操作,刚开始的锁用的是get和set来做的,简单写个demo如下:
<?php
/**
* 此处是redis操作类中的方法
*/
public function lock($key, $ttl)
{
return $this->redis->set($key, true, $ttl);
}
public function unLock($key)
{
return $this->redis->delete($key);
}
public function existLock($key)
{
return $this->get($key) ? true : false;
}
//////////////////////////////////////////////////////////////////////////////////////
/**
* 此处是调用锁
*/
$key = 'lock';
// 如果锁存在则直接返回,否则上锁,继续下边流程
if ($redis->existLock($key)) {
return ;
}
// 进行上锁
$redis->lock($key, $ttl);
// 中间获取对账单数据,然后进行插入操作
$this->insertOrUpdate($where, $data);
...
...
// 一个大致的更新数据库的方法
public function insertOrUpdate($where, $data)
{
// 此时操作因为是并发请求,比如说是5条相同的数据进行了请求,此时五次并发请求都没有查找到相同数据,然后五条同时进行了插入操作,从而导致数据库有五条相同的数据
$res = $model->getOne($where);
if (!$res) {
//执行插入
return $model->insert($data);
}
// 执行更新
return $model->update($res['id'], $data);
}
以上是大概的出问题的代码分布。很明显问题出现在了锁上边,get,set并不合理,并发的时候并不能拦截下所有的请求,原因那和数据库的插入和更新是一样的。这个时候就需要改一下锁的方法,利用redis incr锁的原子性,原子性的解释如下
原子性(atomicity):一个事务是一个不可分割的最小工作单位,事务中包括的诸操作要么都做,要么都不做。
Redis所有单个命令的执行都是原子性的,这与它的单线程机制有关;
Redis命令的原子性使得我们不用考虑并发问题,可以方便的利用原子性自增操作
实现简单计数器功能;
简单来说就是当A、B、C程序同时调用redis->incr(‘incrKey’)的时候,此时incrKey的值必然是由1到3,可能A程序调用的时候incrKey是1,B程序调用的时候incrKey是3,C程序调用的时候incrKey是2,或者其他顺序。我们可以根据此特性来进行拦截并发,只有当incrKey等于1的时候,我们才进行接下来的逻辑操作,其他值全部舍弃掉,就能避免多个程序同时进行请求,导致重复操作,具体代码实现如下,我们改下redis的操作类和具体业务的调用方法
<?php
// redis操作类
// 这次我们在上锁的时候直接进行锁判断,直接返回true或者false
public function lock($key, $ttl)
{
$lock = $this->redis->incr($key);
// 设置过期时间,incr会把key的过期时间设为长期有效,需要加上有效期
$this->redis->expire($key, $expire);
return $this->existLock($key);
}
public function existLock($key)
{
return $this->get($key) == 1 ? true : false;
}
// 然后业务逻辑
if (!$redis->lock($key)) {
echo '已上锁';
}
// 下边正常的逻辑
当完成这个代码以后进行并发测试,然后发现还是有问题,具体问题出现在哪那,出现在了redis的上锁操作中,
public function lock($key, $ttl)
{
$lock = $this->redis->incr($key);
// 设置过期时间,incr会把key的过期时间设为长期有效,需要加上有效期
$this->redis->expire($key, $expire);
// 此时调用existLock的时候又发生了并发,上锁的时候是1,但是返回的时候,因为同时并发请求的原因,$key已经不是1了,导致程序依然有问题,没办法执行
return $this->existLock($key);
}
原因如上:
然后找到原因了,实际上的解决办法也就出来了
<?php
// redis操作类
// 这次我们在上锁的时候直接进行锁判断,直接返回true或者false
public function lock($key, $ttl)
{
$lock = $this->redis->incr($key);
// 设置过期时间,incr会把key的过期时间设为长期有效,需要加上有效期
$this->redis->expire($key, $expire);
// 保证程序完整性,在设置完后,立马进行判断返回
if($lock == 1) {
return true;
} else {
return false;
}
}
// 要什么自行车,要什么检查,直接干掉
//public function existLock($key)
//{
// return $this->get($key) == 1 ? true : false;
//}
// 然后业务逻辑
if (!$redis->lock($key)) {
echo '已上锁';
}
// 下边正常的逻辑
// 当然不要忘记解锁
$redis->unLock($key);
到这个时候,就已经解决了并发导致的数据重复插入问题
THE END