当前位置: 首页 > news >正文

《etcd库——键值存储系统》 - 教程

在使用etcd之前肯定要了解它是什么,为什么要选择使用它?

什么是 etcd?

Etcd 是一个 golang 编写的分布式、高可用的一致性键值存储系统,用于配置共享和服务发现等。它使用 Raft 一致性算法来保持集群数据的一致性,且客户端通过长连接watch 功能,能够及时收到数据变化通知,相较于 Zookeeper 框架更加轻量化。
它主要用于:

  • 存储关键配置信息
  • 服务发现
  • 分布式锁
  • 集群协调

etcd 就像是分布式系统的 “共享内存”让多个节点能够安全可靠地共享状态信息。

通俗理解 ——etcd 就像一个可以跨进程、跨机器、跨网络共享的 “超级 map 容器”

什么场景使用它?

适合用 etcd 的场景概括为什么用 etcd
Kubernetes 集群容器集群的 “大脑”存集群状态,保证所有组件数据一致
配置中心多服务共享的配置改一次全同步,不用重启服务
服务发现找可用的服务地址服务上下线自动更新,不用手动改配置
分布式锁多节点抢资源时排队保证同一时间只有一个节点操作
全局计数器跨机器统计数字(如总订单数)多人同时操作也不会算错

安装 etcd

首先,需要在你的系统中安装 Etcd。Etcd 是一个分布式键值存储,通常用于服务发现
和配置管理。以下是在 Linux 系统上安装 Etcd 的基本步骤:

安装 Etcd:

sudo apt-get install etcd

启动 Etcd 服务:

sudo systemctl start etcd

设置 Etcd 开机自启:

sudo systemctl enable etcd

运行验证

etcdctl put mykey "this is awesome"

如果出现报错:

No help topic for 'put'

则 sudo vi /etc/profile 在末尾声明环境变量 ETCDCTL_API=3 以确定 etcd 版本。

export ETCDCTL_API=3

完毕后,加载配置文件,并重新执行测试指令

ubuntu@VM-16-12-ubuntu:~/code$ source /etc/profile
ubuntu@VM-16-12-ubuntu:~/code$ etcdctl put mykey "this is awesome"
OK
ubuntu@VM-16-12-ubuntu:~/code$ etcdctl get mykey
mykey
this is awesome
ubuntu@VM-16-12-ubuntu:~/code$ etcdctl del mykey

搭建服务注册发现中心

使用 Etcd 作为服务注册发现中心,你需要定义服务的注册和发现逻辑。这通常涉及到
以下几个操作:

  1. 服务注册:服务启动时,向 Etcd 注册自己的地址和端口。
  2. 服务发现:客户端通过 Etcd 获取服务的地址和端口,用于远程调用。
  3. 健康检查:服务定期向 Etcd 发送心跳,以维持其注册信息的有效性。

etcd 采用 golang 编写,v3 版本通信采用 grpc API,即(HTTP2+protobuf);官方只维护了 go 语言版本的
client 库, 因此需要找到 C/C++ 非官方的 client 开发库:etcd-cpp-apiv3

etcd-cpp-apiv3 是一个 etcd 的 C++版本客户端 API。它依赖于 mipsasm, boost, protobuf, gRPC, cpprestsdk 等库。
etcd-cpp-apiv3 的 GitHub 地址是:https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3
依赖安装:

sudo apt-get install libboost-all-dev libssl-dev
sudo apt-get install libprotobuf-dev protobuf-compiler-grpc
sudo apt-get install libgrpc-dev libgrpc++-dev
sudo apt-get install libcpprest-dev

api 框架安装

git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
cd etcd-cpp-apiv3
mkdir build &&
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc) &&
sudo make install

etcd操作命令

基本键值操作

# 写入键值对
etcdctl put /message "Hello etcd"
# 读取键值
etcdctl get /message
# 读取键的详细信息(包括版本等元数据)
etcdctl get /message -w json
# 读取前缀匹配的所有键
etcdctl get / --prefix
# 删除键
etcdctl del /message
# 删除前缀匹配的所有键
etcdctl del / --prefix

监听键变化

etcd 的 Watch 机制可以实时监听键的变化:

