详解PHP解决swoole守护进程Redis假死 ,mysql断线重连问题

news2025/1/15 21:01:40

详解PHP解决swoole守护进程Redis假死 ,mysql断线重连问题

最近公司有个项目,要举办一个线上活动,我这边负责提供接口记录用户访问记录,与操作记录,由于活动参与人数可能比较多,为了不影响正常业务运行,我们决定不在接口中直接写入数据库,而采用异步写入,也就是调用接口,数据先写入reids 队列,然后在编写一个消费进程读取队列消息写入数据库。大概就是这么个流程

为了消费进程能一直读取队列数据写入数据库,我们需要 编写一个守护进程让它一直为我们工作,php编写守护进程,有几个选择可以用,第一个php提供的pcntl*扩展,第二就是workerman,第三就是 Swoole

这里我选择swoole 来编写守护进程

  1. 下载swoole 扩展源码 由于生产环境php版本是7.1 所有 swoole 扩展版本也不能选择最新版本,我这里选用 swoole-4.4.4,下载到php 扩展目录

    cd /usr/local/php/include/php/ext
    wget -c http://pecl.php.net/get/swoole-4.4.4.tgz
    
  2. 接下来我们解压 swoole 源码,进入源码目录

    tar xzvf swoole-4.4.4.tgz
    cd swoole-4.4.4
    
  3. 第三步执行phpize 生成configure配置文件

    phpize
    
  4. 第四步 指定php配置文件进行预编译,如果php配置文件目录不同自行调整

    ./configure --with-php-config=/usr/local/php/bin/php-config
    
  5. 第五步 编译与安装,编译安装完会在 /usr/local/php/lib/php/extensions/no-debug-non-zts-20160303 下找到一个 swoole.so 动态库文件

    #编译
    make
    # 安装
    make install
    

    在这里插入图片描述

  6. 修改 php.ini文件,加入 swoole.so 动态库文件,文件路径不同自行修改

[Swoole]
extension = /usr/local/php/lib/php/extensions/no-debug-non-zts-20160303/swoole.so

在这里插入图片描述

修改完成后就可以使用 php -m 命令查看是否安装上了 swoole 扩展
在这里插入图片描述
接下来进入正题,编写守护进程, swoole 编写守护进程极其简单调用 daemon() 方法就好

<?php

use Swoole\Process;

// 定义回调处理函数
function callback_function (Swoole\Process $worker) {
	//执行一个外部程序,此函数是 exec 系统调用的封装。
    $worker->exec('/usr/bin/php', array(__DIR__.'/think','activityupd'));
};



// 创建一个子进程,第一个参数是子进程创建成功执行的回调函数,第二个参数的意思是 重定向子进程的标准输入和输出。【启用此选项后,在子进程内输出内容将不是打印屏幕,而是写入到主进程管道。读取键盘输入将变为从管道中读取数据,调试代码时可以设置为 tree
$p = new Swoole\Process('callback_function',false);

// 启动子进程
$p->start();
//使当前进程蜕变为一个守护进程。
Swoole\Process::daemon(false,false);
while(1){
sleep(1);
}
// 回收结束运行的子进程。
Swoole\Process::wait();

项目使用的是tp框架,所有使用命令行模式 来编写主流程逻辑

<?php

namespace app\admin\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use think\Log;

class ActivityUpd extends Command
{
    protected function configure()
    {
        $this->setName('activityupd')->setDescription('Command Test');
    }

    protected function execute(Input $input, Output $output)
    {
        $redis = new \Redis();
        $redis->connect('127.0.0.1', 6379);
        
        $db = Db::connect('xxx');
        
        Log::init([
                'type'  =>  'File',
                'path'  =>  APP_PATH.'logs/'
        ]);
        
        try {
            while (true) {
                // 写入数据库
                // ==============
                // do somthing
                // ==============
                // 业务队列
                $data = unserialize($redis->rPop('300activity:recordLists'));
                if ($data) {
                    if ($data['h5'] == 1) {
                        unset($data['h5']);
                        $db->name('carve_upf')->insert($data);
                    } else if ($data['h5'] == 2) {
                        unset($data['h5']);
                        $db->name('letter')->insert($data);
                    }
                    Log::info("消费数据:".json_encode($data));
                } else {
                    sleep(1); // 睡眠 1 秒。
                }
            }
        } catch (\Exception $e) {
            Log::error("Error:{$e->getMessage()}");
            exit("Error:{$e->getMessage()}");
        }
    }
}

