PHP swoole代码提示,类型包:https://github.com/swoole/ide-helper
匿名函数 匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数 callable参数的值。
匿名函数目前是通过 Closure 类来实现的。
use <?php \(message = 'hello'; /* 继承 \)message */ \(example = function () use (\)message) {
var_dump($message);
};
继承之后的参数,是按值传递的,对它的修改是不影响原变量的,如果需要,可以通过引用传递参数,或者在函数代码块内使用 global声明全局变量进行使用。
在类的方法中使用匿名函数,5.4以上的版本无需使用use引入this , 直接可以在匿名函数中使用this,直接可以在匿名函数中使用this,直接可以在匿名函数中使用this来调用当前对象的方法。在swoole编程中,可以利用此特性减少$serv对象的use引入传递。
如果希望在闭包函数中修改外部变量,可以在use时为变量增加&引用符号即可。注意对象类型不需要加&,因为在PHP中对象默认就是传引用而非传值。
普通函数不能使用use,子函数获取父函数的变量,只能通过匿名函数实现,use只能传递所在作用域的变量;
\(sortFun = function (\)a, \(b) use (\)key) {}
PHP对象可以直接通过指定一个属性进行赋值来给对象创建一个新属性。
Swoole基础 必须每个进程单独创建 Redis、MySQL、PDO 连接,其他的存储客户端同样也是如此。原因是如果共用 1 个连接,那么返回的结果无法保证被哪个进程处理,持有连接的进程理论上都可以对这个连接进行读写,这样数据就发生错乱了。
在 Swoole 内,无法 通过 \(_GET/\)_POST/\(_REQUEST/\)_SESSION/\(_COOKIE/\)_SERVER 等 $_开头的变量获取到任何属性参数。
1.swoole Swoole的进程不同于平常的PHP脚本,它是常驻内存的。这意味着程序是一直运行,变量也可以一直存在。例如Swoole提供的异步Websocket服务器。
2.swoole_server中对象的4层生命周期
程序全局期 进程全局期 会话期 请求期
2.1 程序全局期 在swoole_server->start之前就创建好的对象,我们称之为程序全局生命周期。这些变量在程序启动后就会一直存在,直到整个程序结束运行才会销毁。
变量在Worker进程内对这些对象进行写操作时,会自动从共享内存中分离,变为进程全局对象。进程操作的对象是原对象的拷贝,对该对象的操作不影响原对象;
2.2 进程全局期 swoole拥有进程生命周期控制的机制,一个Worker子进程处理的请求数超过max_request配置后,就会自动销毁。Worker进程启动后创建的对象(onWorkerStart中创建的对象),在这个子进程存活周期之内,是常驻内存的。onConnect/onReceive/onClose 中都可以去访问它。
2.3 会话期 onConnect到onClose是一次TCP的会话周期,http keep-alive时,一个连接可能会有多个request。 http是无状态的,一个用户可能也不止一个连接,可以通过创建一个session来关联同一个用户的不同请求。
2.4 请求期 请求期就是指一个完整的请求发来,也就是onReceive收到请求开始处理,直到返回结果发送response。这个周期所创建的对象,会在请求完成后销毁。
swoole中请求期对象与普通PHP程序中的对象就是一样的。请求到来时创建,请求结束后销毁。
3.进程隔离 原因就是全局变量在不同的进程,内存空间是隔离的,所以修改全局变量的值是无效的。
所以使用 Swoole 开发 Server 程序需要了解进程隔离问题,Swoole/Server 程序的不同 Worker 进程之间是隔离的,在编程时操作全局变量、定时器、事件监听,仅在当前进程内有效。
不同的进程中 PHP 变量不是共享,即使是全局变量,在 A 进程内修改了它的值,在 B 进程内是无效的
如果需要在不同的 Worker 进程内共享数据,可以用 Redis、MySQL、文件、Swoole/Table、APCu、shmget 等工具实现
不同进程的文件句柄是隔离的,所以在 A 进程创建的 Socket 连接或打开的文件,在 B 进程内是无效,即使是将它的 fd 发送到 B 进程也是不可用的
4.start()干了些什么
start()运行之后会创建Master 进程 +Manager 进程 +serv->worker_num 个 Worker 进程。 启动失败会立即返回 false,启动成功后将进入事件循环,等待客户端连接请求。start 方法之后的代码不会执行。 服务器关闭后,start 函数返回 true,并继续向下执行 设置了 task_worker_num 会增加相应数量的 Task 进程 方法列表中 start 之前的方法仅可在 start 调用前使用,在 start 之后的方法仅可在 onWorkerStart、onReceive 等事件回调函数中使用
5.运行时进程
Master 主进程,主进程内有多个 Reactor 线程,基于 epoll/kqueue 进行网络事件轮询。收到数据后转发到 Worker 进程去处理; Manager 进程,对所有 Worker 进程进行管理,Worker 进程生命周期结束或者发生异常时自动回收,并创建新的 Worker 进程; Worker 进程,对收到的数据进行处理,包括协议解析和响应请求; Reactor 线程,是在 Master 进程中创建的线程,负责维护客户端 TCP 连接、处理网络 IO、处理协议、收发数据,不执行任何 PHP 代码,将 TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包; Task 进程以及Task Worker进程,是独立于worker进程当中的一个工作进程,用于处理一些耗时较长的逻辑,这些逻辑如果在task 进程当中处理时并不会影响worker 进程处理来自客户端的请求,由此大大提高了swoole处理并发的能力
假设 Server 就是一个工厂,那 Reactor 就是销售,接受客户订单。而 Worker 就是工人,当销售接到订单后,Worker 去工作生产出客户要的东西。而 TaskWorker 可以理解为行政人员,可以帮助 Worker 干些杂事,让 Worker 专心工作。
6.Server 的两种运行模式 SWOOLE_PROCESS 模式的 Server 所有客户端的 TCP 连接都是和主进程建立的,内部实现比较复杂,用了大量的进程间通信、进程管理机制。适合业务逻辑非常复杂的场景。Swoole 提供了完善的进程管理、内存保护机制。 在业务逻辑非常复杂的情况下,也可以长期稳定运行。
SWOOLE_BASE 这种模式就是传统的异步非阻塞 Server。与 Nginx 和 Node.js 等程序是完全一致的。worker_num 参数对于 BASE 模式仍然有效,会启动多个 Worker 进程。当有 TCP 连接请求进来的时候,所有的 Worker 进程去争抢这一个连接,并最终会有一个 worker 进程成功直接和客户端建立 TCP 连接,之后这个连接的所有数据收发直接和这个 worker 通讯,不经过主进程的 Reactor 线程转发。
webscoket 1.创建websocket服务器(异步) \Swoole\Websocket\Server::__construct(string \(host = '0.0.0.0', int \)port = 0, int \(mode = SWOOLE_PROCESS, int \)sockType = SWOOLE_SOCK_TCP): SwooleServer
<?php /创建websocket服务器对象,监听0.0.0.0:9501端口,开启SSL隧道/ \(ws = new swoole_websocket_server("0.0.0.0", 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL); /*配置参数*/ \)ws->set([
'max_conn'=>1000, /*最多连接数量。*/
'task_worker_num' => 2,/*TaskWorker 进程数量。*/
'daemonize' => false, /*守护进程化。*/
/*配置SSL证书和密钥路径*/
'ssl_cert_file' => "/etc/nginx/cert/socket.yuhal.com.pem",
'ssl_key_file' => "/etc/nginx/cert/socket.yuhal.com.key"
]); /监听WebSocket连接打开事件/ \(ws->on('open', function (\)ws, $request) {
echo "client-{$request->fd} is openn";
}); /监听WebSocket消息事件/ \(ws->on('message', function (\)ws, $frame) {
echo "Message: {$frame->data}n";
$ws->push($frame->fd, "server: {$frame->data}");
}); /监听WebSocket连接关闭事件/ \(ws->on('close', function (\)ws, $fd) {
echo "client-{$fd} is closedn";
}); $ws->start();
2.swoole类短名介绍 3.task_worker_num 配置此参数后将会启用 task 功能。所以 Server 务必要注册 onTask、onFinish 2 个事件回调函数。如果没有注册,服务器程序将无法启动。
4.swoole的wss配置了一晚上,怎么都不行,还是Nginx好 location /websocket {
proxy_pass http://s.nicen.cn:5703;
proxy_http_version 1.1;
proxy_read_timeout 3600s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
使用腾讯云CDN时,进行websokect反向代理时,由于cdn链接最多保持10s,将会导致websokect中断。
5.事件执行顺序
所有事件回调均在 $server->start 后发生 服务器关闭程序终止时最后一次事件是 onShutdown 服务器启动成功后,onStart/onManagerStart/onWorkerStart 会在不同的进程内并发执行 onReceive/onConnect/onClose 在 Worker 进程中触发 Worker/Task 进程启动 / 结束时会分别调用一次 onWorkerStart/onWorkerStop onTask 事件仅在 task 进程中发生 onFinish 事件仅在 worker 进程中发生 onStart/onManagerStart/onWorkerStart 3 个事件的执行顺序是不确定的
5.其他 协程入门 一键协程化:
// 协程生效的范围 Co::set([‘hook_flags’=> SWOOLE_HOOK_ALL]); // v4.4+版本使用此方法。 // 或 \Swoole\Runtime::enableCoroutine($flags = SWOOLE_HOOK_ALL); 1.什么是协程 协程就是《操作系统原理》所说的用户态线程,协程内的代码被阻塞时会自动切换运行其他协程。
与常说的线程相比,协程在用户态,调度由程序自身完成,线程在系统态,调度由操作系统完成。
2.使用须知 enable_coroutine 3.协程HTTP服务端
对连接的处理是在单独的子协程中完成,客户端连接的 Connect、Request、Response、Close 是完全串行的。 监听的地址若是本地 UNIXSocket 则应以形如 unix://tmp/your_file.sock 的格式填写 。
3.1 websocket处理流程
\(ws->upgrade():向客户端发送 WebSocket 握手消息 while(true) 循环处理消息的接收和发送 \)ws->recv() 接收 WebSocket 消息帧 \(ws->push() 向对端发送数据帧 \)ws->close() 关闭连接
4.协程设置 <?php \Swoole\Coroutine::set(array $options);
5.退出协程 5.1 defer defer 用于资源的释放,会在协程关闭之前 (即协程函数执行完毕时) 进行调用,就算抛出了异常,已注册的 defer 也会被执行。
需要注意的是,它的调用顺序是逆序的(先进后出), 也就是先注册 defer 的后执行,先进后出。逆序符合资源释放的正确逻辑,后申请的资源可能是基于先申请的资源的,如先释放先申请的资源,后申请的资源可能就难以释放。
5.2 主动退出 在 Swoole 低版本中,协程中使用 exit 强行退出脚本会导致内存错误导致不可预期的结果或 coredump,在 Swoole 服务中使用 exit 会使整个服务进程退出且内部的协程全部异常终止导致严重问题,Swoole 长期以来一直禁止开发者使用 exit,但开发者可以使用抛出异常这种非常规的方式,在顶层 catch 来实现和 exit 相同的退出逻辑。
Swoole v4.1.0 版本及以上直接支持了在协程、服务事件循环中使用 PHP 的 exit,此时底层会自动抛出一个可捕获的 SwooleExitException,开发者可以在需要的位置捕获并实现与原生 PHP 一样的退出逻辑。
5.3 cancel() 可以用于取消某个协程,但不能对当前协程发起取消操作。协程被取消后触发defer回调,然后运行结束。
目前基本支持了绝大部分的协程 API 的取消,包括:
socket AsyncIO (fread, gethostbyname …) sleep waitSignal wait/waitpid waitEvent Co::suspend/Co::yield channel native curl (SWOOLE_HOOK_NATIVE_CURL)
有两个不可中断的场景
被 CPU 中断调度器强制切换的协程 文件锁操作期间
6.协程化API 7.补充知识
协程没有IO等待 正常执行PHP代码,不会产生执行流程切换 协程遇到IO等待 立即将控制权切,待IO完成后,重新将执行流切回原来协程切出的点 协程并行协程依次执行,同上一个逻辑 协程嵌套执行流程由外向内逐层进入,直到发生IO,然后切到外层协程,父协程不会等待子协程结束 Swoole的协程在底层实现上是单线程的,因此同一时间只有一个协程在工作,协程的执行是串行的。这与线程不同,多个线程会**作系统调度到多个CPU并行执行。 一个协程正在运行时,其他协程会停止工作。当前协程执行阻塞IO操作时会挂起,底层调度器会进入事件循环。当有IO完成事件时,底层调度器恢复事件对应的协程的执行。 对CPU多核的利用,仍然依赖于Swoole引擎的多进程机制。 Swoole遇到需要等待的IO才会切换,比如SQL查询、比如网络请求等待响应、比如读取文件等找到那个文件。 从Mysql获取返回的结果集,这个需要CPU来计算,需要写内存,所以Swoole协程的单线程模型是串行来的,所以对于结果集数据量级过大的查询,效果不明显。 搞明白什么时候会发生协程切换,业务无关的任务可以异步处理 Mysql协程客户端,指定是查询需要等待的时候,会进行协程调度,会运行其它没有阻塞的协程,而没有协程调度的客户端,就会一直等着,不会让出cpu。(所以单个worker进程内,同时处理许多个请求时,不会因为上一个请求卡住下一个),代码上是同步的,底层IO是一直在协程化,异步的。 php use &引用,不存在的变量,会创建一个变量,禁止使用引入是避免争强,防止造成数据不一致性
8.协程 CPU 密集场景调度实现 如果服务场景是IO密集型,非抢占式可以表现的非常完美,但是如果服务中加入了CPU密集型操作,就不得不考虑重新协程的调度模式。
试想有以下场景,程序中有A,B两个协程,协程A一直在执行CPU密集运算,非抢占式的调度模型中,A不会主动让出控制权,从而导致B得不到时间片,协程得不到均衡调度。导致的问题是假如当前服务A,B同时对外提供服务,B协程处理的请求就可能因为得不到时间片导致请求超时
9.协程内存开销 新版本4.0使用了C栈+PHP栈的协程实现方案。Server程序每次请求的事件回调函数中会创建一个新协程,处理完成后协程退出。
在协程创建时需要创建一个全新的内存段作为C和PHP的栈,底层默认分配2M©虚拟内存+8K(PHP)内存(PHP-7.2或更高版本)。这里的虚拟内存是指操作系统并不会立即分配2M物理内存,系统会根据在内存实际读写时发生缺页中断,再分配实际内存。
协程与线程不同,在一个进程内创建的多个协程,实际上是串行的。同一CPU时间,只有一个协程在执行,因此协程不存在数据同步问题。
协程内可以安全的修改全局变量的值,而不考虑锁的问题。
在协程的执行过程中,调用IO操作时,会自动让出控制权。这时其他IO操作完成的协程才会执行。当IO操作完成后,底层会重新切换会当前的协程。
10.多进程数据共享 Swoole4由于是单线程多进程的,底层没有使用任何Mutex锁,不存在锁的争抢。 同样带来的问题是,没有超全局变量。只有进程级全局变量,读写PHP全局变量只在当前进程内有效。如果希望多进程共享数据,有3种解决方案:
使用Table和Atomic对象,或者其他共享内存数据结构 使用IPC进程间通信 借助存储实现数据的共享和中转,如Redis、MySQL或文件操作
11.和Go协程的区别 Go的协程是多线程模型,所以按照调度的规则,分配到的CPU时间片比Swoole的单线程要多。
12.协程客户端 Swoole-cli Swoole-Cli 是一个 PHP 的二进制发行版,集成了 swoole、php 内核、php-cli、php-fpm以及多个常用扩展。
查看拓展列表
swoole-cli -m
1.Cygwin和WSL
Windows Subsystem for Linux(简称WSL)是一个在Windows 1011上能够运行原生Linux二进制可执行文件(ELF格式)的兼容层。 Cygwin是一个在windows平台上运行的类UNIX模拟环境,在Windows中增加了一个中间层——兼容POSIX的模拟层,并在此基础上构建了大量Linux-like的软件工具。
2.配置文件 swoole-cli 默认不加载任何 php.ini 配置文件。可通过 -d 参数来设置 PHP 选项或使用 -c 参数指定加载的php.ini配置文件。
swoole-cli -d swoole.use_shortname=off bin/hyperf.php start swoole-cli -c /tmp/php.ini -v
3.启动 PHP-FPM
查看帮助文件
swoole-cli -P -h
运行 FPM
swoole-cli -P –fpm-config /opt/php-8.1/etc/php-fpm.conf -p /opt/php-8.1/var
关闭守护进程
swoole-cli -P –fpm-config /opt/php-8.1/etc/php-fpm.conf -p /opt/php-8.1/var -F
使用 root 账户启动
swoole-cli -P –fpm-config /opt/php-8.1/etc/php-fpm.conf -p /opt/php-8.1/var -F -R
4.启动 CLI Server
使用 Laravel Artisan 工具
swoole-cli artisan serve
启动 CLI Server
swoole-cli -S 127.0.0.1:9001 Coroutine/Scheduler 所有的协程必须在协程容器里面创建,Swoole 程序启动的时候大部分情况会自动创建协程容器,用 Swoole 启动程序的方式一共有三种:
调用异步风格服务端程序的 start 方法,此种启动方式会在事件回调中创建协程容器,参考 enable_coroutine。 调用 Swoole 提供的 2 个进程管理模块 Process 和 ProcessPool 的 start 方法,此种启动方式会在进程启动的时候创建协程容器,参考这两个模块构造函数的 enable_coroutine 参数。 其他直接裸写协程的方式启动程序,需要先创建一个协程容器 (Coroutinerun() 函数,可以理解为 java、c 的 main 函数)
Swoole连接池 Swoole 从 v4.4.13 版本开始提供了内置协程连接池
Swoole Event 1.EventLoop EventLoop,即事件循环,可以简单的理解为 epoll_wait,会把所有要发生事件的句柄(fd)加入到 epoll_wait 中,这些事件包括可读,可写,出错等。
对应的进程就阻塞在 epoll_wait 这个内核函数上,当发生了事件 (或超时) 后 epoll_wait 这个函数就会结束阻塞返回结果,就可以回调相应的 PHP 函数,例如,收到客户端发来的数据,回调 onReceive 回调函数。
当有大量的 fd 放入到了 epoll_wait 中,并且同时产生了大量的事件,epoll_wait 函数返回的时候就会挨个调用相应的回调函数,叫做一轮事件循环,即 IO 多路复用,然后再次阻塞调用 epoll_wait 进行下一轮事件循环。
2.IPC进程间通信 Unix Socket,全名 UNIX Domain Socket, 简称 UDS, 使用套接字的 API (socket,bind,listen,connect,read,write,close 等),和 TCP/IP 不同的是不需要指定 ip 和 port,而是通过一个文件名来表示 (例如 FPM 和 Nginx 之间的 /tmp/php-fcgi.sock),UDS 是 Linux 内核实现的全内存通信,无任何 IO 消耗。在 1 进程 write,1 进程 read,每次读写 1024 字节数据的测试中,100 万次通信仅需 1.02 秒,而且功能非常的强大,Swoole 下默认用的就是这种 IPC 方式。
3.Swoole异步事件 Swoole多进程 1.Swoole/Process 创建一个子进程,运行指定的回调函数。
\(process = new Process(function () use (\)n) {
echo 'Child #' . getmypid() . " start and sleep {$n}s" . PHP_EOL;
sleep($n);
echo 'Child #' . getmypid() . ' exit' . PHP_EOL;
}); $process->start();
父进程会等待所有子进程运行完毕后才会退出。 可以通过event异步监听进程的pipe描述符,触发异步事件。 SwooleProcess->__construct(callable \(function, bool \)redirect_stdin_stdout = false, int \(pipe_type = SOCK_DGRAM, bool \)enable_coroutine = false)
子进程管道通信:
<?php /**
- @date 2023/6/28
- @author 爱心发电丶 */ use SwooleProcess; class Test { public Process \(process; function __construct() { \)this->process = new Process([\(this, "__start"], false, 2, true); \)this->process->start(); //启动进程 \(this->process->name("SwooleProcess"); } public function __start() { register_shutdown_function(function () { echo "进程退出n"; }); swoole_event_add(\)this->process->pipe, function () { echo \(this->process->read() . "n"; }); } } \)start = new Test(); \Swoole\Coroutine\run(function () use (\(start) { while (true) { \Swoole\Coroutine::sleep(1); \)start->process->write(“66666666”); } });
2.\Swoole\Process\Pool 用来创建进程池,会永远保持指定数量的子进程。
\(pool->on('WorkerStart', function (ProcessPool \)pool, $workerId) { // 子进程程序代码 });
3.多进程内存共享 4.定时器
\Swoole\Timer::tick(),设置一个间隔时钟定时器。 \Swoole\Timer::after(),在指定的时间后执行函数。 \Swoole\Timer::clear(),使用定时器 ID 来删除定时器。 \Co::sleep(),协程休眠指定时间 \Swoole\Event::wait(),启用事件监听
5.addProcess Server->addProcess(),用于添加一个用户自定义的工作进程。不需要执行 start。在 Server 启动时会自动创建进程,并执行指定的子进程函数。
创建的子进程可以调用 \(server 对象提供的各个方法,如 getClientList/getClientInfo/stats。 在 Worker/Task 进程中可以调用 \)process 提供的方法与子进程进行通信。 在用户自定义进程中可以调用 $server->sendMessage 与 Worker/Task 进程通信。 用户进程内不能使用 Server->task/taskwait 接口。 用户进程内可以使用 Server->send/close 等接口。 用户进程内应当进行 while(true)(如下边的示例) 或 EventLoop 循环 (例如创建个定时器),否则用户进程会不停地退出重启。
生命周期
用户进程的生存周期与 Master 和 Manager 是相同的,不会受到 reload 影响。 用户进程不受 reload 指令控制,reload 时不会向用户进程发送任何信息。 在 shutdown 关闭服务器时,会向用户进程发送 SIGTERM 信号,关闭用户进程。 自定义进程会托管到 Manager 进程,如果发生致命错误,Manager 进程会重新创建一个。 自定义进程也不会触发 onWorkerStop 等事件。
不兼容更新记录 1.v4.6.3
新增 \Swoole\Coroutine\go 函数 (swoole/library@82f63be) (@matyhtf) 新增 \Swoole\Coroutine\defer 函数 (swoole/library@92fd0de) (@matyhtf)
2.v4.6.0
将 Event::rshutdown() 标记为已弃用,请改用 Coroutinerun (#3881) (@matyhtf)
问题记录 1.redis订阅 socket has already been bound to another coroutine
redis订阅的消息处理是同步的,处理下一条必须等上一条运行结束。
进程全局期调用redis是通过static进行了缓存,导致之后的进程全部是调用的一个redis连接,导致报错。
2.读写分离 对同一个socket链接的操作要进行完全的读写分离,避免协程对同一个socket同时写发生冲突。
// 并发过高时,push可能不会立即进行。 \(ws->push("连接成功"); \)subscribe(); //开始订阅
3.内存增长 swoole脚本在一定的内存范围内会一直增长,然后保持在某个大小才会稳定
4.window后台运行php脚本 新建一个vbs脚本和bat批处理
set ws = wscript.createobject(“wscript.shell”) ws.run “app.bat /start”,0 msgbox “运行中…” 5.内存泄露排查
pmap,查看指定进程的内存申请状态 smem,内存使用情况报告工具 top,查看进程的资源占用情况
PHP的内存分配算法:小于 3072 字节的内存申请 PHP 会认为是小内存,PHP 会把所有申请的小内存块缓存起来,即使释放了也不归还给操作系统,以保证内存管理的效率
PHP 的 gc_mem_caches() 函数是一个垃圾回收函数,用于释放 PHP 内部缓存所占用的内存。这些缓存包括 opcode 缓存、符号表缓存、类缓存等,它们会在 PHP 运行时占用一定的内存空间。
6.websocket报错 使用 “/” 作为websocket路径时,会出现 fd[8] is not a websocket conncetion 报错,是因为浏览器打开页面时默认会触发一次根目录请求。
- 宝塔swoole使用腾讯oss sdk报错 使用原生 curl hook 的前提是在编译 Swoole 扩展时开启–enable-swoole-curl选项 ,比如支持原生 curl multi
没有启用的话使用腾讯云OSS SDK时会报错,安装pecl:
wget http://pear.php.net/go-pear.phar -O go-pear.php php go-pear.php
然后使用pecl自定义安装swoole
8.defer使用过程中的一些坑
swoole 协程的 Barrier::wait() 不会等待 defer 运行结束,所以在defer还没运行完的时候,可能已经运行后续的代码了。 defer还没运行结束,但是协程资源可能已经自动释放了,导致defer使用的redis连接,可能已经分配给别的协程,导致冲突