# 监听单个键
etcdctl watch /config/timeout
# 在另一个终端执行,触发上面的监听
etcdctl put /config/timeout 30s
# 监听前缀
etcdctl watch /config --prefix

租约与临时键

租约 (Lease) 机制可以创建自动过期的临时键,非常适合服务注册等场景:

# 创建一个30秒的租约
LEASE_ID=$(etcdctl lease grant 30 | awk '{print $2}')
echo "Lease ID: $LEASE_ID"
# 创建绑定到租约的键
etcdctl put /services/api-server "192.168.1.100:8080" --lease=$LEASE_ID
# 续租(延长租约有效期)
etcdctl lease keep-alive $LEASE_ID
# 撤销租约(立即删除所有绑定的键)
etcdctl lease revoke $LEASE_ID

事务操作

etcd 支持条件事务,实现 Compare-And-Swap 等原子操作:

# 事务:如果/message的值是"Hello etcd",则更新为"Hello world",否则不做操作
etcdctl txn --interactive
compares:
value("/message") = "Hello etcd"
success requests (get, put, del):
put("/message", "Hello world")
failure requests:
get("/message")

etcd 简单使用流程

客户端类与接口预览

C++
//pplx::task 并行库异步结果对象
//阻塞方式 get(): 阻塞直到任务执行完成,并获取任务结果
//非阻塞方式 wait(): 等待任务到达终止状态,然后返回任务状态
namespace etcd {
class Value
{
bool is_dir()//判断是否是一个目录
std::string const&
key() //键值对的 key 值
std::string const&
as_string()//键值对的 val 值
int64_t lease() //用于创建租约的响应中,返回租约 ID
}
//etcd 会监控所管理的数据的变化,一旦数据产生变化会通知客户端
//在通知客户端的时候,会返回改变前的数据和改变后的数据
class Event
{
enum class EventType
{
PUT, //键值对新增或数据发生改变
DELETE_,//键值对被删除
INVALID,
};
enum EventType event_type()
const Value&
kv()
const Value&
prev_kv()
}
class Response
{
bool is_ok()
std::string const&
error_message()
Value const&
value()//当前的数值 或者 一个请求的处理结果
Value const&
prev_value()//之前的数值
Value const&
value(int index)//
std::vector<Event>const&events();//触发的事件}class KeepAlive{KeepAlive(Client const& client, int ttl, int64_t lease_id =0);//返回租约 IDint64_t Lease();//停止保活动作void Cancel();}class Client{// etcd_url: "http://127.0.0.1:2379"Client(std::string const& etcd_url,std::string const& load_balancer = "round_robin");//Put a new key-value pair 新增一个键值对pplx::task<Response>put(std::string const& key,std::string const& value);//新增带有租约的键值对 (一定时间后,如果没有续租,数据自动删除)pplx::task<Response>put(std::string const& key,std::string const& value,const int64_t leaseId);//获取一个指定 key 目录下的数据列表pplx::task<Response>ls(std::string const& key);//创建并获取一个存活 ttl 时间的租约pplx::task<Response>leasegrant(int ttl);//获取一个租约保活对象,其参数 ttl 表示租约有效时间pplx::task<std::shared_ptr<KeepAlive>>leasekeepalive(intttl);//撤销一个指定的租约pplx::task<Response>leaserevoke(int64_t lease_id);//数据锁pplx::task<Response>lock(std::string const& key);}class Watcher{Watcher(Client const& client,std::string const& key, //要监控的键值对 keystd::function<void(Response)> callback, //发生改变后的回调bool recursive = false);//是否递归监控目录下的所有数据改变Watcher(std::string const& address,std::string const& key,std::function<void(Response)> callback,bool recursive = false);//阻塞等待,直到监控任务被停止bool Wait();bool Cancel();}

etcd 作为分布式键值存储,核心常用场景是 “数据写入(注册)” 与 “数据读取 + 变化监听(发现)”。下面结合 put.cc(写入 / 注册)和 get.cc(读取 / 监听)两份代码,一步步拆解 etcd 的简单使用流程,从环境准备到功能验证全覆盖。

put.cc :