这段代码我们很容易看懂。

它就是通过 循环从 Redis 队列中取出数据并处理。如果没有取到数据就休眠一秒。之所以休眠是为了保证 CPU 能得到充分的利用。

当我们的业务出现任何错误,我们通过try catch进行异常捕获然后将错误信息记录日志并退当前脚本。
这样就可以正常消费写入数据库啦

但是,好景不长。过了一段时间,我发现 Redis 队列的数据出现了未消费的情况。我查看了日志。发现日志里有错误

[ error ] [8]think\db\Connection::free(): send of 9 bytes failed with errno=32 Broken pipe[/usr/share/nginx/html/gift/byd/thinkphp/library/think/db/Connection.php:310]
[ error ] 异常Error:Error while sending STMT_CLOSE packet. PID=16040

解决这个错误前先讲两个概念

交互式连接&非交互式连接

什么是交互式连接?

通俗的说,就是你在你的本机上打开mysql的客户端,就是那个黑窗口,在黑窗口下进行各种sql操作,当然走的肯定是tcp协议。

什么是非交互式操作?

就是你在你的项目中进行程序调用。比如一边是nginx web服务器,一边是数据库服务器,两者怎么通信?在php web里,我们通常会选择pdo或者是mysqli来连接。那么这时候就是非交互式操作。

我们的问题明显出在非交互式操作上

mysql 配置中有两个 参数 对非交互式操作有直接影响

interactive_time:是指如果空余 N秒(N就是这个属性的值),那么就会自动关闭mysql的连接。关闭什么样的mysql连接?刚刚,我们讲了什么是mysql的交互式操作和非交互式操作中, mysql是有两种操作方式,那就有两种连接的,一种是交互式,一种是非交互式。而这个属性控制的是交互式。就是你打开一个mysql客 户端黑窗口,进入操作之后,又隔了N秒你不操作了,之后你想继续操作,对不起,mysql会在之前关闭了你的那个连接,mysql会帮你自动重新连接。

wait_time:是指如果空余 N秒(N就是这个属性的值),那么会自动 kill 掉mysql的一部分连接线程。这里的连接就是指的是非交互式连接。

编写fpm服务的时候,我们基本上不关心这两个属性,都是用的是mysql服务推荐的默认值,就是8小时,反正一个请求结束就会释放所有连接。

但是,我这次编写的cli守护进程需要一直运行,这就出现了 “mysql的8小时自动关闭”问题。

所有这个错误的原因是因为mysql连接长时间没活动被mysql服务端关闭,当进程关闭(停止服务、reload服务、重启服务)时php会向mysql服务端发送一个关闭包告知mysql服务端自己将要关闭,但是因为mysql连接已经断开,所以导致 Warning: Error while sending STMT_CLOSE packet. PID=16040。如果本来进程就是要关闭的,mysql链接断开也无所谓了,但是我并不是要断开,只是因为消费队列里长时间没有数据,自然也就不会执行sql语句,导致服务端主动关闭了客户端连接

我们复现一下这个问题
首先设置 wait_time 为120秒过期 :

set global wait_timeout=120;

在这里插入图片描述
然后启动服务,先写入一条数据到redis队列,等120秒 在写入一条数据就出现下面错误或者出现 Error:PDOStatement::execute(): MySQL server has gone away 错误

在这里插入图片描述

有什么办法解决这个问题呢,可以把mysql服务端 wait_timeout改长一些?治标不治本
或者每次处理数据都建立新连接,处理完数据就关闭mysql连接。这一看就不符合社会主义价值观

好在 V5.0.6+版本开始,thinkphp支持Mysql的断线重连机制,默认关闭,需要的话,是数据库配置文件中添加

// 开启断线重连
‘break_reconnect’ => true,

不过尴尬的是生产环境的tp 版本 5.0.5 没有这个功能,那只能自己撸

<?php
namespace app\admin\command;

