官方文档：https://carbon.nesbot.com/docs/安装方法1. 基本安装# 在项目根目录执行 composer require nesbot/carbon2. 指定版本安装# 安装特定版本 composer require nesbot/carbon:^2.0 # 安装开发版本 composer require nesbot/carbon --dev基本使用方法1. 引入 Carbonuse Carbon\Carbon; // 获取当前时间 $now = Carbon::now(); echo $now; // 2023-10-27 14:30:00 // 创建指定时间 $date = Carbon::create(2023, 12, 25, 10, 30, 0);2. 常用方法示例use Carbon\Carbon; // 格式化输出 echo Carbon::now()->format('Y-m-d H:i:s'); echo Carbon::now()->toDateString(); // 2023-10-27 echo Carbon::now()->toDateTimeString(); // 2023-10-27 14:30:00 // 日期操作 $tomorrow = Carbon::now()->addDay(); $yesterday = Carbon::now()->subDay(); $nextWeek = Carbon::now()->addWeek(); // 日期比较 $date1 = Carbon::create(2023, 10, 27); $date2 = Carbon::create(2023, 10, 28); if ($date1->lt($date2)) { // less than echo "date1 在 date2 之前"; } // 时区设置 $date = Carbon::now('Asia/Shanghai'); $date->setTimezone('America/New_York');3. 本地化（中文支持）# 安装语言包 composer require carbonphp/carbon-doctrine-typesuse Carbon\Carbon; // 设置中文 Carbon::setLocale('zh'); echo Carbon::now()->diffForHumans(); // 刚刚 echo Carbon::now()->subMinutes(5)->diffForHumans(); // 5分钟前配置文件（可选）在 Laravel 项目中，可以在 config/app.php 中配置时区：'timezone' => 'Asia/Shanghai', 'locale' => 'zh-CN',验证安装创建测试文件验证安装：<?php require 'vendor/autoload.php'; use Carbon\Carbon; echo "当前时间：" . Carbon::now() . "\n"; echo "明天：" . Carbon::now()->addDay() . "\n"; echo "一周前：" . Carbon::now()->subWeek()->diffForHumans() . "\n";常见问题解决版本兼容性问题# 查看当前PHP版本支持的Carbon版本 php -v # 如果PHP版本较旧，使用Carbon 1.x composer require nesbot/carbon:^1.39内存问题# 清理composer缓存 composer clear-cache代理设置# 如果下载慢，使用国内镜像 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/在框架中的使用LaravelCarbon 已集成在 Laravel 中，可直接使用：// 在Laravel中直接使用 $date = now(); // Carbon实例 echo $date->format('Y-m-d');其他框架// Slim, Symfony等框架 require __DIR__ . '/vendor/autoload.php'; use Carbon\Carbon; $carbon = new Carbon();这样就成功安装并可以使用 Carbon 来处理日期时间了。
PHP8.0 + 版本按数据处理类型分类的新增函数整理表格，包含函数名、对应版本、功能说明及可直接运行的用法示例
分类函数名PHP版本功能说明用法示例字符串处理str_contains()8.0检查字符串是否包含指定子串（区分大小写），返回布尔值$str = "Hello PHP8"; var_dump(str_contains($str, "PHP8")); // bool(true)var_dump(str_contains($str, "php8")); // bool(false)字符串处理str_starts_with()8.0检查字符串是否以指定子串开头（区分大小写），返回布尔值$str = "2025-12-11"; var_dump(str_starts_with($str, "2025")); // bool(true)var_dump(str_starts_with($str, "2024")); // bool(false)字符串处理str_ends_with()8.0检查字符串是否以指定子串结尾（区分大小写），返回布尔值$str = "test.txt"; var_dump(str_ends_with($str, ".txt")); // bool(true)var_dump(str_ends_with($str, ".php")); // bool(false)字符串处理mb_str_split()8.0多字节安全的字符串分割，支持指定分割长度（解决str_split多字节乱码）$str = "你好PHP8"; print_r(mb_str_split($str)); // [0]=>你 [1]=>好 [2]=>P [3]=>H [4]=>P [5]=>8print_r(mb_str_split($str, 2)); // [0]=>你好 [1]=>PH [2]=>P8字符串处理strlcpy()8.1安全的字符串拷贝（限制长度，含终止符），返回源字符串总长度$dest = ''; $src = "Hello World";$len = strlcpy($dest, $src, 6); echo $dest; // Helloecho $len; // 11字符串处理mb_str_pad()8.3多字节安全的字符串填充（解决str_pad多字节长度计算错误）$str = "你好"; echo mb_str_pad($str, 5, "-", STR_PAD_RIGHT); // 你好---数组处理array_is_list()8.1检查数组是否为“列表”（索引从0开始、连续递增的整数索引），返回布尔值$list = [1,2,3]; $notList = [0=>1,2=>3];var_dump(array_is_list($list)); // bool(true) var_dump(array_is_list($notList)); // bool(false)数组处理array_find()8.3返回数组中第一个满足回调条件的元素，无则返回null$nums = [1,2,3,4,5]; $even = array_find($nums, fn($n) => $n % 2 === 0); echo $even; // 2数组处理array_find_key()8.3返回数组中第一个满足回调条件的键，无则返回null$users = ['a'=>18, 'b'=>22, 'c'=>19]; $key = array_find_key($users, fn($age) => $age > 20); echo $key; // b数组处理array_any()8.3检查数组是否存在至少一个满足回调条件的元素，返回布尔值$nums = [1,3,5]; var_dump(array_any($nums, fn($n) => $n%2===0)); // bool(false)$nums[]=8; var_dump(array_any($nums, fn($n) => $n%2===0)); // bool(true)数组处理array_all()8.3检查数组所有元素是否都满足回调条件，返回布尔值$nums = [2,4,6]; var_dump(array_all($nums, fn($n) => $n%2===0)); // bool(true)$nums[]=9; var_dump(array_all($nums, fn($n) => $n%2===0)); // bool(false)数值处理fdiv()8.0浮点数除法（优雅处理除0场景，返回INF/-INF/NaN，替代普通除法）var_dump(fdiv(10,2)); // float(5)var_dump(fdiv(10,0)); // float(INF)var_dump(fdiv(0,0)); // float(NAN)数值处理enum_exists()8.1检查枚举类型是否已定义（适配8.1新增的枚举特性）enum Status { case Success; case Error; }var_dump(enum_exists('Status')); // bool(true)var_dump(enum_exists('NonExist')); // bool(false)日期时间处理DateTimeInterface::format_iso8601()8.2标准化输出ISO 8601格式日期，支持指定毫秒精度$dt = new DateTime('2025-12-11 10:00:00', new DateTimeZone('Asia/Shanghai')); echo $dt->format_iso8601(); // 2025-12-11T10:00:00+08:00echo $dt->format_iso8601(3); // 2025-12-11T10:00:00.000+08:00对象/类型处理get_debug_type()8.0更精准的变量类型获取（比gettype友好，支持枚举/对象等）$obj = new stdClass(); enum E { case A; } echo get_debug_type("test"); // string echo get_debug_type($obj); // stdClassecho get_debug_type(E::A); // E对象/类型处理ReflectionClass::isReadOnly()8.2反射检查类是否为只读类（适配8.2新增的readonly类特性）readonly class User { public string $name; } $ref = new ReflectionClass(User::class);var_dump($ref->isReadOnly()); // bool(true)JSON处理json_validate()8.3高效验证JSON字符串有效性（无需解码，性能优于json_decode）$valid = '{"name":"PHP8"}'; $invalid = '{"name":"PHP8"';var_dump(json_validate($valid)); // bool(true) var_dump(json_validate($invalid)); // bool(false)正则表达式preg_last_error_msg()8.0返回正则操作的可读错误信息（替代preg_last_error的数值码）preg_match('/[a-z/', 'test'); // 语法错误echo preg_last_error_msg(); // "missing terminating ] for character class"字节处理bytes_format()8.3将字节数格式化为易读单位（B/KB/MB/GB等），支持指定小数位数echo bytes_format(1024); // 1.00 KBecho bytes_format(1048576); // 1.00 MBecho bytes_format(1500, 0); // 2 KB关键说明：版本兼容性：表格中函数均为PHP8.0及以上新增/增强，部分函数（如array_find）仅在PHP8.3+可用，使用前需确认运行环境版本；多字节支持：mb_*开头的函数解决了中文等多字节字符的处理问题，推荐优先使用；性能优化：如json_validate比json_decode验证JSON更高效（无需解析整个字符串）；类型安全：get_debug_type、array_is_list等函数提升了PHP的类型判断精准度，适配8.0+的类型强化特性。
在宝塔环境中实现 Elasticsearch 与数据库的实时同步，主要有以下几种方案：一、同步方案概览方案实时性复杂度数据一致性适用场景应用层双写最高中等最终一致新建项目，可修改代码Logstash JDBC分钟级低延迟一致已有项目，增量同步Canal秒级高最终一致MySQL 数据库，要求高实时性数据库触发器实时高强一致数据库层面同步二、方案一：应用层双写（推荐）实现原理在业务代码中同时写入数据库和 Elasticsearch。PHP 实现示例<?php /** * 应用层双写方案 * 在写入数据库的同时写入 Elasticsearch */ require '/www/wwwroot/your-site/vendor/autoload.php'; use Elasticsearch\ClientBuilder; class DualWriteService { private $esClient; private $db; public function __construct() { // 初始化 ES 客户端 $this->esClient = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); // 初始化数据库连接 $this->initDatabase(); } private function initDatabase() { $host = 'localhost'; $dbname = 'your_database'; $username = 'your_username'; $password = 'your_password'; try { $this->db = new PDO( "mysql:host={$host};dbname={$dbname};charset=utf8mb4", $username, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ] ); } catch (PDOException $e) { throw new Exception("数据库连接失败: " . $e->getMessage()); } } /** * 添加文章（双写） */ public function addArticle($articleData) { $dbResult = $this->writeToDatabase($articleData); if ($dbResult['success']) { $esResult = $this->writeToElasticsearch($articleData, $dbResult['id']); if (!$esResult['success']) { // ES 写入失败，记录日志或加入重试队列 $this->logSyncFailure('add', $dbResult['id'], $articleData); } return $dbResult; } return $dbResult; } /** * 更新文章（双写） */ public function updateArticle($id, $updateData) { $dbResult = $this->updateDatabase($id, $updateData); if ($dbResult['success']) { $esResult = $this->updateElasticsearch($id, $updateData); if (!$esResult['success']) { $this->logSyncFailure('update', $id, $updateData); } } return $dbResult; } /** * 删除文章（双写） */ public function deleteArticle($id) { $dbResult = $this->deleteFromDatabase($id); if ($dbResult['success']) { $esResult = $this->deleteFromElasticsearch($id); if (!$esResult['success']) { $this->logSyncFailure('delete', $id); } } return $dbResult; } /** * 写入数据库 */ private function writeToDatabase($data) { try { $sql = "INSERT INTO articles (title, content, author, category, tags, status, created_at, updated_at) VALUES (:title, :content, :author, :category, :tags, :status, NOW(), NOW())"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':title' => $data['title'], ':content' => $data['content'], ':author' => $data['author'], ':category' => $data['category'], ':tags' => is_array($data['tags']) ? implode(',', $data['tags']) : $data['tags'], ':status' => $data['status'] ?? 1 ]); $id = $this->db->lastInsertId(); return [ 'success' => true, 'id' => $id ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 写入 Elasticsearch */ private function writeToElasticsearch($data, $id) { try { $esData = [ 'id' => (int)$id, 'title' => $data['title'], 'content' => $data['content'], 'author' => $data['author'], 'category' => $data['category'], 'tags' => is_array($data['tags']) ? $data['tags'] : explode(',', $data['tags']), 'status' => $data['status'] ?? 1, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; $params = [ 'index' => 'articles', 'id' => $id, 'body' => $esData ]; $response = $this->esClient->index($params); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 更新数据库 */ private function updateDatabase($id, $data) { try { $sql = "UPDATE articles SET title = :title, content = :content, author = :author, category = :category, tags = :tags, status = :status, updated_at = NOW() WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':title' => $data['title'], ':content' => $data['content'], ':author' => $data['author'], ':category' => $data['category'], ':tags' => is_array($data['tags']) ? implode(',', $data['tags']) : $data['tags'], ':status' => $data['status'] ?? 1, ':id' => $id ]); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 更新 Elasticsearch */ private function updateElasticsearch($id, $data) { try { $updateData = [ 'title' => $data['title'], 'content' => $data['content'], 'author' => $data['author'], 'category' => $data['category'], 'tags' => is_array($data['tags']) ? $data['tags'] : explode(',', $data['tags']), 'status' => $data['status'] ?? 1, 'updated_at' => date('Y-m-d H:i:s') ]; $params = [ 'index' => 'articles', 'id' => $id, 'body' => [ 'doc' => $updateData ] ]; $response = $this->esClient->update($params); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 从数据库删除 */ private function deleteFromDatabase($id) { try { $sql = "DELETE FROM articles WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([':id' => $id]); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 从 Elasticsearch 删除 */ private function deleteFromElasticsearch($id) { try { $params = [ 'index' => 'articles', 'id' => $id ]; $response = $this->esClient->delete($params); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 记录同步失败 */ private function logSyncFailure($operation, $id, $data = null) { $logData = [ 'timestamp' => date('Y-m-d H:i:s'), 'operation' => $operation, 'id' => $id, 'data' => $data ]; file_put_contents( '/www/wwwlogs/es_sync_failures.log', json_encode($logData) . "\n", FILE_APPEND ); } /** * 重试失败的同步 */ public function retryFailedSyncs() { $logFile = '/www/wwwlogs/es_sync_failures.log'; if (!file_exists($logFile)) { return ['success' => true, 'message' => '无失败记录']; } $logs = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $successCount = 0; $failCount = 0; foreach ($logs as $log) { $data = json_decode($log, true); try { switch ($data['operation']) { case 'add': $this->writeToElasticsearch($data['data'], $data['id']); break; case 'update': $this->updateElasticsearch($data['id'], $data['data']); break; case 'delete': $this->deleteFromElasticsearch($data['id']); break; } $successCount++; } catch (Exception $e) { $failCount++; } } // 清空日志文件 file_put_contents($logFile, ''); return [ 'success' => true, 'retried' => $successCount, 'failed' => $failCount ]; } } // 使用示例 $syncService = new DualWriteService(); // 添加文章 $article = [ 'title' => '测试文章标题', 'content' => '这是文章内容...', 'author' => '张三', 'category' => '技术', 'tags' => ['PHP', 'Elasticsearch', '搜索'], 'status' => 1 ]; $result = $syncService->addArticle($article); if ($result['success']) { echo "文章添加成功，ID: " . $result['id']; } else { echo "添加失败: " . $result['error']; } ?>三、方案二：Logstash JDBC 输入插件安装和配置 Logstash1. 在宝塔中安装 Logstash# 进入宝塔终端 cd /www/server # 下载 Logstash（版本需要与 ES 对应） wget https://artifacts.elastic.co/downloads/logstash/logstash-7.17.0-linux-x86_64.tar.gz # 解压 tar -zxvf logstash-7.17.0-linux-x86_64.tar.gz mv logstash-7.17.0 logstash # 下载 MySQL JDBC 驱动 cd logstash wget https://cdn.mysql.com/archives/mysql-connector-java-8.0/mysql-connector-java-8.0.28.tar.gz tar -zxvf mysql-connector-java-8.0.28.tar.gz2. 创建 Logstash 配置文件创建 /www/server/logstash/config/mysql-sync.conf：input { jdbc { # MySQL 连接配置 jdbc_connection_string => "jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&useSSL=false" jdbc_user => "your_username" jdbc_password => "your_password" # JDBC 驱动路径 jdbc_driver_library => "/www/server/logstash/mysql-connector-java-8.0.28/mysql-connector-java-8.0.28.jar" jdbc_driver_class => "com.mysql.cj.jdbc.Driver" # 启用分页 jdbc_paging_enabled => true jdbc_page_size => 50000 # 调度配置（每分钟执行一次） schedule => "* * * * *" # SQL 查询（增量同步） statement => "SELECT * FROM articles WHERE updated_at > :sql_last_value OR created_at > :sql_last_value" # 记录最后一次同步时间 use_column_value => true tracking_column_type => "timestamp" tracking_column => "updated_at" # 记录文件位置 last_run_metadata_path => "/www/server/logstash/last_run_metadata.txt" # 时区设置 jdbc_default_timezone => "Asia/Shanghai" } } filter { # 添加 @timestamp 字段 date { match => [ "updated_at", "yyyy-MM-dd HH:mm:ss" ] target => "@timestamp" } # 处理 tags 字段 if [tags] { mutate { gsub => [ "tags", ",", "|" ] split => { "tags" => "|" } } } # 移除不需要的字段 mutate { remove_field => ["@version", "@timestamp"] } } output { # 输出到 Elasticsearch elasticsearch { hosts => ["localhost:9200"] index => "articles" document_id => "%{id}" # 定义映射模板 template => "/www/server/logstash/config/articles-template.json" template_name => "articles" template_overwrite => true } # 可选：输出到文件用于调试 file { path => "/www/server/logstash/logs/mysql-sync.log" } }3. 创建 Elasticsearch 映射模板创建 /www/server/logstash/config/articles-template.json：{ "index_patterns": ["articles*"], "settings": { "number_of_shards": 1, "number_of_replicas": 0, "analysis": { "analyzer": { "ik_smart_analyzer": { "type": "custom", "tokenizer": "ik_smart" }, "ik_max_analyzer": { "type": "custom", "tokenizer": "ik_max_word" } } } }, "mappings": { "properties": { "id": {"type": "integer"}, "title": { "type": "text", "analyzer": "ik_smart_analyzer", "search_analyzer": "ik_smart_analyzer" }, "content": { "type": "text", "analyzer": "ik_max_analyzer", "search_analyzer": "ik_smart_analyzer" }, "author": {"type": "keyword"}, "category": {"type": "keyword"}, "tags": {"type": "keyword"}, "status": {"type": "integer"}, "created_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"}, "updated_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"} } } }4. 启动 Logstash# 创建日志目录 mkdir -p /www/server/logstash/logs # 启动 Logstash cd /www/server/logstash ./bin/logstash -f config/mysql-sync.conf # 或作为守护进程运行 nohup ./bin/logstash -f config/mysql-sync.conf > logs/logstash.log 2>&1 &5. 创建宝塔计划任务在宝塔面板中添加计划任务：# 命令 cd /www/server/logstash && ./bin/logstash -f config/mysql-sync.conf # 执行周期：每分钟四、方案三：Canal（MySQL Binlog 同步）安装和配置 Canal1. 安装 Canal# 下载 Canal cd /www/server wget https://github.com/alibaba/canal/releases/download/canal-1.1.6/canal.deployer-1.1.6.tar.gz tar -zxvf canal.deployer-1.1.6.tar.gz mv canal.deployer-1.1.6 canal2. 配置 MySQL 开启 Binlog-- 检查是否开启 binlog SHOW VARIABLES LIKE 'log_bin'; -- 如果没有开启，在 MySQL 配置文件中添加 -- /etc/my.cnf [mysqld] log-bin=mysql-bin binlog-format=ROW server_id=13. 创建 Canal 用户CREATE USER 'canal'@'%' IDENTIFIED BY 'canal_password'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;4. 配置 Canal编辑 /www/server/canal/conf/example/instance.properties：# 数据库配置 canal.instance.master.address=127.0.0.1:3306 canal.instance.dbUsername=canal canal.instance.dbPassword=canal_password canal.instance.connectionCharset=UTF-8 # 要监听的数据库和表 canal.instance.filter.regex=.*\\..* canal.instance.filter.black.regex= # 表映射 canal.instance.filter.regex=your_database.articles5. 创建 PHP Canal 客户端<?php /** * Canal PHP 客户端 * 监听 MySQL Binlog 并同步到 Elasticsearch */ class CanalClient { private $esClient; public function __construct() { $this->esClient = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } public function startSync() { // 连接 Canal 服务器 $client = new \Canal\Client(); $client->connect("127.0.0.1", 11111); $client->subscribe("example", "your_database.articles", ""); while (true) { $message = $client->get(100); if ($message->getEntries()) { foreach ($message->getEntries() as $entry) { if ($entry->getEntryType() == \Canal\Protocol\EntryType::ROWDATA) { $this->processRowChange($entry); } } } sleep(1); // 避免 CPU 过高 } } private function processRowChange($entry) { $rowChange = \Canal\Protocol\RowChange::parseFromString($entry->getStoreValue()); foreach ($rowChange->getRowDatasList() as $rowData) { switch ($rowChange->getEventType()) { case \Canal\Protocol\EventType::INSERT: $this->handleInsert($rowData); break; case \Canal\Protocol\EventType::UPDATE: $this->handleUpdate($rowData); break; case \Canal\Protocol\EventType::DELETE: $this->handleDelete($rowData); break; } } } private function handleInsert($rowData) { $afterColumns = $rowData->getAfterColumnsList(); $data = []; foreach ($afterColumns as $column) { $data[$column->getName()] = $column->getValue(); } $this->syncToElasticsearch('index', $data); } private function handleUpdate($rowData) { $afterColumns = $rowData->getAfterColumnsList(); $data = []; $id = null; foreach ($afterColumns as $column) { $data[$column->getName()] = $column->getValue(); if ($column->getName() == 'id') { $id = $column->getValue(); } } $this->syncToElasticsearch('update', $data, $id); } private function handleDelete($rowData) { $beforeColumns = $rowData->getBeforeColumnsList(); $id = null; foreach ($beforeColumns as $column) { if ($column->getName() == 'id') { $id = $column->getValue(); break; } } $this->syncToElasticsearch('delete', null, $id); } private function syncToElasticsearch($operation, $data = null, $id = null) { try { switch ($operation) { case 'index': $params = [ 'index' => 'articles', 'id' => $data['id'], 'body' => $this->transformData($data) ]; $this->esClient->index($params); break; case 'update': $params = [ 'index' => 'articles', 'id' => $id, 'body' => [ 'doc' => $this->transformData($data) ] ]; $this->esClient->update($params); break; case 'delete': $params = [ 'index' => 'articles', 'id' => $id ]; $this->esClient->delete($params); break; } echo "同步成功: {$operation} ID: {$id}\n"; } catch (Exception $e) { echo "同步失败: {$e->getMessage()}\n"; $this->logFailure($operation, $id, $data); } } private function transformData($data) { return [ 'id' => (int)$data['id'], 'title' => $data['title'], 'content' => $data['content'], 'author' => $data['author'], 'category' => $data['category'], 'tags' => !empty($data['tags']) ? explode(',', $data['tags']) : [], 'status' => (int)$data['status'], 'created_at' => $data['created_at'], 'updated_at' => $data['updated_at'] ]; } private function logFailure($operation, $id, $data) { // 记录同步失败日志 file_put_contents( '/www/wwwlogs/canal_sync_failures.log', json_encode([ 'timestamp' => date('Y-m-d H:i:s'), 'operation' => $operation, 'id' => $id, 'data' => $data ]) . "\n", FILE_APPEND ); } } // 启动 Canal 客户端 $canalClient = new CanalClient(); $canalClient->startSync(); ?>五、方案四：数据库触发器 + 消息队列1. 创建消息表CREATE TABLE es_sync_queue ( id BIGINT AUTO_INCREMENT PRIMARY KEY, table_name VARCHAR(100) NOT NULL, record_id BIGINT NOT NULL, operation ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL, sync_status TINYINT DEFAULT 0 COMMENT '0:未同步, 1:已同步, 2:同步失败', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_status (sync_status), INDEX idx_created (created_at) );2. 创建数据库触发器-- INSERT 触发器 DELIMITER $$ CREATE TRIGGER after_article_insert AFTER INSERT ON articles FOR EACH ROW BEGIN INSERT INTO es_sync_queue (table_name, record_id, operation, sync_status) VALUES ('articles', NEW.id, 'INSERT', 0); END$$ DELIMITER ; -- UPDATE 触发器 DELIMITER $$ CREATE TRIGGER after_article_update AFTER UPDATE ON articles FOR EACH ROW BEGIN INSERT INTO es_sync_queue (table_name, record_id, operation, sync_status) VALUES ('articles', NEW.id, 'UPDATE', 0); END$$ DELIMITER ; -- DELETE 触发器 DELIMITER $$ CREATE TRIGGER after_article_delete AFTER DELETE ON articles FOR EACH ROW BEGIN INSERT INTO es_sync_queue (table_name, record_id, operation, sync_status) VALUES ('articles', OLD.id, 'DELETE', 0); END$$ DELIMITER ;3. PHP 消息队列处理器<?php /** * 消息队列同步处理器 */ class QueueSyncService { private $db; private $esClient; public function __construct() { $this->initDatabase(); $this->esClient = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } public function processQueue($batchSize = 100) { // 获取待同步的记录 $sql = "SELECT * FROM es_sync_queue WHERE sync_status = 0 ORDER BY id ASC LIMIT :limit FOR UPDATE"; $stmt = $this->db->prepare($sql); $stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT); $stmt->execute(); $records = $stmt->fetchAll(); foreach ($records as $record) { $this->processRecord($record); } return count($records); } private function processRecord($record) { try { switch ($record['operation']) { case 'INSERT': case 'UPDATE': $this->syncUpsert($record); break; case 'DELETE': $this->syncDelete($record); break; } // 标记为已同步 $this->markAsSynced($record['id']); } catch (Exception $e) { // 标记为同步失败 $this->markAsFailed($record['id'], $e->getMessage()); } } private function syncUpsert($record) { // 从数据库获取最新数据 $data = $this->getRecordData($record['table_name'], $record['record_id']); if ($data) { $params = [ 'index' => $record['table_name'], 'id' => $record['record_id'], 'body' => $this->transformData($data) ]; $this->esClient->index($params); } } private function syncDelete($record) { $params = [ 'index' => $record['table_name'], 'id' => $record['record_id'] ]; $this->esClient->delete($params); } private function getRecordData($tableName, $recordId) { $sql = "SELECT * FROM {$tableName} WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([':id' => $recordId]); return $stmt->fetch(); } private function transformData($data) { // 根据表结构转换数据 if (isset($data['tags']) && !empty($data['tags'])) { $data['tags'] = explode(',', $data['tags']); } return $data; } private function markAsSynced($queueId) { $sql = "UPDATE es_sync_queue SET sync_status = 1 WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([':id' => $queueId]); } private function markAsFailed($queueId, $error) { $sql = "UPDATE es_sync_queue SET sync_status = 2, error_message = :error WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':id' => $queueId, ':error' => $error ]); } public function retryFailed($batchSize = 50) { $sql = "SELECT * FROM es_sync_queue WHERE sync_status = 2 ORDER BY id ASC LIMIT :limit"; $stmt = $this->db->prepare($sql); $stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT); $stmt->execute(); $records = $stmt->fetchAll(); $successCount = 0; foreach ($records as $record) { try { $this->processRecord($record); $successCount++; } catch (Exception $e) { // 记录重试失败 } } return $successCount; } } // 创建宝塔计划任务执行同步 $syncService = new QueueSyncService(); // 每次处理 100 条记录 $processed = $syncService->processQueue(100); echo "已处理 {$processed} 条同步记录"; // 重试失败的记录 $retried = $syncService->retryFailed(50); echo "重试成功 {$retried} 条记录"; ?>六、方案对比和选择建议方案选择指南场景推荐方案理由新建项目应用层双写代码可控，实时性最好已有项目，可修改代码应用层双写 + 全量初始化渐进式改造MySQL 数据库，要求高实时性Canal基于 Binlog，对业务无侵入简单同步，可接受分钟级延迟Logstash JDBC配置简单，稳定可靠数据库层面同步触发器 + 消息队列强一致性，对应用透明宝塔环境推荐配置对于大多数宝塔用户，我推荐：首选：应用层双写方案备选：Logstash JDBC 方案高级需求：Canal 方案性能优化建议// 批量操作优化 public function bulkSync($records) { $params = ['body' => []]; foreach ($records as $record) { $params['body'][] = [ 'index' => [ '_index' => 'articles', '_id' => $record['id'] ] ]; $params['body'][] = $this->transformData($record); } return $this->esClient->bulk($params); }选择最适合你项目需求和团队技术能力的方案。对于宝塔环境，应用层双写和 Logstash 方案相对更容易实施和维护。
IK 分词器安装指南是的，IK 分词器需要单独安装。 Elasticsearch 默认只提供标准分词器，对于中文分词效果不佳，IK 分词器是专门针对中文优化的分词插件。一、IK 分词器安装方法方法 1：手动安装（推荐）步骤 1：下载对应版本的 IK 分词器# 进入 Elasticsearch 插件目录 cd /www/server/elasticsearch/plugins # 创建 ik 目录 mkdir ik cd ik # 下载对应版本的 IK 分词器 # 注意：版本号必须与 Elasticsearch 版本一致！ # 查看 ES 版本：curl -X GET "localhost:9200/" wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip # 解压 unzip elasticsearch-analysis-ik-7.17.0.zip # 删除 zip 文件 rm -f elasticsearch-analysis-ik-7.17.0.zip # 返回插件目录上级，确保目录结构正确 cd .. ls -la ik/步骤 2：重启 Elasticsearch# 通过宝塔面板重启 # 或者使用命令 systemctl restart elasticsearch步骤 3：验证安装# 验证 IK 分词器是否安装成功 curl -X GET "localhost:9200/_cat/plugins?v" # 测试分词效果 curl -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d' { "analyzer": "ik_smart", "text": "中华人民共和国" }'正常响应：{ "tokens" : [ { "token" : "中华人民共和国", "start_offset" : 0, "end_offset" : 7, "type" : "CN_WORD", "position" : 0 } ] }方法 2：使用 Elasticsearch 插件命令安装# 进入 Elasticsearch 安装目录 cd /www/server/elasticsearch # 使用插件管理器安装 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip # 重启 Elasticsearch systemctl restart elasticsearch二、宝塔面板特有的安装方法通过宝塔终端操作打开宝塔面板 → 终端执行安装命令：# 切换到 elasticsearch 用户（宝塔环境可能需要） sudo -u elasticsearch /bin/bash # 进入 ES 插件目录 cd /www/server/elasticsearch/plugins # 下载并安装 IK 分词器 wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip unzip elasticsearch-analysis-ik-7.17.0.zip -d ik rm -f elasticsearch-analysis-ik-7.17.0.zip # 退出 elasticsearch 用户 exit # 重启 Elasticsearch 服务 systemctl restart elasticsearch宝塔文件管理器操作下载 IK 分词器：访问 IK 分词器 Releases下载对应版本的 ZIP 文件通过宝塔文件管理器上传：进入 /www/server/elasticsearch/plugins/创建 ik 文件夹上传 ZIP 文件到 ik 文件夹解压并删除 ZIP 文件重启服务：宝塔面板 → 软件商店 → Elasticsearch → 重启三、IK 分词器版本对应关系Elasticsearch 版本IK 分词器版本7.17.x7.17.07.16.x7.16.07.15.x7.15.07.14.x7.14.07.13.x7.13.07.12.x7.12.07.11.x7.11.07.10.x7.10.07.9.x7.9.0重要：必须确保版本一致，否则 Elasticsearch 无法启动！四、验证安装的完整脚本<?php /** * IK 分词器验证脚本 * 保存为 test_ik.php 并在浏览器访问 */ require '/www/wwwroot/your-site/vendor/autoload.php'; use Elasticsearch\ClientBuilder; class IKValidator { private $client; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } /** * 检查 IK 分词器是否安装 */ public function checkIKInstallation() { echo " IK 分词器安装验证 "; // 1. 检查插件列表 echo " 1. 已安装插件列表 "; try { $plugins = $this->client->cat()->plugins(['v' => true]); echo " "; foreach ($plugins as $plugin) { echo $plugin . "\n"; if (strpos($plugin, 'analysis-ik') !== false) { echo "✅ IK 分词器已安装\n"; } } echo " "; } catch (Exception $e) { echo "❌ 获取插件列表失败: " . $e->getMessage() . "\n"; } // 2. 测试 IK 分词器功能 echo " 2. IK 分词器功能测试 "; $testTexts = [ "中华人民共和国", "北京大学生活动中心", " Elasticsearch中文分词器", "我喜欢吃苹果" ]; foreach ($testTexts as $text) { $this->testAnalyzer($text); } } /** * 测试分词器 */ private function testAnalyzer($text) { echo " 测试文本: \"{$text}\" "; // 测试 ik_smart（智能分词） echo "ik_smart 分词: "; $this->analyzeText($text, 'ik_smart'); // 测试 ik_max_word（细粒度分词） echo "ik_max_word 分词: "; $this->analyzeText($text, 'ik_max_word'); // 测试 standard（标准分词器对比） echo "standard 分词: "; $this->analyzeText($text, 'standard'); echo " "; } private function analyzeText($text, $analyzer) { try { $params = [ 'body' => [ 'analyzer' => $analyzer, 'text' => $text ] ]; $response = $this->client->indices()->analyze($params); echo "分析器: {$analyzer} "; echo "分词结果: "; foreach ($response['tokens'] as $token) { echo "" . $token['token'] . " "; } echo " "; } catch (Exception $e) { echo "❌ 分析器 {$analyzer} 不可用: " . $e->getMessage() . " "; } } /** * 创建测试索引验证 IK 分词器 */ public function createTestIndex() { $indexName = 'test_ik_index'; // 删除已存在的测试索引 if ($this->client->indices()->exists(['index' => $indexName])) { $this->client->indices()->delete(['index' => $indexName]); } // 创建使用 IK 分词器的索引 $params = [ 'index' => $indexName, 'body' => [ 'settings' => [ 'analysis' => [ 'analyzer' => [ 'ik_smart_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_smart' ], 'ik_max_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_max_word' ] ] ] ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer', 'search_analyzer' => 'ik_smart_analyzer' ], 'content' => [ 'type' => 'text', 'analyzer' => 'ik_max_analyzer', 'search_analyzer' => 'ik_smart_analyzer' ] ] ] ] ]; try { $response = $this->client->indices()->create($params); echo " 3. 测试索引创建 "; echo "✅ 测试索引创建成功，IK 分词器配置正常 "; // 测试索引文档 $doc = [ 'title' => 'Elasticsearch中文分词器测试', 'content' => '这是一个关于IK分词器的功能测试文档，用于验证中文分词效果。' ]; $this->client->index([ 'index' => $indexName, 'body' => $doc ]); echo "✅ 测试文档索引成功 "; } catch (Exception $e) { echo " 3. 测试索引创建 "; echo "❌ 创建测试索引失败: " . $e->getMessage() . " "; } } } // 执行验证 $validator = new IKValidator(); $validator->checkIKInstallation(); $validator->createTestIndex(); echo " 验证完成 "; echo "如果看到 IK 分词器正常工作，说明安装成功！"; ?>五、IK 分词器使用示例PHP 中使用 IK 分词器<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class IKSearchExample { private $client; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } /** * 创建使用 IK 分词器的索引 */ public createNewsIndex() { $params = [ 'index' => 'news', 'body' => [ 'settings' => [ 'number_of_shards' => 1, 'number_of_replicas' => 0, 'analysis' => [ 'analyzer' => [ 'ik_smart_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_smart' ], 'ik_max_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_max_word' ] ] ] ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer', // 索引时使用智能分词 'search_analyzer' => 'ik_smart_analyzer' // 搜索时使用智能分词 ], 'content' => [ 'type' => 'text', 'analyzer' => 'ik_max_analyzer', // 索引时使用最大分词 'search_analyzer' => 'ik_smart_analyzer' // 搜索时使用智能分词 ], 'tags' => [ 'type' => 'text', 'analyzer' => 'ik_max_analyzer' ], 'author' => [ 'type' => 'keyword' // 关键字类型，不分词 ] ] ] ] ]; return $this->client->indices()->create($params); } /** * 使用 IK 分词器进行搜索 */ public function searchWithIK($keyword) { $params = [ 'index' => 'news', 'body' => [ 'query' => [ 'multi_match' => [ 'query' => $keyword, 'fields' => ['title^3', 'content^2', 'tags^2'], 'type' => 'best_fields' ] ], 'highlight' => [ 'fields' => [ 'title' => [ 'pre_tags' => [''], 'post_tags' => [''] ], 'content' => [ 'pre_tags' => [''], 'post_tags' => [''], 'fragment_size' => 150, 'number_of_fragments' => 3 ] ] ] ] ]; return $this->client->search($params); } } // 使用示例 $search = new IKSearchExample(); // 创建索引 $search->createNewsIndex(); // 搜索示例 $result = $search->searchWithIK('北京大学生'); print_r($result['hits']['hits']); ?>六、常见问题解决问题 1：版本不匹配症状：Elasticsearch 启动失败解决：# 检查错误日志 tail -f /www/server/elasticsearch/logs/elasticsearch.log # 如果看到版本不匹配错误，下载正确版本 cd /www/server/elasticsearch/plugins/ik rm -rf * wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v[正确版本]/elasticsearch-analysis-ik-[正确版本].zip unzip elasticsearch-analysis-ik-[正确版本].zip问题 2：权限问题症状：无法创建插件目录或文件解决：# 确保 elasticsearch 用户有权限 chown -R elasticsearch:elasticsearch /www/server/elasticsearch/plugins/ik chmod -R 755 /www/server/elasticsearch/plugins/ik问题 3：IK 分词器不生效症状：安装成功但分词效果不对解决：# 重启 Elasticsearch systemctl restart elasticsearch # 验证插件是否加载 curl -X GET "localhost:9200/_cat/plugins?v" # 检查索引配置是否正确使用了 IK 分词器七、IK 分词器扩展词典（可选）如果需要自定义词典，可以编辑 IK 配置文件：# 进入 IK 配置目录 cd /www/server/elasticsearch/plugins/ik/config # 编辑主词典 vi IKAnalyzer.cfg.xml # 添加自定义词典 # 在 <entry key="ext_dict">custom.dic</entry> # 创建自定义词典文件 vi custom.dic # 添加自定义词汇，每行一个词 区块链 人工智能 大数据记得安装完成后一定要重启 Elasticsearch 服务！这样就完成了 IK 分词器的安装和配置，现在可以在 PHP 代码中使用更优秀的中文搜索功能了。
【Elasticsearch】Elasticsearch 安装步骤与常见问题解决
1. 环境准备# 更新系统 sudo apt update && sudo apt upgrade -y # 安装 Java (Elasticsearch 需要 Java 环境) sudo apt install openjdk-11-jdk -y # 验证 Java 安装 java -version2. 安装 Elasticsearch方法一：使用 APT 仓库安装（推荐）# 导入 Elasticsearch GPG 密钥 wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - # 添加 Elasticsearch 仓库 echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list # 更新并安装 sudo apt update sudo apt install elasticsearch方法二：手动下载安装# 下载 Elasticsearch wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.0-amd64.deb # 安装 sudo dpkg -i elasticsearch-7.17.0-amd64.deb3. 配置 Elasticsearch# 编辑配置文件 sudo vi /etc/elasticsearch/elasticsearch.yml主要配置项：# 集群名称 cluster.name: my-elasticsearch-cluster # 节点名称 node.name: node-1 # 数据存储路径 path.data: /var/lib/elasticsearch # 日志存储路径 path.logs: /var/log/elasticsearch # 网络绑定地址 network.host: 0.0.0.0 # HTTP 端口 http.port: 9200 # 集群初始主节点 cluster.initial_master_nodes: ["node-1"] # 允许跨域访问（用于开发） http.cors.enabled: true http.cors.allow-origin: "*"4. 启动和管理 Elasticsearch# 启动 Elasticsearch 服务 sudo systemctl start elasticsearch # 设置开机自启 sudo systemctl enable elasticsearch # 查看服务状态 sudo systemctl status elasticsearch # 重启服务 sudo systemctl restart elasticsearch # 停止服务 sudo systemctl stop elasticsearch5. 验证安装# 检查 Elasticsearch 是否运行 curl -X GET "localhost:9200/" # 查看集群健康状态 curl -X GET "localhost:9200/_cluster/health" # 查看节点信息 curl -X GET "localhost:9200/_nodes"正常响应示例：{ "name" : "node-1", "cluster_name" : "my-elasticsearch-cluster", "cluster_uuid" : "abcd1234", "version" : { "number" : "7.17.0", "build_flavor" : "default", "build_type" : "deb", "build_hash" : "abcdef123456", "build_date" : "2022-01-01T00:00:00.000Z", "build_snapshot" : false, "lucene_version" : "8.11.1", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }6. 安全配置（可选）# 安装 Elasticsearch 安全插件 sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install repository-s3 # 配置防火墙 sudo ufw allow 9200 sudo ufw allow 9300二、常见问题解决1. 内存不足问题# 编辑 JVM 配置 sudo vi /etc/elasticsearch/jvm.options # 调整内存设置（根据服务器内存调整） -Xms1g -Xmx1g2. 无法连接问题# 检查 Elasticsearch 是否运行 sudo systemctl status elasticsearch # 检查端口是否监听 netstat -tulpn | grep 9200 # 检查防火墙设置 sudo ufw status sudo ufw allow 92003. 索引性能优化// 在 PHP 中使用批量操作提高性能 $es->bulkInsert('large_index', $largeDataArray); // 使用滚动查询处理大量数据 $params = [ 'index' => 'large_index', 'scroll' => '1m', 'size' => 1000, 'body' => [ 'query' => [ 'match_all' => new \stdClass() ] ] ];
1. 使用 Composer 安装# 安装 Composer（如果未安装） curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer # 创建项目目录 mkdir elasticsearch-php-demo cd elasticsearch-php-demo # 初始化 Composer composer init # 安装 Elasticsearch PHP 客户端 composer require elasticsearch/elasticsearch2. 手动安装（不使用 Composer）<?php // 手动引入 Elasticsearch 客户端 require_once 'path/to/elasticsearch-php/autoload.php';示例 1：基础连接和索引操作<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class ElasticsearchDemo { private $client; public function __construct() { // 创建 Elasticsearch 客户端 $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) // ES 服务器地址 ->setRetries(2) // 重试次数 ->build(); // 检查连接 if (!$this->checkConnection()) { throw new Exception("无法连接到 Elasticsearch"); } } /** * 检查 Elasticsearch 连接 */ private function checkConnection() { try { return $this->client->ping(); } catch (Exception $e) { echo "连接失败: " . $e->getMessage() . "\n"; return false; } } /** * 创建索引 */ public function createIndex($indexName) { $params = [ 'index' => $indexName, 'body' => [ 'settings' => [ 'number_of_shards' => 2, 'number_of_replicas' => 1 ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'standard' ], 'content' => [ 'type' => 'text', 'analyzer' => 'standard' ], 'author' => [ 'type' => 'keyword' ], 'publish_date' => [ 'type' => 'date' ], 'views' => [ 'type' => 'integer' ] ] ] ] ]; try { $response = $this->client->indices()->create($params); echo "索引 {$indexName} 创建成功\n"; return $response; } catch (Exception $e) { echo "创建索引失败: " . $e->getMessage() . "\n"; return false; } } /** * 删除索引 */ public function deleteIndex($indexName) { $params = ['index' => $indexName]; try { // 检查索引是否存在 if ($this->client->indices()->exists($params)) { $response = $this->client->indices()->delete($params); echo "索引 {$indexName} 删除成功\n"; return $response; } else { echo "索引 {$indexName} 不存在\n"; return false; } } catch (Exception $e) { echo "删除索引失败: " . $e->getMessage() . "\n"; return false; } } /** * 添加文档 */ public function addDocument($indexName, $document, $id = null) { $params = [ 'index' => $indexName, 'body' => $document ]; // 如果指定了 ID if ($id !== null) { $params['id'] = $id; } try { $response = $this->client->index($params); echo "文档添加成功，ID: " . $response['_id'] . "\n"; return $response; } catch (Exception $e) { echo "添加文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 获取文档 */ public function getDocument($indexName, $id) { $params = [ 'index' => $indexName, 'id' => $id ]; try { $response = $this->client->get($params); return $response; } catch (Exception $e) { echo "获取文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 更新文档 */ public function updateDocument($indexName, $id, $updateData) { $params = [ 'index' => $indexName, 'id' => $id, 'body' => [ 'doc' => $updateData ] ]; try { $response = $this->client->update($params); echo "文档更新成功\n"; return $response; } catch (Exception $e) { echo "更新文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 删除文档 */ public function deleteDocument($indexName, $id) { $params = [ 'index' => $indexName, 'id' => $id ]; try { $response = $this->client->delete($params); echo "文档删除成功\n"; return $response; } catch (Exception $e) { echo "删除文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 搜索文档 */ public function search($indexName, $query) { $params = [ 'index' => $indexName, 'body' => [ 'query' => $query ] ]; try { $response = $this->client->search($params); return $response; } catch (Exception $e) { echo "搜索失败: " . $e->getMessage() . "\n"; return false; } } /** * 批量插入文档 */ public function bulkInsert($indexName, $documents) { $params = ['body' => []]; foreach ($documents as $document) { $params['body'][] = [ 'index' => [ '_index' => $indexName ] ]; $params['body'][] = $document; } try { $response = $this->client->bulk($params); echo "批量插入完成，处理了 " . count($documents) . " 个文档\n"; return $response; } catch (Exception $e) { echo "批量插入失败: " . $e->getMessage() . "\n"; return false; } } } // 使用示例 try { $es = new ElasticsearchDemo(); // 创建索引 $es->createIndex('blog'); // 添加单个文档 $document = [ 'title' => 'Elasticsearch PHP 客户端使用指南', 'content' => '这是一篇关于如何使用 PHP 操作 Elasticsearch 的详细教程。', 'author' => '张三', 'publish_date' => '2023-10-01', 'views' => 100 ]; $es->addDocument('blog', $document, '1'); // 批量插入文档 $documents = [ [ 'title' => 'PHP 开发技巧', 'content' => '分享一些 PHP 开发中的实用技巧和最佳实践。', 'author' => '李四', 'publish_date' => '2023-10-02', 'views' => 150 ], [ 'title' => 'Linux 系统管理', 'content' => 'Linux 系统管理的基本命令和操作指南。', 'author' => '王五', 'publish_date' => '2023-10-03', 'views' => 200 ] ]; $es->bulkInsert('blog', $documents); // 搜索文档 $query = [ 'match' => [ 'title' => 'PHP' ] ]; $searchResult = $es->search('blog', $query); echo "搜索到 " . $searchResult['hits']['total']['value'] . " 个结果:\n"; foreach ($searchResult['hits']['hits'] as $hit) { echo "- " . $hit['_source']['title'] . " (作者: " . $hit['_source']['author'] . ")\n"; } } catch (Exception $e) { echo "错误: " . $e->getMessage() . "\n"; } ?>示例 2：高级搜索功能<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class AdvancedSearchDemo { private $client; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } /** * 多字段搜索 */ public function multiFieldSearch($indexName, $keyword) { $params = [ 'index' => $indexName, 'body' => [ 'query' => [ 'multi_match' => [ 'query' => $keyword, 'fields' => ['title^2', 'content', 'author'], // title 字段权重为 2 'type' => 'best_fields' ] ], 'highlight' => [ 'fields' => [ 'title' => new \stdClass(), 'content' => new \stdClass() ] ] ] ]; return $this->client->search($params); } /** * 布尔搜索 */ public function boolSearch($indexName, $must = [], $should = [], $must_not = []) { $boolQuery = []; if (!empty($must)) { $boolQuery['must'] = $must; } if (!empty($should)) { $boolQuery['should'] = $should; } if (!empty($must_not)) { $boolQuery['must_not'] = $must_not; } $params = [ 'index' => $indexName, 'body' => [ 'query' => [ 'bool' => $boolQuery ] ] ]; return $this->client->search($params); } /** * 范围查询 */ public function rangeSearch($indexName, $field, $gte = null, $lte = null) { $range = []; if ($gte !== null) $range['gte'] = $gte; if ($lte !== null) $range['lte'] = $lte; $params = [ 'index' => $indexName, 'body' => [ 'query' => [ 'range' => [ $field => $range ] ] ] ]; return $this->client->search($params); } /** * 聚合查询 */ public function aggregationSearch($indexName) { $params = [ 'index' => $indexName, 'body' => [ 'size' => 0, // 不需要返回具体文档 'aggs' => [ 'authors' => [ 'terms' => [ 'field' => 'author.keyword', 'size' => 10 ] ], 'total_views' => [ 'sum' => [ 'field' => 'views' ] ], 'avg_views' => [ 'avg' => [ 'field' => 'views' ] ] ] ] ]; return $this->client->search($params); } } // 使用示例 $searchDemo = new AdvancedSearchDemo(); // 多字段搜索 $result = $searchDemo->multiFieldSearch('blog', 'PHP'); print_r($result['hits']); // 布尔搜索 $boolResult = $searchDemo->boolSearch('blog', [ // must 条件 ['match' => ['author' => '张三']], ['range' => ['views' => ['gte' => 50]]] ], [ // should 条件 ['match' => ['title' => '教程']], ['match' => ['content' => '指南']] ] ); // 聚合查询 $aggResult = $searchDemo->aggregationSearch('blog'); echo "总浏览量: " . $aggResult['aggregations']['total_views']['value'] . "\n"; echo "平均浏览量: " . $aggResult['aggregations']['avg_views']['value'] . "\n"; ?>示例 3：完整的博客搜索应用<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class BlogSearch { private $client; private $indexName = 'blog_posts'; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); $this->createBlogIndex(); } private function createBlogIndex() { // 检查索引是否存在，不存在则创建 if (!$this->client->indices()->exists(['index' => $this->indexName])) { $params = [ 'index' => $this->indexName, 'body' => [ 'settings' => [ 'number_of_shards' => 1, 'number_of_replicas' => 1, 'analysis' => [ 'analyzer' => [ 'ik_smart_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_smart' ] ] ] ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer' ], 'content' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer' ], 'author' => [ 'type' => 'keyword' ], 'tags' => [ 'type' => 'keyword' ], 'category' => [ 'type' => 'keyword' ], 'publish_date' => [ 'type' => 'date' ], 'views' => [ 'type' => 'integer' ], 'status' => [ 'type' => 'keyword' ] ] ] ] ]; $this->client->indices()->create($params); } } public function addBlogPost($post) { $params = [ 'index' => $this->indexName, 'body' => $post ]; return $this->client->index($params); } public function searchPosts($keyword, $filters = [], $page = 1, $pageSize = 10) { $from = ($page - 1) * $pageSize; $query = [ 'bool' => [ 'must' => [], 'filter' => [] ] ]; // 关键词搜索 if (!empty($keyword)) { $query['bool']['must'][] = [ 'multi_match' => [ 'query' => $keyword, 'fields' => ['title^3', 'content^2', 'tags^2'], 'type' => 'best_fields' ] ]; } // 过滤器 foreach ($filters as $field => $value) { if (!empty($value)) { $query['bool']['filter'][] = [ 'term' => [$field => $value] ]; } } $params = [ 'index' => $this->indexName, 'body' => [ 'from' => $from, 'size' => $pageSize, 'query' => $query, 'sort' => [ ['publish_date' => ['order' => 'desc']], ['views' => ['order' => 'desc']] ], 'highlight' => [ 'fields' => [ 'title' => [ 'number_of_fragments' => 0 ], 'content' => [ 'fragment_size' => 150, 'number_of_fragments' => 3 ] ] ] ] ]; $response = $this->client->search($params); return [ 'total' => $response['hits']['total']['value'], 'posts' => $response['hits']['hits'], 'took' => $response['took'] ]; } public function getRelatedPosts($postId, $limit = 5) { // 先获取当前文章 $currentPost = $this->client->get([ 'index' => $this->indexName, 'id' => $postId ]); $tags = $currentPost['_source']['tags'] ?? []; if (empty($tags)) { return []; } $params = [ 'index' => $this->indexName, 'body' => [ 'size' => $limit, 'query' => [ 'bool' => [ 'must' => [ 'terms' => [ 'tags' => $tags ] ], 'must_not' => [ 'term' => [ '_id' => $postId ] ] ] ], 'sort' => [ ['views' => ['order' => 'desc']], ['publish_date' => ['order' => 'desc']] ] ] ]; $response = $this->client->search($params); return $response['hits']['hits']; } } // 使用示例 $blogSearch = new BlogSearch(); // 添加示例文章 $samplePosts = [ [ 'title' => 'PHP 8 新特性详解', 'content' => 'PHP 8 引入了许多令人兴奋的新特性，如命名参数、联合类型、属性等...', 'author' => '技术达人', 'tags' => ['PHP', '编程', '后端'], 'category' => '编程语言', 'publish_date' => '2023-09-15', 'views' => 245, 'status' => 'published' ], [ 'title' => 'Elasticsearch 入门教程', 'content' => '学习如何使用 Elasticsearch 进行全文搜索和数据聚合...', 'author' => '搜索专家', 'tags' => ['Elasticsearch', '搜索', '数据库'], 'category' => '数据库', 'publish_date' => '2023-09-20', 'views' => 189, 'status' => 'published' ] ]; foreach ($samplePosts as $post) { $blogSearch->addBlogPost($post); } // 搜索文章 $result = $blogSearch->searchPosts('PHP 教程', ['category' => '编程语言'], 1, 10); echo "找到 {$result['total']} 篇文章，耗时 {$result['took']} 毫秒\n"; foreach ($result['posts'] as $post) { echo "标题: " . $post['_source']['title'] . "\n"; if (isset($post['highlight'])) { echo "高亮: " . implode('...', $post['highlight']['content'] ?? []) . "\n"; } echo "---\n"; } ?>
MySQL 版本演进概览MySQL 的主要版本发展可以概括为：5.1 -> 5.5 -> 5.6 -> 5.7 -> 8.0。目前，MySQL 5.7 已于 2023年10月结束官方支持，MySQL 8.0 是绝对的主流和未来。新项目都应选择 8.0 或更高版本。各主要版本核心区别与特性下面我们按版本从新到旧进行梳理，重点对比 5.7 和 8.0 这两个最关键版本。MySQL 8.0 (2018年发布，当前主流)8.0 是一个里程碑式的大版本，引入了大量革命性的新特性。类别核心特性与改进说明性能与存储事务性数据字典将系统表（如 user, db）从 MyISAM 引擎迁移到 InnoDB 引擎，并存储于数据字典表中，支持原子DDL，使元数据管理更安全、可靠。 直方图为优化器提供表中数据分布的统计信息，帮助优化器在非索引字段上生成更优的执行计划。 不可见索引可以将索引设置为“不可见”，优化器会忽略它，用于测试删除某个索引是否会对性能产生负面影响，而无需真正删除。 降序索引支持按降序存储索引，对于 ORDER BY column DESC 的查询性能大幅提升。SQL功能通用表表达式支持 WITH 语句，可以创建命名的临时结果集，支持递归查询，处理树状或层次结构数据非常方便。 窗口函数支持 ROW_NUMBER(), RANK(), LAG() 等强大的分析函数，无需分组即可对行数据进行复杂计算。 资源组可以将线程绑定到特定的CPU核心上，实现更精细化的资源管理。 JSON增强引入了 JSON_TABLE(), JSON_OBJECTAGG(), JSON_ARRAYAGG() 等更多JSON函数，功能更强大。安全与管理角色引入了数据库角色，可以批量管理用户权限，极大简化了权限管理。 默认字符集默认字符集从 latin1 改为 utf8mb4，更好地支持全球化和emoji表情。 默认认证插件默认的身份认证插件改为 caching_sha2_password，比之前的 mysql_native_password 更安全，但可能导致一些旧的客户端/驱动需要升级。 持久化系统变量使用 SET PERSIST 命令设置的变量在服务器重启后依然有效。MySQL 5.7 (2015年发布，已停止支持)5.7 是一个极其成功和稳定的版本，在其生命周期内是生产环境的绝对主力。类别核心特性与改进说明性能与复制InnoDB 增强在线缓冲池调整、在线修改 VARCHAR 长度、多线程页面清理等，提升了并发处理能力和在线操作能力。 组提交实现了多线程组提交，极大改善了在高并发写场景下的性能。 多源复制一个从库可以同时从多个主库复制数据，用于数据汇总。 增强的半同步复制引入了无损复制，保证主从数据强一致性。SQL功能原生JSON支持引入了 JSON 数据类型，并提供 JSON_EXTRACT, JSON_OBJECT 等函数，但功能不如 8.0 强大。 生成列可以创建由其他列计算得出的列，分为 VIRTUAL（不存储）和 STORED（存储）两种。 Sys Schema引入了一个名为 sys 的系统库，提供了一系列视图、函数和存储过程，让性能诊断和问题排查变得非常简单。 空间数据支持增强了对 GIS 数据的支持。安全密码验证策略引入了密码过期策略和密码强度验证插件。MySQL 5.6 (2013年发布，已停止支持)5.6 版本是 InnoDB 引擎和优化器能力大幅提升的版本。核心特性与改进说明InnoDB 性能提升成为绝对的默认存储引擎，提供了全文索引、独立的 undo tablespace。优化器改进引入了索引条件下推，可以在存储引擎层利用索引过滤数据，减少回表次数。复制增强支持基于库的并行复制，提升了从库的复制性能。支持 GTID，使主从切换和故障恢复更方便。在线DDL支持了更多种类的在线 DDL 操作，减少了对业务的影响。MySQL 5.5 (2010年发布，已停止支持)核心特性与改进说明InnoDB 成为默认引擎取代 MyISAM，标志着 MySQL 向事务安全型数据库的彻底转变。半同步复制提供了比异步复制更强的数据一致性保证。性能提升引入了 InnoDB 的缓冲池预加载等特性。总结与版本选择建议版本状态建议MySQL 8.0积极维护，长期支持强烈推荐。所有新项目都应选择此版本。它提供了最好的性能、最丰富的功能和最新的安全更新。MySQL 5.7已停止官方支持不推荐。仅适用于尚未完成升级的遗留系统，应尽快制定迁移计划至 8.0。MySQL 5.6 及更早已停止官方支持绝对禁止。存在已知安全漏洞且无修复，必须立即升级。升级路径通常的升级路径是：5.5/5.6 -> 5.7 -> 8.0。从 5.7 升级到 8.0 前，务必仔细阅读官方文档，进行充分的兼容性测试，因为两者在默认配置（如认证插件、字符集）上有较大变化。希望这份整理能帮助您清晰地了解 MySQL 的版本演进和区别！
Linux 开发与运维命令终极手册封面与使用说明适用人群：PHP开发者、后端工程师、运维人员 核心价值：从日常开发到应急调试的全场景命令参考，兼顾效率与安全 使用方法：按「场景速查」快速定位，按「功能分类」系统学习，注意「安全警告」规避风险一、场景速查索引（紧急情况优先看）问题场景核心命令链功能分类网站突然卡顿/502错误top（看CPU/内存）→ ss -s（连接数）→ systemctl status php-fpm（服务状态）五、一.........二、系统基础与环境监控系统信息uname：查看系统信息uname -a # 完整信息 uname -r # 内核版本（确认Docker/Kernel兼容性）lsb_release：发行版信息lsb_release -a # 所有信息 lsb_release -c # 版本代号（适配apt/yum源）磁盘与存储df：磁盘空间df -h # 人类可读格式 df -i # inode使用情况（紧急清理时，满需删除大量小文件）du：目录大小分析du -sh * # 当前目录各项目大小 du -h --max-depth=1 /var # 一级目录分析 du -sh * \| sort -hr # 从大到小排列内存与负载free：内存监控free -h # 易读格式 free -m # MB单位 watch -n 5 "free -h" # 每5秒刷新（内存泄漏排查）uptime：系统负载uptime # 显示1/5/15分钟平均负载（负载>CPU核心数时需排查进程）高级监控iostat：磁盘IO监控iostat -x 5 # 每5秒显示IO详情（%util接近100%表示IO饱和）which/whereis：命令路径查找which php # 检查php路径 whereis mysql # 查找mysql（区分系统自带与手动安装的程序）三、文件与目录操作基本操作ls：列出内容ls -lh # 详细+大小 ls -lt # 按修改时间排序 # 现代替代：exa -l --git（带Git状态和颜色）cd：切换目录cd ~ # 家目录 cd - # 返回上一目录 # 技巧：cd .. 可简写为 ..（需配置bash）mkdir：创建目录mkdir -p app/{ctrl,model,view} # 递归创建多级目录（一次性创建项目结构）文件操作rm：删除文件 ⚠️极度危险rm file.txt # 删除文件 rm -rf dir/ # 递归强制删除目录 # ⚠️ 安全准则： # 1. 禁止执行 rm -rf / 或 rm -rf /* # 2. 删前先用 ls 确认路径：ls dir/ # 3. 替代方案：trash-put file（移到回收站，需安装trash-cli）cp：复制文件cp -a src/ dest/ # 保留权限复制 cp config.php{,.bak} # 备份（生成config.php.bak） cp *.conf{,.bak} # 批量备份（给所有conf文件加备份后缀）高级文件操作find：查找文件find ./ -name "*.php" -mtime -1 # 1天内修改的PHP文件 find ./ -name "*.log" -mtime +30 -print0 | xargs -0 rm -f # 安全批量处理（处理含空格的文件名）ln：创建链接ln -s /usr/local/php/bin/php /usr/bin/php # 创建软链接（解决命令路径问题）rsync：增量同步rsync -avz --delete ./local/ user@server:/remote/ # 镜像同步（部署代码比scp快）四、文件内容处理查看内容cat：查看文件内容cat config.php # 查看文件 # 现代替代：bat config.php（代码高亮+行号）tail：查看尾部tail -f error.log # 实时监控 tail -n 50 access.log # 最后50行 tail -F error.log # 日志轮转必备（文件重建后仍跟踪）文本处理grep：文本搜索grep -rni "数据库" ./app # 递归+行号+忽略大小写搜索 find ./ -name "*.php" | xargs grep "function getUser" # 结合find搜索sed：文本替换sed -i 's/old/new/g' file.php # 批量替换 sed -i '/^#/d' nginx.conf # 删除注释行 sed -i 's/debug=true/debug=false/g' *.php # 批量修改配置awk：按列处理awk '$9==500 {print $0}' access.log # 筛选500错误 # 统计分析黄金链： awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -10 # Top 10访问IPwc：统计行数wc -l *.php # 统计PHP文件行数 grep "error" log.txt | wc -l # 错误数统计 find ./ -name "*.php" | xargs wc -l # 快速估算代码量五、进程与服务管理进程监控ps：查看进程ps aux --sort=-%cpu # 按CPU排序 ps aux | grep -v grep | grep php-fpm # 筛选特定进程top/htop：实时监控top # 基础监控 htop # 交互式增强版（支持鼠标操作） # 技巧：按P（CPU）/M（内存）排序，k键终止进程kill：终止进程kill 1234 # 正常终止 kill -9 1234 # 强制终止 pkill -f "php server.php" # 按命令名批量终止服务管理systemctl：服务管理systemctl restart nginx # 重启服务 systemctl enable mysql # 开机自启 systemctl status php-fpm -l # 查看状态及完整日志（启动失败排查）网络与调试netstat/ss：网络连接ss -lntup # 监听的端口及进程 ss -ant | grep :80 | wc -l # 统计80端口连接数lsof：文件占用lsof -i :80 # 查看80端口占用 lsof -p 1234 # 查看进程打开的文件 lsof +D /var/www # 查看目录相关进程（解决"文件被占用无法删除"）strace：系统调用跟踪strace -p 1234 # 调试卡死进程（定位程序卡住原因）六、网络操作网络诊断ping：连通性测试ping -c 4 google.com # 发送4个包 # 现代替代：mtr google.com（持续跟踪丢包和延迟）curl：HTTP请求curl -X POST -d "name=test" http://api.com # POST请求 curl -I https://example.com # 仅获取头部 curl -v https://example.com # 详细输出（显示HTTPS握手过程）文件传输wget：文件下载wget -c https://file.com/big.tar.gz # 断点续传 wget -i url_list.txt # 批量下载（从文件读URL）ssh：远程登录ssh -p 2222 user@server.com # 指定端口登录 ssh user@server "command" # 远程执行命令 ssh-copy-id user@server # 免密登录（上传公钥）scp：远程传输scp -r dir/ user@server:/path/ # 递归上传目录 # 大文件传输技巧：pv file.tar.gz | ssh user@server "cat > file.tar.gz"（显示进度）nc：网络调试nc -zv 192.168.1.1 3306 # 测试MySQL端口连通性 # 临时文件传输： # 接收端：nc -l 8080 > recv.txt # 发送端：nc server 8080 < send.txt七、MySQL数据库基本操作mysql：登录数据库mysql -u root -p # 本地登录 mysql -h 10.0.0.5 -P 3307 -u user -p dbname # 远程连接 # 安全提示：生产环境禁止用root直接登录，创建专用用户mysqldump：备份数据库mysqldump -u root -p --single-transaction blog > blog.sql # 热备份（避免锁表）管理操作mysqladmin：数据库管理mysqladmin -u root -p processlist # 查看连接进程 mysqladmin -u root -p kill 123 # 杀死进程ID 123（终止慢查询）数据导入：恢复备份mysql -u root -p blog < blog.sql # ⚠️ 警告：导入前确认目标库正确，避免覆盖生产数据日志查看：调试问题tail -f /var/log/mysql/error.log # 实时错误日志 # 技巧：开启慢查询日志（临时生效）： set global slow_query_log=1八、Docker容器管理镜像管理镜像操作：docker pull php:7.4-fpm # 拉取镜像 docker build -t myphp:v1 . # 构建镜像 docker image prune -a # 清理无用镜像容器管理容器操作：docker run -d -p 80:80 --name mynginx nginx # 后台运行容器 docker ps -a # 查看所有容器 docker exec -it myphp bash # 进入容器交互终端数据与网络数据卷：持久化存储docker volume create phpdata # 创建数据卷 docker run -v phpdata:/var/www/html php:7.4-fpm # 使用数据卷 # 备注：避免用 -v /host/path:/container/path（权限问题多），优先用数据卷网络管理：容器通信docker network create mynet # 创建网络 docker run --network mynet --name db mysql # 容器加入网络 # 技巧：自定义网络避免端口冲突，容器间用名称通信（如mysql://db:3306）九、Swoole扩展安装与验证安装Swoole：pecl install swoole # 安装最新版 pecl install swoole-4.8.12 # 安装指定版本（稳定版） echo "extension=swoole.so" >> php.ini # 启用扩展验证安装：php -m | grep swoole # 检查是否加载 php --ri swoole # 查看扩展信息（确认enable_coroutine等关键特性）服务管理服务管理：php server.php start # 启动服务 php server.php reload # 平滑重启（不中断连接，适合生产环境）进程监控：ps aux | grep server.php # 查看进程 netstat -tulpn | grep 9501 # 查看端口监听 # 备注：配置daemonize=1后台运行，通过ps确认进程数是否符合预期十、编程工具与环境开发工具php：PHP解释器php -l file.php # 语法检查 php -S 0.0.0.0:8000 -t public # 启动内置服务器 php -d display_errors=On index.php # 临时调整配置（显示错误）composer：依赖管理composer install --no-dev # 生产环境安装 composer dump-autoload -o # 优化自动加载 # 加速下载：切换国内镜像 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/git：版本控制git stash # 暂存修改 git bisect start # 二分查找bug git log -S "function_name" # 搜索历史提交（查找引入函数的提交）现代工具jq：JSON处理curl api.com | jq '.data[].name' # 提取数据 jq '.users[] | select(.age>18)' data.json # 条件筛选 cat messy.json | jq . # 格式化JSONag：代码搜索ag "function_name" app/ # 快速搜索 ag -G "*.php" "database" # 指定文件类型搜索 # 备注：比grep -r快10倍，自动忽略.git和vendor目录十一、压缩与归档压缩解压tar：打包/压缩tar -zcvf backup.tar.gz --exclude=vendor ./project # 打包并排除目录 tar -tf backup.tar.gz # 查看内容不解压zip/unzip：ZIP处理zip -r docs.zip ./docs -x "*.git*" # 压缩并排除Git文件 unzip -O GBK chinese.zip # 解压中文文件（避免乱码）gzip：GZIP压缩gzip large.log # 压缩文件（替换原文件） gzip -c file > file.gz # 压缩但保留原文件 gunzip file.gz # 解压十二、包管理与软件安装包管理命令系统更新软件源安装软件搜索软件实用技巧Debian/Ubuntuapt updateapt install <pkg>apt search <pkg>加速：替换国内镜像源CentOS/RHELyum updateyum install <pkg>yum search <pkg>批量安装：yum install php-fpm nginx mysql-server十三、安全警告与最佳实践操作安全rm -rf 安全准则永远不要在根目录/或系统目录执行递归删除删前用ls确认路径：ls /path/to/delete/*推荐用trash-cli（trash-put/trash-empty）替代rm，支持回收站恢复权限操作禁忌⚠️ 禁止执行chmod -R 777 /或chown -R root:root /home/，会导致系统崩溃正确权限：PHP文件644，目录755，Web可写目录属主设为www-data数据库操作安全生产环境执行DROP/TRUNCATE前必须备份禁止直接在生产库执行UPDATE/DELETE不带WHERE的SQL命令执行确认复杂命令先用echo测试：echo rm -rf /path/to/dir/*批量操作前先处理1个样本验证：find ./ -name "*.log" \| head -1 \| xargs rm -f附录：现代工具安装指南工具功能Ubuntu安装命令使用示例bat增强版catsudo apt install batbat config.php（代码高亮）exa增强版lssudo apt install exaexa -l --git（带Git状态）htop增强版topsudo apt install htophtop（交互式监控）mtr网络诊断sudo apt install mtrmtr google.com（路由追踪）jqJSON处理sudo apt install jqcurl api.com \ jq '.data'ag代码搜索sudo apt install silversearcher-agag "function" app/fzf模糊查找sudo apt install fzfCtrl+R搜索历史命令trash-cli回收站工具sudo apt install trash-clitrash-put file（安全删除）使用提示：遇到任何命令不清楚时，使用man <命令>查看详细手册，如man grep。
使用方法 $wechatPayPlatformCertInstance = new \addons\unidrink\library\pay\overseas\wechat\Certificate(); try { $platformCerts = $wechatPayPlatformCertInstance->getPlatformCerts(); if (!empty($platformCerts)) { // 获取最新的证书 $latestCert = $platformCerts[0]; // 返回成功信息 return json([ 'code' => 1, 'msg' => '平台证书生成成功', 'data' => [ 'serial_no' => $latestCert['serial_no'], 'effective_time' => $latestCert['effective_time'], 'expire_time' => $latestCert['expire_time'], 'cert_saved_path' => $wechatPayPlatformCertInstance->getCertPath() . $latestCert['serial_no'] . '.pem', ] ]); } else { throw new Exception('未获取到平台证书'); } } catch (Exception $e) { // 错误处理 return json([ 'code' => 0, 'msg' => '平台证书生成失败: ' . $e->getMessage(), 'data' => [] ]); } 下面是类 <?php namespace addons\unidrink\library\pay\overseas\wechat; use think\Exception; use think\Log; use WeChatPay\Crypto\Rsa; use WeChatPay\Formatter; use WeChatPay\Util\PemUtil; class Certificate { // 配置参数 private $config; // 证书保存路径 private $certPath; // 平台证书列表 private $platformCerts = []; /** * 构造函数 * @param array $config 微信支付配置 */ public function __construct($config = []) { // 默认配置 $defaultConfig = [ 'mch_id' => '', // 商户号 'serial_no' => '', // 商户API证书序列号 'private_key' => ROOT_PATH . 'public/cert/apiclient_key.pem', // 商户私钥内容或路径 'api_v3_key' => '', // APIv3密钥 'cert_path' => ROOT_PATH . 'public/cert/wechatpay/', // 证书保存目录 'ca_cert_path' => ROOT_PATH . 'public/cert/cacert.pem' // CA证书路径 ]; $this->config = array_merge($defaultConfig, $config); $this->certPath = $this->config['cert_path']; // 验证APIv3密钥长度 $this->validateApiV3Key(); // 创建证书目录并确保权限 $this->prepareDirectories(); } /** * 准备目录并确保权限 */ private function prepareDirectories() { // 创建证书目录 if (!is_dir($this->certPath)) { if (!mkdir($this->certPath, 0755, true)) { throw new Exception('无法创建证书目录: ' . $this->certPath); } } // 检查证书目录权限 if (!is_writable($this->certPath)) { throw new Exception('证书目录不可写: ' . $this->certPath); } // 确保CA证书目录可写 $caCertDir = dirname($this->config['ca_cert_path']); if (!is_dir($caCertDir)) { if (!mkdir($caCertDir, 0755, true)) { throw new Exception('无法创建CA证书目录: ' . $caCertDir); } } if (!is_writable($caCertDir)) { throw new Exception('CA证书目录不可写: ' . $caCertDir); } } /** * 验证APIv3密钥长度是否正确 * APIv3密钥必须是32位字符串 * @throws Exception */ private function validateApiV3Key() { // 移除可能存在的空格 $apiV3Key = trim($this->config['api_v3_key']); // 重新设置清理后的密钥 $this->config['api_v3_key'] = $apiV3Key; // 检查长度 $keyLength = strlen($apiV3Key); if ($keyLength !== 32) { throw new Exception("APIv3密钥长度不正确，必须是32位，当前为{$keyLength}位"); } } /** * 获取平台证书 * @return array 平台证书列表 */ public function getPlatformCerts() { try { // 如果有缓存的证书且未过期，直接返回 $cachedCerts = $this->getCachedCerts(); if (!empty($cachedCerts)) { Log::info('使用缓存的平台证书，共' . count($cachedCerts) . '个'); return $cachedCerts; } Log::info('缓存的平台证书不存在或已过期，将从API获取'); // 从微信支付API获取证书 $this->fetchPlatformCerts(); // 缓存证书 $this->cacheCerts(); return $this->platformCerts; } catch (Exception $e) { Log::error('获取微信支付平台证书失败: ' . $e->getMessage() . '，堆栈信息: ' . $e->getTraceAsString()); throw new Exception('获取平台证书失败: ' . $e->getMessage()); } } /** * 从微信支付API获取平台证书 */ private function fetchPlatformCerts() { // 构建请求参数 $timestamp = time(); $nonce = $this->generateNonce(); $method = 'GET'; // $url = '/v3/certificates'; // 中国大陆境内 $url = '/v3/global/certificates'; // 注意：全球版API路径 $body = ''; Log::info("准备请求微信支付证书API: URL={$url}, 时间戳={$timestamp}, 随机串={$nonce}"); // 生成签名 $signature = $this->generateSignature($method, $url, $timestamp, $nonce, $body); // 构建授权头 $token = sprintf( 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $this->config['mch_id'], $nonce, $timestamp, $this->config['serial_no'], $signature ); // 发送请求 $headers = [ 'Accept: application/json', 'User-Agent: FastAdmin/WechatPay', 'Authorization: ' . $token, ]; // 根据商户号判断使用国内还是国际API // $apiDomain = 'https://api.mch.weixin.qq.com'; $apiDomain = 'https://apihk.mch.weixin.qq.com'; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $apiDomain . $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 检查私钥文件是否存在 $privateKeyPath = $this->config['private_key']; if (!file_exists($privateKeyPath)) { throw new Exception('私钥文件不存在: ' . $privateKeyPath); } // 检查私钥文件权限和内容 $this->validatePrivateKey($privateKeyPath); // 设置SSL选项 curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLKEY, $privateKeyPath); // 获取并设置CA证书 $caCertPath = $this->getCACertPath(); if ($caCertPath && file_exists($caCertPath)) { curl_setopt($ch, CURLOPT_CAINFO, $caCertPath); Log::info('使用CA证书: ' . $caCertPath); } else { Log::warning('无法获取CA证书，临时关闭SSL验证'); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $errorNumber = curl_errno($ch); $error = curl_error($ch); curl_close($ch); // 记录完整响应，便于调试 Log::info('微信支付平台证书API响应: HTTP状态码=' . $httpCode . ', 错误代码=' . $errorNumber . ', 错误信息=' . $error . ', 响应内容=' . $response); if ($httpCode !== 200) { // 特殊处理常见错误码 $errorMsg = "请求微信支付API失败，HTTP状态码: {$httpCode}"; switch ($httpCode) { case 401: $errorMsg .= "，可能是签名错误或商户信息不正确"; break; case 403: $errorMsg .= "，权限不足，可能是API未开通或IP白名单问题"; break; case 404: $errorMsg .= "，请求路径错误"; break; case 500: $errorMsg .= "，微信支付服务器内部错误"; break; } $errorMsg .= "，响应: {$response}"; throw new Exception($errorMsg); } if (empty($response)) { throw new Exception('微信支付API返回空响应'); } $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('解析微信支付API响应失败: ' . json_last_error_msg() . '，原始响应: ' . $response); } if (!isset($data['data']) || !is_array($data['data'])) { throw new Exception('微信支付API响应数据异常，未找到证书列表: ' . json_encode($data)); } if (empty($data['data'])) { throw new Exception('微信支付API返回空的证书列表'); } // 处理证书数据前记录原始数据 Log::info('准备处理的证书数据数量: ' . count($data['data']) . '，数据: ' . json_encode($data['data'])); // 处理证书数据 $this->processCertificates($data['data']); } /** * 验证私钥文件 */ private function validatePrivateKey($privateKeyPath) { // 检查文件权限 if (!is_readable($privateKeyPath)) { throw new Exception('私钥文件不可读: ' . $privateKeyPath); } // 检查文件大小 $fileSize = filesize($privateKeyPath); if ($fileSize < 100) { throw new Exception('私钥文件过小，可能不是有效的私钥: ' . $privateKeyPath); } // 检查私钥文件格式 $privateKeyContent = file_get_contents($privateKeyPath); if (strpos($privateKeyContent, '-----BEGIN PRIVATE KEY-----') === false || strpos($privateKeyContent, '-----END PRIVATE KEY-----') === false) { throw new Exception('私钥文件格式不正确，不是有效的PEM格式私钥'); } // 尝试加载私钥验证有效性 $key = openssl_pkey_get_private($privateKeyContent); if (!$key) { throw new Exception('私钥无效，无法加载: ' . openssl_error_string()); } // 检查密钥类型和长度 $keyDetails = openssl_pkey_get_details($key); if ($keyDetails['type'] !== OPENSSL_KEYTYPE_RSA || $keyDetails['bits'] < 2048) { throw new Exception('私钥必须是2048位或以上的RSA密钥'); } Log::info('私钥验证通过'); } /** * 获取CA证书路径 */ private function getCACertPath() { $caCertPath = $this->config['ca_cert_path']; // 如果CA证书不存在，则尝试下载 if (!file_exists($caCertPath)) { try { Log::info('CA证书不存在，尝试下载: ' . $caCertPath); $context = stream_context_create([ 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false ], 'http' => [ 'timeout' => 10, ] ]); $cacert = file_get_contents('https://curl.se/ca/cacert.pem', false, $context); if ($cacert && strlen($cacert) > 10000) { // 确保下载到有效内容 file_put_contents($caCertPath, $cacert); Log::info('CA证书下载成功: ' . $caCertPath); return $caCertPath; } else { Log::error('下载的CA证书内容无效'); } } catch (Exception $e) { Log::error('下载CA证书失败: ' . $e->getMessage()); } } else { Log::info('使用已存在的CA证书: ' . $caCertPath); } return $caCertPath; } /** * 处理证书数据 * @param array $certificates 证书列表 */ private function processCertificates($certificates) { $successCount = 0; $errorDetails = []; foreach ($certificates as $index => $certInfo) { try { // 记录当前处理的证书信息 $serialNo = $certInfo['serial_no'] ?? '未知'; Log::info('处理第' . ($index + 1) . '个证书，序列号: ' . $serialNo); // 验证证书信息是否完整 if (!isset($certInfo['encrypt_certificate'])) { throw new Exception('缺少encrypt_certificate字段'); } $encryptCert = $certInfo['encrypt_certificate']; $requiredFields = ['ciphertext', 'nonce', 'associated_data']; foreach ($requiredFields as $field) { if (!isset($encryptCert[$field]) || empty($encryptCert[$field])) { throw new Exception("证书信息缺少{$field}字段"); } } // 解密证书 $cert = $this->decryptCertificate( $encryptCert['ciphertext'], $encryptCert['nonce'], $encryptCert['associated_data'] ); // 解析证书 $parsedCert = openssl_x509_parse($cert); if (!$parsedCert) { $error = openssl_error_string(); throw new Exception('解析证书失败: ' . $error); } // 验证证书有效期 $now = time(); $effectiveTime = strtotime($certInfo['effective_time'] ?? ''); $expireTime = strtotime($certInfo['expire_time'] ?? ''); if ($effectiveTime === false || $expireTime === false) { throw new Exception('证书有效期格式不正确'); } if ($now < $effectiveTime) { throw new Exception('证书尚未生效'); } if ($now > $expireTime) { throw new Exception('证书已过期'); } // 保存证书信息 $this->platformCerts[] = [ 'serial_no' => $serialNo, 'effective_time' => $certInfo['effective_time'], 'expire_time' => $certInfo['expire_time'], 'cert' => $cert, 'parsed_cert' => $parsedCert, ]; $successCount++; Log::info('成功处理证书，序列号: ' . $serialNo); } catch (Exception $e) { $errorMsg = '处理证书失败: ' . $e->getMessage(); $errorDetails[] = $errorMsg . '，证书信息: ' . json_encode($certInfo); Log::error($errorMsg); } } Log::info("证书处理完成，成功: {$successCount}个，失败: " . (count($certificates) - $successCount) . "个"); if (empty($this->platformCerts)) { throw new Exception('所有证书处理失败，没有可用的平台证书。详细错误: ' . implode('; ', $errorDetails)); } // 按过期时间排序，最新的在前面 usort($this->platformCerts, function ($a, $b) { return strtotime($b['expire_time']) - strtotime($a['expire_time']); }); } /** * 解密证书 * @param string $ciphertext 密文 * @param string $nonce 随机串 * @param string $associatedData 附加数据 * @return string 解密后的证书内容 */ private function decryptCertificate($ciphertext, $nonce, $associatedData) { // 记录解密参数，便于调试 Log::info("开始解密，nonce长度: " . strlen($nonce) . ", associatedData长度: " . strlen($associatedData) . ", 密文长度: " . strlen($ciphertext)); // 验证输入参数 if (empty($ciphertext)) { throw new Exception('密文为空'); } if (empty($nonce)) { throw new Exception('随机串为空'); } if (empty($associatedData)) { throw new Exception('附加数据为空'); } // 尝试解码base64 $decodedCiphertext = base64_decode($ciphertext, true); if ($decodedCiphertext === false) { throw new Exception('密文base64解码失败，可能不是有效的base64字符串'); } if (strlen($decodedCiphertext) < 10) { throw new Exception('解码后的密文长度过短，可能是无效数据'); } // 使用正确的AEAD_AES_256_GCM解密方法 // 微信支付使用的是AEAD_AES_256_GCM，需要正确处理认证标签 $ciphertext = base64_decode($ciphertext); $authTag = substr($ciphertext, -16); $ciphertext = substr($ciphertext, 0, -16); // 清除之前的OpenSSL错误 while (openssl_error_string() !== false) { } // 使用AEAD_AES_256_GCM解密 $cert = openssl_decrypt( $ciphertext, 'aes-256-gcm', $this->config['api_v3_key'], OPENSSL_RAW_DATA, $nonce, $authTag, $associatedData ); // 收集所有OpenSSL错误 $errors = []; while ($error = openssl_error_string()) { $errors[] = $error; } if ($cert === false) { throw new Exception('解密平台证书失败: ' . implode('; ', $errors) . '。检查APIv3密钥是否正确，密钥长度是否为32位'); } // 验证解密结果是否为有效的证书 if (strpos($cert, '-----BEGIN CERTIFICATE-----') === false || strpos($cert, '-----END CERTIFICATE-----') === false) { throw new Exception('解密结果不是有效的证书格式，可能是密钥错误'); } Log::info('证书解密成功，解密结果长度: ' . strlen($cert)); return $cert; } /** * 生成签名 * @param string $method 请求方法 * @param string $url 请求URL * @param int $timestamp 时间戳 * @param string $nonce 随机串 * @param string $body 请求体 * @return string 签名 */ private function generateSignature($method, $url, $timestamp, $nonce, $body) { $message = "{$method}\n{$url}\n{$timestamp}\n{$nonce}\n{$body}\n"; Log::info("生成签名的原始消息: " . base64_encode($message)); // 用base64避免特殊字符问题 // 加载私钥 $privateKey = $this->getPrivateKey(); // 生成签名 $signature = ''; $success = openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256); if (!$success) { throw new Exception('生成签名失败: ' . openssl_error_string()); } $signatureBase64 = base64_encode($signature); Log::info("生成签名成功: {$signatureBase64}"); return $signatureBase64; } /** * 获取私钥 * @return resource 私钥资源 */ private function getPrivateKey() { $privateKey = $this->config['private_key']; // 如果私钥是文件路径，读取文件内容 if (is_file($privateKey)) { $privateKey = file_get_contents($privateKey); } // 加载私钥 $key = openssl_pkey_get_private($privateKey); if (!$key) { throw new Exception('加载商户私钥失败: ' . openssl_error_string()); } return $key; } /** * 获取缓存的证书 * @return array 证书列表 */ private function getCachedCerts() { $cacheFile = $this->certPath . 'platform_certs.cache'; if (!file_exists($cacheFile)) { Log::info('平台证书缓存文件不存在: ' . $cacheFile); return []; } if (!is_readable($cacheFile)) { Log::warning('平台证书缓存文件不可读: ' . $cacheFile); return []; } $cacheData = json_decode(file_get_contents($cacheFile), true); if (json_last_error() !== JSON_ERROR_NONE) { Log::error('解析平台证书缓存失败: ' . json_last_error_msg()); return []; } if (!isset($cacheData['expire_time']) || !isset($cacheData['certs']) || !is_array($cacheData['certs'])) { Log::error('平台证书缓存格式不正确'); return []; } // 检查缓存是否过期（提前1小时刷新） $expireTime = $cacheData['expire_time']; $now = time(); if ($now >= ($expireTime - 3600)) { Log::info("平台证书缓存已过期或即将过期，当前时间: {$now}，过期时间: {$expireTime}"); return []; } return $cacheData['certs']; } /** * 缓存证书 */ private function cacheCerts() { if (empty($this->platformCerts)) { Log::warning('没有可缓存的平台证书'); return; } // 使用最早过期的时间作为缓存过期时间 $expireTime = time(); foreach ($this->platformCerts as $cert) { $certExpire = strtotime($cert['expire_time']); if ($certExpire > $expireTime) { $expireTime = $certExpire; } } $cacheData = [ 'expire_time' => $expireTime, 'certs' => $this->platformCerts, ]; $cacheFile = $this->certPath . 'platform_certs.cache'; $result = file_put_contents($cacheFile, json_encode($cacheData)); if ($result === false) { Log::error('保存平台证书缓存失败: ' . $cacheFile); } else { Log::info('平台证书缓存保存成功，有效期至: ' . date('Y-m-d H:i:s', $expireTime)); } // 保存证书文件 foreach ($this->platformCerts as $cert) { $certFile = $this->certPath . $cert['serial_no'] . '.pem'; if (file_put_contents($certFile, $cert['cert']) === false) { Log::error('保存平台证书文件失败: ' . $certFile); } else { Log::info('平台证书文件保存成功: ' . $certFile); } } } /** * 生成随机字符串 */ private function generateNonce($length = 16) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $nonce = ''; for ($i = 0; $i < $length; $i++) { $nonce .= $chars[random_int(0, strlen($chars) - 1)]; } return $nonce; } /** * 获取最新的平台证书 * @return string 证书内容 */ public function getLatestCert() { $certs = $this->getPlatformCerts(); if (empty($certs)) { throw new Exception('没有可用的平台证书'); } return $certs[0]['cert']; } /** * 根据序列号获取平台证书 * @param string $serialNo 证书序列号 * @return string 证书内容 */ public function getCertBySerialNo($serialNo) { $certs = $this->getPlatformCerts(); foreach ($certs as $cert) { if ($cert['serial_no'] === $serialNo) { return $cert['cert']; } } throw new Exception('找不到序列号为 ' . $serialNo . ' 的平台证书'); } /** * 获取配置参数 * @param string $key 配置键名 * @return mixed 配置值 */ public function getConfig($key = null) { if ($key === null) { return $this->config; } return isset($this->config[$key]) ? $this->config[$key] : null; } /** * 获取证书保存路径 * @return string 证书保存路径 */ public function getCertPath() { return $this->certPath; } }
<?php // 配置连接的IP、端口、以及相应的数据库 $server = array( 'host' => '127.0.0.1', 'port' => 6379, 'database' => 15 ); $redis = new Client($server); // 普通set/get操作 $redis->set('library', 'predis'); $retval = $redis->get('library'); echo $retval; // 显示 'predis' // setex set一个存储时效 $redis->setex('str', 10, 'bar'); // 表示存储有效期为10秒 // setnx/msetnx相当于add操作,不会覆盖已有值 $redis->setnx('foo', 12); // true $redis->setnx('foo', 34); // false // getset操作,set的变种,结果返回替换前的值 $redis->getset('foo', 56); // 返回34 // incrby/incr/decrby/decr 对值的递增和递减 $redis->incr('foo'); // foo为57 $redis->incrby('foo', 2); // foo为59 // exists检测是否存在某值 $redis->exists('foo'); // true // del 删除 $redis->del('foo'); // true // type 类型检测,字符串返回string,列表返回 list,set表返回set/zset,hash表返回hash $redis->type('foo'); // 不存在,返回none $redis->set('str', 'test'); $redis->type('str'); // 字符串，返回string // append 连接到已存在字符串 $redis->append('str', '_123'); // 返回累加后的字符串长度8,此进str为 'test_123' // setrange 部分替换操作 $redis->setrange('str', 0, 'abc'); // 返回3,参数2为0时等同于set操作 $redis->setrange('str', 2, 'cd'); // 返回4,表示从第2个字符后替换,这时'str'为'abcd' // substr 部分获取操作 $redis->substr('str', 0, 2); // 表示从第0个起，取到第2个字符，共3个，返回'abc' // strlen 获取字符串长度 $redis->strlen('str'); // 返回4 // setbit/getbit 位存储和获取 $redis->setbit('binary', 31, 1); // 表示在第31位存入1,这边可能会有大小端问题?不过没关系,getbit 应该不会有问题 $redis->getbit('binary', 31); // 返回1 // keys 模糊查找功能,支持*号以及?号(匹配一个字符) $redis->set('foo1', 123); $redis->set('foo2', 456); $redis->keys('foo*'); // 返回foo1和foo2的array $redis->keys('f?o?'); // 同上 // randomkey 随机返回一个key $redis->randomkey(); // 可能是返回 'foo1'或者是'foo2'及其它任何一存在redis的key // rename/renamenx 对key进行改名,所不同的是renamenx不允许改成已存在的key $redis->rename('str', 'str2'); // 把原先命名为'str'的key改成了'str2' // expire 设置key-value的时效性,ttl 获取剩余有效期,persist 重新设置为永久存储 $redis->expire('foo', 1); // 设置有效期为1秒 $redis->ttl('foo'); // 返回有效期值1s $redis->expire('foo'); // 取消expire行为 // dbsize 返回redis当前数据库的记录总数 $redis->dbsize(); /* * 队列操作 */ // rpush/rpushx 有序列表操作,从队列后插入元素 // lpush/lpushx 和rpush/rpushx的区别是插入到队列的头部,同上,'x'含义是只对已存在的key进行操作 $redis->rpush('fooList', 'bar1'); // 返回一个列表的长度1 $redis->lpush('fooList', 'bar0'); // 返回一个列表的长度2 $redis->rpushx('fooList', 'bar2'); // 返回3,rpushx只对已存在的队列做添加,否则返回0 // llen返回当前列表长度 $redis->llen('fooList'); // 3 // lrange 返回队列中一个区间的元素 $redis->lrange('fooList', 0, 1); // 返回数组包含第0个至第1个共2个元素 $redis->lrange('fooList', 0, -1); // 返回第0个至倒数第一个,相当于返回所有元素,注意redis中很多时候会用到负数,下同 // lindex 返回指定顺序位置的list元素 $redis->lindex('fooList', 1); // 返回'bar1' // lset 修改队列中指定位置的value $redis->lset('fooList', 1, '123'); // 修改位置1的元素,返回true // lrem 删除队列中左起指定数量的字符 $redis->lrem('fooList', 1, '_'); // 删除队列中左起(右起使用-1)1个字符'_'(若有) // lpop/rpop 类似栈结构地弹出(并删除)最左或最右的一个元素 $redis->lpop('fooList'); // 'bar0' $redis->rpop('fooList'); // 'bar2' // ltrim 队列修改，保留左边起若干元素，其余删除 $redis->ltrim('fooList', 0, 1); // 保留左边起第0个至第1个元素 // rpoplpush 从一个队列中pop出元素并push到另一个队列 $redis->rpush('list1', 'ab0'); $redis->rpush('list1', 'ab1'); $redis->rpush('list2', 'ab2'); $redis->rpush('list2', 'ab3'); $redis->rpoplpush('list1', 'list2'); // 结果list1 => array('ab0'), list2 => array('ab1','ab2','ab3') $redis->rpoplpush('list2', 'list2'); // 也适用于同一个队列,把最后一个元素移到头部 list2 => array('ab3','ab1','ab2') // linsert 在队列的中间指定元素前或后插入元素 $redis->linsert('list2', 'before', 'ab1', '123'); // 表示在元素'ab1'之前插入'123' $redis->linsert('list2', 'after', 'ab1', '456'); // 表示在元素'ab1'之后插入'456' // blpop/brpop 阻塞并等待一个列队不为空时，再pop出最左或最右的一个元素（这个功能在php以外可以说非常好用） // brpoplpush 同样是阻塞并等待操作，结果同rpoplpush一样 $redis->blpop('list3', 10); // 如果list3为空则一直等待,直到不为空时将第一元素弹出,10秒后超时 /** * set表操作 */ // sadd 增加元素,返回true,重复返回false $redis->sadd('set1', 'ab'); $redis->sadd('set1', 'cd'); $redis->sadd('set1', 'ef'); // srem 移除指定元素 $redis->srem('set1', 'cd'); // 删除'cd'元素 // spop 弹出首元素 $redis->spop('set1'); // smove 移动当前set表的指定元素到另一个set表 $redis->sadd('set2', '123'); $redis->smove('set1', 'set2', 'ab'); // 移动'set1'中的'ab'到'set2',返回true or false // scard 返回当前set表元素个数 $redis->scard('set2'); // 2 // sismember 判断元素是否属于当前表 $redis->sismember('set2', '123'); // true or false // smembers 返回当前表的所有元素 $redis->smembers('set2'); // array('123','ab') // sinter/sunion/sdiff 返回两个表中元素的交集/并集/补集 $redis->sadd('set1', 'ab'); $redis->sinter('set2', 'set1'); // 返回array('ab') // sinterstore/sunionstore/sdiffstore 将两个表交集/并集/补集元素copy到第三个表中 $redis->set('foo', 0); $redis->sinterstore('foo', 'set1'); // 这边等同于将'set1'的内容copy到'foo'中，并将'foo'转为set表 $redis->sinterstore('foo', array('set1', 'set2')); // 将'set1'和'set2'中相同的元素copy到'foo'表中,覆盖'foo'原有内容 // srandmember 返回表中一个随机元素 $redis->srandmember('set1'); /** * 有序set表操作 */ // zadd 增加元素,并设置序号,返回true,重复返回false $redis->zadd('zset1', 1, 'ab'); $redis->zadd('zset1', 2, 'cd'); $redis->zadd('zset1', 3, 'ef'); // zincrby 对指定元素索引值的增减,改变元素排列次序 $redis->zincrby('zset1', 10, 'ab'); // 返回11 // zrem 移除指定元素 $redis->zrem('zset1', 'ef'); // true or false // zrange 按位置次序返回表中指定区间的元素 $redis->zrange('zset1', 0, 1); // 返回位置0和1之间(两个)的元素 $redis->zrange('zset1', 0, -1); // 返回位置0和倒数第一个元素之间的元素(相当于所有元素) // zrevrange 同上,返回表中指定区间的元素,按次序倒排 $redis->zrevrange('zset1', 0, -1); // 元素顺序和zrange相反 // zrangebyscore/zrevrangebyscore 按顺序/降序返回表中指定索引区间的元素 $redis->zadd('zset1', 3, 'ef'); $redis->zadd('zset1', 5, 'gh'); $redis->zrangebyscore('zset1', 2, 9); // 返回索引值2-9之间的元素 array('ef','gh') // 参数形式 $redis->zrangebyscore('zset1', 2, 9, 'withscores'); // 返回索引值2-9之间的元素并包含索引值 array(array('ef',3),array('gh',5)) $redis->zrangebyscore('zset1', 2, 9, array( 'withscores' => true, 'limit' => array(1, 2) )); // 返回索引值2-9之间的元素,'withscores'=>true表示包含索引值; 'limit'=>array(1,2),表示最多返回2条,结果为array(array('ef',3),array('gh',5)) // zunionstore/zinterstore 将多个表的并集/交集存入另一个表中 $redis->zunionstore('zset3', array('zset1', 'zset2', 'zset0')); // 将'zset1','zset2','zset0'的并集存入'zset3' // 其它参数 $redis->zunionstore('zset3', array('zset1', 'zset2'), array( 'weights' => array(5, 0) )); // weights参数表示权重，其中表示并集后值大于5的元素排在前，大于0的排在后 $redis->zunionstore('zset3', array('zset1', 'zset2'), array( 'aggregate' => 'max' )); // 'aggregate' => 'max'或'min'表示并集后相同的元素是取大值或是取小值 // zcount 统计一个索引区间的元素个数 $redis->zcount('zset1', 3, 5); // 2 $redis->zcount('zset1', '(3', 5); // '(3'表示索引值在3-5之间但不含3,同理也可以使用'(5'表示上限为5但不含5 // zcard 统计元素个数 $redis->zcard('zset1'); // 4 // zscore 查询元素的索引 $redis->zscore('zset1', 'ef'); // 3 // zremrangebyscore 删除一个索引区间的元素 $redis->zremrangebyscore('zset1', 0, 2); // 删除索引在0-2之间的元素('ab','cd'),返回删除元素个数2 // zrank/zrevrank 返回元素所在表顺序/降序的位置(不是索引) $redis->zrank('zset1', 'ef'); // 返回0,因为它是第一个元素;zrevrank则返回1(最后一个) // zremrangebyrank 删除表中指定位置区间的元素 $redis->zremrangebyrank('zset1', 0, 10); // 删除位置为0-10的元素,返回删除的元素个数2 /** * hash表操作 */ // hset/hget 存取hash表的数据 $redis->hset('hash1', 'key1', 'v1'); // 将key为'key1' value为'v1'的元素存入hash1表 $redis->hset('hash1', 'key2', 'v2'); $redis->hget('hash1', 'key1'); // 取出表'hash1'中的key 'key1'的值,返回'v1' // hexists 返回hash表中的指定key是否存在 $redis->hexists('hash1', 'key1'); // true or false // hdel 删除hash表中指定key的元素 $redis->hdel('hash1', 'key2'); // true or false // hlen 返回hash表元素个数 $redis->hlen('hash1'); // 1 // hsetnx 增加一个元素,但不能重复 $redis->hsetnx('hash1', 'key1', 'v2'); // false $redis->hsetnx('hash1', 'key2', 'v2'); // true // hmset/hmget 存取多个元素到hash表 $redis->hmset('hash1', array( 'key3' => 'v3', 'key4' => 'v4' )); $redis->hmget('hash1', array('key3', 'key4')); // 返回相应的值 array('v3','v4') // hincrby 对指定key进行累加 $redis->hincrby('hash1', 'key5', 3); // 返回3 $redis->hincrby('hash1', 'key5', 10); // 返回13 // hkeys 返回hash表中的所有key $redis->hkeys('hash1'); // 返回array('key1','key2','key3','key4','key5') // hvals 返回hash表中的所有value $redis->hvals('hash1'); // 返回array('v1','v2','v3','v4',13) // hgetall 返回整个hash表元素 $redis->hgetall('hash1'); // 返回array('key1'=>'v1','key2'=>'v2','key3'=>'v3','key4'=>'v4','key5'=>13) /** * 排序操作 */ // sort 排序 $redis->rpush('tab', 3); $redis->rpush('tab', 2); $redis->rpush('tab', 17); $redis->sort('tab'); // 返回array(2,3,17) // 使用参数,可组合使用 array('sort' => 'desc','limit' => array(1, 2)) $redis->sort('tab', array('sort' => 'desc')); // 降序排列,返回array(17,3,2) $redis->sort('tab', array('limit' => array(1, 2))); // 返回顺序位置中1的元素2个(这里的2是指个数,而不是位置),返回array(3,17) $redis->sort('tab', array('limit' => array('alpha' => true))); // 按首字符排序返回array(17,2,3),因为17的首字符是'1'所以排首位置 $redis->sort('tab', array('limit' => array('store' => 'ordered'))); // 表示永久性排序,返回元素个数 $redis->sort('tab', array('limit' => array('get' => 'pre_*'))); // 使用了通配符'*'过滤元素,表示只返回以'pre_'开头的元素 /** * redis管理操作 */ // select 指定要操作的数据库 $redis->select('mydb'); // 指定为mydb,不存在则创建 // flushdb 清空当前库 $redis->flushdb(); // move 移动当库的元素到其它库 $redis->set('foo', 'bar'); $redis->move('foo', 'mydb2'); // 若'mydb2'库存在 // info 显示服务当状态信息 $redis->info(); // slaveof 配置从服务器 $redis->slaveof('127.0.0.1', 80); // 配置127.0.0.1端口80的服务器为从服务器 $redis->slaveof(); // 清除从服务器 // 同步保存服务器数据到磁盘 $redis->save(); // 异步保存服务器数据到磁盘 $redis->bgsave(); // ?? $redis->bgrewriteaof(); // 返回最后更新磁盘的时间 $redis->lastsave(); // set/get多个key-value $mkv = array( 'usr:0001' => 'First user', 'usr:0002' => 'Second user', 'usr:0003' => 'Third user' ); $redis->mset($mkv); // 存储多个key对应的value $retval = $redis->mget(array_keys($mkv)); // 获取多个key对应的value print_r($retval); // 批量操作 $replies = $redis->pipeline(function($pipe) { $pipe->ping(); $pipe->flushdb(); $pipe->incrby('counter', 10); // 增量操作 $pipe->incrby('counter', 30); $pipe->exists('counter'); $pipe->get('counter'); $pipe->mget('does_not_exist', 'counter'); }); print_r($replies); // CAS,事务性操作 function zpop($client, $zsetKey) { $element = null; $options = array( 'cas' => true, // Initialize with support for CAS operations 'watch' => $zsetKey, // Key that needs to be WATCHed to detect changes 'retry' => 3, // Number of retries on aborted transactions, after // which the client bails out with an exception. ); $txReply = $client->multiExec($options, function($tx) use ($zsetKey, &$element) { @list($element) = $tx->zrange($zsetKey, 0, 0); if (isset($element)) { $tx->multi(); // With CAS, MULTI *must* be explicitly invoked. $tx->zrem($zsetKey, $element); } }); return $element; } $zpopped = zpop($redis, 'zset'); echo isset($zpopped) ? "ZPOPed $zpopped" : "Nothing to ZPOP!", "\n"; // 对存取的key加前缀,如: 'nrk:' $redis->getProfile()->setPreprocessor(new KeyPrefixPreprocessor('nrk:')); // 分布式存储的一些方法 $multiple_servers = array( array( 'host' => '127.0.0.1', 'port' => 6379, 'database' => 15, 'alias' => 'first', ), array( 'host' => '127.0.0.1', 'port' => 6380, 'database' => 15, 'alias' => 'second', ), ); use Predis\Distribution\IDistributionStrategy; class NaiveDistributionStrategy implements IDistributionStrategy { private $_nodes, $_nodesCount; public function __construct() { $this->_nodes = array(); $this->_nodesCount = 0; } public function add($node, $weight = null) { $this->_nodes[] = $node; $this->_nodesCount++; } public function remove($node) { $this->_nodes = array_filter($this->_nodes, function($n) use ($node) { return $n !== $node; }); $this->_nodesCount = count($this->_nodes); } public function get($key) { $count = $this->_nodesCount; if ($count === 0) { throw new RuntimeException('No connections'); } return $this->_nodes[$count > 1 ? abs(crc32($key) % $count) : 0]; } public function generateKey($value) { return crc32($value); } } // 配置键分布策略 $options = array( 'key_distribution' => new NaiveDistributionStrategy(), ); $redis = new Predis\Client($multiple_servers, $options); for ($i = 0; $i < 100; $i++) { $redis->set("key:$i", str_pad($i, 4, '0', 0)); $redis->get("key:$i"); } $server1 = $redis->getClientFor('first')->info(); $server2 = $redis->getClientFor('second')->info(); printf("Server '%s' has %d keys while server '%s' has %d keys.\n", 'first', $server1['db15']['keys'], 'second', $server2['db15']['keys'] );