  • 定义 etcd 地址 (http://127.0.0.1:2379)
  • 创建客户端对象 Client
  • 创建租约并保活 client.leasekeepalive(3).get();
  • 写入键值对(两种方式,有租约/没有租约) client.put
#include <etcd/Client.hpp>#include <etcd/KeepAlive.hpp>#include <etcd/Response.hpp>#include <thread>int main(int argc, char *argv[]){std::string etcd_host = "http://127.0.0.1:2379";//实例化客户端对象etcd::Client client(etcd_host);//获取租约保活对象--伴随着创建一个指定有效时长的租约auto keep_alive = client.leasekeepalive(3).get();//获取租约IDauto lease_id = keep_alive->Lease();//向etcd新增数据//1.有租约的数据auto resp1 = client.put("/service/user", "127.0.0.1:8080", lease_id).get();if (resp1.is_ok() == false) {std::cout <<"新增数据失败:" << resp1.error_message() << std::endl;return -1;}//2.没有租约的数据auto resp2 = client.put("/service/friend", "127.0.0.1:9090").get();if (resp2.is_ok() == false) {std::cout <<"新增数据失败:" << resp2.error_message() << std::endl;return -1;}std::this_thread::sleep_for(std::chrono::seconds(10));return 0;}

get.cc :

  • 定义回调函数 callback (处理监控到的键值对事件)
  • 连接 etcd + 读数据 ( /service 路径下的所有键值对信息)
  • 启动监听 (watcher)
  • 调用 watcher.Wait() 使程序保持运行以持续监控。
#include <etcd/Client.hpp>#include <etcd/KeepAlive.hpp>#include <etcd/Response.hpp>#include <etcd/Watcher.hpp>#include <etcd/Value.hpp>#include <thread>void callback(const etcd::Response &resp) {if (resp.is_ok() == false) {std::cout <<"收到一个错误的事件通知:" << resp.error_message() << std::endl;return;}for (auto const& ev : resp.events()) {if (ev.event_type() == etcd::Event::EventType::PUT) {std::cout <<"服务信息发生了改变:\n" ;std::cout <<"当前的值:" << ev.kv().key() <<"-" << ev.kv().as_string() << std::endl;std::cout <<"原来的值:" << ev.prev_kv().key() <<"-" << ev.prev_kv().as_string() << std::endl;}else if (ev.event_type() == etcd::Event::EventType::DELETE_) {std::cout <<"服务信息下线被删除:\n";std::cout <<"当前的值:" << ev.kv().key() <<"-" << ev.kv().as_string() << std::endl;std::cout <<"原来的值:" << ev.prev_kv().key() <<"-" << ev.prev_kv().as_string() << std::endl;}}}int main(int argc, char *argv[]){std::string etcd_host = "http://127.0.0.1:2379";//实例化客户端对象etcd::Client client(etcd_host);//获取指定的键值对信息auto resp = client.ls("/service").get();if (resp.is_ok() == false) {std::cout <<"获取键值对数据失败: " << resp.error_message() << std::endl;return -1;}int sz = resp.keys().size();for (int i = 0; i < sz;++i) {std::cout << resp.value(i).as_string() <<"可以提供" << resp.key(i) <<"服务\n";}//实例化一个键值对事件监控对象auto watcher = etcd::Watcher(client, "/service", callback, true);watcher.Wait();return 0;}

etcd的头文件

头文件核心作用包含的主要类 / 接口
<etcd/Client.hpp>etcd 客户端核心类,所有与 etcd 的交互入口etcd::Client:客户端主类,提供键值操作(put/get/del)、租约管理(leasegrant)、事务(txn)等所有核心接口,通过客户端对象连接 etcd 服务,发起各种请求
<etcd/KeepAlive.hpp>租约保活管理,处理租约的自动续期etcd::KeepAlive:租约保活类,通过 leasekeepalive() 创建,负责定期向 etcd 发送续期请求,维持租约有效,提供 Lease() 获取租约 ID、Cancel() 停止保活等接口
<etcd/Response.hpp>请求响应处理,封装 etcd 服务器的返回结果etcd::Response:响应类,包含请求的执行结果(成功 / 失败)、返回数据(键值对、事件、租约信息等),提供 is_ok()(判断成功)、error_message()(错误信息)、kv()(键值数据)等方法解析响应
<etcd/Watcher.hpp>数据变化监听,实时捕获键的新增 / 删除 / 修改etcd::Watcher:监听器类,通过指定键或前缀创建监听,当数据变化时触发回调函数
<etcd/Value.hpp>键值数据封装,存储单个键值对的详细信息etcd::Value(或 etcd::KeyValue):键值对类,包含键(key())、值(as_string())、版本(version())、租约 ID(lease())等元数据,用于从 Response 中提取具体的键值信息

etcd地址

http://127.0.0.1:2379 是 etcd 服务的默认本地地址
实际场景中地址会如何变化?

std::string etcd_host = "http://node1:2379,http://node2:2379,http://node3:2379";

这样即使某个节点故障,客户端仍能连接到其他节点。

  • 自定义端口:如果启动时通过 --listen-client-urls 参数修改了端口(如改为 2380),地址会变为 http://xxx.xxx.xxx.xxx:2380。

注意:这里的地etcd地址属于虚地址,实际存储在 etcd 集群的磁盘上

租约

etcd 租约(Lease)是一种带有效期的资源管理机制。

核心作用是为 etcd 中的键值对(Key-Value)绑定 “生命周期”—— 租约到期或失效时,所有绑定它的键值对会被自动删除,无需手动清理。

它就像给数据加了 “定时自动删除” 功能,尤其适合解决分布式场景下的 “僵尸数据” 问题(如服务下线后残留的注册信息)。

租约的核心概念

makefile:

all : put get
put : put.cc
g++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest
get : get.cc
g++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest
.PHONY:clean
clean:
rm -f all

在这里插入图片描述
在这里插入图片描述

基于 etcd 的服务注册与发现模块

通过封装 etcd 的 C++ 客户端接口,实现了分布式系统中常见的服务注册与发现功能:

服务注册:将服务信息(键值对)注册到 etcd,并通过租约(lease)机制维持服务活性。
服务发现:从 etcd 中获取指定目录下的服务信息,并监控该目录的变化(新增 / 删除服务),通过回调函数通知上层处理。

#pragma once
#include <etcd/Client.hpp>#include <etcd/KeepAlive.hpp>#include <etcd/Response.hpp>#include <etcd/Watcher.hpp>#include <etcd/Value.hpp>#include <functional>#include "logger.hpp" // 自定义的log日志类namespace bite_im{//服务注册客户端类class Registry{public:using ptr = std::shared_ptr<Registry>;Registry(const std::string &host):_client(std::make_shared<etcd::Client>(host)) ,_keep_alive(_client->leasekeepalive(3).get()),_lease_id(_keep_alive->Lease()){}~Registry() { _keep_alive->Cancel();}bool registry(const std::string &key, const std::string &val) {auto resp = _client->put(key, val, _lease_id).get();if (resp.is_ok() == false) {LOG_ERROR("注册数据失败:{}", resp.error_message());return false;}return true;}private:std::shared_ptr<etcd::Client> _client;std::shared_ptr<etcd::KeepAlive> _keep_alive;uint64_t _lease_id;};//服务发现客户端类class Discovery{public:using ptr = std::shared_ptr<Discovery>;using NotifyCallback = std::function<void(std::string, std::string)>;Discovery(const std::string &host,const std::string &basedir,const NotifyCallback &put_cb,const NotifyCallback &del_cb):_client(std::make_shared<etcd::Client>(host)) ,_put_cb(put_cb), _del_cb(del_cb){//先进行服务发现,先获取到当前已有的数据auto resp = _client->ls(basedir).get();if (resp.is_ok() == false) {LOG_ERROR("获取服务信息数据失败:{}", resp.error_message());}int sz = resp.keys().size();for (int i = 0; i < sz;++i) {if (_put_cb) _put_cb(resp.key(i), resp.value(i).as_string());}//然后进行事件监控,监控数据发生的改变并调用回调进行处理_watcher = std::make_shared<etcd::Watcher>(*_client.get(), basedir,std::bind(&Discovery::callback, this, std::placeholders::_1), true);}~Discovery() {_watcher->Cancel();}private:void callback(const etcd::Response &resp) {if (resp.is_ok() == false) {LOG_ERROR("收到一个错误的事件通知: {}", resp.error_message());return;}for (auto const& ev : resp.events()) {if (ev.event_type() == etcd::Event::EventType::PUT) {if (_put_cb) _put_cb(ev.kv().key(), ev.kv().as_string());LOG_DEBUG("新增服务:{}-{}", ev.kv().key(), ev.kv().as_string());}else if (ev.event_type() == etcd::Event::EventType::DELETE_) {if (_del_cb) _del_cb(ev.prev_kv().key(), ev.prev_kv().as_string());LOG_DEBUG("下线服务:{}-{}", ev.prev_kv().key(), ev.prev_kv().as_string());}}}private:NotifyCallback _put_cb;NotifyCallback _del_cb;std::shared_ptr<etcd::Client> _client;std::shared_ptr<etcd::Watcher> _watcher;};}

关键类与成员解析

1. Registry 类(服务注册)

class Registry
{
public:
using ptr = std::shared_ptr<Registry>;Registry(const std::string &host):_client(std::make_shared<etcd::Client>(host)) ,_keep_alive(_client->leasekeepalive(3).get()),_lease_id(_keep_alive->Lease()){}~Registry() { _keep_alive->Cancel();}bool registry(const std::string &key, const std::string &val) {auto resp = _client->put(key, val, _lease_id).get();if (resp.is_ok() == false) {LOG_ERROR("注册数据失败:{}", resp.error_message());return false;}return true;}private:std::shared_ptr<etcd::Client> _client;std::shared_ptr<etcd::KeepAlive> _keep_alive;uint64_t _lease_id;};

功能:向 etcd 注册服务,并通过租约机制自动维持服务心跳,确保服务下线时自动注销。
核心成员:

  • _client:etcd 客户端实例,用于与 etcd 服务器通信。
  • _keep_alive:租约保活器,维持租约的有效性(定时向 etcd 发送心跳)。
  • _lease_id:租约 ID,绑定服务注册的键值对,租约过期后键值对自动删除。

主要方法:

  • 构造函数:初始化 etcd 客户端,创建一个 3 秒过期的租约,并启动保活机制。
  • registry 方法:将键值对(服务信息)注册到 etcd,关联当前租约 ID。
  • 析构函数:取消租约保活,使服务信息在租约过期后自动删除。

2. Discovery 类(服务发现)

//服务发现客户端类
class Discovery
{
public:
using ptr = std::shared_ptr<Discovery>;using NotifyCallback = std::function<void(std::string, std::string)>;Discovery(const std::string &host,const std::string &basedir,const NotifyCallback &put_cb,const NotifyCallback &del_cb):_client(std::make_shared<etcd::Client>(host)) ,_put_cb(put_cb), _del_cb(del_cb){//先进行服务发现,先获取到当前已有的数据auto resp = _client->ls(basedir).get();if (resp.is_ok() == false) {LOG_ERROR("获取服务信息数据失败:{}", resp.error_message());}int sz = resp.keys().size();for (int i = 0; i < sz;++i) {if (_put_cb) _put_cb(resp.key(i), resp.value(i).as_string());}//然后进行事件监控,监控数据发生的改变并调用回调进行处理_watcher = std::make_shared<etcd::Watcher>(*_client.get(), basedir,std::bind(&Discovery::callback, this, std::placeholders::_1), true);}~Discovery() {_watcher->Cancel();}private:void callback(const etcd::Response &resp) {if (resp.is_ok() == false) {LOG_ERROR("收到一个错误的事件通知: {}", resp.error_message());return;}for (auto const& ev : resp.events()) {if (ev.event_type() == etcd::Event::EventType::PUT) {if (_put_cb) _put_cb(ev.kv().key(), ev.kv().as_string());LOG_DEBUG("新增服务:{}-{}", ev.kv().key(), ev.kv().as_string());}else if (ev.event_type() == etcd::Event::EventType::DELETE_) {if (_del_cb) _del_cb(ev.prev_kv().key(), ev.prev_kv().as_string());LOG_DEBUG("下线服务:{}-{}", ev.prev_kv().key(), ev.prev_kv().as_string());}}}private:NotifyCallback _put_cb;NotifyCallback _del_cb;std::shared_ptr<etcd::Client> _client;std::shared_ptr<etcd::Watcher> _watcher;};

功能:从 etcd 中发现指定目录下的服务信息,并实时监控该目录的变化(新增 / 删除服务)。
核心成员:

  • _client:etcd 客户端实例。
  • _watcher:监听器,监控指定目录的键值对变化。
  • _put_cb/_del_cb:回调函数,分别在服务新增(PUT 事件)和服务下线(DELETE 事件)时触发。

主要方法:

  • 构造函数:初始化客户端,先获取指定目录下已有的所有服务信息并通过 - _put_cb 回调通知;然后启动监听器,监控后续变化。
  • callback 方法:内部事件处理函数,解析 etcd 推送的事件(PUT/DELETE),并调用对应的回调函数。
  • 析构函数:取消监听器,停止监控。

3. 测试

registry.cc

#include "etcd.hpp"
#include <gflags/gflags.h>int main(int argc, char *argv[]){google::ParseCommandLineFlags(&argc, &argv, true);init_logger(false, " ", 0);Registry::ptr rclient = std::make_shared<Registry>("http://127.0.0.1:2379");rclient->registry("/service/user/instance", "127.0.0.1:8080");std::this_thread::sleep_for(std::chrono::seconds(600));return 0;}

discovery.cc

#include "etcd.hpp"
#include <gflags/gflags.h>void online(const std::string &service_name, const std::string &service_host) {LOG_DEBUG("上线服务: {}-{}", service_name, service_host);}void offline(const std::string &service_name, const std::string &service_host) {LOG_DEBUG("上线服务: {}-{}", service_name, service_host);}int main(int argc, char *argv[]){google::ParseCommandLineFlags(&argc, &argv, true);init_logger(false, " ", 0);Discovery::ptr rclient = std::make_shared<Discovery>("http://127.0.0.1:2379", "/service", online, offline);std::this_thread::sleep_for(std::chrono::seconds(600));return 0;}

makefile:

all : put get
put : registry.cc
g++ -std=c++17 $^ -o $@ -lspdlog -lfmt -lgflags -letcd-cpp-api -lcpprest
get : discovery.cc
g++ -std=c++17 $^ -o $@ -lspdlog -lfmt -lgflags -letcd-cpp-api -lcpprest
.PHONY:clean
clean:
rm -f put get

在这里插入图片描述

9090是上次测试遗留的

http://www.hskmm.com/?act=detail&tid=18314

相关文章:

  • 有一个函数只会返回0和1,且返回0和返回1的概率不等。要求只能通过这个函数生成一个等概率返回0和1的函数
  • AI智能体开发实战:17种核心架构模式详解与Python代码实现
  • 代码随想录算法训练营第十天 | 232. 用栈实现队列、225. 用队列实现栈、20. 有效的括号、删除字符串中的所有相邻重复项
  • 2025.9.26总结 - A
  • MySQL性能优化
  • 关于“悬荡悟空”决策机制的简要技术说明
  • 最小二乘问题详解1:线性最小二乘
  • 9月26日
  • 工程监理行业多模态视觉​​​​​​​大模型系统,打造工地行业全场景的监理智能生态
  • 完整教程:【鸿蒙心迹】摸蓝图,打地基
  • 正则表达式
  • LuatOS Air780EPM 实现 HTTP 通信:从原理到代码实践
  • 搜维尔科技:Senseglove Nova 2触觉手套:虚拟训练、VR/AR模拟和研究中的触觉反馈
  • 深入解析:盟接之桥EDI软件:中国制造全球化进程中的连接挑战与路径探索
  • 【STM32H7】基于CubeMX从零开始搭建的HAL库工程模板(包含串口重定向和DSP库)
  • 在Windows架构中安装Miniforge及python环境变量配置
  • 搜维尔科技:Force Dimension Omega力反馈设备遥操作工业机器人
  • 3. Ollama 安装,流式输出,多模态,思考模型 - Rainbow
  • C++程序练习(部分未完全完成)
  • C#性能优化基础:垃圾回收机制
  • 实验报告1
  • 2025.9.26——1蓝
  • 根号
  • 【A】杂题选将
  • 有一个[1,5]的等概率随机函数fx(),在不改变fx()函数的情况下,利用fx()函数做出一个[1,7]的等概率随机函数。
  • WSL2 磁盘清理
  • 质因数分解
  • 关于OneBot的QQ机器人探索2
  • putty
  • 深入解析:PHP 8.0+ 高级特性深度探索:架构设计与性能优化