use think\Db;
/**
 * 数据库主动重连
 */
class ReloadDb {

	private static $time = null;
    private static $db;
    /**
     * 检测或执行主动重连
     * @author andy3513
     * @param int     $timeout 超时时间
     * @param array $config   连接参数
     */
    public static function init($timeout = 7200){
		$time = time();
		if(null === self::$time){
		self::$time = $time;
		}
		$exprie = $time - self::$time;
		if($exprie >= $timeout || empty(self::$db)){
		self::$db = Db::connect('db_config_gift', true);
		self::$time = $time;
		}
		return self::$db;
    }
}

增加 mysql 主动重连代码

<?php

namespace app\admin\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use think\Log;

class ActivityUpd extends Command
{
    protected function configure()
    {
        $this->setName('activityupd')->setDescription('Command Test');
    }

    protected function execute(Input $input, Output $output)
    {
        $redis = new \Redis();
        $redis->connect('127.0.0.1', 6379);
        Log::init([
                'type'  =>  'File',
                'path'  =>  APP_PATH.'logs/'
        ]);
        
        try {
            while (true) {
                // 写入数据库
                // ==============
                // do somthing
                // ==============
                // 业务队列
                $data = unserialize($redis->rPop('300activity:recordLists'));
                if ($data) {
                	$db = ReloadDb::init();
                    if ($data['h5'] == 1) {
                        unset($data['h5']);
                        $db->name('carve_upf')->insert($data);
                    } else if ($data['h5'] == 2) {
                        unset($data['h5']);
                        $db->name('letter')->insert($data);
                    }
                    Log::info("消费数据:".json_encode($data));
                } else {
                    sleep(1); // 睡眠 1 秒。
                }
            }
        } catch (\Exception $e) {
            Log::error("Error:{$e->getMessage()}");
            exit("Error:{$e->getMessage()}");
        }
    }
}

通过代码对比,我们在第一版代码的基础上加了如下代码:

   	$db = ReloadDb::init();

修改后的代码 mysql的8小时自动关闭 的问题再也没出现了。

可是天不遂人愿过了1天,我发现 Redis 队列的数据又出现了未消费的情况,查看日志,的确没有产生新的消费日志。也没发现有任何的错误发生。

  • 常驻后台进程处理存活状态。并没有变成孤儿进程。
  • 常驻后台进程内存也没有出现泄漏。
  • 系统 CPU/内存 资源都处理正在状态。
  • 系统打开的句柄资源也是低消状态。

redis 服务端我测试也正常运行写入,也就排除了 Redis 故障的问题。

我当时也怀疑过是不是像MySQL一样常时间连接不进行任何操作,服务器端会主动断开连接。但是,MySQL 服务器端主动段掉连接会提示:MySQL server has gone away的错误。但是,我们的 Redis 服务器端没有给我们报任何错误信息呀。

后面我尝试升级 redis 版本 结果 Redis 还是假死了。或者说我们的 Redis 处于伪活状态。

你认为 Redis 活着,其实它早已经死了。你认为 Redis 死了,但是它却没有死亡的特征。

最后去redis 官网查看 Redis 的 API。发现它提供了一个ping()的方法来检测连接是否存活。
我把这个命令加入逻辑中,队列没有数据操作我就ping 一下连接,预防连接出问题

<?php

namespace app\admin\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use think\Log;

class ActivityUpd extends Command
{
    protected function configure()
    {
        $this->setName('activityupd')->setDescription('Command Test');
    }

    protected function execute(Input $input, Output $output)
    {
        $redis = new \Redis();
        $redis->connect('127.0.0.1', 6379);
        
        Log::init([
                'type'  =>  'File',
                'path'  =>  APP_PATH.'logs/'
        ]);
        
        try {
            while (true) {
                // 写入数据库
                // ==============
                // do somthing
                // ==============
                // 业务队列
                $data = unserialize($redis->rPop('300activity:recordLists'));
                if ($data) {
                   	$db = ReloadDb::init();
                    if ($data['h5'] == 1) {
                        unset($data['h5']);
                        $db->name('carve_upf')->insert($data);
                    } else if ($data['h5'] == 2) {
                        unset($data['h5']);
                        $db->name('letter')->insert($data);
                    }
                    Log::info("消费数据:".json_encode($data));
                } else {
                //此方法TRUE在成功时返回
                    $pong = $redis->ping();
                    if (!$pong) {
                        throw new \Exception('Redis ping failure!', 500);
                    }
                    sleep(1); // 睡眠 1 秒。
                }
            }
        } catch (\Exception $e) {
            Log::error("Error:{$e->getMessage()}");
            exit("Error:{$e->getMessage()}");
        }
    }
}

