一文读懂 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" /]
CSS 按钮边框旋转动画：炫酷效果轻松实现
效果图代码 css按钮边框旋转动画 边框按钮
轻松掌握屏幕坐标和窗口通信的实用技巧
我们通过一个实战例子来学习屏幕坐标、窗口通信效果图思考一个问题，上述效果图是不是多个窗口公用一个元素?显然这是不可能的，肯定是三个元素。同一个页面通过url传参的方式根据参数不同，给元素设置不同的背景。做到上述效果图需要哪些必要条件？保证元素的位置在同一个坐标系下相同。只有在电脑屏幕下，它们的坐标系才是相同的。涉及到的知识点导航栏的高度计算 //获取浏览器窗口的工具栏、菜单栏和边框等元素的总高度 function barHeight() { return window.outerHeight - window.innerHeight; }屏幕坐标和视口坐标的转换 解析图 //将视口坐标转换为屏幕坐标 function clientToScreen(clientX, clientY) { //clientX, clientY 是视口坐标 //window.screenX 、window.screenY 是浏览器窗口左上角相对于屏幕左上角的距离 let screenX = clientX + window.screenX; //注意：window.screenY 是浏览器窗口左上角相对于屏幕左上角的距离，不包括浏览器窗口的工具栏、菜单栏和边框等元素的总高度 let screenY = clientY + window.screenY + barHeight(); return [screenX, screenY]; } //将屏幕坐标转换为视口坐标 function screenToClient(screenX, screenY) { let clientX = screenX - window.screenX; let clientY = screenY - window.screenY - barHeight(); return [clientX, clientY]; }拖拽事件 // 选择类名为.container 的元素 const card = document.querySelector('.container'); // 为选中的元素添加鼠标按下事件监听器 card.onmousedown = function (e) { // 计算鼠标指针在卡片元素上的相对偏移量 const x = e.pageX - card.offsetLeft; const y = e.pageY - card.offsetTop; window.onmousemove = function (e) { // 根据相对偏移量和鼠标移动事件的坐标计算卡片新的位置 let cx = e.pageX - x; let cy = e.pageY - y; // 设置卡片元素的新位置 card.style.left = cx + "px"; card.style.top = cy + "px"; } window.onmouseup = function () { // 释放鼠标时，取消鼠标移动和鼠标释放的事件监听器 window.onmousemove = null; window.onmouseup = null; } } 窗口之间的通信 // 创建一个名为'card'的广播频道,这个频道允许不同窗口或标签之间的脚本进行通信 const channel = new BroadcastChannel('card'); // 将当前窗口元素位置的坐标转换为屏幕坐标 let points = clientToScreen(cx, cy); // 向其他窗口传递当前窗口元素位置的屏幕坐标 channel.postMessage(points); // 监听其他窗口传递过来的消息 channel.onmessage = function (event) { // 使用展开运算符将事件数据转换为当前可视坐标 let [clientX, clientY] = screenToClient(...event.data); // 设置元素的left属性值，将视口的X坐标应用于card元素 card.style.left = clientX + "px"; // 设置元素的top属性值，将视口的Y坐标应用于card元素 card.style.top = clientY + "px"; 完整代码html 屏幕坐标和窗口通信 css.container{ width:300px; height: 300px; position: absolute; }js //获取浏览器窗口的工具栏、菜单栏和边框等元素的总高度 function barHeight() { return window.outerHeight - window.innerHeight; } //将视口坐标转换为屏幕坐标 function clientToScreen(clientX, clientY) { let screenX = clientX + window.screenX; let screenY = clientY + window.screenY + barHeight(); return [screenX, screenY]; } //将屏幕坐标转换为视口坐标 function screenToClient(screenX, screenY) { let clientX = screenX - window.screenX; let clientY = screenY - window.screenY - barHeight(); return [clientX, clientY]; } //创建一个名为'card'的广播频道,这个频道允许不同窗口或标签之间的脚本进行通信 const channel = new BroadcastChannel('card'); // 监听'card'频道上的消息事件 channel.onmessage = function (event) { // 使用展开运算符将事件数据转换为视口坐标 let [clientX, clientY] = screenToClient(...event.data); // 设置元素的left属性值，将视口的X坐标应用于card元素 card.style.left = clientX + "px"; // 设置元素的top属性值，将视口的Y坐标应用于card元素 card.style.top = clientY + "px"; } // 选择类名为.container 的元素 const card = document.querySelector('.container'); // 为选中的元素添加鼠标按下事件监听器 card.onmousedown = function (e) { // 计算鼠标指针在卡片元素上的相对偏移量 const x = e.pageX - card.offsetLeft; const y = e.pageY - card.offsetTop; window.onmousemove = function (e) { // 根据相对偏移量和鼠标移动事件的坐标计算卡片新的位置 let cx = e.pageX - x; let cy = e.pageY - y; // 设置卡片元素的新位置 card.style.left = cx + "px"; card.style.top = cy + "px"; // 将卡片当前位置的坐标转换为屏幕坐标 let points = clientToScreen(cx, cy); // 通过某个数据通道发送屏幕坐标信息 channel.postMessage(points); } window.onmouseup = function () { // 释放鼠标时，取消鼠标移动和鼠标释放的事件监听器 window.onmousemove = null; window.onmouseup = null; } } // 定义初始化函数 init function init() { // 创建一个 URL 对象，用于解析当前窗口的 URL let url = new URL(window.location.href); // 使用 URL 对象的 searchParams 属性获取 URL 中的查询字符串参数 type let type = url.searchParams.get("type") || "a"; // 判断 type 是否等于 a，如果是，将 card 元素的背景颜色设置为红色 if (type === "a") { card.style.background = "red"; } // 判断 type 是否等于 b，如果是，将 card 元素的背景颜色设置为绿色 else if (type === "b") { card.style.background = "green"; } // 判断 type 是否等于 c，如果是，将 card 元素的背景颜色设置为蓝色 else if (type === "c") { card.style.background = "blue"; } } // 调用 init 函数，启动初始化逻辑 init();
学会正确使用一些form表单元素：提升网页表单质量
元素（支持多选） 用于创建一个支持多选的下拉列表。 元素用于对选项进行分组：label 属性用于为分组指定标签，以描述该组的类别。每组内包含多个 元素。 元素的 value 属性用于指定选项的值，若为空，则默认使用选项的文本作为值。示例代码： 1 2 3 4 以下是为您完善的关于这段代码的笔记：单选按钮组（爱好选择）这段代码通过 标签和 标签创建了一组单选按钮，用于选择爱好。每个 元素的type="radio"表示这是一个单选按钮。name="sex"属性确保在同一组中，只能选择一个选项。通过 标签为每个单选按钮提供了对应的文本描述，并使用for属性与相应的 元素的id属性关联，以提高用户交互性。示例代码：爱好： 篮球 足球 游泳 羽毛球 以下是为您完善的关于这段代码的笔记：浏览器类型选择输入框在这段代码中： 标签用于创建一个段落。段落内包含文本“浏览器类型：”以及一个关联了数据列表的输入框。 定义了一个文本输入框，通过 list 属性关联到 id 为 browser 的数据列表。 定义了数据列表。其中每个 元素代表一个可选项，value 属性指定了实际的值，元素内的文本为显示给用户的描述。当用户在输入框中输入时，会根据输入内容显示匹配的数据列表选项，方便用户快速选择浏览器类型。示例代码： 浏览器类型： 谷歌浏览器 火狐浏览器 苹果浏览器 欧朋浏览器 IE 浏览器 微软浏览器
iframe 元素框架页iframe 元素通常用于在网页中嵌入其他网页。iframe 是可替换的元素。通常表现为行盒。其显示的内容主要取决于元素自身设置的属性。CSS 对其内部样式的控制存在一定局限性。具有行块盒的部分特点。示例代码： 点击打开京东页面 在页面中使用 flash在网页中使用 flash 可以通过 object 和 embed 元素来实现。这两个元素均为可替换元素。示例代码：
透明度opacity 它设置的是整个元素的透明，取值 0-1， 0 完全透明， 1 完全不透明在颜色位置设置alpha通道(rgba) 可以设置颜色的透明度， 取值 0-1， 0 完全透明， 1 完全不透明鼠标cursor: pointer; 鼠标指针变成手型cursor: move; 鼠标指针变成移动的图标cursor: crosshair; 鼠标指针变成十字形cursor: text; 鼠标指针变成文本输入框cursor: default; 鼠标指针变成默认的箭头cursor: not-allowed; 鼠标指针变成禁止的图标cursor: none; 鼠标指针变成隐藏的图标cursor: inherit; 鼠标指针继承父元素的样式cursor: url(图片路径), auto; 鼠标指针变成自定义的图标，并且鼠标指针变成默认的箭头盒子隐藏display:none, 不生成盒子visibility:hidden, 生成盒子，但是隐藏。背景图img 元素是属于html的概念背景图属于css的概念1.当图片属于网页内容时，必须使用img元素2.当图片仅用于美化网页时，必须使用背景图涉及到的css属性background-image:url(图片路径), 背景图background-repeat:no-repeat, 背景图不重复background-repeat:repeat-x, 背景图只在x轴重复background-repeat:repeat-y, 背景图只在y轴重复background-repeat:repeat, 背景图在x轴和y轴都重复background-size:cover, 背景图覆盖整个盒子background-size:contain, 背景图覆盖整个盒子，并且保持图片的长宽比background-size:100px 100px, 背景图覆盖整个盒子，并且保持图片的长宽比background-position:center, 背景图居中background-position:center center, 背景图居中background-position:right bottom, 背景图居右下方background-attachment:fixed, 背景图固定在页面上background-attachment:scroll, 背景图跟随页面滚动background-color:red, 背景颜色
一、更多伪类选择器伪类选择器在 CSS 中起着至关重要的作用，它们允许你根据元素的特定状态或位置来选择元素，从而实现更加精细的样式控制。（一）first-child作用：选中父元素的第一个子元素。这个选择器对于为特定元素的第一个子元素应用独特的样式非常有用。示例：p:first-child { color: red; }在这个例子中，会选中作为父元素第一个子元素的 元素，并将其文本颜色设置为红色。比如在一个包含多个段落的容器中，只有第一个段落会被应用这个样式。（二）first-of-type作用：选中同类型元素中的第一个元素。与first-child不同，它只关注同类型的元素，而不考虑在父元素中的位置顺序。示例：p:first-of-type { font-weight: bold; }这里会选中页面中第一个 元素，并将其字体加粗。即使这个段落不是父元素的第一个子元素，但只要它是页面中出现的第一个 元素，就会被选中。（三）last-child作用：选中父元素的最后一个子元素。可以用来为父元素的最后一个子元素添加特殊的样式效果。示例：p:last-child { text-decoration: underline; }此代码会给父元素的最后一个 元素添加下划线。例如在一个列表中，最后一个列表项可以通过这个选择器进行突出显示。（四）last-of-type作用：选中同类型元素中的最后一个元素。专注于同类型元素的末尾元素进行样式设置。示例：p:last-of-type { background-color: yellow; }这会将页面中最后一个 元素的背景颜色设置为黄色。无论这个段落在页面中的哪个位置，只要它是同类型元素中的最后一个，就会被选中。（五）nth-child作用：选中父元素中的第 n 个子元素。可以使用具体的数字来指定特定的子元素，也可以使用表达式来选择特定模式的子元素。示例：li:nth-child(3) { list-style-type: square; }这里会选中列表中的第三个 元素，并将其列表样式设置为方块。此外，even可以选中偶数个元素，等同于2n，odd可以选中奇数个元素，等同于2n + 1。li:nth-child(even) { background-color: gray; }这会将列表中的偶数位置的 元素的背景颜色设置为灰色。可以根据需要灵活运用这些选择方式来实现不同的布局效果。（六）nth-of-type作用：选中同类型元素中的第 n 个元素。与nth-child类似，但只针对同类型的元素进行选择。示例：p:nth-of-type(2) { font-style: italic; }此代码会选中页面中第二个 元素，并将其字体设置为斜体。如果页面中有多个段落，这个选择器可以精确地选择特定位置的段落进行样式调整。二、更多的伪元素选择器伪元素选择器能够选择元素的特定部分，为样式设计提供了更多的可能性。（一）first-letter作用：选中元素的第一个字母。可以用于为文本的首字母添加特殊的样式，增强文本的视觉效果。示例：p::first-letter { font-size: 2em; }这会将 元素的第一个字母的字体大小设置为原来的两倍。在段落开头的字母可以通过这个选择器进行突出显示，吸引读者的注意力。（二）first-line作用：选中元素的第一行。对于控制文本的首行样式非常有用，可以实现如首行缩进、特殊字体样式等效果。示例：p::first-line { color: blue; }这里会将 元素的第一行文本颜色设置为蓝色。如果段落内容较长，首行的独特样式可以使文本更具可读性和吸引力。（三）selection作用：选中用户选择的文本。可以为用户选择的文本添加特定的样式，提供更好的交互体验。示例：::selection { background-color: pink; }当用户在页面上选择文本时，被选中的文本背景颜色会变为粉色。这种样式可以让用户更容易识别他们选择的内容，同时也可以增加页面的视觉吸引力。
一、定位概述在网页布局中，定位可以让我们手动控制元素在其包含块中的精确位置，这主要通过 CSS 的position属性来实现。二、position属性取值及特点（一）static（默认值）当元素的position属性为static时，这是默认的状态。在这种情况下，元素按照正常的文档流排列，不会有任何特殊的定位行为。（二）relative（相对定位）文档流与偏移：元素在保持按照正常的文档流排列的同时，根据top、right、bottom、left属性进行位置偏移。这意味着它不会影响其他元素在文档流中的原始位置。控制偏移属性：可以通过四个 CSS 属性（left、right、top、bottom）来精确控制元素的偏移量。例如，可以设置top: 20px; left: 30px;让元素相对于其在文档流中的原始位置向下移动 20 像素，向右移动 30 像素。无影响其他盒子：相对定位的盒子的偏移不会对其他盒子造成任何影响。其他元素在布局时会完全忽略这个偏移，就好像这个元素仍然在其原始位置一样。（三）absolute（绝对定位）文档流与偏移：元素按照正常的文档流排列后，根据top、right、bottom、left属性进行位置偏移。与相对定位不同的是，绝对定位会使元素脱离文档流。脱离文档流的影响：元素摆放忽略：当一个元素被绝对定位并脱离文档流后，文档流中的其他元素在摆放时会忽略这个元素。这意味着其他元素会好像这个绝对定位的元素不存在一样进行布局。自动高度计算忽略：在文档流中元素计算自动高度时，也会忽略脱离文档流的绝对定位元素。这可能会影响到包含块的高度计算以及其他依赖于文档流布局的元素。宽度自适应与包含块变化：宽度自适应：当绝对定位元素的宽度为auto时，它会适应内容的宽度。这意味着元素的宽度将根据其内部内容的大小自动调整。包含块变化：绝对定位元素的包含块会发生变化。它会找祖先元素中第一个定位元素，该元素的填充盒为其包含块。如果找不到任何定位的祖先元素，则包含块为整个网页（初始化包含块）。（四）fixed（固定定位）与绝对定位相似性：在大多数情况下，固定定位和绝对定位非常相似。元素会根据top、right、bottom、left属性进行位置偏移，并且也会脱离文档流，具有与绝对定位类似的影响。不同的包含块：固定定位的元素的包含块与绝对定位不同。固定定位的元素固定为视口，即浏览器的可视窗口。这意味着无论页面如何滚动，固定定位的元素始终保持在相对于视口的相同位置。三、定位下的居中在绝对定位和固定定位中，可以通过以下步骤实现某个方向上的居中：定宽（高）：首先确定要居中的元素的宽度（或高度，如果是垂直方向上的居中）。可以通过设置具体的像素值或使用相对单位来确定宽度或高度。设置距离为 0：将元素的左右（或上下，如果是垂直方向上的居中）距离设置为 0。这可以通过设置left: 0; right: 0;（或top: 0; bottom: 0;）来实现。设置margin为auto：最后，将元素的margin设置为auto。在绝对定位和固定定位中，margin设置为auto时，会自动吸收剩余空间，从而实现元素在该方向上的居中。例如，margin: auto 0;可以实现水平方向上的居中，margin: 0 auto;可以实现垂直方向上的居中。四、多个定位元素重叠时（一）堆叠上下文当多个定位元素重叠时，会涉及到堆叠上下文的概念。可以通过设置z-index属性来控制元素的堆叠顺序。通常情况下，z-index值越大，元素越靠近用户。需要注意以下几点：仅对定位元素有效：只有定位元素设置z-index时才会生效。如果一个元素的position属性为static，设置z-index将不会产生任何效果。负数的影响：z-index可以为负数。当z-index为负数时，如果遇到常规流元素或浮动元素，这个定位元素会被覆盖。五、补充说明绝对定位与固定定位的元素类型：绝对定位和固定定位的元素一定是块盒。这意味着它们会像块级元素一样占据一行空间，可以设置宽度、高度、内外边距等属性。与浮动的关系：绝对定位和固定定位的元素一定不是浮动。它们不会参与浮动布局，也不会受到浮动元素的影响。外边距合并问题：绝对定位和固定定位的元素没有外边距合并的情况。这与常规流中的块盒和行盒不同，在常规流中，块盒之间可能会发生外边距合并。