一文读懂 Redis 哨兵：如何为数据安全 “站岗放哨”？
宕机应对流程及关键问题探讨将宕机的master下线找一个slave作为master通知所有的slave连接新的master启动新的master和slave可能会出现N台全量复制或者部分复制谁来确认master宕机了,标准是什么？找一个master,怎么找？谁来找？修改配置后，原始的master恢复后，如何处理？哨兵简介哨兵(sentine)是一个分布式系统,用于对主从结构的每一个服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master上。哨兵的作用监控检查master和slave是否正常运行master存活检测、master与slave运行情况检测通知(提醒)当被监控的服务器出现问题时,向其他(哨兵间、客户端)发送通知。自动故障转移断开master与slave的连接,选取一个slave作为新的master,并将其他slave连接到新的master上，并告知客户端新的服务器地址。注意哨兵也是一台redis服务器,只是不提供数据服务。通常哨兵配置数量为单数,且数量不小于3。环境配置，准备工作使用vagrant搭建虚拟机下载vagrant,版本2.4.3,下载地址下载virtualbox,版本7.1.6,下载地址centos7镜像,下载地址安装cd D:\virtualmachine vagrant init centos/7 --box-version 2004.01 vagrant upvagrant配置和命令使用请移步以下的文章了解kong接入网关vagrantfile创建多个HostMac上下载安装Vagrant构建Redis哨兵模式一主二从架构搭配三哨兵配置主节点：127.0.0.1:63792个从节点：127.0.0.1:6380,127.0.0.1:63813个哨兵：127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381开始安装# 登录到虚拟机 vagrant ssh # 切换到root用户 sudo su root # 备份并更新yum源 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo yum clean all yum makecache yum update -y # 安装依赖包 sudo yum install -y gcc make tcl vim wget epel-release # 安装python3 yum install -y https://repo.ius.io/ius-release-el7.rpm yum install -y python36 python36-pip ln -s /usr/bin/python3.6 /usr/bin/python3 ln -s /usr/bin/pip3.6 /usr/bin/pip3 # 创建redis用户和组 groupadd redis useradd -g redis -s /sbin/nologin -M redis # 创建redis安装目录 mkidr -p /usr/local/software cd /usr/local/software # 下载redis安装包 wget https://download.redis.io/releases/redis-7.2.4.tar.gz # 解压和安装redis tar -zxvf redis-7.2.4.tar.gz cd redis-7.2.4/ sudo make sudo make test sudo make install # 配置redis mkdir -p /usr/local/software/redis-7.2.4/data/redis/6379/ mkdir -p /usr/local/software/redis-7.2.4/data/redis/6380/ mkdir -p /usr/local/software/redis-7.2.4/data/redis/6381/ mkdir -p /usr/local/software/redis-7.2.4/data/sentinel/26379/ mkdir -p /usr/local/software/redis-7.2.4/data/sentinel/26380/ mkdir -p /usr/local/software/redis-7.2.4/data/sentinel/26381/ # 创建配置文件目录 mkdir -p /usr/local/software/redis-7.2.4/config # 创建文件 cd /usr/local/software/redis-7.2.4/config cat ../redis.conf | grep -v "#" | grep -v "^$" | tee redis-6379.conf > /dev/null # 配置redis-6379.conf vim redis-6379.conf ##################################redis-6379.conf#################################### pidfile "/usr/local/software/redis-7.2.4/data/redis/6379/redis_6379.pid" dir "/usr/local/software/redis-7.2.4/data/redis/6379/" ######################################################################################### # 配置redis-6380.conf cp redis-6379.conf redis-6380.conf sed -i 's/6379/6380/g' redis-6380.conf # 指定主从关系 vim redis-6380.conf replicaof 127.0.0.1 6379 # 配置redis-6381.conf cp redis-6379.conf redis-6381.conf sed -i 's/6379/6381/g' redis-6381.conf # 指定主从关系 vim redis-6380.conf replicaof 127.0.0.1 6379 # 配置sentinel-26379.conf cat ../sentinel.conf | grep -v "#" | grep -v "^$" | tee sentinel-26379.conf > /dev/null vim sentinel-26379.conf ##################################sentinel-26379.conf#################################### pidfile "/usr/local/software/redis-7.2.4/data/sentinel/26379/sentinel_26379.pid" dir "/usr/local/software/redis-7.2.4/data/sentinel/26379/" ######################################################################################### # 配置sentinel-26380.conf cp sentinel-26379.conf sentinel-26380.conf sed -i's/26379/26380/g' sentinel-26380.conf # 配置sentinel-26381.conf cp sentinel-26379.conf sentinel-26381.conf sed -i's/26379/26381/g' sentinel-26381.conf # 赋权 sudo chown -R redis:redis /usr/local/software/redis-7.2.4/ sudo chmod -R 775 /usr/local/software/redis-7.2.4/ # 为了优化 Redis 的运行环境，需要对 Linux 系统的内核参数进行调整，以下是具体的配置步骤 vim /etc/sysctl.conf ##################################/etc/sysctl.conf##################################### # vm.overcommit_memory 控制着系统的内存分配策略，其取值有 0、1、2 三种 # 取值为 0 时，内核会尝试智能判断可用内存和进程的内存需求，避免过度分配内存，但可能导致某些程序申请内存失败 # 取值为 1 时，内核允许内存过度分配，即进程可以申请超过系统实际可用物理内存和交换空间总和的内存 # Redis 在运行过程中可能会一次性分配大量内存，设置为 1 可避免 Redis 因内存分配失败而启动失败 vm.overcommit_memory = 1 # 设置 fs.file-max 参数为 65536 # fs.file-max 定义了系统中所有进程可以同时打开的最大文件描述符数量 # 文件描述符用于表示打开的文件、网络套接字等资源，Redis 作为高性能的键值对存储数据库，在处理大量客户端连接和文件操作时 # 可能会同时打开很多文件描述符，增大该值可以避免 Redis 出现“Too many open files”的错误 fs.file-max = 65536 # 设置 net.core.somaxconn 参数为 511 # net.core.somaxconn 是内核参数，用于设置每个网络套接字（socket）监听队列的最大长度 # 当有大量客户端向 Redis 发起连接请求时，这些请求会被放入监听队列等待处理 # 如果队列长度过小，可能会导致部分连接请求被拒绝，增大该值可以提高系统处理大量并发连接的能力 net.core.somaxconn = 511 ######################################################################################### # 注释说明接下来的操作是让刚才修改的内核参数立即生效 sysctl -p # 为了让 Redis 能够稳定且高效地运行，有时需要调整系统对用户或进程资源使用的限制。 vim /etc/security/limits.conf ##################################/etc/security/limits.conf############################## # 为 redis 用户设置硬限制的最大文件打开数为 65536。 # 硬限制是一个严格的上限，普通用户无法自行突破该限制，通常由系统管理员进行设置。 # Redis 在处理大量客户端连接、进行持久化操作等场景下，可能会同时打开大量的文件描述符。 # 将硬限制设置为 65536，可以确保 Redis 在高并发情况下有足够的文件描述符可用，避免因达到文件打开数限制而出现异常。 redis hard nofile 65536 # 为 redis 用户设置软限制的最大文件打开数为 65536。 # 软限制是一个建议性的上限，用户可以在不超过硬限制的情况下，通过特定命令自行调整软限制的值。 # 这里将软限制和硬限制设置为相同的值，意味着 redis 用户在正常使用过程中可以直接使用最大为 65536 的文件打开数，无需额外调整。 redis soft nofile 65536 ######################################################################################### # 为redis创建软链接 ln -s /usr/local/software/redis-7.2.4/bin/redis-server /usr/bin/redis-server ln -s /usr/local/software/redis-7.2.4/bin/redis-cli /usr/bin/redis-cli ln -s /usr/local/software/redis-7.2.4/bin/redis-sentinel /usr/bin/redis-sentinel # 启动redis sudo -u redis redis-server /usr/local/software/redis-7.2.4/config/redis-3679.conf sudo -u redis redis-server /usr/local/software/redis-7.2.4/config/redis-3680.conf sudo -u redis redis-server /usr/local/software/redis-7.2.4/config/redis-3681.conf主从日志解析########################################master日志解析################################################ # 3734 是 Redis 服务器进程的 ID，M 表示该进程是主节点（Master）。 # 02 Mar 2025 04:35:14.547 是日志记录的时间戳。 # 此日志表明 Redis 服务器已经完成初始化过程，内部基本结构和配置已设置好。 3734:M 02 Mar 2025 04:35:14.547 * Server initialized # 说明 Redis 正在加载 RDB（Redis Database）文件，该文件由 Redis 7.2.4 版本生成。 # RDB 是 Redis 用于持久化数据的一种快照方式。 3734:M 02 Mar 2025 04:35:14.547 * Loading RDB produced by version 7.2.4 # 表示加载的 RDB 文件从生成到现在已经过去了 40 秒，即该 RDB 文件的“年龄”为 40 秒。 3734:M 02 Mar 2025 04:35:14.547 * RDB age 40 seconds # 指出在创建这个 RDB 文件时，Redis 服务器在内存中占用的空间是 1.50 兆字节（Mb）。 3734:M 02 Mar 2025 04:35:14.547 * RDB memory usage when created 1.50 Mb # 表明 RDB 文件加载完成，从 RDB 文件中成功加载了 1 个键（key），且没有过期的键。 3734:M 02 Mar 2025 04:35:14.547 * Done loading RDB, keys loaded: 1, keys expired: 0. # 说明 Redis 从磁盘加载数据库（即 RDB 文件）所花费的时间为 0 秒（可能因精度问题显示为 0）。 3734:M 02 Mar 2025 04:35:14.547 * DB loaded from disk: 0.000 seconds # 意味着 Redis 服务器已准备好通过 TCP 协议接受客户端的连接请求。 3734:M 02 Mar 2025 04:35:14.547 * Ready to accept connections tcp # 表示 IP 地址为 127.0.0.1，端口号为 6380 的从节点（Replica）向主节点（当前 Redis 服务器）请求数据同步。 # 在 Redis 主从复制架构中，从节点会定期请求同步数据以保持数据一致。 3734:M 02 Mar 2025 04:35:23.995 * Replica 127.0.0.1:6380 asks for synchronization # 说明主节点接受了来自 127.0.0.1:6380 从节点的部分重同步请求。 # 部分重同步是 Redis 主从复制的优化机制，当从节点断开重连时，若主节点复制积压缓冲区有足够数据， # 可进行部分重同步，无需全量同步。这里主节点将从偏移量 880134 开始发送 0 字节的积压数据给从节点。 3734:M 02 Mar 2025 04:35:23.995 * Partial resynchronization request from 127.0.0.1:6380 accepted. Sending 0 bytes of backlog starting from offset 880134. # 表示 IP 地址为 127.0.0.1，端口号为 6381 的从节点向主节点请求数据同步。 3734:M 02 Mar 2025 04:35:28.759 * Replica 127.0.0.1:6381 asks for synchronization # 说明主节点接受了来自 127.0.0.1:6381 从节点的部分重同步请求， # 并将从偏移量 880134 开始发送 14 字节的积压数据给该从节点。 3734:M 02 Mar 2025 04:35:28.759 * Partial resynchronization request from 127.0.0.1:6381 accepted. Sending 14 bytes of backlog starting from offset 880134. #########################################################################################测试主写从读# 连接主节点 [vagrant@vbox ~]$ redis-cli -h 127.0.0.1 -p 6379 127.0.0.1:6379> set name niuzheng OK # 连接从节点读取 [root@vbox vagrant]# redis-cli -h 127.0.0.1 -p 6380 127.0.0.1:6380> get name "niuzheng" # 测试从节点写入禁止 [vagrant@vbox ~]$ redis-cli -h 127.0.0.1 -p 6380 127.0.0.1:6380> set name xiaozhao (error) READONLY You can't write against a read only replica.启动哨兵sudo -u redis redis-sentinel /usr/local/software/redis-7.2.4/config/sentinel-26379.conf sudo -u redis redis-sentinel /usr/local/software/redis-7.2.4/config/sentinel-26380.conf sudo -u redis redis-sentinel /usr/local/software/redis-7.2.4/config/sentinel-26381.conf测试主故障转移# 查看主节点的信息 [root@vbox vagrant]# ps -ef | grep 'redis-server.*:6379' redis 3734 3732 0 04:35 pts/9 00:00:03 redis-server 127.0.0.1:6379 root 3826 3782 0 04:48 pts/12 00:00:00 grep --color=auto redis-server.*:6379 # 停止主节点，模拟主故障，查看哨兵日志 sudo kill -9 3734 # 解析哨兵日志 ####################################sentinel日志###################################### # 进程 ID 为 3756，这里的 X 可能是日志记录的一种标识（正常 Sentinel 主进程标识为 'X' 表示它是 Sentinel 实例）。 # 时间为 02 Mar 2025 04:42:06.711，表明 Sentinel 实例启动并生成了自身的唯一 ID，该 ID 用于在 Sentinel 集群中标识自己。 3756:X 02 Mar 2025 04:42:06.711 * Sentinel ID is 07022b9fda7f507210f654a376c8572f5cf8cf94 # 时间为 02 Mar 2025 04:42:06.711，这行日志表示 Sentinel 开始监控名为 mymaster 的 Redis 主节点， # 该主节点的 IP 地址是 127.0.0.1，端口是 6379，quorum 2 表示判断主节点下线至少需要 2 个 Sentinel 节点的同意。 3756:X 02 Mar 2025 04:42:06.711 # +monitor master mymaster 127.0.0.1 6379 quorum 2 # 时间为 02 Mar 2025 04:51:40.891，这行日志表示 Sentinel 检测到名为 mymaster 的主节点进入主观下线（Subjectively Down，sdown）状态， # 即当前 Sentinel 实例认为主节点出现了故障。 3756:X 02 Mar 2025 04:51:40.891 # +sdown master mymaster 127.0.0.1 6379 # 时间为 02 Mar 2025 04:51:40.904，这行日志表示 Sentinel 检测到主节点状态变化后，将新的配置保存到了磁盘上， # 以确保配置信息的持久化，防止重启后丢失。 3756:X 02 Mar 2025 04:51:40.904 * Sentinel new configuration saved on disk # 时间为 02 Mar 2025 04:51:40.904，这行日志表示 Sentinel 生成了一个新的纪元（epoch），值为 3。 # 纪元用于在 Sentinel 集群中标识不同的故障转移（failover）周期。 3756:X 02 Mar 2025 04:51:40.904 # +new-epoch 3 # 时间为 02 Mar 2025 04:51:40.923，这行日志再次表明 Sentinel 将新的配置保存到磁盘上，可能是因为新的纪元生成等配置变化。 3756:X 02 Mar 2025 04:51:40.923 * Sentinel new configuration saved on disk # 时间为 02 Mar 2025 04:51:40.923，这行日志表示当前 Sentinel 向 ID 为 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 的 Sentinel 节点 # 投票，推举它为领导者来进行故障转移操作，纪元为 3。 3756:X 02 Mar 2025 04:51:40.923 # +vote-for-leader 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 3 # 时间为 02 Mar 2025 04:51:40.992，这行日志表示主节点 mymaster 进入客观下线（Objectively Down，odown）状态， # 即多个 Sentinel 节点都认为主节点出现了故障，#quorum 3/2 表示总共有 3 个 Sentinel 节点参与判断，至少 2 个同意才认定主节点客观下线。 3756:X 02 Mar 2025 04:51:40.992 # +odown master mymaster 127.0.0.1 6379 #quorum 3/2 # 时间为 02 Mar 2025 04:51:40.992，这行日志表示 Sentinel 计算出下一次故障转移的延迟时间， # 即在此之前不会启动故障转移操作，时间为 Sun Mar 2 04:57:41 2025。 3756:X 02 Mar 2025 04:51:40.992 * Next failover delay: I will not start a failover before Sun Mar 2 04:57:41 2025 # 时间为 02 Mar 2025 04:51:41.666，这行日志表示 Sentinel 从 ID 为 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 的 Sentinel 节点（IP 127.0.0.1，端口 26380） # 接收到了配置更新信息，涉及的主节点是 mymaster，其 IP 地址为 127.0.0.1，端口为 6379。 3756:X 02 Mar 2025 04:51:41.666 # +config-update-from sentinel 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 127.0.0.1 26380 @ mymaster 127.0.0.1 6379 # 时间为 02 Mar 2025 04:51:41.666，这行日志表示 Sentinel 执行了故障转移操作，将主节点从 127.0.0.1:6379 切换到了 127.0.0.1:6380。 3756:X 02 Mar 2025 04:51:41.666 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380 # 时间为 02 Mar 2025 04:51:41.666，这行日志表示将 127.0.0.1:6381 的 Redis 实例添加为新主节点（127.0.0.1:6380）的从节点， # 并且该从节点关联的主节点名称为 mymaster。 3756:X 02 Mar 2025 04:51:41.666 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380 # 时间为 02 Mar 2025 04:51:41.666，这行日志表示将 127.0.0.1:6379 的 Redis 实例添加为新主节点（127.0.0.1:6380）的从节点， # 并且该从节点关联的主节点名称为 mymaster。 3756:X 02 Mar 2025 04:51:41.666 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380 # 时间为 02 Mar 2025 04:51:41.683，这行日志表示 Sentinel 在故障转移操作完成后，将新的配置保存到了磁盘上， # 以反映新的主从节点关系等配置信息。 3756:X 02 Mar 2025 04:51:41.683 * Sentinel new configuration saved on disk # 时间为 02 Mar 2025 04:52:11.675，这行日志表示 Sentinel 检测到 127.0.0.1:6379 的从节点进入主观下线（sdown）状态， # 该从节点关联的主节点是 127.0.0.1:6380，名称为 mymaster。 3756:X 02 Mar 2025 04:52:11.675 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380 ###################################################################################### # 重新启动6379机器 # 解析哨兵日志 ####################################sentinel日志###################################### # 到了 2025 年 3 月 2 日 04:54:21.580，Sentinel 实例检测到之前处于主观下线状态的 127.0.0.1:6379 从节点恢复正常， # 即退出了主观下线状态。这里的 "-sdown" 表示该节点不再处于主观下线的情况。 # 该从节点仍然关联到名为 mymaster 且地址为 127.0.0.1:6380 的主节点上。 3756:X 02 Mar 2025 04:54:21.580 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380 ######################################################################################汇总监控阶段用于同步各个节点的状态信息获取各个sentine的状态(是否在线)获取master的状态(属性和信息)各个slave的状态(属性和信息)解释说明第一个哨兵连进去master节点，获取master节点的信息。解析master节点信息，在第一个哨兵节点上保存master节点信息和slave节点信息,并且尝试连接每个slave节点。在master节点上保存第一个哨兵信息。第二个哨兵连进去master节点，获取master节点的信息。解析master节点信息，在第二个哨兵节点上保存master节点信息和slave节点信息,并且尝试连接每个slave节点。因为matser节点保存了第一个哨兵信息，所以第二个哨兵获取到了第一个哨兵信息，并尝试和第一个哨兵建立连接，双方把对方信息也保存到各自的配置文件，方便后续的信息交互。通知阶段哨兵之间互相通知，哨兵之间就相当于建了一个微信群聊，互通matser和slave节点的信息。故障转移阶段主观下线判定：当某个哨兵持续向 master 节点发送消息却未收到回应时，该哨兵会率先判定 master 节点为主观下线状态，并在 “群聊” 中发出询问，告知其他哨兵 master 节点疑似挂掉，呼吁大家一同检测。客观下线确认：其他哨兵收到消息后，即刻尝试与 master 节点重新建立连接。一旦超过半数的哨兵均判定 master 节点无法正常连接，便会在 “群聊” 中确认 master 节点进入客观下线状态。故障转移决策：确认 master 节点客观下线后，哨兵们会在 “群聊” 中发起故障转移讨论，以投票的形式决定由谁主导故障转移工作。当某个哨兵获得超过半数的选票时，它将肩负起处理故障转移的重任，保障系统的持续运行。负责故障转移的哨兵，会依次检查 slave 节点状态，依优先级、复制偏移量筛选，节点 ID 排序，从中选出新的 master。选出新 master 后，首要任务是将其角色从slave切换为master，重新配置相关参数，让其具备主节点功能。随后，负责故障转移的哨兵会迅速通知其他旧slave节点，使其断开与原master的连接，重新配置并接入新master，开始数据同步，保障集群数据一致性。与此同时，哨兵还会向客户端发布新maste 的地址信息，引导客户端请求转向新主节点，确保业务正常运行。此外，哨兵会持续监控整个集群状态，对新master及各slave节点进行健康检查，一旦发现新问题，及时启动相应的应对机制，保障Redis集群持续稳定运行。拓展多个哨兵之间怎么识别新增了其他哨兵，哨兵之间并没有互相配置信息？如果配置文件里没有直接配置其他哨兵的信息，但是哨兵之间会通过master节点的信息来互相识别。哨兵之间通过master节点的发送订阅消息来获取其他哨兵的信息，然后存储在自己的配置文件中。如果一个哨兵还没开始订阅master节点的信息，master节点就挂了，这个哨兵就没办法获取到其他哨兵消息了？不是绝对的，哨兵还可以基于配置信息初始探测其他哨兵的信息或者手动干预。客户端如何在无感状态下切换主从节点？哨兵会在故障转移的时候，发布新的master地址，客户端可以通过哨兵获取到新的master地址，然后切换到新的master节点。这里以PHP为例，实现代码如下： <?php # composer require predis/predis // 引入 Composer 的自动加载文件，使得我们可以使用Predis库 require'vendor/autoload.php'; // 引入 Predis 客户端类，用于与 Redis 服务器进行交互 use Predis\Client; // 引入 Predis 的 Sentinel 复制连接类，用于处理 Redis Sentinel 模式下的连接 use Predis\Connection\Replication\SentinelReplication; // 定义从节点选择策略接口，所有具体的选择策略类都需要实现这个接口 interface SlaveSelectionStrategy { // 定义一个抽象方法，用于从 SentinelReplication 中获取一个从节点 public function getSlave(SentinelReplication $replication); } // 使用 random_int 改进后的随机选择从节点策略类，实现了 SlaveSelectionStrategy 接口 class EnhancedRandomSelection implements SlaveSelectionStrategy { // 实现接口中的 getSlave 方法，用于随机选择一个从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点，返回 null if (empty($slaves)) { return null; } // 获取从节点的数量 $count = count($slaves); // 使用 random_int 生成一个在有效索引范围内的随机索引 $randomIndex = random_int(0, $count - 1); // 返回随机选择的从节点 return $slaves[$randomIndex]; } } // 轮询选择从节点策略类，实现了 SlaveSelectionStrategy 接口 class RoundRobinSelection implements SlaveSelectionStrategy { // 用于记录当前轮询到的索引位置 private $index = 0; // 实现接口中的 getSlave 方法，用于轮询选择从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点，返回 null if (empty($slaves)) { return null; } // 通过取模运算确保索引不会越界，选择当前索引对应的从节点 $selectedSlave = $slaves[$this->index % count($slaves)]; // 索引加 1，为下一次选择做准备 $this->index++; // 返回选择的从节点 return $selectedSlave; } } // 加权轮询选择从节点策略类，实现了 SlaveSelectionStrategy 接口 class WeightedRoundRobinSelection implements SlaveSelectionStrategy { // 存储每个从节点的权重 private $weights; // 记录当前轮询到的索引位置 private $index = 0; // 存储根据权重扩展后的从节点列表 private $weightedNodes = []; // 构造函数，接收从节点的权重配置 public function __construct($weights) { $this->weights = $weights; // 根据权重将从节点扩展到 weightedNodes 数组中 foreach ($weights as $node => $weight) { for ($i = 0; $i < $weight; $i++) { $this->weightedNodes[] = $node; } } } // 实现接口中的 getSlave 方法，用于加权轮询选择从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点，返回 null if (empty($slaves)) { return null; } // 通过取模运算确保索引不会越界，选择当前索引对应的从节点 $selectedNode = $this->weightedNodes[$this->index % count($this->weightedNodes)]; // 索引加 1，为下一次选择做准备 $this->index++; // 遍历所有从节点，找到与选中节点匹配的从节点并返回 foreach ($slaves as $slave) { if ((string)$slave === $selectedNode) { return $slave; } } // 如果没有找到匹配的从节点，返回 null return null; } } // 最少连接数选择从节点策略类，实现了 SlaveSelectionStrategy 接口 class LeastConnectionsSelection implements SlaveSelectionStrategy { // 实现接口中的 getSlave 方法，用于选择连接数最少的从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点，返回 null if (empty($slaves)) { return null; } // 初始化最少连接数为 PHP 能表示的最大整数 $leastConnections = PHP_INT_MAX; // 用于存储选择的从节点 $selectedSlave = null; // 遍历所有从节点 foreach ($slaves as $slave) { try { // 创建一个新的客户端连接到当前从节点 $client = new Client((string)$slave); // 获取从节点的客户端信息 $info = $client->info('clients'); // 提取连接的客户端数量 $connections = intval($info['connected_clients']); // 如果当前从节点的连接数小于最少连接数 if ($connections < $leastConnections) { // 更新最少连接数 $leastConnections = $connections; // 更新选择的从节点 $selectedSlave = $slave; } } catch (\Exception $e) { // 如果获取连接数失败，继续尝试下一个从节点 continue; } } // 返回选择的从节点 return $selectedSlave; } } // 响应时间选择从节点策略类，实现了 SlaveSelectionStrategy 接口 class ResponseTimeSelection implements SlaveSelectionStrategy { // 实现接口中的 getSlave 方法，用于选择响应时间最短的从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点，返回 null if (empty($slaves)) { return null; } // 初始化最小响应时间为 PHP 能表示的最大整数 $minResponseTime = PHP_INT_MAX; // 用于存储选择的从节点 $selectedSlave = null; // 遍历所有从节点 foreach ($slaves as $slave) { try { // 创建一个新的客户端连接到当前从节点 $client = new Client((string)$slave); // 记录开始时间 $startTime = microtime(true); // 向从节点发送 ping 命令 $client->ping(); // 记录结束时间 $endTime = microtime(true); // 计算响应时间，转换为毫秒 $responseTime = ($endTime - $startTime) * 1000; // 如果当前从节点的响应时间小于最小响应时间 if ($responseTime < $minResponseTime) { // 更新最小响应时间 $minResponseTime = $responseTime; // 更新选择的从节点 $selectedSlave = $slave; } } catch (\Exception $e) { // 如果获取响应时间失败，继续尝试下一个从节点 continue; } } // 返回选择的从节点 return $selectedSlave; } } // 策略上下文类，用于管理和切换不同的从节点选择策略 class SlaveSelectionContext { // 存储当前使用的从节点选择策略 private $strategy; // 构造函数，接收一个从节点选择策略实例 public function __construct(SlaveSelectionStrategy $strategy) { $this->strategy = $strategy; } // 设置新的从节点选择策略 public function setStrategy(SlaveSelectionStrategy $strategy) { $this->strategy = $strategy; } // 调用当前策略的 getSlave 方法来选择从节点 public function getSlave(SentinelReplication $replication) { return $this->strategy->getSlave($replication); } } // 定义哨兵节点连接信息，用于连接到 Redis Sentinel 集群 $parameters = [ 'tcp://127.0.0.1:26379', 'tcp://127.0.0.1:26380', 'tcp://127.0.0.1:26381' ]; // 定义从节点权重，用于加权轮询策略 $weights = [ 'tcp://127.0.0.1:6380' => 2, 'tcp://127.0.0.1:6381' => 1 ]; // 创建不同策略实例 $enhancedRandomStrategy = new EnhancedRandomSelection(); $roundRobinStrategy = new RoundRobinSelection(); $weightedRoundRobinStrategy = new WeightedRoundRobinSelection($weights); $leastConnectionsStrategy = new LeastConnectionsSelection(); $responseTimeStrategy = new ResponseTimeSelection(); // 以下是对不同从节点选择策略进行测试的代码 // 改进后的随机选择策略测试 $enhancedRandomContext = new SlaveSelectionContext($enhancedRandomStrategy); $enhancedRandomOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($enhancedRandomContext) { return $enhancedRandomContext->getSlave($replication); } ]; $enhancedRandomClient = new Client($parameters, $enhancedRandomOptions); writeAndRead($enhancedRandomClient, 'enhanced_random_key', 'enhanced_random_value'); // 轮询选择策略测试 $roundRobinContext = new SlaveSelectionContext($roundRobinStrategy); $roundRobinOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($roundRobinContext) { return $roundRobinContext->getSlave($replication); } ]; $roundRobinClient = new Client($parameters, $roundRobinOptions); writeAndRead($roundRobinClient, 'round_robin_key', 'round_robin_value'); // 加权轮询选择策略测试 $weightedRoundRobinContext = new SlaveSelectionContext($weightedRoundRobinStrategy); $weightedRoundRobinOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($weightedRoundRobinContext) { return $weightedRoundRobinContext->getSlave($replication); } ]; $weightedRoundRobinClient = new Client($parameters, $weightedRoundRobinOptions); writeAndRead($weightedRoundRobinClient, 'weighted_key', 'weighted_value'); // 最少连接数选择策略测试 $leastConnectionsContext = new SlaveSelectionContext($leastConnectionsStrategy); $leastConnectionsOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($leastConnectionsContext) { return $leastConnectionsContext->getSlave($replication); } ]; $leastConnectionsClient = new Client($parameters, $leastConnectionsOptions); writeAndRead($leastConnectionsClient, 'least_connections_key', 'least_connections_value'); // 响应时间选择策略测试 $responseTimeContext = new SlaveSelectionContext($responseTimeStrategy); $responseTimeOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($responseTimeContext) { return $responseTimeContext->getSlave($replication); } ]; $responseTimeClient = new Client($parameters, $responseTimeOptions); writeAndRead($responseTimeClient,'response_time_key','response_time_value'); // 该函数用于向 Redis 主库写入数据，并从从库读取数据进行验证 function writeAndRead(Client $client, $key, $value) { try { // 主库写入操作 $writeResult = $client->set($key, $value); // 输出写入结果 echo "写入结果: ". ($writeResult? "成功" : "失败"). PHP_EOL; // 从库读取操作 $readResult = $client->get($key); if ($readResult === null) { // 如果读取结果为空，输出提示信息 echo "Key '$key' does not exist." . PHP_EOL; } else { // 输出从从库读取到的值 echo "Value read from slave: ". $readResult. PHP_EOL; } } catch (\Predis\Connection\ConnectionException $e) { // 捕获连接异常并输出错误信息 echo "连接错误: ". $e->getMessage() . PHP_EOL; } }
超详细！ES 安装配置全攻略及 PHP 简单使用
遭遇了 “黑天鹅” 事件近期真是遭遇了 “黑天鹅” 事件，公司因卷入非法问题，面临一系列棘手处理，无奈之下只得遣散全体员工，我也因此瞬间失业。原本按部就班的工作节奏戛然而止，每天醒来，看着毫无安排的一天，心里满是茫然与失落。与其在这种不确定中焦虑徘徊，倒不如主动出击，给自己找点事儿做。我打算投身到文档撰写中，既能有效填补空闲时间，让生活重回正轨，也能借此梳理过往积累的知识与经验，为未来求职筑牢根基，让这段意外失业的日子，不至于虚度 。回顾上一篇我们一同探索了 Sphinx 这个强大的全文搜索引擎，从它的功能特性到安装配置，相信大家对如何利用 Sphinx 搭建高效搜索系统有了清晰认知。不过技术的世界从不止步，在搜索领域，还有一位 “大咖”——Elasticsearch（简称 ES），它凭借分布式、高扩展、实时搜索等卓越优势，在企业级应用中大放异彩。这一篇，就让我们把目光聚焦 ES，深入剖析它的安装配置流程，探寻如何在 PHP 开发环境里，巧妙借助 ES 的力量，实现更智能、更高速的数据检索，解锁不一样的搜索体验。什么是Elasticsearch？它有什么优缺点？和 sphinx 相比，差异在哪里？Elasticsearch 是基于 Lucene 的分布式开源搜索引擎，用于全文搜索、数据分析等。优点是分布式、实时性强、功能丰富。缺点是硬件要求高、索引维护成本高。与 Sphinx 相比，ES 分布式和扩展性更好，功能更全面；Sphinx 在特定场景性能较好，配置相对简单，更适合中小规模数据。安装和配置Elasticsearch官网下载地址服务器：CentOS7,Java版本21.0.2(Elasticsearch基于Java开发)没有Java环境的自行去下载合适的版本，并把java加到环境变量中。sudo vim /etc/profile export JAVA_HOME=/www/server/java/jdk-21.0.2 export PATH=$JAVA_HOME/bin:$PATH安装# 创建存放Elasticsearch软件目录 mkdir -p /usr/local/es/ # 进入/usr/local/es/文件夹 cd /usr/local/es/ # 下载对应的glibc版本的sphinx软件包 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.17.2-linux-x86_64.tar.gz # 解压 tar -zxvf elasticsearch-8.17.2-linux-x86_64.tar.gz # es 禁止以root用户运行，所以我们创建一个组和用户，用于启动es # 创建组 elasticsearch groupadd elasticsearch # 创建用户，指定用户所在的组 useradd youer -g elasticsearch # 设置用户密码 passwd youer配置# 进入/usr/local/es/elasticsearch-8.17.2/config 配置目录，修改以下配置 cd /usr/local/es/elasticsearch-8.17.2/config vim elasticsearch.yml # 找到以下配置并且修改 =============================================================================== # 集群名称，用于标识一个 Elasticsearch 集群， # 同一集群内的节点需要具有相同的集群名称才能相互通信并组成集群。 # 这里设置为 "my-es"，你可以根据实际情况修改为合适的名称。 cluster.name: my-es # 节点名称，用于在集群中唯一标识一个节点。 # 每个节点都应有一个独特的名称，这里设置为 "node-1"， # 可以根据实际需求进行调整。 node.name: node-1 # 绑定的网络地址，"0.0.0.0" 表示该节点将监听所有可用的网络接口， # 使得其他机器可以通过网络访问该 Elasticsearch 节点。 # 这在生产环境中需要谨慎配置，以确保安全性。 network.host: 0.0.0.0 # HTTP 服务端口，Elasticsearch 通过此端口提供 HTTP 接口供外部访问， # 这里设置为 9200，是 Elasticsearch 默认的 HTTP 端口， # 可根据实际情况修改，但要避免与其他服务端口冲突。 http.port: 9200 # 种子节点列表，用于在节点启动时发现集群中的其他节点。 # 这里指定了 "127.0.0.1"，表示在本地寻找其他节点。 # 在分布式集群环境中，应填写其他实际节点的 IP 地址。 discovery.seed_hosts: ["127.0.0.1"] # 初始化主节点列表，用于在集群启动时确定哪些节点有资格成为主节点。 # 这里设置为 ["node-1"]，意味着 node-1 节点有资格成为主节点。 # 主节点负责管理集群的状态和元数据等重要任务。 cluster.initial_master_nodes: ["node-1"] # 这一行被注释掉了，原本是用于控制系统调用过滤器的配置。 # bootstrap.system_call_filter: false # 禁止节点在启动时锁定内存。 # Elasticsearch 默认会尝试锁定内存以提高性能，但在某些环境下可能会失败。 # 设置为 false 表示不进行内存锁定，可根据实际情况调整。 bootstrap.memory_lock: false # 启用 HTTP CORS（跨域资源共享）功能。 # 当设置为 true 时，允许来自不同域名的请求访问 Elasticsearch 的 HTTP 接口。 # 这在浏览器或其他跨域访问场景中很有用。 http.cors.enabled: true # 允许的跨域来源，这里设置为 "*" 表示允许所有来源的请求。 # 在生产环境中，建议明确指定具体的域名，以提高安全性。 http.cors.allow-origin: "*" ===============================================================================Elasticsearch配置JVM参数Elasticsearch配置JVM参数合理配置JVM参数对其性能和稳定性至关重要。-Xms 设定JVM堆内存初始大小，避免频繁内存分配；-Xmx 限制堆内存最大是多少，防止过度占用系统资源。建议二者设为相同值，依实际情况调整。cd /usr/local/es/elasticsearch-8.17.2/config vim jvm.options # 我的设置是根据我服务器的配置来的，你们根据实际情况而定 -Xms512m -Xmx512m增加ES最大文件、进程数限制，保其稳定运行/etc/security/limits.conf 通用资源限制配置文件，可全面设限。/etc/security/limits.d/20-nproc.conf 用于补充细化，配置优先级或更高。sudo vim /etc/security/limits.conf ============================================ # 指定用户设置软限制的最大打开文件描述符数量 youer soft nofile 65536 # 指定用户设置硬限制的最大打开文件描述符数量 youer hard nofile 65536 # 指定用户设置软限制的最大进程数量 youer soft nproc 4096 # 指定用户设置硬限制的最大进程数量 youer hard nproc 4096 ============================================sudo vim /etc/security/limits.conf ====================================== youer soft nproc 4096 youer hard nproc 4096 ====================================== 或者 ===================================== * soft nproc 4096 root soft nproc unlimited =====================================调整系统的内核参数,保证Elasticsearch应用跑起来sudo vim /etc/sysctl.conf ================================ # 设置虚拟内存中最大内存映射区域的数量 # 对于像 Elasticsearch 这类应用，在启动和运行过程中会进行大量的内存映射操作 # 若该值过小，可能会导致 Elasticsearch 启动失败或运行过程中出现内存映射错误 # 这里将其设置为 655360，为应用提供更充足的内存映射资源 vm.max_map_count=655360 # 设置系统允许的最大文件数 # 系统在运行时会涉及大量文件的打开、读写等操作 # 增大该值可以确保系统有足够的资源来处理更多的文件操作 # 避免因文件描述符耗尽而导致应用无法正常工作 # 这里设置为 65535 fs.file-max = 65535 ================================ # 读取配置并应用新的内核参数，使修改后的配置立即生效 sudo sysctl -p安装ik插件ik插件下载，下载与之对应的ik中文分词插件cd /usr/local/es/ wget https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-8.17.2.zip unzip elasticsearch-analysis-ik-8.17.2.zip mv elasticsearch-analysis-ik-8.17.2 /usr/local/es/elasticsearch-8.17.2/plugins/权限# 给文件夹赋权和用户以及组 sudo chown -R youer:elasticsearch /usr/local/es/elasticsearch-8.17.2 sudo chmod -R 755 /usr/local/es/elasticsearch-8.17.2启动# 切换到youer用户 su youer # 启动es cd /usr/local/es/elasticsearch-8.17.2/bin ./elasticsearch -d查看端口和es服务# 查看端口 ps -ef | grep elasticsearch|grep -v "grep" netstat -an | grep 9200 # 对外防火墙放开端口 # 查询是否开启 sudo firewall-cmd --query-port=9200/tcp # 开启 sudo firewall-cmd --add-port=9200/tcp --permanent # 重启生效 sudo firewall-cmd --reloadphp使用安装类库 composer require elasticsearch/elasticsearchdeclare(strict_types=1); set_time_limit(-1); require __DIR__ . "./vendor/autoload.php"; use Elastic\Elasticsearch\ClientBuilder; try { $client = ClientBuilder::create() ->setHosts(['http://142.171.102.86:9200']) ->setSSLVerification(false) ->build(); // 定义用于创建 Elasticsearch 索引的参数数组 $t = $_GET['type'] ?? 1; switch ($t) { case 1://创建索引时配置 IK 分词器 $params = [ // 指定要创建的索引的名称为 my_ik_index // 在 Elasticsearch 里，索引是存储文档的逻辑容器，类似关系型数据库中的数据库 // 后续对该索引的操作（如添加文档、搜索等）都会使用此名称定位 'index' => 'my_ik_index', // 包含索引设置和映射信息的数组，是创建索引请求的核心部分 'body' => [ // 用于设置索引的各种参数，这里主要关注与分词分析相关的设置 'settings' => [ // 配置索引的分析器和分词器 // 分析器负责将文本转换为适合索引和搜索的词项，分词器是分析器的核心，负责拆分文本 'analysis' => [ // 定义自定义分析器的部分，可以配置多个分析器，每个有唯一名称 'analyzer' => [ // 自定义分析器 ik_max_word_analyzer 'ik_max_word_analyzer' => [ // 表示这是一个自定义分析器 // 除内置分析器外，可自定义组合分词器、过滤器等创建特定分析器 'type' => 'custom', // 指定该分析器使用 ik_max_word 作为分词器 // ik_max_word 是 IK 分词器的一种模式，会对文本做最细粒度拆分 'tokenizer' => 'ik_max_word' ], // 自定义分析器 ik_smart_analyzer 'ik_smart_analyzer' => [ // 表示这是一个自定义分析器 'type' => 'custom', // 指定该分析器使用 ik_smart 作为分词器 // ik_smart 是 IK 分词器的另一种模式，会对文本做最粗粒度拆分 'tokenizer' => 'ik_smart' ] ] ] ], // 定义索引中文档的结构和字段类型，以及每个字段使用的分析器 'mappings' => [ // 定义索引中各个字段的属性，每个字段有唯一名称，可配置类型、分词器等 'properties' => [ // 定义 title 字段 'title' => [ // 指定 title 字段的类型为 text，用于存储文本数据 // text 类型字段在索引时会进行分词处理以支持全文搜索 'type' => 'text', // 指定在对 title 字段进行索引时使用 ik_max_word_analyzer 分析器 // 这样在添加文档到索引时，title 字段文本会被 ik_max_word 分词器细粒度拆分 'analyzer' => 'ik_max_word_analyzer', // 指定在对 title 字段进行搜索时使用 ik_smart_analyzer 分析器 // 搜索时，搜索关键词会被 ik_smart 分词器粗粒度拆分以提高搜索准确性 'search_analyzer' => 'ik_smart_analyzer' ], // 定义 content 字段 'content' => [ // 指定 content 字段的类型为 text，用于存储文本数据 'type' => 'text', // 指定在对 content 字段进行索引时使用 ik_max_word_analyzer 分析器 // 索引时，content 字段文本会被 ik_max_word 分词器细粒度拆分 'analyzer' => 'ik_max_word_analyzer', // 指定在对 content 字段进行搜索时使用 ik_smart_analyzer 分析器 // 搜索时，搜索关键词会被 ik_smart 分词器粗粒度拆分以提高搜索准确性 'search_analyzer' => 'ik_smart_analyzer' ] ] ] ] ]; $response = $client->indices()->create($params); if ($response['acknowledged']) { echo "索引创建成功！"; } else { echo "索引创建失败！"; } break; case 2://添加文档 $params = [ 'index' => 'my_ik_index', 'id' => '1', 'body' => [ 'title' => '自然语言处理技术', 'content' => '自然语言处理是人工智能的一个重要领域。' ] ]; $response = $client->index($params); // 判断添加文档操作是否成功 if (isset($response['result']) && $response['result'] === 'created') { echo "文档添加成功，文档 ID 为: " . $response['_id']; } else { echo "文档添加失败，错误信息: " . json_encode($response); } break; case 3://搜索文档 $query = "领域"; $params = [ 'index' => 'my_ik_index', 'body' => [ 'query' => [ 'multi_match' => [ 'query' => $query, 'fields' => ['title', 'content'], 'analyzer' => 'ik_smart_analyzer' ] ] ] ]; $response = $client->search($params); if (isset($response['hits']['total']['value']) && $response['hits']['total']['value'] > 0) { echo "搜索成功，共找到 " . $response['hits']['total']['value'] . " 个匹配文档： "; foreach ($response['hits']['hits'] as $hit) { $source = json_encode($hit['_source'], JSON_UNESCAPED_UNICODE); $source = str_replace($query, "{$query}",$source); echo "文档 ID: " . $hit['_id'] . ", 得分: " . $hit['_score'] . ", 内容: " . $source . " "; } } elseif (isset($response['hits']['total']['value']) && $response['hits']['total']['value'] === 0) { echo "搜索成功，但未找到匹配的文档。"; } else { echo "搜索失败，错误信息: " . json_encode($response); } break; case 4://删除文档 $params = [ 'index' => 'my_index' ]; // 执行删除索引操作 $response = $client->indices()->delete($params); // 判断删除是否成功 if (isset($response['acknowledged']) && $response['acknowledged'] === true) { echo "索引删除成功。"; } else { echo "索引删除失败，错误信息: " . json_encode($response); } break; case 5://获取集群健康状态 // 调用 _cluster/health API 获取集群健康状态 $params = [ 'index' => null, // 不指定特定索引，获取整个集群的健康状态 ]; $response = $client->cluster()->health($params); // 解析和处理返回结果 $status = $response['status']; echo "Elasticsearch 集群健康状态: ". $status. "\n"; switch ($status) { case 'green': echo "集群状态良好，所有主分片和副本分片都可用。\n"; break; case 'yellow': echo "集群状态正常，但存在部分副本分片不可用。\n"; break; case 'red': echo "集群状态异常，存在主分片不可用的情况。\n"; break; default: echo "未知的集群健康状态。\n"; } break; } } catch (Exception $e) { echo "发生错误：" . $e->getMessage(); } 查询结果访问 http://142.171.102.86:9200查询
一步到位！Sphinx 安装配置全流程，PHP 轻松对接
遭遇了 “黑天鹅” 事件近期真是遭遇了 “黑天鹅” 事件，公司因卷入非法问题，面临一系列棘手处理，无奈之下只得遣散全体员工，我也因此瞬间失业。原本按部就班的工作节奏戛然而止，每天醒来，看着毫无安排的一天，心里满是茫然与失落。与其在这种不确定中焦虑徘徊，倒不如主动出击，给自己找点事儿做。我打算投身到文档撰写中，既能有效填补空闲时间，让生活重回正轨，也能借此梳理过往积累的知识与经验，为未来求职筑牢根基，让这段意外失业的日子，不至于虚度 。什么是Sphinx? 有什么优缺点呢？phinx 是开源全文搜索引擎。优点是高性能、功能丰富、可扩展性好、多语言支持、开源成本低；缺点是配置复杂、对内存要求高、实时性有限、缺乏图形化管理界面。安装和配置sphinx官网下载页面服务器：CentOS7，glibc版本2.17安装# 创建存放sphinx软件目录 mkdir -p /usr/local/sphinx/ # 进入/usr/local/sphinx/文件夹 cd /usr/local/sphinx/ # 下载对应的glibc版本的sphinx软件包 wget https://sphinxsearch.com/files/sphinx-3.7.1-da9f8a4-linux-amd64-glibc2.17.tar.gz # 解压 tar -zxvf sphinx-3.7.1-da9f8a4-linux-amd64-glibc2.17.tar.gz配置# 进入到配置目录 cd /usr/local/sphinx/sphinx-3.7.1/etc/ # 创建文件，把配置粘贴进去 vim sphinx.conf ====================================================== # 通用配置部分，定义一些全局的设置 common { # 数据目录，用于存放索引文件和其他相关数据 datadir = ./sphinxdata } # 数据源配置，这里以 MySQL 数据库为例 source names_1_source { # 数据源类型为 MySQL type = mysql # 数据库主机地址，使用 127.0.0.1 以确保端口配置生效 sql_host = 127.0.0.1 # 数据库用户名 sql_user = active # 数据库密码 sql_pass = PcjcrN53sR8bdZyx # 要连接的数据库名 sql_db = active # 数据库端口号，默认是 3306 sql_port = 3306 # SQL 查询语句，用于从数据库中获取要索引的数据 sql_query = SELECT id, surname, name, first, second, gender, type, score, details FROM names_1 # 在执行查询前设置字符集为 UTF-8，确保中文等字符能正确处理 sql_query_pre = SET NAMES utf8mb4 } # 索引配置 index names_1_index { # 索引类型为普通索引 type = plain # 关联的数据源 source = names_1_source # 要进行全文搜索的字段 field = surname, details # 定义属性，用于排序、过滤等操作 attr_uint = gender, type # 无符号整数类型的属性 attr_float = score # 浮点数类型的属性 attr_string = name, first, second # 字符串类型的属性 # 字符集表，涵盖了数字、英文字母（大小写统一）、中文、日文、韩文等字符范围 # charset_table = # 最小词长度，设置为 1 表示单字符也会被索引，适用于中文和单字母搜索 min_word_len = 1 # 词形处理方式，这里设置为不进行词形处理 morphology = none # N - gram 长度，设置为 1 表示按单字符进行 N - gram 索引，有助于中文搜索 ngram_len = 1 # 指定用于 N - gram 索引的字符范围，这里包含了中文、日文、韩文等字符 ngram_chars = U+4E00..U+9FBB, U+3400..U+4DB5, U+20000..U+2A6D6, U+FA0E, U+FA0F, U+FA11, U+FA13, U+FA14, U+FA1F, U+FA21, U+FA23, U+FA24, U+FA27, U+FA28, U+FA29, U+3105..U+312C, U+31A0..U+31B7, U+3041, U+3043, U+3045, U+3047, U+3049, U+304B, U+304D, U+304F, U+3051, U+3053, U+3055, U+3057, U+3059, U+305B, U+305D, U+305F, U+3061, U+3063, U+3066, U+3068, U+306A..U+306F, U+3072, U+3075, U+3078, U+307B, U+307E..U+3083, U+3085, U+3087, U+3089..U+308E, U+3090..U+3093, U+30A1, U+30A3, U+30A5, U+30A7, U+30A9, U+30AD, U+30AF, U+30B3, U+30B5, U+30BB, U+30BD, U+30BF, U+30C1, U+30C3, U+30C4, U+30C6, U+30CA, U+30CB, U+30CD, U+30CE, U+30DE, U+30DF, U+30E1, U+30E2, U+30E3, U+30E5, U+30E7, U+30EE, U+30F0..U+30F3, U+30F5, U+30F6, U+31F0, U+31F1, U+31F2, U+31F3, U+31F4, U+31F5, U+31F6, U+31F7, U+31F8, U+31F9, U+31FA, U+31FB, U+31FC, U+31FD, U+31FE, U+31FF, U+AC00..U+D7A3, U+1100..U+1159, U+1161..U+11A2, U+11A8..U+11F9, U+A000..U+A48C, U+A492..U+A4C6 } # 搜索服务配置 searchd { # 监听的地址和端口，客户端通过此端口连接搜索服务 listen = 9312 # 搜索服务的日志文件路径，记录服务的运行信息 log = no # 查询日志文件路径，记录用户的查询请求，方便后续分析和优化 query_log = no # 进程 ID 文件路径，用于管理搜索服务进程 # pid_file = "" # 启用无缝索引切换，在重建索引时不影响搜索服务的正常运行 seamless_rotate = 1 # 是否预先打开所有索引文件，根据服务器内存和性能情况调整 preopen_indexes = 0 # 自动删除旧的索引文件，节省磁盘空间 unlink_old = 1 # 客户端连接超时时间（秒） client_timeout = 30 } ======================================================测试 数据库active下的表names_1大概是这样的启动sphinx和停止# 可读权限 sudo chmod -R a+r /usr/local/sphinx/sphinx-3.7.1 # 启动 ./searchd -c /usr/local/sphinx/sphinx-3.7.1/etc/sphinx.conf # 停止 ./searchd --stop -c /usr/local/sphinx/sphinx-3.7.1/etc/sphinx.conf创建和更新索引sudo chmod -R a+r /usr/local/sphinx/sphinx-3.7.1 sudo chmod u+w /usr/local/sphinx/sphinx-3.7.1/bin/indexer cd /usr/local/sphinx/sphinx-3.7.1/bin/ ./indexer -c /usr/local/sphinx/sphinx-3.7.1/etc/sphinx.conf --all --rotate # ./indexer是 Sphinx 索引器程序。 # -c /usr/local/sphinx/sphinx-3.7.1/etc/sphinx.conf指定配置文件路径。 # --all对配置里所有索引操作。 # --rotate在searchd运行时无缝更新索引，不中断搜索服务。查看端口和sphinx服务# 查看端口 ps -ef | grep searchd|grep -v "grep" netstat -an | grep 9312 # 对外防火墙放开端口 # 查询是否开启 sudo firewall-cmd --query-port=9312/tcp # 开启 sudo firewall-cmd --add-port=9312/tcp --permanent # 重启生效 sudo firewall-cmd --reloadphp 使用首先复制一份/usr/local/sphinx/sphinx-3.7.1/api/sphinxapi.php 文件到你的项目目录。<?php declare(strict_types=1); require __DIR__.'./vendor/autoload.php'; require __DIR__."/sphinxapi.php"; use Fukuball\Jieba\Jieba; //use Fukuball\Jieba\Posseg; ini_set('memory_limit','512M'); //分词 Jieba::init(); //Posseg::init(); $searchQuery = 'Firaxis'; if (hasChinese($searchQuery)) { $cutWord = Jieba::cut($searchQuery); $searchQuery = implode('|', $cutWord); } echo '查找关键词：'.$searchQuery.' '; // 创建 Sphinx 客户端对象 $cl = new SphinxClient(); // 设置 Sphinx 服务器的主机和端口 $cl->SetServer('127.0.0.1',9312); $t = $_GET['type'] ?? 1; switch ($t) { case 1: // 设置搜索的偏移量和返回结果数量 $cl->SetLimits(0, 10); // 设置排序模式为按相关性排序 $cl->SetSortMode(SPH_SORT_RELEVANCE); $index = 'names_1_index'; // $cl->SetFilterString('details','Firaxis'); $result = $cl->query($searchQuery, $index); if ($result === false) { echo '查询错误: '. $cl->GetLastError(); } else { // 处理查询结果 if ($result['total'] > 0) { foreach ($result['matches'] as $id => $match) { echo '文档ID: '. $id. ',姓名：'.$match['attrs']['name'].',score：'.$match['attrs']['score']. ',相关性得分: '. $match['weight']. ' '; // 可以根据索引中的字段获取更多信息 } } else { echo '没有找到匹配的结果'; } } break; case 2: //TODO 更新数据库 $index = 'names_1_index'; $attrs = ['score']; $values = [ 4048551 => [100], // 文档 ID 为 1 的文档，将 'price' 属性更新为 100 4048552 => [200] // 文档 ID 为 2 的文档，将 'price' 属性更新为 200 ]; $updated = $cl->UpdateAttributes($index, $attrs, $values); if ($updated >= 0) { echo "成功更新 $updated 条文档的属性。"; } else { echo "更新属性时出错: ". $cl->GetLastError(); } break; } function hasChinese($str) { for ($i = 0; $i < mb_strlen($str); $i++) { $char = mb_substr($str, $i, 1); $ord = mb_convert_encoding($char, 'UCS-2BE', 'UTF-8'); $code = hexdec(bin2hex($ord)); if ($code >= 0x4E00 && $code <= 0x9FA5) { return true; } } return false; } 查询结果[post cid="388" /]
当你使用get_browser函数时出现一下错误提示Warning: get_browser(): browscap ini directive not set in ...因为get_browser的实现原理是通过查找browscap.ini配置文件中的浏览器信息，从而尝试检测用户的浏览器所具有的功能。既然依赖于browscap.ini配置文件，就需要去下载这个配置文件，因为这个浏览器版本等都是处于经常变化的状态，所以这种文件必须到browscap官网下载而且保持更新。下载browscap配置文件，版本自行根据需要下载，然后配置php.ini，重启php-fpm[browscap] browscap=/www/server/php/74/etc/lite_php_browscap.ini下载地址这是browscap.ini的一个特殊版本，仅供PHP用户使用！php_browscap.ini (23,321 KB)这是php_browscap.ini的一个更大版本，包含了所有新属性。full_php_browscap.ini (113,097 KB)这是php_browscap.ini文件的较小版本，包含主要的浏览器和搜索引擎。此文件适用于大多数网站。lite_php_browscap.ini (1,114 KB)
所谓的压缩，无非就是把换行空格等符号替换为空css<?php header('Content-type: text/css'); ob_start("compress"); function compress($buffer) { $buffer = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $buffer); $buffer = str_replace(array(" ", "\r", "\n", "\t", ' ', ' ', ' '), '', $buffer); return $buffer; } //文件太多的话可以自行写一个递归 include('第一个CSS文件'); //例如当前PHP文件下的 ./test.css include('第二个CSS文件'); //复制调用更多css文件 ./test1.css ob_end_flush(); //浏览器打开，输出压缩后的内容 ?>js<?php error_reporting(E_ALL & ~E_NOTICE); if (extension_loaded('zlib')) { ob_start('ob_gzhandler'); } header("content-type:application/x-javascript; charset: UTF-8"); header("cache-control: must-revalidate"); $offset = 60 * 60 * 24; //js文件的距离现在的过期时间，这里设置为一天 $expire = "expires: " . gmdate("D, d M Y H:i:s", time() + $offset) . " GMT"; header($expire); ob_start("compress"); function compress($buffer) { $buffer = preg_replace('/\/\/.*/', '', $buffer); $buffer = str_replace(array(" ", "\r", "\n", "\t", ' ', ' ', ' '), '', $buffer); return $buffer; } include('js文件一'); //例如当前PHP文件下的main.js include('js文件二'); //复制调用更多JS文件 if (extension_loaded('zlib')) { ob_end_flush(); //输出压缩后的内容 } ?>
(防抖) 前后端防重复提交常用的那些方法
防抖（Debounce）是一种防止重复提交的策略，它通过延迟一定时间来合并连续的操作，以确保只执行一次。前端 JavaScript 实现function debounce(func, delay) { let timer; return function() { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, arguments); }, delay); }; } const submitForm = debounce(function() { // 在这里执行表单提交操作 }, 1000); // 延迟 1 秒执行 Submit 防抖Session Token防抖// 生成随机的 token $token = md5(uniqid()); // 存储 token 到会话 $_SESSION['submit_token'] = $token; // 在表单中嵌入 token echo ' '; // 处理表单提交时的校验 if ($_POST['submit_token'] === $_SESSION['submit_token']) { // 处理表单提交 // 清除会话中的 token unset($_SESSION['submit_token']); }Token防抖（利用随机生成的 token 来防止重复提交）$token = md5(uniqid()); echo ' '; if ($_POST['submit_token'] === $token) { // 处理表单提交 }时间戳防抖$currentTime = time(); $lastSubmitTime = $_SESSION['last_submit_time'] ?? 0; if ($currentTime - $lastSubmitTime > 5) { // 处理表单提交 $_SESSION['last_submit_time'] = $currentTime; } IP地址防抖 （利用IP地址来防止相同IP的重复提交）$userIP = $_SERVER['REMOTE_ADDR']; $lastSubmitIP = $_SESSION['last_submit_ip'] ?? ''; if ($userIP !== $lastSubmitIP) { // 处理表单提交 $_SESSION['last_submit_ip'] = $userIP; } Cookie防抖 （利用Cookie来防止在一段时间内的重复提交）if (!isset($_COOKIE['submit_cookie'])) { // 处理表单提交 setcookie('submit_cookie', 'submitted', time() + 60); // 60秒内不允许重复提交 }延时防抖（在最后一次操作后的一段时间内，只执行一次提交操作）if (!isset($_SESSION['submit_timer'])) { $_SESSION['submit_timer'] = time(); } if (time() - $_SESSION['submit_timer'] > 10) { // 处理表单提交 $_SESSION['submit_timer'] = time(); } 数据库唯一性约束（利用数据库的唯一性约束来防止重复插入数据）try { // 尝试插入数据，如果插入失败会抛出异常 // 添加数据库的唯一性索引或唯一性约束以防止重复数据 } catch (Exception $e) { // 处理插入失败的情况 } 缓存防抖（利用缓存系统来记录提交状态）$cacheKey = 'submit_status_' . $userIP; if (!cache_get($cacheKey)) { // 处理表单提交 cache_set($cacheKey, 'submitted', 60); // 60秒内不允许重复提交 }验证码防抖（要求用户输入特定的验证码来提交表单，防止恶意重复提交）if ($_POST['captcha'] === $_SESSION['captcha_code']) { // 处理表单提交 // 清除验证码，以防止多次使用同一个验证码 unset($_SESSION['captcha_code']); }
为什么需要API管理系统自从互联网服务开发以来，作为开发者阵营的我们，已经在实践中证明了前端和后端分离开发模型正逐渐成为越来越多的互联网公司构建服务和应用程序的一种方式。前端和后端分离有许多优点。一个重要的优点是，对于后台服务（系统），它只需要提供一个统一的API接口，即可被多个客户端复用，并且分工和协作更加精细。效率大大提高。与此同时带来的一些缺点，如下：接口文档管理混乱。以前，许多公司管理API接口，有用的Wiki，Word文档和HTML。经常引起的问题有：例如添加参数，更改参数名称，删除参数等未及时更新文档的种种情况。接口测试没有保障。毕竟，前端开发依赖于后端接口。如果前端和后端开发不同步，则及时测试接口将成为问题。因此，需要随时提供一组可用的API接口数据测试服务。资源分散，难以共享。每个开发人员都维护自己的一组测试接口集合，并且不能共享其他接口集合。开发过程中充满了很多重复的数据，填api的工作，且效率不高。其他问题。除此之外还有可能碰到诸如 文档导出、接口分类规划、操作便利性等一系列问题。因此接下来小编就来给大家推荐几个常用的 API管理系统，帮助前后端分离开发模式下提升效率和可靠性，总有一个你看得上！ShowDocShowDoc 是一个非常适合IT团队的在线文档分享工具，它可以加快团队之间沟通的效率。免费开源！支持如下特性：API文档： 随着移动互联网的发展，BaaS（后端即服务）越来越流行。服务端提供API，APP端或者网页前端便可方便调用数据。用 ShowDoc可以非常方便快速地编写出美观的API文档。数据字典： 一份好的数据字典可以很方便地向别人说明你的数据库结构，如各个字段的释义等。说明文档： 你完全可以使用showdoc来编写一些工具的说明书,也可以编写一些技术规范说明文档以供团队查阅官网地址易文档从需求文档、API文档、部署文档到使用手册，支持多种定制文档编辑器；同时也支持接口在线测试，一键生成文档、调用示例、mock配置。官网地址SwaggerSwagger是用于生成，描述，调用和可视化RESTful样式Web服务的规范和完整框架。 它可以与诸如Spring MVC和Spring Boot之类的框架快速集成，并且通过组合诸如Swagger-UI之类的组件，可以轻松可视化控制器层的方法，例如方法注释，方法参数和方法返回值等提供相应的用户界面。官网地址eolinkereolinker也非常易于使用。 用户界面很好看。 这是一个现成的API研发管理解决方案。 0代码实现API自动测试。功能特性：开箱即用的API研发管理方案：无需繁琐的配置。它支持阅读代码注释以生成API文档，或通过UI界面快速创建全面的API文档。通过Mock API，API更改通知和版本管理等服务使团队更加敏捷。强大的“0代码”API测试 / 自动化测试：全面支持HTTPS，Restful，Web Service和其他类型的API。强大的API自动化测试和用例管理功能，使您无需编写代码即可实现API自动化测试，实时生成测试报告，并提高测试覆盖率。实用的团队协作功能：提供强大的人员权限管理功能，可以为不同成员设置允许的操作，系统会自动记录所有成员的操作日志。并可以通过丰富的插件系统扩展现有的系统功能。开放的平台设计：一键导入产品数据，例如Postman，Swagger等，使您可以快速享受EOLINKER API Studio的服务。您还可以通过Open API将EOLINKER API Studio对接到各种企业内部系统，以实现开发，测试，操作和维护集成。支持离线的私有化部署：EOLINKER API Studio支持离线私有化部署。产品和数据存储在企业中。您可以根据部门/分支机构设置隔离的工作空间，并通过Open API与Jenkins等各种持续集成系统对接。官网地址YApiYApi是一个高效，易于使用且功能强大的api管理平台，旨在为开发人员，产品和测试人员提供更优雅的界面管理服务。 它可以帮助开发人员轻松创建，发布和维护API。 此外，YApi还为用户提供了出色的交互体验。 开发人员只需要使用平台提供的接口数据编写工具和简单的点击操作即可实现接口管理。功能特性：基于 Json5 和 Mockjs 定义接口返回数据的结构和文档，效率提升多倍扁平化权限设计，即保证了大型企业级项目的管理，又保证了易用性类似 postman 的接口调试自动化测试, 支持对 Response 断言MockServer 除支持普通的随机 mock 外，还增加了 Mock 期望功能，根据设置的请求过滤规则，返回期望数据支持 postman, har, swagger 数据导入免费开源，内网部署，信息再也不怕泄露了官网地址
什么是MongoDBMongoDB 是一个介于关系数据库和非关系数据库之间的产品，是非关系数据库当中功能最丰富，最像关系数据库的。他支持的数据结构非常松散，是类似json的bson格式，因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大，其语法有点类似于面向对象的查询语言，几乎可以实现类似关系数据库单表查询的绝大部分功能，而且还支持对数据建立索引。特点高性能、易部署、易使用，存储数据非常方便。面向集合存储，易存储对象类型的数据。模式自由。支持动态查询。支持完全索引，包含内部对象。支持查询。支持复制和故障恢复。使用高效的二进制数据存储，包括大型对象（如视频等）。自动处理碎片，以支持云计算层次的扩展性。支持RUBY，PYTHON，JAVA，C ，PHP，C#等多种语言。文件存储格式为BSON（一种JSON的扩展）。使用原理- 所谓“面向集合”（Collection-Oriented），意思是数据被分组存储在数据集中，被称为一个集合（Collection)。每个集合在数据库中都有一个唯一的标识名，并且可以包含无限数目的文档。 - 集合的概念类似关系型数据库（RDBMS）里的表（table），不同的是它不需要定义任何模式（schema)。Nytro MegaRAID技术中的闪存高速缓存算法，能够快速识别数据库内大数据集中的热数据，提供一致的性能改进。 - 模式自由（schema-free)，意味着对于存储在mongodb数据库中的文件，我们不需要知道它的任何结构定义。如果需要的话，你完全可以把不同结构的文件存储在同一个数据库里。 - 存储在集合中的文档，被存储为键-值对的形式。键用于唯一标识一个文档，为字符串类型，而值则可以是各种复杂的文件类型。我们称这种存储形式为BSON（Binary Serialized Document Format）。 适用场景- MongoDB 的主要目标是在键/值存储方式（提供了高性能和高度伸缩性）和传统的RDBMS 系统（具有丰富的功能）之间架起一座桥梁，它集两者的优势于一身。根据官方网站的描述，Mongo 适用于以下场景。 - 网站数据：Mongo 非常适合实时的插入，更新与查询，并具备网站实时数据存储所需的复制及高度伸缩性。 - 缓存：由于性能很高，Mongo 也适合作为信息基础设施的缓存层。在系统重启之后，由Mongo 搭建的持久化缓存层可以避免下层的数据源过载。 - 大尺寸、低价值的数据：使用传统的关系型数据库存储一些数据时可能会比较昂贵，在此之前，很多时候程序员往往会选择传统的文件进行存储。 - 高伸缩性的场景：Mongo 非常适合由数十或数百台服务器组成的数据库，Mongo 的路线图中已经包含对MapReduce 引擎的内置支持。 - 用于对象及JSON 数据的存储：Mongo 的BSON 数据格式非常适合文档化格式的存储及查询。 不适场景- 高度事务性的系统：例如，银行或会计系统。传统的关系型数据库目前还是更适用于需要大量原子性复杂事务的应用程序。 - 传统的商业智能应用：针对特定问题的BI 数据库会产生高度优化的查询方式。对于此类应用，数据仓库可能是更合适的选择。 - 需要SQL 的问题。 Mognodb数据库连接<?php // 默认格式 (这里采用默认连接本机的27017端口，当然也可以连接远程主机。如 192.168.0.4:27017,如果端口是27017，端口可以省略。) $m = new Mongo(); //实例 (数据库的用户名和密码都是admin) $m = new Mongo("mongodb://127.0.0.1:27017/admin:admin");插入数据<?php //这里采用默认连接本机的27017端口，当然你也可以连接远程主机如192.168.0.4:27017 //如果端口是27017，端口可以省略 $m = new Mongo("mongodb://127.0.0.1:27017/admin:admin"); //选择comedy数据库，如果以前没该数据库会自动创建，也可以用$m->selectDB("comedy"); $db = $m->comedy; //选择comedy里面的collection集合，相当于RDBMS里面的表，也可以使用 $collection = $db->collection; $db->selectCollection("collection"); /*********添加一个元素**************/ $obj = array("title" => "php1", "author" => "Bill Watterson"); //将$obj 添加到$collection 集合中 $collection->insert($obj); /*********添加另一个元素**************/ $obj = array("title" => "huaibei", "online" => true); $collection->insert($obj); //$query = array("title" => "huaibei"); $query = array("_id" => $obj['_id']); $cursor = $collection->find($query); //遍历所有集合中的文档 foreach ($cursor as $obj) { echo $obj["title"] . "\n"; echo $obj["_id"] . "\n"; } //断开MongoDB连接 $m->close();带条件的查询mysql: id = 123 mongo: array(‘id'=>123) mysql: name link '%bar%' mongo: array(‘name' => new MongoRegex(‘/.*bar.*/i')) mysql: where id > 10 mongo: array(‘id' => array(‘$gt' => 10)) mysql: where id >= 10 mongo: array(‘id' => array(‘$gte' => 10)) mysql: where id < 10 mongo: array(‘id' => array(‘$lt' => 10)) mysql: where id <= 10 mongo: array(‘id' => array(‘$lte' => 10)) mysql: where id > 1 and id < 10 mongo: array(‘id' => array(‘$gt' => 1,'$lt' => 10)) mysql: where id <> 10 mongo: array(‘id' => array(‘$ne' => 10)) mysql: where id in(123) mongo: array(‘id' => array(‘$in' => array(1,2,3))) mysql: where id not in(123) mongo: array(‘id' => array(‘$nin' => array(1,2,3))) mysql: where id = 2 or id = 9 mongo: array(‘id' => array(‘$or' => array(array(‘id'=>2),array(‘id'=>9)))) mysql: order by name asc mongo: array(‘sort'=>array(‘name'=>1)) mysql: order by name desc mongo: array(‘sort'=>array(‘name'=>-1)) mysql: limit 0,2 mongo: array(‘limit'=>array(‘offset'=>0,'rows'=>2)) mysql: select name,email mongo: array(‘name','email') mysql: select count(name) mongo: array(‘COUNT') //注意：COUNT为大写查询时，每个Object插入时都会自动生成一个独特的_id,它相当于RDBMS中的主键，用于查询时非常方便 （_id每一都不同，很像自动增加的id）<?php $param = array("name" => "joe"); $collection->insert($param); $joe = $collection->findOne(array("_id" => $param['_id'])); print_R($joe); $m->close(); //返回结果：Array ( [_id] => MongoId Object ( [$id] => 4fd30e21870da83416000002 ) [name] => joe )更改字段值<?php $sign = array("title" => 'php1'); $param = array("title" => 'php1','author'=>'test'); $joe = $collection->update($sign, $param);删除一个数据库$m -> dropDB(“comedy”);列出所有可用数据库$m->listDBs(); //无返回值创建一个MongoDB对象<?php $mo = new Mongo(); $db = new MongoDB($mo,'dbname');//通过创建方式获得一个MongoDB对象删除当前DB<?php $db = $mo->dbname; $db->drop();获得当前数据库名<?php $db = $mo->dbname; $db->_tostring();选择想要的collection//A: $mo = new Mongo(); $coll = $mo->dbname->collname;//获得一个collection对象 //B： $db = $mo->selectDB('dbname'); $coll = $db->collname; //C: $db = $mo->dbname; $coll = $db->collname; //D: $db = $mo->dbname; $coll = $db->selectCollectoin('collname');//获得一个collection对象插入数据（MongoCollection对象）$coll = $mo->db->foo; $a = array('a'=>'b'); $options = array('safe'=>true); $rs =$coll->insert($a,$options);删除数据库中的记录（MongoCollection对象）$coll = $mo->db->coll; $c = array('a'=>1,'s'=>array('$lt'=>100)); $options = array('safe'=>true);更新数据库中的记录（MongoCollection对象）$coll = $mo->db->coll; $c = array('a'=>1,'s'=>array('$lt'=>100)); $newobj = array('e'=>'f','x'=>'y'); $options = array('safe'=>true,'multiple'=>true);查询collection获得单条记录（MongoCollection类）$coll = $mo->db->coll; $query = array('s'=>array('$lt'=>100)); $fields = array('a'=>true,'b'=>true); $rs = $coll->findOne($query,$fields);查询collection获得多条记录（MongoCollection类）$coll = $mo->db->coll; $query = array('s'=>array('$lt'=>100)); $fields = array('a'=>true,'b'=>true); $cursor = $coll->find($query,$fields); //排序 $cursor->sort(array(‘字段'=>-1));(-1倒序，1正序) //跳过部分记录 $cursor->skip(100);跳过100行 //只显示部分记录 $cursor->limit(100);只显示100行 返回一个游标记录对象MongoCursor。 针对游标对象MongoCursor的操作(MongoCursor类)$cursor = $coll->find($query,$fields); while($cursor->hasNext()){ $r = $cursor->getNext(); var_dump($r); } //或者 $cursor = $coll->find($query,$fields); foreache($cursor as $k=>$v){ var_dump($v); } //或者 $cursor = $coll->find($query,$fields); $array= iterator_to_array($cursor);PHP操作Mongodb封装类完整实例<?php /** * Mongodb 基本操作API，支持基本类似关系统型数据库的操作接口 * * * [说明] * * 1：该版本API实现了 Mongodb 中最基本的插入/修改/查询/删除操作的封装 * 2：其它更高级的操作可通过 $this->getMongo() 得到原生的对象，更多API请自行查阅 Mongo PHP手册,后续版本将会对增加更多的原生API封装 * 3：该类所有API接口中的 $query 查询参数的使用请以下有关 [查询条件说明文档] * 4: 如果要存储中文字符，则一定要使用 utf8 的编码． * 5：有了本类接口基本可以按关系型数据库的概念完成Mongodb的大部分开发操作。 * * [查询条件说明文档] * * 参数：array('id'=>1) * 等同：where id=1 * * 参数：array('id'=>1,'num'=>10) * 等同：where id=1 and num=10 * * 参数：array('id'=>array($mongo->cmd('>')=>5)) * 等同：where id>5 * * 参数：array('id'=>array($mongo->cmd('!=')=>5)) * 等同：where id!=5 * * 参数：array('id'=>array($mongo->cmd('>')=>5, $mongo->cmd('<')=>10)) * 等同：where id>5 and id<10 * * 参数：array('id'=>array($mongo->cmd('in')=>array(2,5,6))) * 等同：where id in (2,5,6) * * 参数：array('id'=>array($mongo->cmd('%')=>array(2,1))) * 等同：where id % 2 = 1 * * 参数：array($mongo->cmd('or') => array( array('id'=>array($mongo->cmd('>')=>5)), array('id'=>array($mongo->cmd('<')=>10)) ) ) * 等同：where id>5 or id<10 * * 参数：array('username' => new mongoRegex("/yhp.*/")) * 等同 where username like "%yhp%" **/ class Library_Mongodb { /** * Mongodb 对象句柄 * * @var object Mongo */ private $_mongo = null; /** * 当前选择的数据库 * * @var object MongoDB */ private $_db = null; /** * 修改器命令前缀 * * @var string */ private $_cmd = '$'; /** * 调试模式 TRUE 打开 FALSE 关闭 * @var boolean */ const DEBUG = TRUE; /** * 查询条件映射关系 * * @var array */ private $_condMap = array( '<' => 'lt', // id > 1 '<=' => 'lte', // id <= 10 '>' => 'gt', // id > 5 '>=' => 'gte', // id >= 4 '!=' => 'ne', // id != 4 '%' => 'mod', // id % 4 = 0 'in' => 'in', // id in (1,2,3,4) 'notin' => 'nin',// id not in (1,2,3,4) 'or' => 'or', // id=1 or id=2 'not' => 'not', // !(id=1) ); /** * 构造函数 * * @param array $config 服务器配置,默认为: * array( * 'host'=>'localhost', // 主机名或IP地址 * 'port'=>27017, // 端口 * 'cmd'=>'$', // 修改器命令前缀 * ) */ public function __construct($config = array('host' => 'xxx', 'port' => 27017, 'username' => 'xxx', 'password' => 'xxx', 'db' => 'xxx', 'cmd' => '$')){ $server = sprintf("mongodb://%s:%s@%s:%s/%s", $config['username'], $config['password'], $config['host'], $config['port'], $config['db']); // echo "connect\n"; try { $this->_mongo = new MongoClient($server, array('connect'=>true));// 立即连接 }catch (MongoConnectionException $e){ if(self::DEBUG) { echo $e->getMessage(); } return false; } $this->selectDB($config['db']); // 命令前缀 if(!isset($config['cmd'])){ $this->_cmd = ini_get('mongo.cmd'); if($this->_cmd == ''){ $this->_cmd = '$'; } } } /* ==================================== 基本操作接口API ================================= */ /** * 向集合(表)中插入新文档 * * 说明： * 1:类似mysql中的: insert into $colName set id=1,name='name1'; * * @param string $colName 集合名 * @param array $sets 数据,如: array('id'=>1,'name'=>'name1') * @param boolean $safe 是否安全操作 false:不等待服务器的响应直接返回 true:等待服务器的响应(数据非常重要时推荐) * @param boolean $fsync 操作后是否立即更新到碰盘,默认情况下由服务器决定 * * @return boolean */ public function insert($colName, $sets, $safe=false, $fsync=false){ $col = $this->_getCol($colName); try { $col->insert($sets,array('w'=>$safe,'fsync'=>$fsync)); return true; }catch (MongoCursorException $e){ return false; } } /** * 保存文档 * * 说明： * 1:如果 $sets 中有字段 "_id" 的话，则更新对应的文档；否则插入新文档 * * @param string $colName 集合名 * @param array $sets 数据,如: array('id'=>1,'name'=>'name1') * @param boolean $safe 是否安全操作 false:不等待服务器的响应直接返回 true:等待服务器的响应(数据非常重要时推荐) * @param boolean $fsync 操作后是否立即更新到碰盘,默认情况下由服务器决定 * * @return boolean */ public function save($colName, $sets, $safe=false, $fsync=false){ // 处理 '_id' 字段 $sets = $this->_parseId($sets); $ret = $this->_getCol($colName)->save($sets,array('w'=>$safe,'fsync'=>$fsync)); return $ret; } /** * 删除集合中的文档记录 * * 说明： * 1：类似mysql中的: delete from $colName where id=1; * * @param string $colName 集合名 * @param array $query 查询条件,如果为空数组的话，则会删除所有记录．具体请看 [查询条件说明文档] * @param boolean $delAll 是否删除所以条例查询的记录,默认为 true,当为 false是，类似效果 delete from tab where id=1 limit 1; * @param boolean $safe 是否安全操作 false:不等待服务器的响应直接返回 true:等待服务器的响应(数据非常重要时推荐) * @param boolean $fsync 操作后是否立即更新到碰盘,默认情况下由服务器决定 * * @return boolean */ public function delete($colName,$query=array(),$delAll=true,$safe=false,$fsync=false){ // 自动处理 '_id' 字段 $query = $this->_parseId($query); // 删除选项 $options = array( 'justOne' => !$delAll, 'w' => $safe, 'fsync' => $fsync, ); $col = $this->_getCol($colName); return $col->remove($query,$options); } /** * 删除整个集合 * * 说明： * 1：集合中的索引也会被删除 * * @param string $colName 集合名 * * @return array */ public function dropCol($colName){ return $this->_getCol($colName)->drop(); } /** * 更新集合文档记录 * * 说明: * 1：类似mysql中的: update $colName set name='mongo' where id=10; * * @param string $colName 集合名 * @param array $newDoc 要更新的文档记录 * @param array $query 查询条件,如果为空数组则更新所有记录．具体请看 [查询条件说明文档] * @param string $option 操作选项,可选择项如下； * * 'set'：只修改指定的字段（默认值,如果这个键不存在，则创建它。存在则更新）. * 示例: update('user', array('name'=>'mongo'), array('id'=>10)); * 类似: update user set name='mongo' where id=10; * * 'inc'：将指定的字段累加/减(如果值为负数则是相减,不存在键则创建。字段类型一定要是数字) * 示例：update('user', array('num'=>1), array('id'=>10), 'inc'); * 类似: update user set num=num+1 where id=10; * * 'push'：将文档添加到指定键中（数组），如果键不存在则会自动创建，存在则添加到该键的尾端。 * 示例：update('user', array('comm'=>array('commid'=>1,'title'=>'title1')), array('id'=>1), 'push'); * 解说：为 id=1 的记录添加一个 comm 的评论字段，该字段对应一个 array('commid'=>1,'title'=>'title1') 的新文档。 * * 'pop':将指定键中的文档删除（数组） * 示例：update('user', array('comm'=>array('commid'=>1)), array('id'=>1), 'pop'); * 解说：删除 id=1 的记录中 comm 对应的文档集合中 'commid'=>1 对应的文档. * * 'unset':在文档中删除指定的键 * 示例：update('user', array('name'=>1), array('id'=>1), 'unset'); * 解说: 将 user 集合中将 id=1 对应的文档中的 name 字段删除 * * 'pull':删除文档中匹配其值的键 * 示例：update('user', array('name'=>'youname'), array('id'=>1), 'pull'); * 解说：将 user 集合中将 id=1 对应的文档中的 name='youname' 的字段删除 * * 'addToSet':如果值不存在就添加（避免重复添加） * 示例：update('user', array('names'=>'youname'), array('id'=>1), 'addToSet'); * 解说：向 user 集合中 id=1 对应的文档中的 names 字段添加 'youname' 这个值(不存在时才添加) * * 'replace'：用 $newDoc 新文档替换 $query 所找到的文档 * 示例：update('user', array('newid'=>1,'newnames'=>'name1'), array('id'=>1), 'replace'); * 解说：将 user 集合中 id=1 对应的文档用 array('newid'=>1,'newnames'=>'name1') 的新文档替换 * * @param boolean $upAll 是否更新找到的所有记录 * @param boolean $upsert 如果查询条件不存在时，是否以查询条件和要更新的字段一起新建一个集合 * @param boolean $safe 是否安全删除 false:不等待服务器的响应直接返回 true:等待服务器的响应(数据非常重要时推荐) * @param boolean $fsync 操作后是否立即更新到碰盘,默认情况下由服务器决定 * * @return boolean */ public function update($colName,$newDoc,$query=array(),$option='set',$upAll=true,$upsert=false,$safe=false,$fsync=false){ // 自动处理 '_id' 字段 $query = $this->_parseId($query); // 得到集合 $col = $this->_getCol($colName); // 重新组合新文档 if($option != 'replace'){ $newDoc = array($this->cmd($option) => $newDoc); } // 更新条件 $options = array( 'upsert' => $upsert, 'multiple' => $upAll, 'w' => $safe, 'fsync' => $fsync, ); return $col->update($query,$newDoc,$options); } /** * 查询文档集,返回二维数组 * * 说明: * 1:类似mysql中的 select * from table * * 示例：select('user'); * 类似：select * from user; * * 示例：select('user',array('id','name')); * 类似：select id,name from user; * * 示例：select('user',array('id','name'),array('id'=>1)); * 类似：select id,name from user where id=1; * * 示例：select('user',array('id','name'),array('id'=>1),array('num'=>1)); * 类似：select id,name from user where id=1 order by num asc; * * 示例：select('user',array('id','name'),array('id'=>1),array('num'=>1),10); * 类似：select id,name from user where id=1 order by num asc limit 10; * * 示例：select('user',array('id','name'),array('id'=>1),array('num'=>1),10,2); * 类似：select id,name from user where id=1 order by num asc limit 2,10; * * * * @param string $colName 集合名 * @param array $query 查询条件,具体请看 [查询条件说明文档] * @param array $fields 结果集返回的字段, array():表示返回所有字段 array('id','name'):表示只返回字段 "id,name" * @param array $sort 排序字段, array('id'=>1):表示按id字段升序 array('id'=>-1):表示按id字段降序 array('id'=>1, 'age'=>-1):表示按id升序后再按age降序 * @param int $limit 取多少条记录 * @param int $skip 跳过多少条(从多少条开始) * * @return array */ public function select($colName,$query=array(),$fields=array(),$sort=array(),$limit=0,$skip=0){ // 得到集合 $col = $this->_getCol($colName); // 自动处理 '_id' 字段 $query = $this->_parseId($query); // 结果集偏历 $cursor = $col->find($query,$fields); // 排序 if($sort){ $cursor->sort($sort); } // 跳过记录数 if($skip > 0){ $cursor->skip($skip); } // 取多少行记录 if($limit > 0){ $cursor->limit($limit); } $result = array(); foreach($cursor as $row){ $result[] = $this->_parseArr($row); } return $result; } /** * 统计文档记录数 * * @param string $colName 集合名 * @param array $query 查询条件,具体请看 [查询条件说明文档] * @param int $limit 取多少条记录 * @param int $skip 跳过多少条 * @return unknown */ public function count($colName,$query=array(),$limit=0,$skip=0){ return $this->_getCol($colName)->count($query,$limit,$skip); } /** * 返回集合中的一条记录(一维数组) * * @param string $colName 集合名 * @param array $query 查询条件,具体请看 [查询条件说明文档] * @param array $fields 结果集返回的字段, array():表示返回所有字段 array('id','name'):表示只返回字段 "id,name" * * @return array */ public function fetchRow($colName,$query=array(), $fields=array()){ // 得到集合名 $col = $this->_getCol($colName); // 自动处理 '_id' 字段 $query = $this->_parseId($query); // 处理结果集 return $this->_parseArr($col->findOne($query,$fields)); } /** * 返回符合条件的文档中字段的值 * * @param string $colName 集合名 * @param array $query 查询条件,具体请看 [查询条件说明文档] * @param string $fields 要取其值的字段,默认为 "_id" 字段,类似mysql中的自增主键 * * @return mixed */ public function fetchOne($colName,$query=array(), $fields='_id'){ $ret = $this->fetchRow($colName,$query,array($fields)); return isset($ret[$fields]) ? $ret[$fields] : false; } /** * 返回查询文档集合集中指定字段的值(一维数组) * * @param string $colName 集合名 * @param array $query 查询条件,具体请看 [查询条件说明文档] * @param string $fields 要取其值的字段,默认为 "_id" 字段,类似mysql中的自增主键 * * @return array */ public function fetchCol($colName,$query=array(), $fields='_id'){ $result = array(); $list = $this->select($colName,$query,array($fields)); foreach ($list as $row){ $result[] = $row[$fields]; } return $result; } /** * 返回指定下标的查询文档集合(二维数组) * * @param string $colName 集合名 * @param array $query 查询条件,具体请看 [查询条件说明文档] * @param string $fields 要取其值的字段,默认为 "_id" 字段,类似mysql中的自增主键 * * @return array */ public function fetchAssoc($colName,$query=array(), $fields='_id'){ $result = array(); $list = $this->select($colName,$query); foreach ($list as $row){ $key = $row[$fields]; $result[][$key] = $row; } return $result; } /* ==================================== 辅助操作接口API ================================= */ /** * 返回命令或命令前缀 * * @param string $option 命令，如果为空时则返回命令前缀 * * @return string */ public function cmd($option=''){ // 只返回命令前缀 if($option == ''){ return $this->_cmd; } // 如果是操作符 if(isset($this->_condMap[$option])){ $option = $this->_condMap[$option]; } return $this->_cmd.$option; } /** * 选择或创建数据库(注意：新创建的数据库如果在关闭连接前没有写入数据将会被自动删除) * * @param string $dbname 数据库名 */ public function selectDB($dbname){ $this->_db = $this->_mongo->selectDB($dbname); } /** * 得到所有的数据库 * * @param boolean $onlyName 是否只返回数据库名的数组 * @return array */ public function allDB($onlyName=false){ $ary = $this->_mongo->listDBs(); if($onlyName){ $ret = array(); foreach ($ary['databases'] as $row){ $ret[] = $row['name']; } return $ret; }else{ return $ary; } } /** * 删除数据库 * * @return array */ public function dropDB($dbname){ return $this->_mongo->dropDB($dbname); } /** * 关闭连接 * */ public function close(){ $this->_mongo->close(); } /** * 得到 Mongo 原生对象，进行其它更高级的操作，详细请看PHP手册 * */ public function getMongo(){ return $this->_mongo; } /** * 返回最后的错误信息 * * @return array */ public function getError(){ return $this->_db->lastError(); } /* ======================= 以下为私有方法 ====================== */ // 解析数据组中的'_id'字段(如果有的话) private function _parseId($arr){ if(isset($arr['_id'])){ $arr['_id'] = new MongoId($arr['_id']); } return $arr; } // 得到集合对象 private function _getCol($colName){ return $this->_db->selectCollection($colName); } // 解析数组中的"_id"并且返回 private function _parseArr($arr){ if(!empty($arr)) { $ret = (array)$arr['_id']; $arr['_id'] = $ret['$id']; } return $arr; } }//End Class ?>
其他语言大多都有函数传参解包的功能,php8也加入了这个功能。php version < 8<?php function test(...$arg) { var_dump($arg); } test(); //array(0) { } test(NULL); //array(1) { [0]=> NULL } test("foo", "bar"); //array(2) { [0]=> string(3) "foo" [1]=> string(3) "bar" } test(NULL, array(), "dummy"); //array(3) { [0]=> NULL [1]=>[] [2]=> string(5) "dummy" } function test1($a, $b = NULL, $c = NULL, $d = NULL, $e = NULL) { var_dump($a, $b, $c, $d, $e); } $arg = array( "First arg", NULL, NULL, NULL, "Fifth arg", ); test1(...$arguments); // string(14) "First arg" // NULL // NULL // NULL // string(14) "Fifth arg"php version >=8<?php function dummy($a, $b = NULL, $c = NULL, $d = NULL, $e = NULL) { var_dump($a, $b, $c, $d, $e); } dummy(a:"dummy", e:"foo"); // string(5) "dummy" // NULL // NULL // NULL // string(3) "foo"
<?php $str = "中文"; $filename = '1.txt'; file_put_contents($filename,$str); echo '测试1-检测本地文件编码：' . detect_encoding($filename) . ' '; $filename = '2.txt'; file_put_contents($filename,mb_convert_encoding($str,'UTF-8',mb_detect_encoding($str))); echo '测试2-检测本地文件编码：' . detect_encoding($filename) . ' '; $fp =fopen($filename,"w+"); $header ="\xef\xbb\xbf"; fwrite($fp,$header); fwrite($fp,$str); fclose($fp); echo '测试3-检测本地文件编码：' . detect_encoding($filename) . ' '; function detect_encoding($file) { $list = ['GBK', 'UTF-8', 'UTF-16LE', 'UTF-16BE', 'ISO-8859-1']; $str = file_get_contents($file); foreach ($list as $item) { $tmp = mb_convert_encoding($str, $item, $item); if (md5($tmp) == md5($str)) { return $item; } } return null; }UTF-8编码的TXT文件必须以0xef , 0xbb , 0xbf三个字符开头！