通过代码对比,我们在第一版代码的基础上加了如下代码:

//此方法TRUE在成功时返回
$pong = $redis->ping();
if (!$pong) {
    throw new \Exception('Redis ping failure!', 500);
}

注意:在 PhpRedis 5.0.0 之前,此命令只返回字符串+PONG。

 $pong = $redis->ping();
 if ($pong != '+PONG') {
     throw new \Exception('Redis ping failure!', 500);
 }

当我们每次 ping 的时候,Redis 服务器就会认为我们的 Redis 客户端连接处于存活状态。就不会断掉我们的连接了。

修改后代码假死的问题再也没出现了。

为了提高稳定性我,后面又给 redis 加上断线重连逻辑,预防网络问题导致连接断开出现的问题

<?php

namespace app\admin\command;

class Redis
{

    private static $_instance; //存储对象

    private function __construct()
    {
        self::$_instance= new \Redis();
        self::$_instance->connect('127.0.0.1', 6379);

    }

    public static function getInstance()
    {
        if (!self::$_instance) {
            new self();
        } else {
            try {
            	// 定义用户错误级别
                @trigger_error('flag', E_USER_NOTICE);
                self::$_instance->ping();
                $error = error_get_last();
                if ($error['message'] != 'flag')
                    throw new \Exception('Redis server went away');
            } catch (\Exception $e) {
				// 断线重连
                new self();
            }
        }
        return self::$_instance;
    }
}

引入redis 断线重连逻辑

<?php

namespace app\admin\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use think\Log;

class ActivityUpd extends Command
{
	private  $handler;
    protected function configure()
    {
        $this->setName('activityupd')->setDescription('Command Test');
    }

    protected function execute(Input $input, Output $output)
    {   
        Log::init([
                'type'  =>  'File',
                'path'  =>  APP_PATH.'logs/'
        ]);
        
        try {
            while (true) {
                // 写入数据库
                // ==============
                // do somthing
                // ==============
                // 业务队列
                $this->handler = Redis::getInstance();
                $data = unserialize($this->handler->rPop('300activity:recordLists'));
                if ($data) {
                   	$db = ReloadDb::init();
                    if ($data['h5'] == 1) {
                        unset($data['h5']);
                        $db->name('carve_upf')->insert($data);
                    } else if ($data['h5'] == 2) {
                        unset($data['h5']);
                        $db->name('letter')->insert($data);
                    }
                    Log::info("消费数据:".json_encode($data));
                } else {
                    sleep(1); // 睡眠 1 秒。
                }
            }
        } catch (\Exception $e) {
            Log::error("Error:{$e->getMessage()}");
            exit("Error:{$e->getMessage()}");
        }
    }
}

我们测试一下加了连接重试与不加的效果

不加redis 连接重试

先启动服务,在通过命令 : CLIENT list 查看所有客户端连接
在这里插入图片描述

id=107 就是我们编写的客户端连接,id=108 就是我们当前操作的窗口,如何分辨呢?,看 cmd 代表客户端正在执行的命令就知道了

服务端主动kill 掉客户端连接,执行 redis kill 命令 :CLIENT KILL 127.0.0.1:52115

查看我们的服务发现已经异常退出
在这里插入图片描述

加上redis 连接重试,关闭客户端连接,客户端会再次主动连接上

在这里插入图片描述
虽然redis 报错误但是进程依然运行
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安全性归约(游戏)

文章目录基于游戏的安全性定义归约中的概率关系某事件发生某事件不发生互斥事件基于游戏的安全性定义 在将攻击 Γ\GammaΓ 的算法 A′AA′ 归约到攻击 Π\PiΠ 的算法 AAA 时&#xff0c; 让 A′AA′ 根据 ChΓCh_\GammaChΓ​ 提供的信息&#xff0c;为 AAA 模拟出同分布的 …

