1.多并发请求函数封装
public function multiRequest($urls, $nums)
{
// 创建一对cURL资源
// $ch1 = curl_init();
// $ch2 = curl_init();
// 设置URL和相应的选项
// curl_setopt($ch1, CURLOPT_URL, "http://81.69.45.114/test/test.php");
// curl_setopt($ch1, CURLOPT_HEADER, 0);
// curl_setopt($ch2, CURLOPT_URL, "http://81.69.45.114/test/test.php");
// curl_setopt($ch2, CURLOPT_HEADER, 0);
// 创建批处理cURL句柄
$mh = curl_multi_init();
// 增加2个句柄
// curl_multi_add_handle($mh, $ch1);
// curl_multi_add_handle($mh, $ch2);
$key = 0;
do {
$ch[$key] = curl_init();
curl_setopt($ch[$key], CURLOPT_URL, $urls);
curl_setopt($ch[$key], CURLOPT_HEADER, 0);
$key++;
$nums--;
}while($nums>0);
foreach ($ch as $c) {
curl_multi_add_handle($mh, $c);
}
$active = null;
// 执行批处理句柄
do {
$mrc = curl_multi_exec($mh, $active);
var_dump($mrc);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
while ($active && $mrc == CURLM_OK) {
if (curl_multi_select($mh) != -1) {
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
}
}
// 关闭全部句柄
foreach ($ch as $c) {
curl_multi_remove_handle($mh, $c);
}
// curl_multi_remove_handle($mh, $ch1);
// curl_multi_remove_handle($mh, $ch2);
curl_multi_close($mh);
}
// 函数调用
multiRequest(请求地址,并发数量)
multiRequest("http://127.0.0.1:88/test_back/public/index.php/test/count", 10);
2.解决措施
1.使用数据库自减或者自增方法,并设置数据字段的符号是否为无符号,通过捕获修改异常来返回错误信息,解决并发下的数据更新问题(但是不适用于新增记录,会存在同时新增的情况,所以将使用下面的方法解决)
假设数据库的要修改的字段的值为10,但是现在同时发送十个请求过来,每个请求都是修改这个字段的值,但是因为是并发,每个请求拿到的值可能是一样的,因为就存在了最后的值不为理想的那个值
// 存在重复的可能,十个请求触发十次这个接口,值减一,但是数据库最后返回的值为4(不为0)
public function count()
{
// 请求超时时间为100s
set_time_limit(100);
$num = $this->user->where('id', '2')->value('num');
var_dump('num' . $num);
$this->user->where('id', '2')->update(['num' => $num - 1]);
// 返回查询到的数量
$count = $this->user->where('id', '2')->value('num');
return $count;
}
// 修改之后的方法,虽然依旧是查询的值,但是增减用的是自增或者自减,以原有的数据库为基准,而不是读出来的为基准,十次请求过后,值为0
public function count()
{
// 请求超时时间为100s
set_time_limit(100);
$this->user->where('id','2')->dec('num')->update();
// 返回查询到的数量
$count = $this->user->where('id', '2')->value('num');
return $count;
}
2.文件锁 -- 加入文件读取锁定,限制每个请求都得完成后才能读取,避免了同时读取数据库的可能,从而降低并发量。在文件锁定过程中进行数据库的数据修改读取(实际项目中并不支持使用)
// 文件锁
function fileLock()
{
$file = fopen(config('path.fileLock') . '/userLock.txt', 'w+');
if (flock($file, LOCK_EX)) {// 独占资源锁定,不允许其他进程访问
// 进行数据库操作
$num = $this->user->where('id', '2')->value('num');
$this->user->where('id', '2')->update(['num' => $num - 1]);
$count = $this->user->where('id', '2')->value('num');
var_dump('count' . $count);
// 释放释放一个共享锁定或独占锁定
flock($file,LOCK_UN);
}else{
return '系统出错';
}
// 请求完都要关闭资源
fclose($file);
}
3.悲观锁+数据库事务 默认数据是悲观的,就是每次拿数据的时候这个数据一定会发生改变,因此使用数据库的操作锁定,只有操作完成后解锁记录才能被另外的进程调用
public function bad()
{
// 开启事务
$this->user->startTrans();
try {
// 锁定记录操作
$num = $this->user->lock(true)->where('id','2')->value('num');
$this->user->where('id','2')->update(['num' => $num-1]);
$count = $this->user->where('id','2')->value('num');
var_dump('count' . $count);
$this->user->commit();
} catch (\Exception $e) {
$this->user->rollback();
var_dump($e->getMessage());
}
}
4.乐观锁+数据库事务 默认数据是乐观的,就是每次拿数据的时候默认数据是不会发生改变,没有被别的进程调用,会在数据库表中加一个版本字段,只有在修改数据的时候查看改数据版本是否发生改变,如果发生版本改变,则取消事务操作。如果修改成功则修改版本字段。(会丢失部分的数据请求修改)
public function well()
{
// 开启事务
$this->user->startTrans();
try {
$num = $this->user->where('id', '2')->value('num');
$version = $this->user->where('id', '2')->value('version');
// 要更新的数据
$data = [
'num' => $num - 1,
'version' => $version + 1
];
// 只有版本号相同才会更新
if ($this->user->where(['id' => '2', 'version' => $version])->update($data)) {
$this->user->commit();
} else {
// 版本号不同放弃修改,事务回滚
$this->user->rollback();
}
} catch (\Exception $e) {
var_dump($e->getMessage());
$this->user->rollback();
}
}
5.redis缓存
1.悲观锁适合频繁写入的场景,不适合用于多次读取的场景
悲观锁是将资源进行锁定,利用key的过期时间进行处理,传入key(存储了时间戳),并且使用setnx获取是否能够写入,不能写入则获取现在的时间和key的值进行比对,如果现有时间大于和key的值,则进行删除过期key并且写入新的过期时间,然后返回可以继续操作的标志
/**
* 获取锁---用于悲观锁
* @param String $key 锁标识
* @param Int $expire 锁过期时间
* @return Boolean
*/
public static function lock($key = '', $expire = 5)
{
$is_lock = Cache::store('redis')->setnx($key, time() + $expire);
//不能获取锁
if (!$is_lock) {
//判断锁是否过期
$lock_time = Cache::store('redis')->get($key);
//锁已过期,删除锁,重新获取
if (time() > $lock_time) {
self::unlock($key);
$is_lock = Cache::store('redis')->setnx($key, time() + $expire);
}
}
return $is_lock ? true : false;
}
/**
* 释放锁
* @param String $key 锁标识
* @return Boolean
*/
public static function unlock($key = '')
{
return Cache::store('redis')->del($key);
}
//调用:
$key = 'str:lock';
$is_lock = lock($key, 10);
if ($is_lock) {
// 获得锁之后的操作
echo 'get lock success<br>';
echo 'do sth..<br>';
sleep(5);
echo 'success<br>';
Redis::unlock($key);
} else {
//获取锁失败
echo 'request too frequently<br>';
}
2.乐观锁适合于多次读取的场景,不适合用于多次写入的场景
主要的操作是使用watch监听key的变化,然后使用multi开启redis的事务,进行增删改查等别的操作后,然后使用exec进行redis事务提交,在并发过程中如果watch监听的数据发生改变,这个事务的提交就会失败
/**
* 获取锁---用于乐观锁
* @param String $key 锁标识
*/
public static function wellLock($key = '')
{
$num = Cache::get($key); // redis库中值为10
var_dump('num:' . $num);
$rob_total = 10; //抢购数量
if ($num < $rob_total) {
Cache::store('redis')->watch('num');
Cache::store('redis')->multi(); //redis事务开始
// 休眠是为了让并行的修改有足够的时间
sleep(10);
// num在事务前后没有变化,这里的操作,下面会提交事务,而与此同时别的事务会失败
// Cache::store('redis')->incr("testwatch"); //乐观锁的版本号+1
$rob_result = Cache::store('redis')->exec(); //redis事务提交 ,如果事务过程中num这个值发生变化,这里redis事务失败
var_dump($rob_result);
if ($rob_result) {
$num = Cache::store('redis')->get('num');
var_dump($num);
echo "成功";
} else {
echo "再来一次,抢购失败";
}
} else {
echo "卖完!";
}
}
// 用于并行触发乐观锁接口
public static function wellLock2($key = '')
{
$num = Cache::get($key); // redis库中值为10
var_dump('num:' . $num);
$rob_total = 10; //抢购数量
if ($num < $rob_total) {
Cache::store('redis')->watch('num');
Cache::store('redis')->multi(); //redis事务开始
// num在事务前后没有变化,这里的操作,下面会提交事务,而与此同时别的事务会失败
// Cache::store('redis')->incr("testwatch"); //乐观锁的版本号+1
$num = Cache::store('redis')->set('num',7);
$rob_result = Cache::store('redis')->exec(); //redis事务提交 ,如果事务过程中num这个值发生变化,这里redis事务失败
var_dump($rob_result);
if ($rob_result) {
$num = Cache::store('redis')->get('num');
var_dump($num);
echo "成功";
} else {
echo "再来一次,抢购失败";
}
} else {
echo "卖完!";
}
}
解析:最后redis的数据值为7。在执行wellLock函数中,使用了休眠,在休眠的期间,使用postman触发wellLock2接口,修改了侦听的num的值,接口wellLock的事务提交失败,最后响应wellLock2执行成功