股票自动下单接口够接入多种股票数据源吗?

很多的股票交易接口在原先只能接入行情的CTP程序&#xff0c;那么股票自动下单接口现在可以通过openctp提供的CTPAPI&#xff0c;可以接入到多种多样的股票数据源&#xff01;但是目前由于大多的股票交易接口是受到监管限制的就很难接入实盘&#xff0c;那么股票自动下单接口通…

知心世界姐王瑞平:谷传民与大衣哥朱之文是沟通问题不是人品问题

大衣哥和谷传民的官司走到现在&#xff0c;互联网上面也出现了两大阵营对垒&#xff0c;一方面是大衣哥的粉丝&#xff0c;旗帜鲜明地支持自己的偶像&#xff0c;另一大阵营的人&#xff0c;则一心一意支持谷传民。虽然每个阵营都有自己的道理&#xff0c;但是毕竟都太过极端&a…

Virtual Data Augmentation: 虚拟数据扩增技术

听说过数据扩增&#xff08;Data Augmentation&#xff09;&#xff0c;也听说过虚拟对抗训练&#xff08;Virtual Adversarial Traning&#xff09;&#xff0c;但是我没想到会有人将其结合&#xff0c;谓之虚拟数据扩增&#xff08;Virtual Data Augmentation&#xff09;。这…

CANoe诊断测试

诊断协议那些事儿 本文为诊断协议那些事儿专栏文章&#xff0c;当我们在开发工程中越来越多的需要使用到总线测试工具&#xff0c;其中包括BUSMASTER、周立功、PCAN、CANOE等&#xff0c;本文将使用德国Vector公司的CANoe介绍诊断测试的基本环境。 文章目录诊断协议那些事儿一…

Python编程从入门到实践 第五章:if语句 练习答案记录

Python编程从入门到实践 第五章&#xff1a;if语句 练习答案记录 练习题导航Python编程从入门到实践 第五章&#xff1a;if语句 练习答案记录5.1 一个简单示例5.2 条件测试5.2.1 检查是否相等5.2.2 检查是否相等时忽略大小写5.2.3 检查是否不相等5.2.4 数值比较5.2.5 检查多个文…

运行yolov5 v6遇到的问题

1. Arial.ttf无法在运行时下载的问题 可以选择用浏览器下载&#xff0c;然后拷贝到docker或者ubuntu下&#xff0c;创建服务器的http访问方式。 具体为&#xff1a; 1.1 下载文件 Arial.ttf 并拷贝到docker或者ubuntu下 1.2 在ubuntu下创建http访问方式&#xff1a; # 安装…

有限元仿真分析误差来源之边界条件,约束和point mass

导读&#xff1a;前不久&#xff0c;我在这里分享了一篇《有限元仿真分析误差来源之材料参数设置&#xff0c;小心为妙》的文章&#xff0c;引发了同行们的关注和讨论。在此感谢仿真秀平台讲师们的批评和指正&#xff0c;一起认真交流技术和进步。今天我将继续带来关于边界条件…

spring data jpa在mysql分页中的实例(一次访问同时获取数据和总数)

一、原生sql语句 mysql中语句如下 select SQL_CALC_FOUND_ROWS sn,max(count) as active_count from sn_state_changed where sn_year zz group by sn limit 0,10; select FOUND_ROWS() as total; 解释&#xff1a; SQL_CALC_FOUND_ROWS 供后面的查询总数sql语句使…

(STM32)从零开始的RT-Thread之旅--SPI驱动ST7735(1)

上一篇&#xff1a; (STM32)从零开始的RT-Thread之旅--GPIO 我使用的开发板是WeAct的H743板子&#xff0c;板子带一个0.96的SPI驱动的LCD&#xff0c;给的有现成的测试用例&#xff0c;看源码应该是ST的工程师写的ST7735的驱动&#xff0c;打算把这个驱动直接拿到RTT工程里面使…

SolidWorks 入门笔记01:草图绘制

全文目录简介1. 草图的创建1.1 在基准面上新建一个二维草图多学一招&#xff1a;退出草图绘制模式&#xff0c;快捷键切换视图1.2 从已有的草图派生新的草图。1.3 在零件的平面上绘制草图多学一招&#xff1a;SolidWorks 中鼠标滚轮放大缩小功能反的解决办法 。2. 基本图形绘制…

【2022秋线上作业-第5次-第11-13周】判断题

1-1 一棵有124个结点的完全二叉树&#xff0c;其叶结点个数是确定的。T 解析&#xff1a; 一棵124个叶节点的完全二叉树&#xff0c;假设n0为叶子节点数&#xff0c;n1为度为1结点数&#xff0c;n2为度为2结点数&#xff0c;则有总结点数为n0n1n2&#xff1b;而n2n0-1123&#…

如何杜绝 spark history server ui 的未授权访问?

如何杜绝 spark history server ui 的未授权访问? 1 问题背景 默认状况下&#xff0c;Spark history Sever ui 是没有任何访问控制机制的&#xff0c;任何用户只要知道 shs 对应的 url&#xff0c;就可以访问链接查看 spark 作业的运行状况。 在证券基金银行等金融行业中&a…

Kotlin 开发Android app(六):Kotlin 中的空判断 问号和感叹号

如果有人对程序的崩溃原因做下统计的话&#xff0c;那么由于对象为空&#xff0c;但是又访问了对象的某个属性而导致的崩溃&#xff0c;也许会是程序崩溃的第一大原因了。 比如我们在使用字符串的时候&#xff0c;变量字符串为空的时候&#xff0c;我们去访问了这个字符串变量的…

2022-11-16 每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形)

每日打卡&#xff1a;单调栈解决最大矩形问题&#xff08;一维直方图&#xff0c;二维最大红矩形&#xff09; 柱状图中最大的矩形 思路 这个题最明显的思路就是&#xff1a;矩形面积底高。 版本1&#xff1a;底的长度可以通过二重循环来完成&#xff0c;高通过循环来寻找最…

44、Spring AMQP 数据转换器

1、操作案例 2、发送一个对象到队列中 3、控制台查看 4、使用消息转换器 5、消费者接收消息&#xff0c; 传递什么类型&#xff0c;就接受什么类型&#xff0c;发送方与接收方所使用的消息转换器必须对应 6、总结分析 默认的消息推送是通过JDK序列化的方式进行的&#xff0c;…

【STM32+cubemx】0029 HAL库开发:HMC5883L磁力计的应用(电子指南针)

今天我们来学习电子磁力计HMC5883L的使用。先介绍磁力计的基础知识&#xff0c;再给一个获取磁力计数据的例子&#xff0c;最后讲解HMC5883L磁力计的校准&#xff0c;以及一些使用中的经验。 1&#xff09;HMC5883L磁力计的基础知识 磁力计是用来测量磁场强弱&#xff08;也就…

Android 录音没有声音,设置AudioSource.VOICE_CALL直接MediaRecorder.start异常等系列问题

一、我的需求&#xff1a;来电后&#xff0c;我的三方应用主动开启录音&#xff0c;挂断后结束录音&#xff0c;查验音频 我遇到的问题&#xff1a;录制的音频没有声音。 通过各种尝试&#xff0c;结果如下 &#xff1a;设置不同的录音来源的效果 MediaRecorder API\创建MediaR…

Springboot 结合 MQTT、Redis ,对接硬件以及做消息分发,最佳实践

Springboot 结合 mqtt、redis对接硬件以及做消息分发&#xff0c;最佳实践 一&#xff0c;认识 需要了解EMQX 基本知识原理&#xff0c;不了解的可以查看我之间的博客&#xff0c;以及网上的资料&#xff0c;这里不在过多撰述。 二&#xff0c;开发思路 这里以对接雷达水位计…

【最优化理论】03-无约束优化

无约束优化无约束优化问题无约束优化问题的应用无约束优化问题的最优性条件无约束-凸函数-最优性条件&#xff08;充要&#xff09;无约束-一般函数-最优性条件必要条件一阶必要条件&#xff1a;梯度为0二阶必要条件&#xff1a;hessian矩阵半正定充分条件二阶充分条件&#xf…