<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>狂盗一枝梅的博客</title>
    <link>https://blog.kdyzm.cn</link>
    <description>狂盗一枝梅的博客 - 曾梦想仗剑走天涯，后来工作忙没去！</description>
    <language>zh-CN</language>
    <atom:link href="https://blog.kdyzm.cn/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>解决Windows命令行cmd中文乱码问题</title>
      <link>https://blog.kdyzm.cn/post/339</link>
      <guid>https://blog.kdyzm.cn/post/339</guid>
      <pubDate>Wed, 08 Apr 2026 18:46:12 +0800</pubDate>
      <description>&lt;h2 id=&quot;一乱码现象&quot;&gt;一、乱码现象&lt;/h2&gt;
&lt;p&gt;最近使用DataX迁移数据的时候命令行中文输出乱码了，如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/08/9a1d7505c4124ff990bb054393888bc6.png&quot; alt=&quot;image-20260408182534883&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这是因为DataX程序是使用UTF-8编码开发、打包的，而Windows命令行默认使用的不是UTF-8编码，&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/08/8970c03509654e8685b24aba5fc5c913.png&quot; alt=&quot;image-20260408182758303&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;命令行属性中可以看到，默认使用的是GBK编码，所以产生了中文乱码。&lt;/p&gt;
&lt;h2 id=&quot;二临时解决方案&quot;&gt;二、临时解决方案&lt;/h2&gt;
&lt;p&gt;在命令行中直接执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;chcp 65001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完成之后就可以发现控制台编码已经变成了UTF-8&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/08/3850e95412974f368131e289970427bf.png&quot; alt=&quot;image-20260408183457816&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;常见代码页对照表&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;代码页&lt;/th&gt;
&lt;th&gt;对应编码&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;936&lt;/td&gt;
&lt;td&gt;GBK&lt;/td&gt;
&lt;td&gt;简体中文系统默认，兼容性最佳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;65001&lt;/td&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;td&gt;跨平台开发、国际通用字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;437&lt;/td&gt;
&lt;td&gt;ASCII&lt;/td&gt;
&lt;td&gt;美国英语（原始DOS默认）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;950&lt;/td&gt;
&lt;td&gt;Big5&lt;/td&gt;
&lt;td&gt;繁体中文（台湾、香港）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;三永久解决方案&quot;&gt;三、永久解决方案&lt;/h2&gt;
&lt;p&gt;修改注册表：快捷键&lt;code&gt;Win+R&lt;/code&gt;，键入&lt;code&gt;regedit&lt;/code&gt;，找到注册表&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Command Processor&lt;/code&gt;，并找到AutoRun选项，如果没有，则新建一个。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/08/032f79577e0d4856acc28aa8c0400320.png&quot; alt=&quot;image-20260408184101488&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;修改值为&lt;code&gt;chcp 65001&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这样，每次打开新的控制台窗口，就会自动设置编码为UTF-8了。&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;转载自：&lt;a href=&quot;https://cloud.tencent.com/developer/article/2084809&quot;&gt;https://cloud.tencent.com/developer/article/2084809&lt;/a&gt;&lt;/p&gt;
</description>
      <category>windows</category>
      <category>乱码</category>
      <category>java</category>
      <category>python</category>
    </item>
    <item>
      <title>记一次Mybatis查询中的数组越界异常</title>
      <link>https://blog.kdyzm.cn/post/338</link>
      <guid>https://blog.kdyzm.cn/post/338</guid>
      <pubDate>Thu, 02 Apr 2026 15:01:51 +0800</pubDate>
      <description>&lt;p&gt;最近遇到了一件极其诡异的Mybatis查询数组越界异常（java.sql.SQLException: java.lang.ArrayIndexOutOfBoundsException）的问题，排查了一天，最终解决了该问题。&lt;/p&gt;
&lt;h2 id=&quot;一问题描述&quot;&gt;一、问题描述&lt;/h2&gt;
&lt;p&gt;Mapper代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Mapper
public interface ISmeBidDailyMapper extends BaseMapper&amp;lt;SmeBidDaily&amp;gt; {

    List&amp;lt;SmeBidDaily&amp;gt; listQuery(@Param(&amp;quot;lastMaxId&amp;quot;) Long lastMaxId,
                                @Param(&amp;quot;maxDataTime&amp;quot;) String maxDataTime,
                                @Param(&amp;quot;pageSize&amp;quot;) Integer pageSize);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的XMl代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;    &amp;lt;select id=&amp;quot;listQuery&amp;quot; resultType=&amp;quot;com.cosmoplat.gfqd.gxh.data.syn.bid.sme_bid_daily.entity.SmeBidDaily&amp;quot;&amp;gt;
        SELECT
          *
        FROM `sme_bid_daily`
        WHERE id &amp;amp;gt; #{lastMaxId}
        AND `created_at` &amp;amp;lt;= #{maxDataTime}
        ORDER BY id ASC
        limit #{pageSize}
    &amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码运行的时候，会抛出两种错误：&lt;/p&gt;
&lt;p&gt;第一种错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;JDBC Connection [HikariProxyConnection@1915066553 wrapping com.mysql.cj.jdbc.ConnectionImpl@2c9a855f] will not be managed by Spring
==&amp;gt; Preparing: SELECT id, mid, attachment_flag, bid_type, bid_info_type, bid_project_title, bid_num, bid_location, bid_province, bid_project_category, budgetary, bid_ent, proxy_ent, bid_win, contact, contact_type, contact_person, contact_has_more, bid_pubtime, updated, region_code, title_key_word, text_key_word, product_word, ent_nic, match_text, bid_text, state, created_at, original_url FROM `sme_bid_daily` WHERE id &amp;gt; ? AND `created_at` &amp;lt;= ? ORDER BY id ASC limit ?
==&amp;gt; Parameters: 1604549(Long), 2026-04-30T00:00(LocalDateTime), 10000(Integer)
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@49af67c7]
2026-04-01 15:23:37 [Thread-22] ERROR c.c.g.g.d.s.b.s.j.SynSmeBidDailyToEs -
org.springframework.dao.TransientDataAccessResourceException:
### Error querying database. Cause: java.sql.SQLException: java.lang.ArrayIndexOutOfBoundsException: 31
### The error may exist in class path resource [mappers/ISmeBidDailyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT id, mid, attachment_flag, bid_type, bid_info_type, bid_project_title, bid_num, bid_location, bid_province, bid_project_category, budgetary, bid_ent, proxy_ent, bid_win, contact, contact_type, contact_person, contact_has_more, bid_pubtime, updated, region_code, title_key_word, text_key_word, product_word, ent_nic, match_text, bid_text, state, created_at, original_url FROM `sme_bid_daily` WHERE id &amp;gt; ? AND `created_at` &amp;lt;= ? ORDER BY id ASC limit ?
### Cause: java.sql.SQLException: java.lang.ArrayIndexOutOfBoundsException: 31
; java.lang.ArrayIndexOutOfBoundsException: 31; nested exception is java.sql.SQLException: java.lang.ArrayIndexOutOfBoundsException: 31
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;2026-04-01 15:23:43 [Thread-22] ERROR c.c.g.g.d.s.b.s.j.SynSmeBidDailyToEs -
org.springframework.dao.TransientDataAccessResourceException:
### Error querying database. Cause: java.sql.SQLException: java.lang.ArrayIndexOutOfBoundsException
### The error may exist in class path resource [mappers/ISmeBidDailyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: SELECT id, mid, attachment_flag, bid_type, bid_info_type, bid_project_title, bid_num, bid_location, bid_province, bid_project_category, budgetary, bid_ent, proxy_ent, bid_win, contact, contact_type, contact_person, contact_has_more, bid_pubtime, updated, region_code, title_key_word, text_key_word, product_word, ent_nic, match_text, bid_text, state, created_at, original_url FROM `sme_bid_daily` WHERE id &amp;gt; ? AND `created_at` &amp;lt;= ? ORDER BY id ASC limit ?
### Cause: java.sql.SQLException: java.lang.ArrayIndexOutOfBoundsException
; java.lang.ArrayIndexOutOfBoundsException; nested exception is java.sql.SQLException: java.lang.ArrayIndexOutOfBoundsException
    ......
Caused by: java.lang.ArrayIndexOutOfBoundsException: null
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;二规律总结&quot;&gt;二、规律总结&lt;/h2&gt;
&lt;p&gt;一开始怀疑本地的jdk和k3s集群运行的jre版本不同，我本地的jdk是oracle jdk，在k3s集群容器环境下运行的是openjdk-internel版本，我将k3s集群容器中的jre和我本地的jdk统统都换成了jdk8u482-b08版本，但是问题依旧，所以排除了和运行的jdk版本的关系；&lt;/p&gt;
&lt;p&gt;又怀疑jenkins打包是否发生了什么问题，所以我将容器中的jar包取出来在本地运行，结果本地运行一点问题没有，无法复现；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;至此，我本地的jdk和jar包都和线上保持一致了，but我本地运行N次还是没有复现问题，线上请求一两次就会报错，甚至连续请求连续报错。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我都开始怀疑人生了，干了这么多年的后端开发，第一次遇到这种诡异的问题。&lt;/p&gt;
&lt;p&gt;后来怀疑是不是k3s集群的问题，我将同样的镜像部署到k8s集群中运行，结果k8s集群中运行是好的，没法复现，这说明是k3s集群有问题？&lt;/p&gt;
&lt;p&gt;怀疑k3s网络有问题，听说MTU设置的不对也会出现这个问题，但是为什么别的服务没有问题，就单单这个服务出了问题？接着又怀疑jdbc链接中设置的不对，禁止了Mysql服务端预编译，仍然没有解决问题。&lt;/p&gt;
&lt;p&gt;经过反复测试，最终发现了如下规律：&lt;/p&gt;
&lt;p&gt;1、在本地环境下无法复现&lt;/p&gt;
&lt;p&gt;2、在k3s集群中稳定复现，通常调用两三次就能出现上述异常&lt;/p&gt;
&lt;p&gt;3、在k8s集群中无法复现&lt;/p&gt;
&lt;p&gt;4、在linux环境非容器环境无法复现&lt;/p&gt;
&lt;p&gt;5、和Mysql的JDBC服务端预编译配置无关&lt;/p&gt;
&lt;p&gt;通过保持单一变量原则的测试，唯一有可能存在问题的是k3s的运行环境有问题，否则说不通为什么相同的镜像在k8s集群中运行没有问题。&lt;/p&gt;
&lt;p&gt;最后没法子了，只能远程debug，远程debug也行不通，因为这个问题时好时坏，不能每次都重现，找到了抛出异常的地方，但是真正执行逻辑的方法已经在方法栈中弹出，堆栈信息已经没有了，断点都不知道打在什么地方。&lt;/p&gt;
&lt;p&gt;问题陷入了死循环，似乎根本无解，至此我唯一能想到的解决方案就是不再在容器中运行，把jar包拿出来直接放到宿主机使用java -jar 启动。&lt;/p&gt;
&lt;h2 id=&quot;三解决问题&quot;&gt;三、解决问题&lt;/h2&gt;
&lt;p&gt;要不说还得是大模型，大模型随口提了一句检查下mysql驱动是否有问题，最后实在没招的我，把mysql驱动从&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
    &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;version&amp;gt;8.0.31&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;替换成了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-j&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;8.0.33&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题竟然解决了。。。问题解决了，解决了问题的我内心没有丝毫波澜，只有满肚子的疑问。&lt;/p&gt;
&lt;p&gt;1、如果是驱动的问题，为什么在本地无法复现问题，在k8s集群中没有复现问题，只有在k3s集群中出现了数组越界异常的问题。&lt;/p&gt;
&lt;p&gt;2、为什么在k3s集群中有时候有问题，有时候没问题，这个还能有偶发性情况吗。&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>mybatis</category>
      <category>mysql</category>
      <category>java</category>
      <category>spring</category>
    </item>
    <item>
      <title>IDEA远程Debug</title>
      <link>https://blog.kdyzm.cn/post/337</link>
      <guid>https://blog.kdyzm.cn/post/337</guid>
      <pubDate>Thu, 02 Apr 2026 14:00:13 +0800</pubDate>
      <description>&lt;p&gt;线上出了问题，但是本地不能重现，可以通过IDEA远程Debug功能远程调试排查问题。具体操作如下：&lt;/p&gt;
&lt;h2 id=&quot;一本地idea配置&quot;&gt;一、本地IDEA配置&lt;/h2&gt;
&lt;p&gt;单击项目顶部“Edit Configurations”&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/02/004770dbc5d5470fac5a94b8826db70d.png&quot; alt=&quot;image-20260402132713787&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;新增运行配置：&amp;quot;Remote JVM Debug&amp;quot;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/02/a09f155816824b32a84ff1b48934fde7.png&quot; alt=&quot;image-20260402132816314&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;之后进入远程Debug配置页面：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/02/8cdbac92cfda4959812305b59fd86290.png&quot; alt=&quot;image-20260402133044513&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;页面上有些配置，配置详情如下所示：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1、Debugger mode&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有两种模式：&lt;/p&gt;
&lt;p&gt;Attach to remote JVM：最常使用的模式，本地连接远程JVM，需要确保远程端口号（默认5005,可自定义）开放能够被本地连接&lt;/p&gt;
&lt;p&gt;Listen to remote JVM：不常使用的模式，远端连接本地JVM，需要确保本地端口号（默认5005,可自定义）开放能够被远端连接&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、Transport&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;调试和被调试的JVM的通信方式。&lt;/p&gt;
&lt;p&gt;Socket：套接字连接的方式，通过TCP方式通信，适用于调试的JVM和被调试的JVM在不同机器上的情况。&lt;/p&gt;
&lt;p&gt;Shared memory：共享内存方式，适用于调试的JVM和被调试的JVM在相同的机器上的情况，传输速度比Socket快。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、JDK选择&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据运行的JVM情况，选择对应的JDK版本，不同的JDK版本对应不同的运行命令&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/02/62d36cb37c6c44c8850ef20daf15f66c.png&quot; alt=&quot;image-20260402134032355&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;将命令行参数复制到被调试JVM的启动命令行中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4、use module classpath选择&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果是maven组织的多模块项目，选择根模块即可。&lt;/p&gt;
&lt;h2 id=&quot;二远程jvm配置&quot;&gt;二、远程JVM配置&lt;/h2&gt;
&lt;p&gt;根据上一步复制的命令行参数，比如Java8，需要将命令行参数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制到启动命令行，比如完整的jar包启动命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar app.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;三调试&quot;&gt;三、调试&lt;/h2&gt;
&lt;p&gt;先启动远程jar包，jar包启动命令行第一行输出会提示&amp;quot;Listening for transport dt_socket at address: 5005&amp;quot;，这表示自己已经准备好客户端远程调试了。&lt;/p&gt;
&lt;p&gt;之后点击IDEA的调试启动按钮&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/04/02/b3f7ee2eef0140feabb882800bcd7bad.png&quot; alt=&quot;image-20260402135649297&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;如果连接成功，则会在输出控制台输出类似如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Connected to the target VM, address: &apos;10.182.50.44:31819&apos;, transport: &apos;socket&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后正常打断点，远程JVM运行到断点处代码，本地IDEA会自动跳转到对应的断点处代码。&lt;/p&gt;
&lt;p&gt;END。&lt;/p&gt;
</description>
      <category>idea</category>
      <category>java</category>
      <category>jvm</category>
    </item>
    <item>
      <title>Python脚本：查询占用端口号进程</title>
      <link>https://blog.kdyzm.cn/post/336</link>
      <guid>https://blog.kdyzm.cn/post/336</guid>
      <pubDate>Tue, 31 Mar 2026 17:12:42 +0800</pubDate>
      <description>&lt;p&gt;作为后端开发，端口号被占用是经常遇到的事情，但是为了查询该问题却并不容易，下面使用一个脚本解决该问题，首先需要安装依赖：&lt;code&gt;pip install psutil&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;完整的脚本如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import subprocess
import re
import psutil
import argparse
from typing import Optional, Dict, List


def get_process_by_port(port: int) -&amp;gt; Optional[Dict]:
    &amp;quot;&amp;quot;&amp;quot;
    通过端口号获取进程信息
    &amp;quot;&amp;quot;&amp;quot;
    try:
        # 使用netstat命令查找占用端口的进程
        cmd = f&amp;quot;netstat -ano | findstr :{port}&amp;quot;
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

        if result.returncode != 0:
            return None

        output_lines = result.stdout.strip().split(&apos;\n&apos;)
        processes = []

        for line in output_lines:
            if not line.strip():
                continue

            # 解析netstat输出
            parts = re.split(r&apos;\s+&apos;, line.strip())

            if len(parts) &amp;gt;= 5:
                protocol = parts[0]
                local_address = parts[1]
                pid = parts[-1]  # 最后一个是PID

                # 验证端口匹配
                if f&amp;quot;:{port}&amp;quot; in local_address:
                    try:
                        pid_int = int(pid)
                        process = psutil.Process(pid_int)

                        process_info = {
                            &apos;pid&apos;: pid_int,
                            &apos;name&apos;: process.name(),
                            &apos;exe&apos;: process.exe(),
                            &apos;status&apos;: process.status(),
                            &apos;create_time&apos;: process.create_time(),
                            &apos;cmdline&apos;: &apos; &apos;.join(process.cmdline()),
                            &apos;username&apos;: process.username(),
                            &apos;protocol&apos;: protocol,
                            &apos;local_address&apos;: local_address
                        }
                        processes.append(process_info)

                    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                        # 进程可能已结束或无权限访问
                        continue

        return processes if processes else None

    except Exception as e:
        print(f&amp;quot;错误: {e}&amp;quot;)
        return None


def get_all_processes() -&amp;gt; List[Dict]:
    &amp;quot;&amp;quot;&amp;quot;
    获取所有进程的基本信息
    &amp;quot;&amp;quot;&amp;quot;
    processes = []
    for proc in psutil.process_iter([&apos;pid&apos;, &apos;name&apos;, &apos;exe&apos;]):
        try:
            process_info = proc.info
            processes.append(process_info)
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    return processes


def search_process_by_keyword(keyword: str) -&amp;gt; List[Dict]:
    &amp;quot;&amp;quot;&amp;quot;
    通过关键字搜索进程
    &amp;quot;&amp;quot;&amp;quot;
    matched = []
    for proc in psutil.process_iter([&apos;pid&apos;, &apos;name&apos;, &apos;exe&apos;, &apos;cmdline&apos;]):
        try:
            info = proc.info
            # 在进程名、可执行文件路径和命令行中搜索
            if (keyword.lower() in info[&apos;name&apos;].lower() or
                    (info[&apos;exe&apos;] and keyword.lower() in info[&apos;exe&apos;].lower()) or
                    (info[&apos;cmdline&apos;] and any(keyword.lower() in str(arg).lower()
                                             for arg in info[&apos;cmdline&apos;]))):
                matched.append(info)
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    return matched


def display_process_info(processes: List[Dict], show_details: bool = False):
    &amp;quot;&amp;quot;&amp;quot;
    显示进程信息
    &amp;quot;&amp;quot;&amp;quot;
    if not processes:
        print(&amp;quot;未找到相关进程&amp;quot;)
        return

    print(f&amp;quot;\n找到 {len(processes)} 个进程:&amp;quot;)
    print(&amp;quot;=&amp;quot; * 80)

    for i, proc in enumerate(processes, 1):
        print(f&amp;quot;\n[{i}] PID: {proc.get(&apos;pid&apos;)}&amp;quot;)
        print(f&amp;quot;    进程名: {proc.get(&apos;name&apos;, &apos;N/A&apos;)}&amp;quot;)
        print(f&amp;quot;    可执行文件: {proc.get(&apos;exe&apos;, &apos;N/A&apos;)}&amp;quot;)

        if &apos;protocol&apos; in proc:
            print(f&amp;quot;    协议: {proc.get(&apos;protocol&apos;)}&amp;quot;)
            print(f&amp;quot;    本地地址: {proc.get(&apos;local_address&apos;)}&amp;quot;)

        if show_details:
            print(f&amp;quot;    状态: {proc.get(&apos;status&apos;, &apos;N/A&apos;)}&amp;quot;)
            print(f&amp;quot;    用户名: {proc.get(&apos;username&apos;, &apos;N/A&apos;)}&amp;quot;)
            print(f&amp;quot;    命令行: {proc.get(&apos;cmdline&apos;, &apos;N/A&apos;)}&amp;quot;)
            if &apos;create_time&apos; in proc:
                from datetime import datetime
                create_time = datetime.fromtimestamp(proc[&apos;create_time&apos;])
                print(f&amp;quot;    创建时间: {create_time.strftime(&apos;%Y-%m-%d %H:%M:%S&apos;)}&amp;quot;)

    print(&amp;quot;=&amp;quot; * 80)


def main():
    parser = argparse.ArgumentParser(description=&apos;Windows端口进程查询工具&apos;)
    parser.add_argument(&apos;-p&apos;, &apos;--port&apos;, type=int, help=&apos;指定端口号&apos;)
    parser.add_argument(&apos;-s&apos;, &apos;--search&apos;, help=&apos;搜索进程名或路径中的关键字&apos;)
    parser.add_argument(&apos;-l&apos;, &apos;--list&apos;, action=&apos;store_true&apos;, help=&apos;列出所有进程&apos;)
    parser.add_argument(&apos;-d&apos;, &apos;--details&apos;, action=&apos;store_true&apos;, help=&apos;显示详细信息&apos;)
    parser.add_argument(&apos;-k&apos;, &apos;--kill&apos;, type=int, nargs=&apos;+&apos;, help=&apos;终止指定PID的进程&apos;)

    args = parser.parse_args()

    if args.port:
        # 查询指定端口
        processes = get_process_by_port(args.port)
        if processes:
            display_process_info(processes, args.details)
        else:
            print(f&amp;quot;端口 {args.port} 未被任何进程占用&amp;quot;)

    elif args.search:
        # 搜索进程
        processes = search_process_by_keyword(args.search)
        display_process_info(processes, args.details)

    elif args.list:
        # 列出所有进程
        processes = get_all_processes()
        print(f&amp;quot;系统中共有 {len(processes)} 个进程:&amp;quot;)
        display_process_info(processes[:50], args.details)  # 默认显示前50个
        if len(processes) &amp;gt; 50:
            print(f&amp;quot;\n(只显示前50个进程，使用搜索功能查看特定进程)&amp;quot;)

    elif args.kill:
        # 终止进程
        for pid in args.kill:
            try:
                process = psutil.Process(pid)
                process_name = process.name()
                process.terminate()  # 尝试正常终止
                print(f&amp;quot;已发送终止信号给进程 {pid} ({process_name})&amp;quot;)

                # 等待进程结束
                try:
                    process.wait(timeout=3)
                    print(f&amp;quot;进程 {pid} 已成功终止&amp;quot;)
                except psutil.TimeoutExpired:
                    process.kill()  # 强制终止
                    print(f&amp;quot;进程 {pid} 被强制终止&amp;quot;)

            except psutil.NoSuchProcess:
                print(f&amp;quot;进程 {pid} 不存在&amp;quot;)
            except psutil.AccessDenied:
                print(f&amp;quot;无权限终止进程 {pid}&amp;quot;)
            except Exception as e:
                print(f&amp;quot;终止进程 {pid} 时出错: {e}&amp;quot;)

    else:
        # 交互模式
        while True:
            print(&amp;quot;\n&amp;quot; + &amp;quot;=&amp;quot; * 50)
            print(&amp;quot;Windows 端口进程查询工具&amp;quot;)
            print(&amp;quot;=&amp;quot; * 50)
            print(&amp;quot;1. 查询端口占用&amp;quot;)
            print(&amp;quot;2. 搜索进程&amp;quot;)
            print(&amp;quot;3. 列出所有进程&amp;quot;)
            print(&amp;quot;4. 终止进程&amp;quot;)
            print(&amp;quot;0. 退出&amp;quot;)

            try:
                choice = input(&amp;quot;\n请选择操作 (0-4): &amp;quot;).strip()

                if choice == &apos;1&apos;:
                    port = input(&amp;quot;请输入端口号: &amp;quot;).strip()
                    if port.isdigit():
                        processes = get_process_by_port(int(port))
                        if processes:
                            display_process_info(processes, True)
                        else:
                            print(f&amp;quot;端口 {port} 未被占用&amp;quot;)
                    else:
                        print(&amp;quot;请输入有效的端口号&amp;quot;)

                elif choice == &apos;2&apos;:
                    keyword = input(&amp;quot;请输入搜索关键字: &amp;quot;).strip()
                    if keyword:
                        processes = search_process_by_keyword(keyword)
                        display_process_info(processes, True)
                    else:
                        print(&amp;quot;请输入搜索关键字&amp;quot;)

                elif choice == &apos;3&apos;:
                    processes = get_all_processes()
                    print(f&amp;quot;系统中共有 {len(processes)} 个进程&amp;quot;)
                    display_process_info(processes[:20], True)

                elif choice == &apos;4&apos;:
                    pid_input = input(&amp;quot;请输入要终止的PID (多个PID用空格分隔): &amp;quot;).strip()
                    if pid_input:
                        pids = []
                        for pid_str in pid_input.split():
                            if pid_str.isdigit():
                                pids.append(int(pid_str))

                        if pids:
                            for pid in pids:
                                try:
                                    process = psutil.Process(pid)
                                    process.terminate()
                                    print(f&amp;quot;已发送终止信号给进程 {pid}&amp;quot;)
                                except Exception as e:
                                    print(f&amp;quot;终止进程 {pid} 时出错: {e}&amp;quot;)
                        else:
                            print(&amp;quot;未输入有效的PID&amp;quot;)

                elif choice == &apos;0&apos;:
                    print(&amp;quot;退出程序&amp;quot;)
                    break
                else:
                    print(&amp;quot;无效选择，请重新输入&amp;quot;)

            except KeyboardInterrupt:
                print(&amp;quot;\n\n程序被用户中断&amp;quot;)
                break
            except Exception as e:
                print(f&amp;quot;发生错误: {e}&amp;quot;)


if __name__ == &amp;quot;__main__&amp;quot;:
    # 检查是否安装了psutil
    try:
        import psutil
    except ImportError:
        print(&amp;quot;错误: 需要安装psutil库&amp;quot;)
        print(&amp;quot;请运行: pip install psutil&amp;quot;)
        exit(1)

    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行效果：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/03/31/70589ea041bd4cc8b1e8c0493f3128ce.png&quot; alt=&quot;image-20260331171104102&quot; style=&quot;zoom:50%;&quot; /&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>记一次腾讯云报警 『Lighthouse存在对外攻击的违规通知』</title>
      <link>https://blog.kdyzm.cn/post/335</link>
      <guid>https://blog.kdyzm.cn/post/335</guid>
      <pubDate>Fri, 30 Jan 2026 11:21:49 +0800</pubDate>
      <description>&lt;p&gt;2026年1月29日晚上下班的时候，收到了腾讯云发来的邮件通知：&lt;strong&gt;Lighthouse存在对外攻击的违规通知&lt;/strong&gt;，着实把我吓了一跳，以为服务器要被封禁了。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/01/30/2865e6939a9144bfabe95ffc7834781c.png&quot; alt=&quot;image-20260130095325299&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;看到这个告警，我真是丈二的和尚摸不着头脑，这个服务器已经运行了好几年了一直没问题，怎么会突然报警呢？邮件中提到为什么会收到这个通知：&lt;strong&gt;通常是由于您的服务存在对外攻击、网络扫描、恶意爬虫、钓鱼网站、垃圾或钓鱼邮件、传播病毒木马、存在黑链或网站被篡改等违法违规行为。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;也就是说我攻击别人网站被腾讯发现了？&lt;strong&gt;真是滑天下之大稽，我都不懂网络攻防好不好，别给我开玩笑了。跟着指引来到解决方案的页面：&lt;a href=&quot;https://cloud.tencent.com/document/product/301/9610&quot;&gt;《安全违规处理帮助指引》&lt;/a&gt; ，根据指引，我发现最有可能出现问题的就是我在服务器上安装的&lt;/strong&gt;frp&lt;/strong&gt;服务出了问题。&lt;/p&gt;
&lt;h2 id=&quot;一排查过程&quot;&gt;一、排查过程&lt;/h2&gt;
&lt;p&gt;根据腾讯云提供的排查文档：﻿&lt;a href=&quot;https://cloud.tencent.com/document/product/296/9604&quot;&gt;Linux 主机安全排查指引&lt;/a&gt;﻿ ，一步一步排查是哪里出现的问题。&lt;/p&gt;
&lt;h3 id=&quot;1检查弱口令&quot;&gt;1、检查弱口令&lt;/h3&gt;
&lt;p&gt;我的服务器已经关闭了密码登录，只能根据RSA秘钥登录，除非我的秘钥泄露，否则不可能有人能登录我的服务器，通过last命令查看服务器近期登录的账户记录，确认是否有可疑 IP 登录过机器：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/01/30/a2a1f23948e549cea4ba0a6ad2567719.png&quot; alt=&quot;image-20260130100900321&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这几个ip地址都是我本地的ip地址，没有任何问题，说明不是非法登录导致的。&lt;/p&gt;
&lt;p&gt;通过命令&lt;code&gt;less /var/log/secure|grep &apos;Accepted&apos;&lt;/code&gt;再次确认：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/01/30/815605a6e3b94c7e90bded755556b06d.png&quot; alt=&quot;image-20260130101110493&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以百分百确认没有问题。&lt;/p&gt;
&lt;h3 id=&quot;2检查恶意进程及非法端口&quot;&gt;2、检查恶意进程及非法端口&lt;/h3&gt;
&lt;p&gt;官方文档中有提到“&lt;strong&gt;排查设备是否搭建了网络代理服务（如 frp、HAProxy 等），被恶意利用作为对外攻击跳板&lt;/strong&gt;”，正巧，我搭建了frp服务做内网穿透，frp 服务的日志非常诡异：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/01/30/3921bf9debe348358ba7053be8b9cc7a.png&quot; alt=&quot;image-20260130101806576&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;可以看到，这个日志显示了有个frp client中有个叫做test9080的配置每秒一次访问我的frp server，客户端ip地址是185.135.77.63&lt;/strong&gt;，我的frp client并没有test9080的配置。。重启frp server，这个test9080会立马重新注册上来，查了下185.135.77.63这个ip地址：&lt;/p&gt;
&lt;p&gt;百度智能云的结果：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/01/30/664c4c9de38e46e7894f043b34ee9278.png&quot; alt=&quot;image-20260130102130527&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;没错，对方使用了VPN恶意注册到了我的frp server，不知道做了什么事情，被腾讯云检测出来我这台机器发起了网络攻击，所以被腾讯云警告了。&lt;/p&gt;
&lt;p&gt;把日志下载下来，好好排查了一下，发现在2026年1月27日的时候，第一次出现了testXXX的访问记录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;2026/01/27 15:00:10 [1;34m[I] [service.go:449] [a02bb422d2875ddd] client login info: ip [185.135.77.63:63368] version [0.44.0] hostname [] os [windows] arch [amd64][0m
2026/01/27 15:00:10 [1;34m[I] [tcp.go:63] [a02bb422d2875ddd] [test9083.] tcp proxy listen port [7083][0m
2026/01/27 15:00:10 [1;34m[I] [control.go:446] [a02bb422d2875ddd] new proxy [test9083.] success[0m
2026/01/27 15:00:10 [1;34m[I] [tcp.go:63] [a02bb422d2875ddd] [test5556...] tcp proxy listen port [5556][0m
2026/01/27 15:00:10 [1;34m[I] [control.go:446] [a02bb422d2875ddd] new proxy [test5556...] success[0m
2026/01/27 15:00:10 [1;34m[I] [tcp.go:63] [a02bb422d2875ddd] [test9080...] tcp proxy listen port [9080][0m
2026/01/27 15:00:10 [1;34m[I] [control.go:446] [a02bb422d2875ddd] new proxy [test9080...] success[0m
2026/01/27 15:00:10 [1;34m[I] [tcp.go:63] [a02bb422d2875ddd] [test9082...] tcp proxy listen port [9082][0m
2026/01/27 15:00:10 [1;34m[I] [control.go:446] [a02bb422d2875ddd] new proxy [test9082...] success[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录成功的ip地址正是185.135.77.63，同时它注册了好几个端口号到frp server。后来它换了好几个ip地址，其中有一个ip地址162.142.125.193被直接认定为攻击威胁ip地址：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2026/01/30/d0cdd3577a814fe8814277239ca66f74.png&quot; alt=&quot;image-20260130103000262&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;毫无疑问，我的机器被当做肉鸡发起了对外的攻击，但是原理我还没搞明白。&lt;/p&gt;
&lt;h3 id=&quot;3frp服务端被恶意利用作为攻击跳板的原理&quot;&gt;3、FRP服务端被恶意利用作为攻击跳板的原理&lt;/h3&gt;
&lt;p&gt;大模型告诉我，攻击者并没有破解我的服务器密码，而是利用了我&lt;strong&gt;FRP服务端配置上的安全漏洞&lt;/strong&gt;，将其变成了一个“公共代理”或“跳板服务器”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;攻击者的完整利用链条：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;扫描发现&lt;/strong&gt;：攻击者使用工具（如Shodan, Zoomeye, Fofa）全网扫描开放了7000端口的IP。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;尝试连接&lt;/strong&gt;：攻击者尝试用默认配置或常见的弱token连接我的的FRPS。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;连接成功&lt;/strong&gt;：由于我缺少认证或使用了弱token，攻击者成功将自己的FRP客户端连接到我的服务器上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;建立恶意隧道&lt;/strong&gt;：攻击者在自己的FRP客户端配置中，设置类型为 &lt;code&gt;socks5&lt;/code&gt;或 &lt;code&gt;tcp&lt;/code&gt;的代理，指向他想攻击的目标。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;发起攻击&lt;/strong&gt;：攻击者所有的后续请求（DDoS流量、漏洞扫描、爬取数据、爆破其他服务）都经由&lt;strong&gt;我的FRP服务器&lt;/strong&gt;转发出去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;后果&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;受害者&lt;/strong&gt;看到攻击流量全部来源于&lt;strong&gt;我的服务器IP&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我收到腾讯服务商的安全投诉，或被目标方的防火墙封禁IP。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我的服务器因转发大量攻击流量，可能产生高带宽消耗，甚至被服务商关停。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果攻击行为触犯法律，我可能需要配合调查，证明自身也是受害者。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;TCP代理隧道配置案例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[common]
server_addr = B.B.B.B  # 你的FRP服务端
server_port = 7000
token = 你的密码（已被窃取）

[attack_proxy]
type = tcp
local_ip = C.C.C.C     # 关键：这里填第三方受害者的IP！
local_port = 80        # 关键：这里填第三方受害者的端口！
remote_port = 22222    # 在你的服务器上开启的端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;攻击流程：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;攻击者在自己的机器上运行此配置&lt;/li&gt;
&lt;li&gt;隧道建立后，所有发给 &lt;code&gt;B.B.B.B:22222&lt;/code&gt; 的流量&lt;/li&gt;
&lt;li&gt;都会被你的FRP服务端转发到 &lt;code&gt;C.C.C.C:80&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;受害者看到的所有攻击都来自 &lt;strong&gt;你的服务器IP（B.B.B.B）&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;作为Socks5代理（更常见、更危险）案例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[socks5_attack]
type = tcp
remote_port = 10808
plugin = socks5
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;攻击者建立Socks5代理隧道&lt;/li&gt;
&lt;li&gt;设置浏览器、扫描器、攻击工具使用Socks5代理：&lt;code&gt;B.B.B.B:10808&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;所有攻击流量先到你的服务器，再被转发给任意第三方目标&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;受害者只能溯源到你的IP&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;二解决方案&quot;&gt;二、解决方案&lt;/h2&gt;
&lt;p&gt;我的frp服务器使用了默认的7000端口号，并且没有设置token验证导致攻击方利用漏洞将我的服务器恶意利用作为对外攻击跳板。解决方案就非常简单了：修改默认端口号并且加上token认证就好了。&lt;/p&gt;
&lt;p&gt;frps配置修改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# 修改默认的端口号
bind_port = xxxx
# 使用token认证
token = xxxx
# 添加端口号白名单
allow_ports = 8081,8082,8083...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的frpc配置修改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# 修改服务端默认端口号
server_port = xxxx
# 使用token认证
authentication_method = token
authenticate_heartbeats = true
authenticate_new_work_conns = true
# 修改使用的认证token
token = xxxx
heartbeat_interval = 30
heartbeat_timeout = 90
login_fail_exit = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依次重启服务端和客户端，再次观察日志，就可以发现对方已经无法注册上去了。&lt;/p&gt;
</description>
      <category>linux</category>
      <category>网络安全</category>
    </item>
    <item>
      <title>MySql8.0公共表表达式『CTE』</title>
      <link>https://blog.kdyzm.cn/post/334</link>
      <guid>https://blog.kdyzm.cn/post/334</guid>
      <pubDate>Tue, 28 Oct 2025 15:40:32 +0800</pubDate>
      <description>&lt;p&gt;CTE是『common table expression』的缩写，中文翻译过来就是『公共表表达式』，使用它可以为临时查询结果命名，命名后可以在后续的查询语句中反复引用。CTE完整语法格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH [RECURSIVE]
    cte_name [(column_list)] AS (
        subquery
    )
    [, cte_name [(column_list)] AS (subquery)] ...
SELECT ... FROM cte_name ...;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;『RECURSIVE』是递归的意思，但是它可选，表示CTE有两种模式：普通CTE和递归CTE。&lt;/p&gt;
&lt;p&gt;以下内容节选自官方文档：&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/with.html#common-table-expressions-recursive-examples&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/with.html#common-table-expressions-recursive-examples&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;一普通cte&quot;&gt;一、普通CTE&lt;/h2&gt;
&lt;p&gt;普通CTE写法是最常用的写法，一个经典的写法如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH
  cte1 AS (SELECT a, b FROM table1),
  cte2 AS (SELECT c, d FROM table2)
SELECT b, d FROM cte1 JOIN cte2
WHERE cte1.a = cte2.c;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个没什么好说的，非常简单，需要注意的是，不只是可以写select语句，还可以写update、delete语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH ... SELECT ...
WITH ... UPDATE ...
WITH ... DELETE ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;二递归cte&quot;&gt;二、递归CTE&lt;/h2&gt;
&lt;p&gt;递归CTE是一种在查询中引用自身的写法，是处理层次结构或树形数据的强大工具。其完整语法如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte_name [(column_list)] AS (
    -- 初始化查询提供初始结果集
    SELECT initial_columns
    FROM initial_table
    WHERE initial_condition
    
    UNION [ALL | DISTINCT]
    
    -- 递归部分
    SELECT recursive_columns
    FROM recursive_table
    JOIN cte_name ON join_condition
    WHERE recursive_condition
)
SELECT * FROM cte_name [OPTIONAL_CLAUSES];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归CTE的子查询分为两部分，通过&lt;code&gt;UNION [ALL | DISTINCT]&lt;/code&gt;连接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT ...      -- 非递归select语句，提供初始结果集，不引用CTE名字
UNION [ALL | DISTINCT]
SELECT ...      -- 递归select语句，引用CTE名字
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是基础部分和递归部分是通过&lt;code&gt;UNION [ALL | DISTINCT]&lt;/code&gt;连接的，虽然语法上允许&lt;code&gt;UNION DISTINCT&lt;/code&gt;去重，但是实际上几乎都是使用&lt;code&gt;UNION  ALL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;递归CTE语法有如下限制使用条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;基础部分的列长度将会限制递归部分的列长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;递归select语句不能包含如下语句：聚合函数比如&lt;code&gt;SUM()&lt;/code&gt;、&lt;a href=&quot;https://blog.kdyzm.cn/post/288&quot;&gt;窗口函数&lt;/a&gt;、&lt;code&gt;GROUP BY&lt;/code&gt;、&lt;code&gt;order by&lt;/code&gt;、&lt;code&gt;distinct&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;递归select语句只能引用一次CTE，并且只能在from语句中使用，不能在任何子查询中使用；可以使用join语句与其它表连接，但是在这种使用场景中，CTE不能位于&lt;code&gt;LEFT JOIN&lt;/code&gt;的右侧。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;1案例讲解&quot;&gt;1、案例讲解&lt;/h3&gt;
&lt;p&gt;递归CTE不是很容易理解，下面通过几个案例来说明下：&lt;/p&gt;
&lt;h4 id=&quot;案例1打印1到5&quot;&gt;&lt;strong&gt;案例1：打印1到5&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n &amp;lt; 5
)
SELECT * FROM cte;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;+------+
| n    |
+------+
|    1 |
|    2 |
|    3 |
|    4 |
|    5 |
+------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先执行基础查询部分 &lt;code&gt;SELECT 1&lt;/code&gt;，生成初始结果集，此时CTE的结果为：&lt;code&gt;[1]&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一次递归迭代&lt;/strong&gt;：从CTE中取出n=1，执行递归部分 &lt;code&gt;SELECT n + 1 FROM cte WHERE n &amp;lt; 5&lt;/code&gt;，计算：1 + 1 = 2，，将结果2添加到CTE中，现在CTE的结果为：&lt;code&gt;[1, 2]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二次递归迭代&lt;/strong&gt;：从CTE中取出n=2，执行递归部分，计算：2 + 1 = 3，将结果3添加到CTE中，现在CTE的结果为：&lt;code&gt;[1, 2, 3]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三次递归迭代&lt;/strong&gt;：从CTE中取出n=3，执行递归部分，计算：3 + 1 = 4，将结果4添加到CTE中，现在CTE的结果为：&lt;code&gt;[1, 2, 3, 4]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四次递归迭代&lt;/strong&gt;：从CTE中取出n=4，执行递归部分，计算：4 + 1 = 5，将结果5添加到CTE中，现在CTE的结果为：&lt;code&gt;[1, 2, 3, 4, 5]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;终止条件检查&lt;/strong&gt;：下一次迭代时n=5，不满足WHERE条件 &lt;code&gt;n &amp;lt; 5&lt;/code&gt;，递归终止。&lt;/p&gt;
&lt;p&gt;所以，最终的查询结果就是&lt;code&gt;[1, 2, 3, 4, 5]&lt;/code&gt;&lt;/p&gt;
&lt;h4 id=&quot;案例2重复字符串&quot;&gt;案例2：重复字符串&lt;/h4&gt;
&lt;p&gt;在下面这个案例中，将要验证递归CTE的一个特性：基础部分的列长度将会限制递归部分的列长度。什么意思呢？看下面的sql语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte AS
(
  SELECT 1 AS n, &apos;abc&apos; AS str
  UNION ALL
  SELECT n + 1, CONCAT(str, str) FROM cte WHERE n &amp;lt; 3
)
SELECT * FROM cte;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常来说，我们预测输出如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;+------+--------------+
| n    | str          |
+------+--------------+
|    1 | abc          |
|    2 | abcabc       |
|    3 | abcabcabcabc |
+------+--------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在实际执行过程中，如果是严格模式下的运行，会提示报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;错误代码： 1406
Data too long for column &apos;str&apos; at row 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是非严格模式下的运行，则会有结果如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;+------+------+
| n    | str  |
+------+------+
|    1 | abc  |
|    2 | abc  |
|    3 | abc  |
+------+------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果总是和我们的预测不一样。这是因为在初始语句&lt;code&gt;SELECT 1 AS n, &apos;abc&apos; AS str&lt;/code&gt;中，&lt;code&gt;&apos;abc&apos;&lt;/code&gt;字符串长度为3，这限制了之后的递归查询语句中的所有str列长度都是3，那么&lt;code&gt;&apos;abcabc&apos;&lt;/code&gt;就存储不了了，在非严格模式下，结果会被截断，仍然是&lt;code&gt;&apos;abc&apos;&lt;/code&gt;；在严格模式下，则会报错。&lt;/p&gt;
&lt;p&gt;解决方案就是在初始语句中重新定义列长度：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte AS
(
  SELECT 1 AS n, CAST(&apos;abc&apos; AS CHAR(20)) AS str
  UNION ALL
  SELECT n + 1, CONCAT(str, str) FROM cte WHERE n &amp;lt; 3
)
SELECT * FROM cte;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;CAST(&apos;abc&apos; AS CHAR(20)) &lt;/code&gt;将列长度扩展到了20，这样就能在接下来的递归中容纳的了&lt;code&gt;abcabcabc&lt;/code&gt;了。&lt;/p&gt;
&lt;h4 id=&quot;案例3斐波那契数列&quot;&gt;案例3：斐波那契数列&lt;/h4&gt;
&lt;p&gt;斐波那契数列的定义如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/10/28/c4f19642c4364bec961bf08e1484a9d7.png&quot; alt=&quot;image-20251028104635874&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;按照定义，前10个斐波那契额数列如下所示：0,1,1,2,3,5,8,13,21,34&lt;/p&gt;
&lt;p&gt;写下sql打印如上斐波那契额数列：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte AS 
(
SELECT 1 AS n,0 AS fn,1 AS fn_next
UNION ALL
SELECT n+1,fn_next,fn+fn_next FROM cte WHERE n&amp;lt;10
)
SELECT * FROM cte
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;案例4日期序列生成&quot;&gt;案例4：日期序列生成&lt;/h4&gt;
&lt;p&gt;首先先创建表并插入数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 创建 sales 表
CREATE TABLE sales (
    DATE DATE,
    price DECIMAL(10,2)
);

-- 插入数据
INSERT INTO sales (DATE, price) VALUES
(&apos;2017-01-03&apos;, 100.00),
(&apos;2017-01-03&apos;, 200.00),
(&apos;2017-01-06&apos;, 50.00),
(&apos;2017-01-08&apos;, 10.00),
(&apos;2017-01-08&apos;, 20.00),
(&apos;2017-01-08&apos;, 150.00),
(&apos;2017-01-10&apos;, 5.00);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在根据日期分组统计销售额：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&amp;gt; SELECT date, SUM(price) AS sum_price
       FROM sales
       GROUP BY date
       ORDER BY date;
+------------+-----------+
| date       | sum_price |
+------------+-----------+
| 2017-01-03 |    300.00 |
| 2017-01-06 |     50.00 |
| 2017-01-08 |    180.00 |
| 2017-01-10 |      5.00 |
+------------+-----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然查询结果是对的，但是这并不是我想要的结果，我想要的结果应该是连续的日期，如果销售额为0则显示0，我想要的输出应该是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;+------------+-----------+
| date       | sum_price |
+------------+-----------+
| 2017-01-03 |    300.00 |
| 2017-01-04 |      0.00 |
| 2017-01-05 |      0.00 |
| 2017-01-06 |     50.00 |
| 2017-01-07 |      0.00 |
| 2017-01-08 |    180.00 |
| 2017-01-09 |      0.00 |
| 2017-01-10 |      5.00 |
+------------+-----------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以借助递归CTE实现该功能：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE dates (date) AS
(
  SELECT MIN(date) FROM sales
  UNION ALL
  SELECT date + INTERVAL 1 DAY FROM dates
  WHERE date + INTERVAL 1 DAY &amp;lt;= (SELECT MAX(date) FROM sales)
)
SELECT dates.date, COALESCE(SUM(price), 0) AS sum_price
FROM dates LEFT JOIN sales ON dates.date = sales.date
GROUP BY dates.date
ORDER BY dates.date;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然这个查询有些复杂，最重要的一部分是这部分sql：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE dates (DATE) AS
(
  SELECT MIN(DATE) FROM sales
  UNION ALL
  SELECT DATE + INTERVAL 1 DAY FROM dates
  WHERE DATE + INTERVAL 1 DAY &amp;lt;= (SELECT MAX(DATE) FROM sales)
)
SELECT dates.date FROM dates
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分sql生成了sales表中从最小日期到最大日期之间的所有日期列表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;+------------+
| date       |
+------------+
| 2017-01-03 |
| 2017-01-04 |
| 2017-01-05 |
| 2017-01-06 |
| 2017-01-07 |
| 2017-01-08 |
| 2017-01-09 |
| 2017-01-10 |
+------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后通过左外连接sales表分组统计销售额，这样就实现了日期的连续列表。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;COALESCE&lt;/code&gt;函数用于返回第一个不为NULL的值，在此处&lt;code&gt;COALESCE(SUM(price), 0)&lt;/code&gt;的意思是如果没有销售额，则为0。&lt;/p&gt;
&lt;h4 id=&quot;案例5树状组织架构&quot;&gt;案例5：树状组织架构&lt;/h4&gt;
&lt;p&gt;接下来创建一个雇员表来演示树状组织架构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE employees (
  id         INT PRIMARY KEY NOT NULL,
  name       VARCHAR(100) NOT NULL,
  manager_id INT NULL,
  INDEX (manager_id),
FOREIGN KEY (manager_id) REFERENCES employees (id)
);
INSERT INTO employees VALUES
(333, &amp;quot;Yasmina&amp;quot;, NULL),  # Yasmina is the CEO (manager_id is NULL)
(198, &amp;quot;John&amp;quot;, 333),      # John has ID 198 and reports to 333 (Yasmina)
(692, &amp;quot;Tarek&amp;quot;, 333),
(29, &amp;quot;Pedro&amp;quot;, 198),
(4610, &amp;quot;Sarah&amp;quot;, 29),
(72, &amp;quot;Pierre&amp;quot;, 29),
(123, &amp;quot;Adil&amp;quot;, 692);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完上述sql，表中内容如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;+------+---------+------------+
| id   | name    | manager_id |
+------+---------+------------+
|   29 | Pedro   |        198 |
|   72 | Pierre  |         29 |
|  123 | Adil    |        692 |
|  198 | John    |        333 |
|  333 | Yasmina |       NULL |
|  692 | Tarek   |        333 |
| 4610 | Sarah   |         29 |
+------+---------+------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们要写一个查询sql，用于查询每个雇员向上汇报的路径，其查询结果应当如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;+------+---------+-----------------+
| id   | name    | path            |
+------+---------+-----------------+
|  333 | Yasmina | 333             |
|  198 | John    | 333,198         |
|   29 | Pedro   | 333,198,29      |
| 4610 | Sarah   | 333,198,29,4610 |
|   72 | Pierre  | 333,198,29,72   |
|  692 | Tarek   | 333,692         |
|  123 | Adil    | 333,692,123     |
+------+---------+-----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询sql如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE employee_paths (id, name, path) AS
(
  SELECT id, name, CAST(id AS CHAR(200))
    FROM employees
    WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.name, CONCAT(ep.path, &apos;,&apos;, e.id)
    FROM employee_paths AS ep JOIN employees AS e
      ON ep.id = e.manager_id
)
SELECT * FROM employee_paths ORDER BY path;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个CTE语句从根节点（CEO）节点开始查找，查找谁的上级是CEO，然后依次递归查找，直到普通雇员为止，由于普通雇员不是谁的上级，所以JOIN语句会返回空，递归结束。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进阶问题1：查询Sarah的向上汇报路径，每个节点返回一条数据&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE employees_paths(id,NAME,manager_id) AS
(
SELECT id,NAME,manager_id FROM `employees`
WHERE NAME = &apos;Sarah&apos;
UNION ALL
SELECT employees.id,employees.name,employees.manager_id FROM employees_paths 
JOIN `employees` ON employees_paths.manager_id = employees.`id`
)
SELECT * FROM employees_paths
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;    id  name     manager_id  
------  -------  ------------
  4610  Sarah              29
    29  Pedro             198
   198  John              333
   333  Yasmina        (NULL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;进阶问题2：查询John的所有下级节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个问题要求的是John的所有下级节点，由于子节点也有子节点，所以需要递归查询，查询sql如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE employees_paths(id,NAME,manager_id) AS
(
SELECT id,NAME,manager_id FROM `employees`
WHERE NAME = &apos;John&apos;
UNION ALL
SELECT employees.id,employees.name,employees.manager_id FROM employees_paths 
JOIN `employees` ON employees_paths.id = employees.`manager_id`
)
SELECT * FROM employees_paths
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;    id  NAME    manager_id  
------  ------  ------------
   198  John             333
    29  Pedro            198
    72  Pierre            29
  4610  Sarah             29
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个问题的查询sql和上一个问题的查询sql非常像，只是将连接条件互换了下，其结果就完全不一样了。&lt;/p&gt;
&lt;h3 id=&quot;2递归失控&quot;&gt;2、递归失控&lt;/h3&gt;
&lt;p&gt;如果递归终止条件设置的不正确，则会导致递归失控，看下面的案例sql：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte
)
SELECT * FROM cte
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在mysql中执行上述语句，会提示如下报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;错误代码： 3636
Recursive query aborted after 1001 iterations. Try increasing @@cte_max_recursion_depth to a larger value.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意思是递归次数超过了系统限制，拒绝执行剩余递归查询。从报错上来看，系统默认设置的递归最大次数是1000次，可以在命令行中动态调整&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_cte_max_recursion_depth&quot;&gt;&lt;code&gt;cte_max_recursion_depth&lt;/code&gt;&lt;/a&gt;的值设置最大递归次数，比如我执行了命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SET SESSION cte_max_recursion_depth = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次执行程序，报错提示就变成了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;错误代码： 3636
Recursive query aborted after 11 iterations. Try increasing @@cte_max_recursion_depth to a larger value.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，还可以设置 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_execution_time&quot;&gt;&lt;code&gt;max_execution_time&lt;/code&gt;&lt;/a&gt; 值限制递归查询最大超时时间，单位是毫秒，默认值0表示无超时时间限制。&lt;/p&gt;
&lt;h3 id=&quot;3使用limit终止递归&quot;&gt;3、使用limit终止递归&lt;/h3&gt;
&lt;p&gt;在mysql8.0.19开始，mysql开始支持使用limit语句终止递归，比如在上面的查询中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte
)
SELECT * FROM cte
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于没有设置递归终止条件，会在递归1000次以后，触发系统递归最大次数的阈值上限从而报错。通过limit语句也可以终止递归：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH RECURSIVE cte (n) AS
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte LIMIT 1001
)
SELECT * FROM cte
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样会查询出来1到1001共1001个数。&lt;/p&gt;
</description>
      <category>mysql</category>
    </item>
    <item>
      <title>Redis（十三）：Caffeine+Redis高性能二级缓存实现</title>
      <link>https://blog.kdyzm.cn/post/333</link>
      <guid>https://blog.kdyzm.cn/post/333</guid>
      <pubDate>Fri, 17 Oct 2025 17:33:06 +0800</pubDate>
      <description>&lt;p&gt;上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/327&quot;&gt;Redis（十二）：Java客户端之Spring Data Redis&lt;/a&gt;》最后讲了使用Redis作为缓存集成到SpringBoot，虽然Redis比较高效，但是相对于本地缓存来说效率还是比较低。本篇文章将基于&lt;code&gt;Java21&lt;/code&gt;+&lt;code&gt;Springboot3.5.4&lt;/code&gt;，使用&lt;code&gt;Caffeine&lt;/code&gt;作为一级缓存，&lt;code&gt;Redis&lt;/code&gt;作为二级缓存实现高性能二级缓存功能，并封装成SpringBoot Starter上传到Maven中央仓库。&lt;/p&gt;
&lt;h2 id=&quot;一springboot-caching&quot;&gt;一、SpringBoot Caching&lt;/h2&gt;
&lt;p&gt;SpringBoot中有个&lt;code&gt;spring-boot-starter-cache&lt;/code&gt;组件，该组件是 Spring Boot 提供的用于简化缓存管理的 Starter 依赖，其核心作用是为 Spring 应用提供声明式缓存支持，通过抽象层统一不同缓存技术的使用方式，从而提升应用性能并降低开发复杂度。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;spring-boot-starter-cache&lt;/code&gt;本身不实现具体缓存技术，而是通过 Spring Cache 抽象层（如 &lt;code&gt;CacheManager&lt;/code&gt;和 &lt;code&gt;Cache&lt;/code&gt;接口）支持多种缓存后端（如 Redis、Caffeine、EhCache 等）。开发者只需通过注解配置即可切换缓存实现，无需修改业务代码。通过 &lt;code&gt;@EnableCaching&lt;/code&gt;注解启用缓存后，只需更换依赖（如从 &lt;code&gt;ConcurrentMap&lt;/code&gt;切换到 &lt;code&gt;Redis&lt;/code&gt;），Spring Boot 会自动配置对应的 &lt;code&gt;CacheManager&lt;/code&gt;。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/18/6c9d78ae958c4eebb5a766660d55600d.png&quot; alt=&quot;image-20250818162806431&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;从上图可以看到，spring-boot-starter-cache组件没有任何代码，它只是通过pom文件将相关的依赖引入进来：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-boot-starter&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;3.5.4&amp;lt;/version&amp;gt;
      &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-context-support&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;6.2.9&amp;lt;/version&amp;gt;
      &amp;lt;scope&amp;gt;compile&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最重要的依赖就是&lt;code&gt;spring-context-support&lt;/code&gt;，它是&lt;code&gt;spring-context&lt;/code&gt;的扩展组件。&lt;/p&gt;
&lt;p&gt;在使用的时候，&lt;/p&gt;
&lt;p&gt;用户需&lt;strong&gt;显式引入&lt;/strong&gt;所需的缓存库（如 &lt;code&gt;caffeine&lt;/code&gt;或 &lt;code&gt;spring-boot-starter-data-redis&lt;/code&gt;），Spring Boot 会根据依赖自动配置对应的 &lt;code&gt;CacheManager&lt;/code&gt;。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引入 &lt;code&gt;caffeine&lt;/code&gt;后，Spring Boot 会生成 &lt;code&gt;CaffeineCacheManager&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;引入 Redis 后，会生成 &lt;code&gt;RedisCacheManager&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1cachemanager&quot;&gt;1、CacheManager&lt;/h3&gt;
&lt;p&gt;CacheManager类是Spring抽象出来的缓存管理器接口，从名字上就知道它是干什么的了：缓存管理器，似乎是专门管理缓存的，那我们猜测它应该有CRUD方法，实际它长什么样子呢？&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/09/22/d0ba6962824240ef8fffc143e09018e2.png&quot; alt=&quot;image-20250922153701020&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到该接口特别简单，它只提供了getCache和getCacheNames方法，和我们预想的它应该有CRUD方法有很大不同，为什么会这样子呢？&lt;/p&gt;
&lt;p&gt;因为**动态缓存管理是特定于底层技术的，无法被完美抽象，Spring &lt;code&gt;CacheManager&lt;/code&gt; 的核心职责不是一个缓存实例的管理器，而是一个缓存抽象的门面或访问点。**因此它没有将对缓存的具体操作抽象出来，具体的操作都封装到了&lt;code&gt;Cache&lt;/code&gt;接口中，由不同的服务提供商实现。&lt;/p&gt;
&lt;p&gt;RedisCacheManager不是必须的，以Redis为例，在我们使用&lt;code&gt;spring-boot-starter-data-redis&lt;/code&gt;的时候，如果只是使用了RedisTemplate直接操作Redis，则RedisCacheManager就算不存在也没关系，因为这两者是服务于不同目的、不同抽象层次的工具，理论上可以完全独立使用。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;特性&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;RedisTemplate&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;RedisCacheManager&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;抽象层级&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;命令式、底层操作&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;声明式、高层抽象&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;核心目的&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;提供一套丰富的、类型化的 API 来直接执行 Redis 命令（如 &lt;code&gt;opsForValue()&lt;/code&gt;, &lt;code&gt;opsForHash()&lt;/code&gt;）。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;作为 Spring &lt;strong&gt;缓存抽象&lt;/strong&gt; 的实现，将 &lt;code&gt;@Cacheable&lt;/code&gt; 等注解的行为映射到 Redis。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;使用方式&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;在代码中显式调用其方法，如 &lt;code&gt;redisTemplate.opsForValue().set(&amp;quot;key&amp;quot;, &amp;quot;value&amp;quot;)&lt;/code&gt;。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;由 Spring 框架在背后&lt;strong&gt;自动调用&lt;/strong&gt;，开发者通过注解来使用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;数据序列化&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;高度可控。可以为不同的操作（value, hash key, hash value等）单独配置序列化器。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;通常为缓存值（Cache）统一配置序列化器，Key 的生成也遵循特定规则。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;是否必需&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;是&lt;/strong&gt; 直接操作 Redis 的&lt;strong&gt;必需组件&lt;/strong&gt;。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;否&lt;/strong&gt; 仅在使用 Spring 的声明式缓存注解（&lt;code&gt;@Cacheable&lt;/code&gt;等）时才需要。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果我们想设计Caffeine+Reids二级缓存实现，并且使用Redis注解实现缓存功能，则自己实现CacheManager即可。那问题来了，是不是RedisCacheManager也会自动创建，如果自动创建了，那会使用哪个CacheManager呢？实际上应用程序只允许一个CacheManager存在，看看RedisCacheManager的配置（&lt;code&gt;org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration&lt;/code&gt;）：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/09/22/e321f915f66e40de8f8544e91efe52e6.png&quot; alt=&quot;image-20250922161023037&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这里面有个很重要的注解：&lt;code&gt;@ConditionalOnMissingBean(CacheManager.class)&lt;/code&gt;，意思就是如果我们自定义了CacheManager，就不会创建默认的RedisCacheManager了。这样我们自定义CacheManager就可以取而代之了。&lt;/p&gt;
&lt;h3 id=&quot;2cache&quot;&gt;2、Cache&lt;/h3&gt;
&lt;p&gt;Cache接口和CacheManager接口搭配使用，CacheManager&lt;strong&gt;对外提供统一的访问入口&lt;/strong&gt;，对缓存的CRUD底层操作则依赖于Cache接口。可以这么认为：&lt;strong&gt;&lt;code&gt;CacheManager&lt;/code&gt; 是&lt;/strong&gt;一个&lt;strong&gt;工具箱&lt;/strong&gt;，而 &lt;strong&gt;&lt;code&gt;Cache&lt;/code&gt; 是工具箱里的&lt;/strong&gt;一把把&lt;strong&gt;具体的工具&lt;/strong&gt;（比如螺丝刀、锤子、扳手）。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;特性&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;CacheManager (工具箱)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Cache (工具)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;角色&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;容器、工厂、管理器&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;内容、产品、被管理对象&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;核心职责&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1. &lt;strong&gt;管理所有 &lt;code&gt;Cache&lt;/code&gt; 实例的生命周期&lt;/strong&gt;。 2. &lt;strong&gt;根据名称提供（获取或创建）&lt;code&gt;Cache&lt;/code&gt; 对象&lt;/strong&gt;。 3. &lt;strong&gt;对外提供统一的访问入口&lt;/strong&gt;。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1. &lt;strong&gt;定义缓存的基本操作接口&lt;/strong&gt;（CRUD）。 2. &lt;strong&gt;实际存储和访问缓存数据&lt;/strong&gt;（键值对）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;关注点&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;“有哪些缓存？”&lt;/strong&gt; &lt;strong&gt;“给我名叫‘A’的缓存”&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;“缓存里有什么？”&lt;/strong&gt; &lt;strong&gt;“根据Key‘123’获取值”&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;方法&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;Cache getCache(String name)&lt;/code&gt; &lt;code&gt;Collection&amp;lt;String&amp;gt; getCacheNames()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;ValueWrapper get(Object key)&lt;/code&gt; &lt;code&gt;void put(Object key, Object value)&lt;/code&gt; &lt;code&gt;void evict(Object key)&lt;/code&gt; &lt;code&gt;void clear()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;类比&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;JDBC 中的 &lt;code&gt;DataSource&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;JDBC 中的 &lt;code&gt;Connection&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;由于我们要设计并实现Caffeine+Redis二级缓存架构，所以可以预想到的是必须自定义Cache并在里面实现二级缓存的CRUD功能。&lt;/p&gt;
&lt;h3 id=&quot;3enablecaching&quot;&gt;3、@EnableCaching&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@EnableCaching&lt;/code&gt; 和 &lt;code&gt;spring-boot-starter-data-redis&lt;/code&gt; 提供的自动配置（Auto-Configuration）是&lt;strong&gt;两个完全独立的功能&lt;/strong&gt;。&lt;strong&gt;&lt;code&gt;@EnableCaching&lt;/code&gt;&lt;/strong&gt; 只负责&lt;strong&gt;激活 Spring 的声明式缓存抽象层&lt;/strong&gt;（即让 &lt;code&gt;@Cacheable&lt;/code&gt;, &lt;code&gt;@CacheEvict&lt;/code&gt; 等注解生效），&lt;strong&gt;&lt;code&gt;spring-boot-starter-data-redis&lt;/code&gt;&lt;/strong&gt; 负责&lt;strong&gt;自动配置所有与 Redis 连接和操作相关的基础设施 Bean&lt;/strong&gt;，其中最核心的就是 &lt;code&gt;RedisConnectionFactory&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这样，我们实现了自定义的CacheManager以及Cache，通过@EnableCaching注解就可以通过Spring原生注解方式使用我们的二级缓存了。&lt;/p&gt;
&lt;h2 id=&quot;二caffeine&quot;&gt;二、Caffeine&lt;/h2&gt;
&lt;p&gt;caffeine是最流行的本地缓存组件，被称为“本地缓存之王”。&lt;/p&gt;
&lt;p&gt;github地址：&lt;a href=&quot;https://github.com/ben-manes/caffeine&quot;&gt;https://github.com/ben-manes/caffeine&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;关于它的使用，可以参考其wiki：&lt;a href=&quot;https://github.com/ben-manes/caffeine/wiki/Home-zh-CN&quot;&gt;https://github.com/ben-manes/caffeine/wiki/Home-zh-CN&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;1caffeine的crud操作&quot;&gt;1、Caffeine的CRUD操作&lt;/h3&gt;
&lt;p&gt;首先加入Caffeine的依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.github.ben-manes.caffeine&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;caffeine&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.2.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先创建一个缓存实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static Cache&amp;lt;String, Object&amp;gt; cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
        .maximumSize(1000) // 最大缓存数量
        .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后测试其CRUD功能&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Data
@AllArgsConstructor
public static class User {
    private String name;
}

@Test
public void testCRUD() {
    User user1 = new User(&amp;quot;张三&amp;quot;);
    User user2 = new User(&amp;quot;李四&amp;quot;);
    //添加到缓存
    cache.put(user1.getName(), user1);
    cache.put(user2.getName(), user2);
    //查询缓存中的信息
    System.out.println(cache.getIfPresent(user1.getName()));
    System.out.println(cache.getIfPresent(user2.getName()));
    //删除缓存中的信息
    cache.invalidate(user1.name);
    System.out.println(cache.getIfPresent(user1.getName()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，其实Caffeine的使用非常简单，更多高级功能使用，可以参考wiki。&lt;/p&gt;
&lt;h3 id=&quot;2整合caffeine到springboot&quot;&gt;2、整合Caffeine到SpringBoot&lt;/h3&gt;
&lt;p&gt;在之前的一篇文章中讲了redis spring-boot-starter的使用，redis spring-boot-starter属于Spring Data家族的，关于Spring Data可以看看文档：&lt;a href=&quot;https://spring.io/projects/spring-data&quot;&gt;https://spring.io/projects/spring-data&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://hd-oss.cosmoplat.com/hdCosmo58:gfcoe/public/2025/09/24/29d1eb335ba74e38abb3a6dec8b36f5d.png&quot; alt=&quot;image-20250924144500077&quot; style=&quot;zoom: 50%;&quot; /&gt;
&lt;p&gt;可以看到Spring Data支持Redis、ES、Mongo等组件，但是并不支持Caffeine，但是这并不意味着Caffeine无法集成到SpringBoot使用Spring Cache原生注解。之前已经分析过，要想SpringBoot支持使用Spring Cache的原生注解，需要创建CacheManager，实际上Spring已经帮我们做好了支持CacheManager的准备：&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;spring-context-support&lt;/code&gt;模块，已经定义好了CaffeineCacheManager：&lt;code&gt;org.springframework.cache.caffeine.CaffeineCacheManager&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;spring-boot-autoconfigure&lt;/code&gt;模块，已经定义好了CaffeineCacheConfiguration：&lt;code&gt;org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://hd-oss.cosmoplat.com/hdCosmo58:gfcoe/public/2025/09/24/a6ee85c59c3f4490853edd44aaed7daf.png&quot; alt=&quot;image-20250924145708164&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，只要引入了Caffeine的依赖以及spring-context-support的依赖，CaffeineCacheManager就会自动被创建。&lt;/p&gt;
&lt;p&gt;需要注意的是，如果同时引入了Spring Data Redis和Caffeine，则Redis会优先生效，为了避免意外的缓存行为，推荐在实践过程中“明确配置”而非依赖默认行为。&lt;/p&gt;
&lt;h4 id=&quot;基础使用&quot;&gt;基础使用&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;第一步：引入maven依赖配置：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.5.4&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-cache&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;com.github.ben-manes.caffeine&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;caffeine&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spring-context-support会在引入spring-boot-starter-cache的同时被引入，所以不需要单独引入了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：使用Cache&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.component.cache.service;

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author kdyzm
 * @date 2025/9/24
 */
@Service
public class UserService {

    private static final String CACHE_NAME = &amp;quot;users&amp;quot;;

    @Data
    @AllArgsConstructor
    public static class User {
        private Long id;
        private String name;
    }

    @Cacheable(value = CACHE_NAME, key = &amp;quot;#id&amp;quot;, unless = &amp;quot;#result == null&amp;quot;)
    public User getUserById(Long id) {
        System.out.println(&amp;quot;从数据库查询用户: &amp;quot; + id);
        return new User(id, &amp;quot;用户&amp;quot;);
    }

    /**
     * 更新操作 - 清除缓存
     */
    @CacheEvict(value = CACHE_NAME, key = &amp;quot;#user.id&amp;quot;)
    public User updateUser(User user) {
        System.out.println(&amp;quot;更新用户并清除缓存: &amp;quot; + user.getId());
        return user;
    }

    /**
     * 删除操作 - 清除缓存
     */
    @CacheEvict(value = CACHE_NAME, key = &amp;quot;#id&amp;quot;)
    public void deleteUser(Long id) {
        System.out.println(&amp;quot;删除用户并清除缓存: &amp;quot; + id);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第三步：测试&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm;

import cn.kdyzm.component.cache.Application;
import cn.kdyzm.component.cache.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
 * @author kdyzm
 * @date 2025/9/24
 */
@SpringBootTest(classes = {Application.class})
@Slf4j
public class CaffeineSpringBootTest {

    @Autowired
    private UserService userService;

    @Test
    public void testCache() {
        // 第一次查询 - 会访问数据库
        UserService.User user1 = userService.getUserById(1L);
        assertNotNull(user1);

        // 第二次查询 - 从缓存获取
        UserService.User user2 = userService.getUserById(1L);
        assertNotNull(user2);

        // 删除用户 - 会清除缓存
        userService.deleteUser(1L);

        // 再次查询 - 会访问数据库（因为缓存被清除了）
        UserService.User user3 = userService.getUserById(1L);
        assertNotNull(user3);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;配置参数&quot;&gt;配置参数&lt;/h4&gt;
&lt;p&gt;上述案例中并没有单独的配置文件配置，实际上可以对缓存行为进行配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  cache:
    caffeine:
      spec: maximumSize=500,expireAfterAccess=600s
    type: caffeine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样配置很不方便，没有配置提示什么的，完整的配置有哪些参数可以参考类：&lt;code&gt;com.github.benmanes.caffeine.cache.CaffeineSpec&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;完整的配置参数如下所示：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;&lt;strong&gt;配置键 (Key)&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;&lt;strong&gt;数据类型&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;&lt;strong&gt;默认值&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;&lt;strong&gt;语法示例&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;initialCapacity&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;设置缓存的初始容量（空间大小）。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;UNSET_INT&lt;/code&gt; (-1)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;initialCapacity=100&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;maximumSize&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;设置缓存的最大条数（基于条目数量驱逐）。 &lt;strong&gt;注意&lt;/strong&gt;：与 &lt;code&gt;maximumWeight&lt;/code&gt; 互斥。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;UNSET_INT&lt;/code&gt; (-1)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;maximumSize=1000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;maximumWeight&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;设置缓存的最大权重（基于权重驱逐，需配合 &lt;code&gt;weigher&lt;/code&gt; 使用）。 &lt;strong&gt;注意&lt;/strong&gt;：与 &lt;code&gt;maximumSize&lt;/code&gt; 互斥。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;UNSET_INT&lt;/code&gt; (-1)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;maximumWeight=200000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;expireAfterAccess&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;Duration&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;设置最后一次访问（读取或写入）后的过期时间。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;expireAfterAccess=10m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;expireAfterWrite&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;Duration&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;设置最后一次写入后的过期时间。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;expireAfterWrite=1h&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;refreshAfterWrite&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;Duration&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;设置写入后多久自动刷新缓存值（需配置 &lt;code&gt;CacheLoader&lt;/code&gt;）。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;refreshAfterWrite=30s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;weakKeys&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;boolean&lt;/code&gt; (无值)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;将键（Key）设置为弱引用，允许JVM在内存不足时回收键。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;false&lt;/code&gt; / &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;weakKeys&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;weakValues&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;boolean&lt;/code&gt; (无值)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;将值（Value）设置为弱引用，允许JVM在内存不足时回收值。 &lt;strong&gt;注意&lt;/strong&gt;：与 &lt;code&gt;softValues&lt;/code&gt; 互斥。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;false&lt;/code&gt; / &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;weakValues&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;softValues&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;boolean&lt;/code&gt; (无值)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;将值（Value）设置为软引用，在JVM内存不足时优先回收。 &lt;strong&gt;注意&lt;/strong&gt;：与 &lt;code&gt;weakValues&lt;/code&gt; 互斥。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;false&lt;/code&gt; / &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;softValues&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;recordStats&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;boolean&lt;/code&gt; (无值)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;开启缓存统计功能（如命中率），可通过 &lt;code&gt;Cache.stats()&lt;/code&gt; 获取。&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;recordStats&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;自定义caffeinecachemanager&quot;&gt;自定义CaffeineCacheManager&lt;/h4&gt;
&lt;p&gt;官方默认创建CaffeineCacheManager的代码位于&lt;code&gt;org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration&lt;/code&gt;，如果觉得spring官方提供的配置Caffeine的方式不方便，可以自定义创建方式，从而取代官方的创建方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    @Bean
    CaffeineCacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .recordStats()
                .xxxx
        );
        return cacheManager;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以自定义配置文件的格式灵活配置CacheManager了。&lt;/p&gt;
&lt;h4 id=&quot;创建独立使用的caffeine-cache&quot;&gt;创建独立使用的Caffeine Cache&lt;/h4&gt;
&lt;p&gt;上述创建CaffeineCacheManager的方法适用于SpringCache缓存注解使用，如果想直接操作Caffeine，则需要创建一个新的客户端。值得注意的是Caffeine的所有操作都是线程安全的，所以只需要全局创建一个单例实例即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    @Bean
    public Cache&amp;lt;String,Object&amp;gt; caffeineCache(){
        return Caffeine.newBuilder()
                .initialCapacity(xxx)//初始大小
                .maximumSize(xxx)//最大数量
                .expireAfterWrite(xxx)//过期时间
            	.xxx
                .build();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;三二级缓存架构实现&quot;&gt;三、二级缓存架构实现&lt;/h2&gt;
&lt;p&gt;接下来使用Caffeine作为一级缓存，Redis作为二级缓存实现多级缓存功能，其查询架构如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/09/26/2ac264c0d92c4f57a9688d5809ec73f2.png&quot; alt=&quot;image-20250926171748363&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;我的目标是实现一个基于&lt;code&gt;Java21&lt;/code&gt;+&lt;code&gt;Springboot3.5.4&lt;/code&gt;的SpringBoot Starter组件，名字为cache-spring-boot-starter，最终实现实现功能：可独立开启Caffeine、Redis或者二级缓存三种功能模式，三种模式下均支持SpringCache基于注解的缓存架构，暴露统一独立操控缓存API。&lt;/p&gt;
&lt;p&gt;我将相关功能封装成了一个组件，并发布到了gitee：&lt;a href=&quot;https://gitee.com/kdyzm/cache-spring-boot-starter&quot;&gt;https://gitee.com/kdyzm/cache-spring-boot-starter&lt;/a&gt; ，该组件已经上传到了中央仓库（ &lt;a href=&quot;https://central.sonatype.com/artifact/cn.kdyzm/cache-spring-boot-starter&quot;&gt;https://central.sonatype.com/artifact/cn.kdyzm/cache-spring-boot-starter&lt;/a&gt; ），GAV坐标如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;cn.kdyzm&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;cache-spring-boot-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于组件使用java21+Springboot3.5.4，所以在测试的时候需要搭建相应环境的项目，这里已经搭建好了测试环境，可以直接拉下来测试：&lt;a href=&quot;https://gitee.com/kdyzm/cache-spring-boot-demo&quot;&gt;https://gitee.com/kdyzm/cache-spring-boot-demo&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;1配置文件&quot;&gt;1、配置文件&lt;/h3&gt;
&lt;p&gt;在pom.xml中引入cache-spring-boot-starter的依赖后，配置下配置文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  data:
    redis:
      database: 1
      password: 123456
      host: localhost
      port: 6379
  cache:
    redis:
      time-to-live: 5s
    caffeine:
      spec: maximumSize=500,expireAfterAccess=600s
      #配置了spec，则所有其它配置都将无效
      expire-after-access: 5s
      #caffeine其它配置

logging:
  level:
    cn.kdyzm.component.cache: DEBUG
#    org.springframework.cache: TRACE
#    org.springframework.data.redis: DEBUG
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其配置和原生spring-boot-starter-cache和spring-boot-starter-data-redis是一模一样的，特别需要注意的是spring.cache.caffeine的spec配置，它原生的配置是一段字符串，格式是这样的：&lt;code&gt;maximumSize=500,expireAfterAccess=600s,...&lt;/code&gt;，为了方便使用，我将其抽离出来形成一个个单独的配置，格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  cache:
    caffeine:
      expire-after-access: 5s
      maximum-size: 500
      ......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样配置就和spec配置冲突了，所以&lt;strong&gt;代码里有约定：如果spec有值，则完全使用spec的配置；否则将使用其它独立的配置。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;2激活缓存配置&quot;&gt;2、激活缓存配置&lt;/h3&gt;
&lt;p&gt;这里提供了三种不同的注解，用于激活不同的功能项：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@EnableCaffeineCache&lt;/code&gt;：仅使用Caffeine本地缓存。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@EnableRedisCache&lt;/code&gt;：仅使用Redis缓存&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@EnableMultipleCache&lt;/code&gt;：使用Caffeine+Redis二级缓存。&lt;/p&gt;
&lt;p&gt;将注解加在启动类上即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@SpringBootApplication
//@EnableMultipleCache
//@EnableCaffeineCache
//@EnableRedisCache
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目启动后会打印当前使用的CacheManager：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/010/17/904ae423b45643b2bbf7c8703c98f85e.png&quot; alt=&quot;image-20251017143238055&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;需要注意的是，如果不使用以上三种自定义注解，使用&lt;code&gt;@EnableCaching&lt;/code&gt;注解，则会退回使用原生spring-cache相关功能。&lt;/p&gt;
&lt;h3 id=&quot;3业务代码中使用&quot;&gt;3、业务代码中使用&lt;/h3&gt;
&lt;p&gt;配置完成之后，就可以在代码中使用了，使用方式和spring-cache注解方式是一模一样的，案例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author kdyzm
 * @date 2025/9/24
 */
@Service
public class UserService {

    private static final String CACHE_NAME = &amp;quot;users&amp;quot;;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class User {
        private Long id;
        private String name;
    }

    @Cacheable(value = CACHE_NAME, key = &amp;quot;#id&amp;quot;, unless = &amp;quot;#result == null&amp;quot;)
    public User getUserById(Long id) {
        System.out.println(&amp;quot;从数据库查询用户: &amp;quot; + id);
        return new User(id, &amp;quot;用户&amp;quot;);
    }

    /**
     * 更新操作 - 清除缓存
     */
    @CacheEvict(value = CACHE_NAME, key = &amp;quot;#user.id&amp;quot;)
    public User updateUser(User user) {
        System.out.println(&amp;quot;更新用户并清除缓存: &amp;quot; + user.getId());
        return user;
    }

    /**
     * 删除操作 - 清除缓存
     */
    @CacheEvict(value = CACHE_NAME, key = &amp;quot;#id&amp;quot;)
    public void deleteUser(Long id) {
        System.out.println(&amp;quot;删除用户并清除缓存: &amp;quot; + id);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4测试二级缓存功能&quot;&gt;4、测试二级缓存功能&lt;/h3&gt;
&lt;p&gt;拉取代码仓库代码：&lt;a href=&quot;https://gitee.com/kdyzm/cache-spring-boot-demo，使用&quot;&gt;https://gitee.com/kdyzm/cache-spring-boot-demo，使用&lt;/a&gt;&lt;code&gt;@EnableMultipleCache&lt;/code&gt;注解激活二级缓存功能，配置好配置文件中redis的链接，之后就可以测试二级缓存功能了。&lt;/p&gt;
&lt;p&gt;在正式测试前，将spring.cache的ttl的配置先全部注释掉，以方便测试。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/010/17/5d50cef4f9f94a87913a711abf561e88.png&quot; alt=&quot;image-20251017145130098&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;第一步：测试缓存保存功能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;调用接口：&lt;a href=&quot;http://localhost:8080/test/testGet&quot;&gt;http://localhost:8080/test/testGet&lt;/a&gt; ，观察日志输出&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/010/17/a4ac5c54d1a04b878246677579449dbe.png&quot; alt=&quot;image-20251017144735120&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，由于没有缓存，所以会去查询数据库，之后再调用Cache的put方法将数据保存到一级缓存、二级缓存，同时触发Redis消息队列发消息，通知其它服务缓存变更，更新一级缓存（二级缓存已经由本服务更新）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：重复调用，测试缓存是否生效&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;再次调用接口：&lt;a href=&quot;http://localhost:8080/test/testGet&quot;&gt;http://localhost:8080/test/testGet&lt;/a&gt; ，观察日志输出&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/010/17/58198531aaa249cb9348aa19367134d1.png&quot; alt=&quot;image-20251017145512992&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，没有再查询数据库，直接从缓存中查询到了数据并返回了，缓存生效了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：验证二级缓存&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前一步缓存已经生效了，如何验证它获取的是一级缓存的数据还是二级缓存的数据呢？当然可以debug代码，其实还有个更简单的方法，那就是重启服务。由于caffeine使用的是本地缓存，重启后将会失效，所以理论上来说重启后再次调用接口，服务不会从数据库获取，而是从redis一级缓存中获取数据。&lt;/p&gt;
&lt;p&gt;重启服务后，重新请求接口：&lt;a href=&quot;http://localhost:8080/test/testGet&quot;&gt;http://localhost:8080/test/testGet&lt;/a&gt; ，观察日志输出：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/010/17/dfccf699d1004640a334020a7f773ae3.png&quot; alt=&quot;image-20251017161435633&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，并没有请求数据库。caffein从redis二级缓存中获取到了数据。&lt;/p&gt;
&lt;p&gt;为了更直观的看到使用的是一级缓存，将redis中的数据删除：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/010/17/f33e0497d3c4492cb36c7da7b96c56f6.png&quot; alt=&quot;image-20251017161736802&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;删除后，重新请求 &lt;a href=&quot;http://localhost:8080/test/testGet&quot;&gt;http://localhost:8080/test/testGet&lt;/a&gt; ，观察日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/010/17/ec5e66079eb74bb987a0a752b2852075.png&quot; alt=&quot;image-20251017161826374&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到还是从缓存中获取了，观察redis，却没有新增key，说明当前缓存使用的是一级缓存caffeine。&lt;/p&gt;
&lt;h2 id=&quot;四技术细节&quot;&gt;四、技术细节&lt;/h2&gt;
&lt;h3 id=&quot;1通过注解隔离三种缓存模式&quot;&gt;1、通过注解隔离三种缓存模式&lt;/h3&gt;
&lt;p&gt;通过三种注解&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@EnableMultipleCache
@EnableCaffeineCache
@EnableRedisCache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启三种不同的缓存模式，本质上是通过注解中的@Import功能激活相对应的Configuration：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MultipleCacheConfiguration.class)
public @interface EnableMultipleCache {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个Configuration是相互隔离的，并不会同时生效。&lt;/p&gt;
&lt;h3 id=&quot;2多级缓存的实现&quot;&gt;2、多级缓存的实现&lt;/h3&gt;
&lt;p&gt;实际上核心就是自定义CacheManager以及自定义Cache，对应着代码中的 &lt;a href=&quot;https://gitee.com/kdyzm/cache-spring-boot-starter/blob/master/src/main/java/cn/kdyzm/component/cache/multiple/core/MultipleCacheManager.java&quot;&gt;MultipleCacheManager&lt;/a&gt; 以及 &lt;a href=&quot;https://gitee.com/kdyzm/cache-spring-boot-starter/blob/master/src/main/java/cn/kdyzm/component/cache/multiple/core/MultipleCache.java&quot;&gt;MultipleCache&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;MultiCacheManager用于根据CacheName查询对应的MultipleCache对象，实际的缓存CRUD都通过MultipleCache对象实现。&lt;/p&gt;
&lt;p&gt;MultipleCache类并没有直接实现Cach接口，而是通过继承AbstractValueAdaptingCache类实现，这是借鉴了CaffeineCache、RedisCache的实现，通过继承该抽象类，可以减小实现难度。&lt;/p&gt;
&lt;h3 id=&quot;3缓存一致性&quot;&gt;3、缓存一致性&lt;/h3&gt;
&lt;p&gt;缓存一致性需要分具体场景讨论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景一&lt;/strong&gt;：分布式场景下，服务A有两个实例，实例1通过接口从数据库取出数据并添加到了缓存，所以实例1的一级缓存和二级缓存都有了，但是实例2只有二级Redis缓存，一级缓存缺失。&lt;/p&gt;
&lt;p&gt;解决方式是通过Redis发布订阅模式，当实例1添加缓存之后，立即通过消息队列通知其它服务，其它服务得到消息后构建一级缓存。&lt;/p&gt;
&lt;p&gt;相对于新增一级缓存，缓存的失效同步更加重要，消息一共有三种类型：缓存更新、缓存失效、缓存全部失效。具体可看类：&lt;a href=&quot;https://gitee.com/kdyzm/cache-spring-boot-starter/blob/master/src/main/java/cn/kdyzm/component/cache/multiple/msg/MsgCacheActionType.java&quot;&gt;MsgCacheActionType &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景二&lt;/strong&gt;：Redis发布订阅失效，场景1中实例2没接收到更新的消息，对于新增的场景来说，一级缓存可以从二级缓存中取出数据保存到一级缓存达成缓存一致性；对于缓存失效的场景来说会出现一致性问题。&lt;/p&gt;
&lt;p&gt;所以最佳实践是&lt;strong&gt;一级缓存一定要设置有效期&lt;/strong&gt;，这样可以达到最终一致性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景三&lt;/strong&gt;：一级缓存和二级缓存的有效期不同，导致两个缓存可能会各自独立存在一段时间，出现缓存一致性问题。&lt;/p&gt;
&lt;p&gt;我们追求的是最终一致性，所以短暂的缓存不一致是可以接受的。二级缓存是多服务共享的，所以不必担心，一级缓存只要设置了有效期，其必定会和一级缓存同步。&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://gitee.com/kdyzm/cache-spring-boot-starter/blob/master/src/main/java/cn/kdyzm/component/cache/multiple/msg/RedisCacheSynMessageReceiver.java&quot;&gt;RedisCacheSynMessageReceiver&lt;/a&gt; 类中实现了对一级缓存的同步。&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
      <category>springboot</category>
      <category>spring</category>
      <category>java</category>
    </item>
    <item>
      <title>2025新版Maven中央仓库发布jar包教程</title>
      <link>https://blog.kdyzm.cn/post/332</link>
      <guid>https://blog.kdyzm.cn/post/332</guid>
      <pubDate>Fri, 17 Oct 2025 11:16:50 +0800</pubDate>
      <description>&lt;p&gt;本文转载自 &lt;a href=&quot;https://blog.hanqunfeng.com/2024/08/01/mvn-depoly-maven-center-repository-new/&quot;&gt;发布Jar到Maven中央仓库--Maven版(最新方式)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Maven发布jar包到中央仓库的方式发生了变化，之前的教程（&lt;a href=&quot;https://blog.kdyzm.cn/post/171&quot;&gt;上传jar包到maven中央仓库&lt;/a&gt;）部分失效了，&lt;a href=&quot;https://oss.sonatype.org/&quot;&gt;https://oss.sonatype.org&lt;/a&gt;已经不再支持新用户注册，新的注册地址为 &lt;a href=&quot;https://central.sonatype.com&quot;&gt;https://central.sonatype.com&lt;/a&gt; 。如果按照之前的教程推送jar包，会得到一个403响应。下面说下新版发布方式。&lt;/p&gt;
&lt;h2 id=&quot;摘要&quot;&gt;摘要&lt;/h2&gt;
&lt;p&gt;新版 Maven 中央仓库发布流程主要改变在于通过 Central Portal 进行，不再使用传统的 Jira 注册方式。你需要注册 Sonatype 账号并创建或验证命名空间，生成发布 Token 替代旧的用户名和密码进行认证，然后通过配置 Maven 的 &lt;code&gt;settings.xml&lt;/code&gt; 和 &lt;code&gt;pom.xml&lt;/code&gt; 文件，使用 GPG 生成签名密钥，最后打包并上传到中央仓库。&lt;/p&gt;
&lt;p&gt;新版本推送方式从注册到验证到发布，都在统一的网站 &lt;a href=&quot;https://central.sonatype.com&quot;&gt;https://central.sonatype.com&lt;/a&gt; ，实际上比以前的发布方式要简单的多。&lt;/p&gt;
&lt;h2 id=&quot;一将项目推送到远程仓库如-github或者gitee&quot;&gt;一、将项目推送到远程仓库，如 &lt;code&gt;Github&lt;/code&gt;或者&lt;code&gt;Gitee&lt;/code&gt;&lt;/h2&gt;
&lt;h2 id=&quot;二注册-sonatype-账户&quot;&gt;二、注册 &lt;code&gt;Sonatype&lt;/code&gt; 账户&lt;/h2&gt;
&lt;p&gt;进入 &lt;a href=&quot;https://central.sonatype.com/&quot;&gt;https://central.sonatype.com&lt;/a&gt; 注册一个账号，邮箱要真实。&lt;/p&gt;
&lt;h2 id=&quot;三登录-sonatype-创建namespace&quot;&gt;三、登录 Sonatype 创建&lt;code&gt;Namespace&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;进入 Namespace 模块，然后点击 Add Namespace 按钮，Namespace 具有唯一性。发布前一定要创建&lt;code&gt;Namespace&lt;/code&gt;，实际上就是项目的&lt;code&gt;Group Id&lt;/code&gt;，因为要进行验证，所以这里不能随便填写，你可以配置为你拥有的域名，比如我的域名是&lt;code&gt;hanqunfeng.com&lt;/code&gt;，这里就填写&lt;code&gt;com.hanqunfeng&lt;/code&gt;。域名的验证方法是配置DNS TXT记录。&lt;/p&gt;
&lt;p&gt;如果你没有域名，也可以使用Github的域名，比如我的Github用户名是&lt;code&gt;hanqunfeng&lt;/code&gt;，则这里可以配置为&lt;code&gt;io.github.hanqunfeng&lt;/code&gt;。Github的验证方式是根据给定个名称创建一个public repository。&lt;/p&gt;
&lt;h2 id=&quot;四发布&quot;&gt;四、发布&lt;/h2&gt;
&lt;h3 id=&quot;1准备签名&quot;&gt;1.准备签名&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;方法一：可以使用工具创建密钥对&lt;/strong&gt;
需要下载一个签名工具，我是mac电脑，下载的是&lt;a href=&quot;https://gpgtools.org/&quot;&gt;https://gpgtools.org&lt;/a&gt;。
安装后点击新建，按照提示创建一个密钥对即可，注意高级选项里有个过期时间，默认是&lt;code&gt;3年&lt;/code&gt;。创建好后会主动提示你是否将公钥发布到&lt;code&gt;key server&lt;/code&gt;，点击&lt;code&gt;Upload Public key&lt;/code&gt;即可。也可以在创建后的证书列表页面邮件选择&lt;code&gt;证书&lt;/code&gt;–&amp;gt;&lt;code&gt;Send Public Key To Key Server&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;导出证书时，勾选密码并设置密码就是私钥和公钥证书，不勾选密码就是公钥，看生成文件的名称就可以，公开就是公钥，私密就是私钥，格式都是&lt;code&gt;asc&lt;/code&gt;，其实就是&lt;code&gt;字符串&lt;/code&gt;，可以用记事本打开查看。&lt;/p&gt;
&lt;p&gt;如果windows系统，可以下载&lt;a href=&quot;https://www.gpg4win.org/&quot;&gt;https://www.gpg4win.org/&lt;/a&gt; ，使用方式差不多，最后点击“将公钥上传的目录服务”。&lt;/p&gt;
&lt;p&gt;公钥发布到&lt;code&gt;key server&lt;/code&gt;后要稍微等一会，大约&lt;code&gt;10分钟&lt;/code&gt;吧，因为&lt;code&gt;key server&lt;/code&gt;有多个，同步需要一些时间。
记住你创建密钥对时的密码，发布项目时要使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法二：也可以使用命令行创建密钥对，版本&lt;code&gt;[gpg (GnuPG/MacGPG2) 2.2.24]&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建密钥对，按提示输入用户名称和邮箱地址
gpg --generate-key

# 列出密钥，hanqunfeng就是创建密钥对是的用户名，此处也可以使用邮箱
# 结果中第二行一长串的后8位就是keyId，比如：30FF8D58，gradle构建时会用到
gpg --list-keys hanqunfeng
# 也可以直接通过id查询
gpg --list-keys 30FF8D58

# 上传公钥到server key，默认上传到hkps://keys.openpgp.org，但是提示上传失败
# 看到网上的示例可以通过--keyserver指定上传的服务器地址，但是我这个版本[gpg (GnuPG/MacGPG2) 2.2.24]没有这个参数
# 使用 https://gpgtools.org 上传公钥就会成功
gpg --send-keys 30FF8D58

# 查看指纹
gpg --fingerprint 30FF8D58

# 删除私钥，这里也可以使用用户名称或者邮箱，如果唯一的话
gpg --delete-secret-keys 30FF8D58

# 删除公钥
gpg --delete-keys 30FF8D58
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2settingsxml&quot;&gt;2.settings.xml&lt;/h3&gt;
&lt;p&gt;配置 &lt;code&gt;maven&lt;/code&gt; 的 &lt;code&gt;settings.xml&lt;/code&gt; 文件，设置一个 &lt;code&gt;server&lt;/code&gt;，里面添加 &lt;code&gt;User Token&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;登录&lt;a href=&quot;https://central.sonatype.com/&quot;&gt;https://central.sonatype.com&lt;/a&gt;后点击右上角的用户名称–&amp;gt; View Account --&amp;gt; Generate User Token&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;settings&amp;gt;
    [...]
    &amp;lt;servers&amp;gt;
       [...]
       &amp;lt;server&amp;gt;
          &amp;lt;id&amp;gt;maven-central&amp;lt;/id&amp;gt;
          &amp;lt;username&amp;gt;username&amp;lt;/username&amp;gt;
          &amp;lt;password&amp;gt;token&amp;lt;/password&amp;gt;
       &amp;lt;/server&amp;gt;
    &amp;lt;/servers&amp;gt;
&amp;lt;/settings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3pomxml&quot;&gt;3.pom.xml&lt;/h3&gt;
&lt;p&gt;重点是后面几个plugin
这里重点说一下&lt;code&gt;central-publishing-maven-plugin&lt;/code&gt;这个插件，该插件会将jar包推送到&lt;code&gt;Maven Central&lt;/code&gt;仓库，如果插件没有配置参数&lt;code&gt;autoPublish&lt;/code&gt;为&lt;code&gt;true&lt;/code&gt;，则但此时发布的jar会处于&lt;code&gt;VALIDATED&lt;/code&gt;状态，需要登录&lt;a href=&quot;https://central.sonatype.com/&quot;&gt;https://central.sonatype.com&lt;/a&gt;后切换到Deployment，找到我们刚刚上传的包名，然后点击右侧的&lt;code&gt;Publish&lt;/code&gt;按钮，如果一切顺利，大于10分钟后其状态变为&lt;code&gt;PUBLISHED&lt;/code&gt;，表示发布成功。如果发布失败，其状态会变更为&lt;code&gt;FAILED&lt;/code&gt;，可以在&lt;code&gt;Component Summary&lt;/code&gt;中查看失败原因，重新发布即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot; xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;
         xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;io.github.hanqunfeng&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;aws-s3-v2-tools&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0.1&amp;lt;/version&amp;gt;
    &amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;
    &amp;lt;name&amp;gt;aws-s3-v2-tools&amp;lt;/name&amp;gt;

    &amp;lt;description&amp;gt;
        AWS S3 Tools.
    &amp;lt;/description&amp;gt;
    &amp;lt;url&amp;gt;https://blog.hanqunfeng.com&amp;lt;/url&amp;gt;
    &amp;lt;licenses&amp;gt;
        &amp;lt;license&amp;gt;
            &amp;lt;name&amp;gt;The Apache License, Version 2.0&amp;lt;/name&amp;gt;
            &amp;lt;url&amp;gt;http://www.apache.org/licenses/LICENSE-2.0.txt&amp;lt;/url&amp;gt;
        &amp;lt;/license&amp;gt;
    &amp;lt;/licenses&amp;gt;
    &amp;lt;developers&amp;gt;
        &amp;lt;developer&amp;gt;
            &amp;lt;id&amp;gt;hanqf&amp;lt;/id&amp;gt;
            &amp;lt;name&amp;gt;hanqunfeng&amp;lt;/name&amp;gt;
            &amp;lt;email&amp;gt;qunfeng_han@126.com&amp;lt;/email&amp;gt;
        &amp;lt;/developer&amp;gt;
    &amp;lt;/developers&amp;gt;
    &amp;lt;scm&amp;gt;
        &amp;lt;connection&amp;gt;scm:git:https://github.com/hanqunfeng/aws-s3-v2-tools.git
        &amp;lt;/connection&amp;gt;
        &amp;lt;developerConnection&amp;gt;
            scm:git:https://github.com:hanqunfeng/aws-s3-v2-tools.git
        &amp;lt;/developerConnection&amp;gt;
        &amp;lt;url&amp;gt;https://github.com/hanqunfeng/aws-s3-v2-tools&amp;lt;/url&amp;gt;
    &amp;lt;/scm&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;1.8&amp;lt;/java.version&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;8&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;8&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;software.amazon.awssdk&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;bom&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;2.23.10&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;software.amazon.awssdk&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;s3&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!-- 为避免Potential Conflicts，你应该将commons-logging.jar从classpath中删除。你可以尝试从项目依赖中排除commons-logging，这样你的应用程序就会被强制使用Spring JCL而不是Commons Logging。 --&amp;gt;
        &amp;lt;!-- 问题：Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;software.amazon.awssdk&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;apache-client&amp;lt;/artifactId&amp;gt;
            &amp;lt;exclusions&amp;gt;
                &amp;lt;exclusion&amp;gt;
                    &amp;lt;groupId&amp;gt;commons-logging&amp;lt;/groupId&amp;gt;
                    &amp;lt;artifactId&amp;gt;commons-logging&amp;lt;/artifactId&amp;gt;
                &amp;lt;/exclusion&amp;gt;
            &amp;lt;/exclusions&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;software.amazon.awssdk&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;s3-transfer-manager&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;software.amazon.awssdk&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;aws-crt-client&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
            &amp;lt;version&amp;gt;1.18.32&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-core&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;5.3.29&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;

    &amp;lt;/dependencies&amp;gt;


    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.8.1&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;${java.version}&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;${java.version}&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;

            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-source-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.2.1&amp;lt;/version&amp;gt;
                &amp;lt;executions&amp;gt;
                    &amp;lt;execution&amp;gt;
                        &amp;lt;id&amp;gt;attach-source&amp;lt;/id&amp;gt;
                        &amp;lt;phase&amp;gt;verify&amp;lt;/phase&amp;gt;
                        &amp;lt;goals&amp;gt;
                            &amp;lt;!--生成源代码的jar --&amp;gt;
                            &amp;lt;goal&amp;gt;jar&amp;lt;/goal&amp;gt;
                        &amp;lt;/goals&amp;gt;
                    &amp;lt;/execution&amp;gt;
                &amp;lt;/executions&amp;gt;
            &amp;lt;/plugin&amp;gt;

            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-javadoc-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.2.0&amp;lt;/version&amp;gt;
                &amp;lt;executions&amp;gt;
                    &amp;lt;execution&amp;gt;
                        &amp;lt;id&amp;gt;attach-javadoc&amp;lt;/id&amp;gt;
                        &amp;lt;phase&amp;gt;verify&amp;lt;/phase&amp;gt;
                        &amp;lt;goals&amp;gt;
                            &amp;lt;!--生成javadoc的jar --&amp;gt;
                            &amp;lt;goal&amp;gt;jar&amp;lt;/goal&amp;gt;
                            &amp;lt;!--生成javadoc的html --&amp;gt;
                            &amp;lt;goal&amp;gt;javadoc&amp;lt;/goal&amp;gt;
                        &amp;lt;/goals&amp;gt;
                        &amp;lt;configuration&amp;gt;
                            &amp;lt;!--不显示javadoc警告--&amp;gt;
                            &amp;lt;additionalOptions&amp;gt;-Xdoclint:none&amp;lt;/additionalOptions&amp;gt;
                            &amp;lt;additionalJOption&amp;gt;-Xdoclint:none&amp;lt;/additionalJOption&amp;gt;
                        &amp;lt;/configuration&amp;gt;
                    &amp;lt;/execution&amp;gt;
                &amp;lt;/executions&amp;gt;
            &amp;lt;/plugin&amp;gt;

            &amp;lt;!-- gpg plugin,用于签名认证 --&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-gpg-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;1.6&amp;lt;/version&amp;gt;
                &amp;lt;executions&amp;gt;
                    &amp;lt;execution&amp;gt;
                        &amp;lt;id&amp;gt;sign-artifacts&amp;lt;/id&amp;gt;
                        &amp;lt;phase&amp;gt;verify&amp;lt;/phase&amp;gt;
                        &amp;lt;goals&amp;gt;
                            &amp;lt;goal&amp;gt;sign&amp;lt;/goal&amp;gt;
                        &amp;lt;/goals&amp;gt;
                    &amp;lt;/execution&amp;gt;
                &amp;lt;/executions&amp;gt;
            &amp;lt;/plugin&amp;gt;

            &amp;lt;!-- 发布到私服时需要注释掉下面两个插件 --&amp;gt;
            &amp;lt;!--staging puglin,用于自动执行发布阶段(免手动)--&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.sonatype.central&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;central-publishing-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;0.5.0&amp;lt;/version&amp;gt;
                &amp;lt;extensions&amp;gt;true&amp;lt;/extensions&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;!-- 这里的publishingServerId是在settings.xml中配置的server认证信息 --&amp;gt;
                    &amp;lt;publishingServerId&amp;gt;maven-central&amp;lt;/publishingServerId&amp;gt;
                    &amp;lt;!-- 这里的autoPublish是自动发布，而不是手动发布 --&amp;gt;
                    &amp;lt;autoPublish&amp;gt;true&amp;lt;/autoPublish&amp;gt;
                    &amp;lt;!-- 这里的waitUntil配置为published是等待发布完成，因为发布完成的时间比较长，所以可以不加这个参数 --&amp;gt;
                    &amp;lt;waitUntil&amp;gt;published&amp;lt;/waitUntil&amp;gt;
                    &amp;lt;!-- 这里的deploymentName是发布到中央仓库的名称 --&amp;gt;
                    &amp;lt;deploymentName&amp;gt;${project.groupId}:${project.artifactId}:${project.version}&amp;lt;/deploymentName&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
            &amp;lt;!-- release plugin,用于发布到release仓库部署插件 --&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-release-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;2.5.3&amp;lt;/version&amp;gt;
            &amp;lt;/plugin&amp;gt;



        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

    &amp;lt;distributionManagement&amp;gt;
        &amp;lt;!--        &amp;lt;repository&amp;gt;--&amp;gt;
        &amp;lt;!--            &amp;lt;id&amp;gt;maven-releases&amp;lt;/id&amp;gt;--&amp;gt;
        &amp;lt;!--            &amp;lt;name&amp;gt;Nexus Release Repository&amp;lt;/name&amp;gt;--&amp;gt;
        &amp;lt;!--            &amp;lt;url&amp;gt;http://nexus.cxzh.ltd:8081/repository/maven-releases/&amp;lt;/url&amp;gt;--&amp;gt;
        &amp;lt;!--        &amp;lt;/repository&amp;gt;--&amp;gt;
        &amp;lt;!--        &amp;lt;snapshotRepository&amp;gt;--&amp;gt;
        &amp;lt;!--            &amp;lt;id&amp;gt;maven-snapshots&amp;lt;/id&amp;gt;--&amp;gt;
        &amp;lt;!--            &amp;lt;name&amp;gt;Nexus Snapshot Repository&amp;lt;/name&amp;gt;--&amp;gt;
        &amp;lt;!--            &amp;lt;url&amp;gt;http://nexus.cxzh.ltd:8081/repository/maven-snapshots/&amp;lt;/url&amp;gt;--&amp;gt;
        &amp;lt;!--        &amp;lt;/snapshotRepository&amp;gt;--&amp;gt;

    &amp;lt;/distributionManagement&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4执行命令&quot;&gt;4.执行命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mvn clean package # 完成打包和测试
mvn clean verify  # 完成源码打包和javadoc打包，同时完成签名
mvn clean deploy  # 完成本地部署和maven中央仓库部署
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;执行过程中会提示你输入创建密钥对时的密码，如果不想人工参与，也可以使用如下方式（参考：&lt;a href=&quot;http://maven.apache.org/plugins/maven-gpg-plugin/usage.html）&quot;&gt;http://maven.apache.org/plugins/maven-gpg-plugin/usage.html）&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在执行命令时指定密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mvn clean deploy -Dgpg.passphrase=thephrase
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;setting.xml中创建一个server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;settings&amp;gt;
[...]
&amp;lt;servers&amp;gt;
    [...]
    &amp;lt;server&amp;gt;
    &amp;lt;id&amp;gt;gpg.passphrase&amp;lt;/id&amp;gt;
    &amp;lt;passphrase&amp;gt;clear or encrypted text&amp;lt;/passphrase&amp;gt;
    &amp;lt;/server&amp;gt;
&amp;lt;/servers&amp;gt;
&amp;lt;/settings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5更新部署&quot;&gt;5.更新部署&lt;/h3&gt;
&lt;p&gt;修改&lt;code&gt;pom.xml&lt;/code&gt;中的版本号，重新执行&lt;code&gt;mvn clean deploy&lt;/code&gt;即可。发布的jar包可以在&lt;a href=&quot;https://central.sonatype.com/&quot;&gt;https://central.sonatype.com&lt;/a&gt;中检索。&lt;/p&gt;
</description>
      <category>maven</category>
    </item>
    <item>
      <title>IntelliJ IDEA 2025.2 Maven设置不生效的解决方案</title>
      <link>https://blog.kdyzm.cn/post/331</link>
      <guid>https://blog.kdyzm.cn/post/331</guid>
      <pubDate>Fri, 26 Sep 2025 10:55:04 +0800</pubDate>
      <description>&lt;p&gt;前些日子升级了IDEA到2025.2，使用了一段时间之后出现了一些问题，尤其让我头疼的问题就是Maven的配置总是失效，需要重新配置。&lt;/p&gt;
&lt;h2 id=&quot;一问题重现&quot;&gt;一、问题重现&lt;/h2&gt;
&lt;p&gt;后端有很多个微服务，我每次都是对着文件夹右键打开代码仓库：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/09/26/c5313e63bec3476097b3e92ac087d7ad.png&quot; alt=&quot;image-20250925155409739&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;升级IDEA到2025.2之后重新配置了maven设置，但是一旦重新打开新项目，maven设置就会失效，默认使用IDEA内置的maven（此时部分项目运行的时候会提示Groovy错误）。这是为什么呢？&lt;/p&gt;
&lt;h2 id=&quot;二问题分析&quot;&gt;二、问题分析&lt;/h2&gt;
&lt;p&gt;为了搞清楚这是不是新版的bug（老版2020.2没有这个问题），我在社区提了一个bug：&lt;a href=&quot;https://youtrack.jetbrains.com/issue/IDEA-378154&quot;&gt;https://youtrack.jetbrains.com/issue/IDEA-378154&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;原来，&lt;code&gt;File-&amp;gt;Settings-&amp;gt;Build,Execution,Deployment-&amp;gt;Build Tools-&amp;gt;Maven&lt;/code&gt; 设置并非全局设置，而是只针对于当前项目的“局部配置”，这种当前项目的配置都有特殊的符号，如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/09/26/1c1a87c861df4569afc94e772299207f.png&quot; alt=&quot;image-20250926104907074&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;三解决问题&quot;&gt;三、解决问题&lt;/h2&gt;
&lt;p&gt;社区给了解决方案，那就是到&lt;code&gt;File-&amp;gt;New Projects Setup-&amp;gt;Settings For New Projects&lt;/code&gt;，在里面配置的Maven设置将应用于新建的项目。&lt;/p&gt;
&lt;p&gt;需要注意的是，有可能设置好了之后打开项目Maven设置还是内置的Maven，这表示在设置&lt;code&gt;Settings For New Projects&lt;/code&gt;前你使用IDEA打开过项目，已经生成了项目配置文件，所以即使设置了&lt;code&gt;Settings For New Projects&lt;/code&gt;，IDEA也不会应用新项目设置。这时候只需要删除项目根目录的&lt;code&gt;.idea&lt;/code&gt;目录以及&lt;code&gt;.iml&lt;/code&gt;文件，然后重新打开项目就可以了。&lt;/p&gt;
</description>
      <category>maven</category>
      <category>java</category>
      <category>idea</category>
    </item>
    <item>
      <title>Diskgenius 破解版下载</title>
      <link>https://blog.kdyzm.cn/post/330</link>
      <guid>https://blog.kdyzm.cn/post/330</guid>
      <pubDate>Mon, 25 Aug 2025 09:04:25 +0800</pubDate>
      <description>&lt;p&gt;下载地址：&lt;a href=&quot;https://clamowo.lanzoui.com/b05agns3g&quot;&gt;https://clamowo.lanzoui.com/b05agns3g&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/25/2f80151e012c4582afd4b876cfc869ff.png&quot; alt=&quot;image-20250825090231755&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;转载自：&lt;a href=&quot;https://weilining.github.io/251.html&quot;&gt;https://weilining.github.io/251.html&lt;/a&gt;&lt;/p&gt;
</description>
      <category>diskgenius</category>
    </item>
    <item>
      <title>JetBrains系列软件2025.2版本激活教程</title>
      <link>https://blog.kdyzm.cn/post/329</link>
      <guid>https://blog.kdyzm.cn/post/329</guid>
      <pubDate>Fri, 15 Aug 2025 22:50:40 +0800</pubDate>
      <description>&lt;h2 id=&quot;第一步安装新版本软件&quot;&gt;&lt;strong&gt;第一步：安装新版本软件&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;在&lt;strong&gt;彻底卸载&lt;/strong&gt;旧版JetBrains软件之后，去官网下载软件并安装：&lt;/p&gt;
&lt;p&gt;webstorm下载：&lt;a href=&quot;https://www.jetbrains.com/webstorm/download/?section=windows&quot;&gt;https://www.jetbrains.com/webstorm/download/?section=windows&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;pycharm下载：&lt;a href=&quot;https://www.jetbrains.com/pycharm/download/?section=windows&quot;&gt;https://www.jetbrains.com/pycharm/download/?section=windows&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;IntelliJ IDEA下载：&lt;a href=&quot;https://www.jetbrains.com/idea/download/?section=windows&quot;&gt;https://www.jetbrains.com/idea/download/?section=windows&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;按部就班完成安装以后，&lt;strong&gt;不要运行软件&lt;/strong&gt;。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/15/30fb70fd357446e885d0cd0fe78c46bb.png&quot; alt=&quot;image-20250815223320602&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;第二步激活软件&quot;&gt;第二步：激活软件&lt;/h2&gt;
&lt;p&gt;下载激活工具：&lt;a href=&quot;https://blog.kdyzm.cn/blog/public/share/software/jetbrains-2025.2.zip&quot;&gt;激活工具&lt;/a&gt; ，下载完成后解压文件，注意要将解压后的文件夹放到一个固定目录，不能删除或移动目录，否则会导致激活激活后失效。&lt;/p&gt;
&lt;p&gt;打开scripts目录，运行&lt;code&gt;install-current-user.vbs&lt;/code&gt;脚本，会有个弹框提示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/15/f11db33792ce4670864165cd9f99f27b.png&quot; alt=&quot;image-20250815223436966&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这意思是激活可能会耗费一些时间，点击确定按钮之后要耐心等待新的弹框提示：Done。实测我的激活时间比较久可能得有三五分钟，块的话可能得有五秒钟就行了，激活完成后有弹框提示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/15/f88730f330114eafa8d98e6e392fcef4.png&quot; alt=&quot;20250815223354&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;第三步输入激活码&quot;&gt;第三步：输入激活码&lt;/h2&gt;
&lt;p&gt;打开安装好的软件：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/15/04b28f35a59d4353a35cd1962cf36b57.png&quot; alt=&quot;image-20250815223752809&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;点击&amp;quot;Skip Import&amp;quot;，进入如下页面，点击左下角的设置图标：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/15/5c32516542884bca980272157256e1e1.png&quot; alt=&quot;image-20250815223947078&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;选择“Manage Licenses ”，进入激活管理页面，点击&amp;quot;Activation code&amp;quot;：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/15/0e32fe3671104dce8e46839f74449de7.png&quot; alt=&quot;image-20250815224136819&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;根据不同的产品选择不同的激活码，激活成功的页面：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/15/e4185f02202b4b2eba720f29075fced5.png&quot; alt=&quot;20250815224248&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;不同产品的激活码不同，具体如下所示：&lt;/p&gt;
&lt;h3 id=&quot;idea-激活码&quot;&gt;IDEA 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;FV8EM46DQYC5AW9-eyJsaWNlbnNlSWQiOiJGVjhFTTQ2RFFZQzVBVzkiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQUlIiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBEQiIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUFNJIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJJSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6ZmFsc2V9XSwibWV0YWRhdGEiOiIwMjIwMjQwNzAyUFNBWDAwMDAwNVgiLCJoYXNoIjoiMTIzNDU2NzgvMC01NDE4MTY2MjkiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwidHJpYWwiOmZhbHNlLCJhaUFsbG93ZWQiOnRydWV9-cH8qBniG31nF8954hthJJuzF6Fk4RQ9T03IfNxsFkuxUcwaAGHKOcRudvBZIAbLwDDFw63q2QZsnpwthBb/6IqBYnJrjRC83a8wkYKGN8HqAyDtbqdLOxLjcaiAiSKzektfAXn6nGNfDeygcFr/WzMfI0on/43ByuwxmSrjwYc4M8SCR0nkDAi0XwXNnFp3vSp0gJQd+lJtkSHO2KR7gUyNDZOPVduljJGbdLJUK6UcUjrlAd6NrTNqpu5P7hcYRaNzjoJ0KeIx5k9KmMCdcfQBia/zSHUbwZiecFsyjxqtIU0C3TDaX1OM4siJVDpgrXi+ocY86hiiYE79ygJf2IA==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pycharm-激活码&quot;&gt;PyCharm 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;GAJWL09BT5RSXDR-eyJsaWNlbnNlSWQiOiJHQUpXTDA5QlQ1UlNYRFIiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQQyIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX1dLCJtZXRhZGF0YSI6IjAyMjAyNDA3MDJQU0FYMDAwMDA1WCIsImhhc2giOiIxMjM0NTY3OC8wLTQ2MTc4NjQwOSIsImdyYWNlUGVyaW9kRGF5cyI6NywiYXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwiaXNBdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJ0cmlhbCI6ZmFsc2UsImFpQWxsb3dlZCI6dHJ1ZX0=-UeOCCiS72PGvOIS9go0yIhDFVmPBvbKM56D9w0adVaGcYLtC7YxNr/5MQ/3+Mr05tQQAhMz12vBTb9sjJAXBo+HBzCv1o9IFZnJK2rf3pCXl83ulriBUQ6M0H6GUUy+Mc1fl0EGWquoNExZMujCkReWoeabxwwKPNCvHqHqkW1rU/+cwiVKjVfbIgQW9aChIwyYwexzSlM0TlHvQGfncEzI0+uYNxjRQUjemLlGJooYD0ycSMMTyTvM95QHi25DZjmQRkdzIhDA2l4uPp+C+XEAIdIST2rjEPolvJGcVu7P/DI77LDDqZwLtD8mFXh9lFqMEw9titvy4mYFlYp/xaw==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;datagrip-激活码&quot;&gt;DataGrip 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;54H0PAD972IO7OS-eyJsaWNlbnNlSWQiOiI1NEgwUEFEOTcySU83T1MiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBEQiIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiREIiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJQU0kiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMjIwMjQwNzAyUFNBWDAwMDAwNVgiLCJoYXNoIjoiMTIzNDU2NzgvMC00MTI3ODYxMTYiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwidHJpYWwiOmZhbHNlLCJhaUFsbG93ZWQiOnRydWV9-kOcI3r7/OB1foH2R44HwHoAZJfdfTo0y7c1AZF/I9SBxiyNErjzyyFslUgkOD7XqHzhBgy53J2edgvSVy0DhmNswVK8V5YSXO+SLQc0RrQkZy43fb1fbLK26+LHj8gUUkFZuUwlDaXIb3D6SWyWx6tXAFet0ot6O7+lwZ/vGrRDXVdpaL/LBuCVt1pz2a77orzxWKbtgLNmVLVRGi7sFpUgv5syvGDgWG0gClSZHiAyEDzvoGdAJ3e8Y4LDBInHxrGwZYx7uY50lRBewjLvitbfzFm9dVz5bM9+3g+jSlXzKF5aZ9x2TyeGiOy9snk0QpnyGkudLECvI5vM8drRIIw==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;webstorm-激活码&quot;&gt;WebStorm 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;1Z9RTLFFQRXP923-eyJsaWNlbnNlSWQiOiIxWjlSVExGRlFSWFA5MjMiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJXUyIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX1dLCJtZXRhZGF0YSI6IjAyMjAyNDA3MDJQU0FYMDAwMDA1WCIsImhhc2giOiIxMjM0NTY3OC8wNzY0Njg1NzM3IiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsInRyaWFsIjpmYWxzZSwiYWlBbGxvd2VkIjp0cnVlfQ==-bDIQ6Ihy8nWMd6+TEQxRYY2FDAO5yhA1knCEpdqRbstNWI78rmC/WTYm8PFmDK2QEEE7uawIblm6c2FAXLrLzkA7R+V3G+q/xZyYPGftNp4n7mW+VT35+t131R5UvTIoQNzf3dCJDFBD2fiJwDNgUzdwEKEjagjNLlSk5HQam3wZh8+x8aao2yEzgoBHgGrs+8Y8BHKecS9eZImboDJ+e8cT+JI92nNCx1IhPgVJDieNKlbYbCxJ9HgvHFUMSrkh0MPORn9IxmKLt6ssMU2kEMWR4HqcQjUgGRzt8cWJh0nihrqBlDFZwfKQj25oK07vAu22ysN4CkcNRyMcJBBP9A==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;clion-激活码&quot;&gt;CLion 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;I1EKS18DJA9M4KU-eyJsaWNlbnNlSWQiOiJJMUVLUzE4REpBOU00S1UiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJDTCIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX1dLCJtZXRhZGF0YSI6IjAyMjAyNDA3MDJQU0FYMDAwMDA1WCIsImhhc2giOiIxMjM0NTY3OC8wMzMxNTgxNTM4IiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsInRyaWFsIjpmYWxzZSwiYWlBbGxvd2VkIjp0cnVlfQ==-aBxu18kI8ObI+2malKBqZXqpCPT2T7+0adYDU0HBQMeY1J+M/lDGpdQjpDnlDWW3W7wb4QbES3TXl8kKRrVG8LgtwVg9DhtNSWcolfL0R5x73smRjZQB5Jfv4fwBtBu+I4fTrkP9HAwciOIKO3iyI8wsH8HVGR9AXgpl/wTLCSlj8/7IBAFz6wN8mgSdxV6ReEGSjQSDSevdQUbsFkq3LVYG9EXVvleltbdFq7wqVCmvmcnW1idgfDKzJrwlxtJRiLZoZIoEFJ/PXcivuKTJyiqKlDfOuaPt6wZU+aqw/xB5dBS51rzD8UwoLcnN5zlG5WwQPWZja1/UwTokUxR8Gw==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;phpstorm-激活码&quot;&gt;PhpStorm 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;LK57VMVACTAYXFP-eyJsaWNlbnNlSWQiOiJMSzU3Vk1WQUNUQVlYRlAiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQUyIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX1dLCJtZXRhZGF0YSI6IjAyMjAyNDA3MDJQU0FYMDAwMDA1WCIsImhhc2giOiIxMjM0NTY3OC8wOTIwNDIzMTY5IiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsInRyaWFsIjpmYWxzZSwiYWlBbGxvd2VkIjp0cnVlfQ==-giMzZfCCESsBnJddcEgB017IbEg9bfmvITIA+KzajF26fvSBLHtN5qGuPNtAFuB1hPoRvAb4Xqj8rSrIfD0sDxF2ZwS4rkTsqbVRFz70y5+j1yFg7L2Z0g6pw5RNt2fkaYJ2s2T6522tWGcNKEQYDEOaH4JftAB3b66ht7kymV+fdNsD3ffa0TZUke+GfK0wcrJxUtePCUHQV3DsbKxLPERLLoGp19si57cDrZNZwMEUPDphDW5nesd0SAWgYEQSx2CN/0CJOZE9wCvyHETmpBShyOt5Lgq02Ve9jIfn/ARTFEeFXtbVrYSbTOARabhJ9duWKs+v2CiwcuEKLV9mrg==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;goland-激活码&quot;&gt;GoLand 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;BPTY4JK1CO0P29R-eyJsaWNlbnNlSWQiOiJCUFRZNEpLMUNPMFAyOVIiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJHTyIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6ZmFsc2V9LHsiY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX1dLCJtZXRhZGF0YSI6IjAyMjAyNDA3MDJQU0FYMDAwMDA1WCIsImhhc2giOiIxMjM0NTY3OC8wLTIxMzg4MjAwMjAiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwidHJpYWwiOmZhbHNlLCJhaUFsbG93ZWQiOnRydWV9-OhYvoISC/Bq2JPS8zO7RMvG6+V3z3Cw2tIhUfU0rpcWF5H8LimtMCO4Vdw69OYo3VfeBJkoDm0Lu4fwPOllxlx5gbpwerW2pJCubD/TnfDlxK+gt9rUSihL2mCU4k66VB+8NOLdYSH7zukG8ghtbNoeE4BuAiFP6BLTSEipOy2SKKybEzQ1JArnqJyXa66SrxTegCkz8QSwqaBAtJHaBIJ+1MmnQ8y5wjBXXQJ/+9W8AyaxKM14dKvqYxxcf71PDfgzl12taubbpyL4DzGYUiu5cAdjfpqdx2ZEoalj+IW3M3FWB+2eTzHZ+wvgFMqPt1+GiaC2kZwFbeyD9bDF62Q==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;code-with-me-激活码&quot;&gt;Code With Me 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;1MLIZRC8PBSEBKZ-eyJsaWNlbnNlSWQiOiIxTUxJWlJDOFBCU0VCS1oiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjpmYWxzZX1dLCJtZXRhZGF0YSI6IjAyMjAyNDA3MDJQU0FYMDAwMDA1WCIsImhhc2giOiIxMjM0NTY3OC8wMTM2NTcwNTE1IiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsInRyaWFsIjpmYWxzZSwiYWlBbGxvd2VkIjp0cnVlfQ==-E74NvEIq7TVw53KaIZ8Uz0yFAXSPp58KsoTuRjYQXbkiOf8lH/wn79NlQyeLbMqSkYeTUNseKbZFvxS/oNOwrbBOkFrcr2e38wxjlRGpfyOxs0JwMpCS3T1aWN+blS7SEZSCk3mLGkJGrLs3MhsVnEvDDHS+TCAm2vbiMSIOckM3NIJZI2SYGzI2hAblRlTFXBtVAP4AScGv3LDvfA10zSWaUbLB5gAhqjbfc2RTy5d3uGhAK6fYidkjGTguYufHVT7yiDDKz24qXjt1/ZZ9ZmgVwngD+mPbL+sSuo9EQ1Y+hc7CAMIP++wJi3TDGXdVtuR1kXpriycSERWl4FAcrA==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;rubymine-激活码&quot;&gt;RubyMine 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;3PZMNOLHULXID52-eyJsaWNlbnNlSWQiOiIzUFpNTk9MSFVMWElENTIiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQU0kiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlJNIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjpmYWxzZX1dLCJtZXRhZGF0YSI6IjAyMjAyNDA3MDJQU0FYMDAwMDA1WCIsImhhc2giOiIxMjM0NTY3OC8wODg1OTA3NTUiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwidHJpYWwiOmZhbHNlLCJhaUFsbG93ZWQiOnRydWV9-Gw4PbXUwvDcHyuCc3xGpt3BRpRRCxGCmOS4rQIJ/N0Aj0aHsf56ne8oMF593cz4KnDAgeacqGSi9+BrIXTEW52BOiogh5xukZSxHhd8FoKmuaBBYsM3ydaZPWhYL1rp65c0AZ5niIULRbb1hReiz1khjdkiGJqs9VHaR6/rF7HGmU1oagPhCw1u8+3nQM1oMiwl/2lxefZGAAk+/+7kRM/uZjg7TsF6a7UCvjYCAoCLxso58r45WWCIVU7Ofec2rFAIFjwflojDv3HjUdjw7ehSS529fJ4c7wqf6Z2o9o+G//po4eco5Sd5xw3dhocj6XX1Q7phz0VeL97UjwLQ8VQ==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;rustrover-激活码&quot;&gt;RustRover 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;TRY38TEUMV07NQH-eyJsaWNlbnNlSWQiOiJUUlkzOFRFVU1WMDdOUUgiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQUlIiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjYtMDktMTQiLCJwYWlkVXBUbyI6IjIwMjYtMDktMTQiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUlIiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOmZhbHNlfV0sIm1ldGFkYXRhIjoiMDIyMDI0MDcwMlBTQVgwMDAwMDVYIiwiaGFzaCI6IjEyMzQ1Njc4LzAxNzYyNjY5OTAxIiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsInRyaWFsIjpmYWxzZSwiYWlBbGxvd2VkIjp0cnVlfQ==-MGoF9KNRZYTdjvzbKDtQ7JsVcUBuDej9qMukqhQHbF+byyY25F2H7YbW4lrMSOevKWRiehBjeSh6oJmWkrkECQvZJe17BhXKAP1QikuHar6pzDrIZy5gpPYVo6i509dtbDnqV8tt1rXvedkCkG5t3Jmxe2ZJGKKtPJSFZhiPPWOvRGbnL9GNydj9M3N2AaSVhRKidVXuVY26L5I01rRW+jDSlbD/h4e0CZZap5SHYOizQ+5nNBEef6lS0O8LJT1ov9NfHL7XzcGVzBDOFCRH6q1c9F+kMvkOtA7tNjUziTGaZ8tExlulPDg0skVyrHq24z6aSl8okqJufcKjrteK4w==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;rider-激活码&quot;&gt;Rider 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;0H5PC974JDMA7B0-eyJsaWNlbnNlSWQiOiIwSDVQQzk3NEpETUE3QjAiLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlJEIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUERCIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQU0kiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMjIwMjQwNzAyUFNBWDAwMDAwNVgiLCJoYXNoIjoiMTIzNDU2NzgvMC0xODc3MTIwNzU2IiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsInRyaWFsIjpmYWxzZSwiYWlBbGxvd2VkIjp0cnVlfQ==-YTG7LSYI79iFNfKNuHw85uQexHaQ1PMFTv5aBVxkrf2xcHmtX/oJZG9sE2bi32OVyMCnPfTV+SFkArsEw0jKNfGcCmjHIT3HctA+epp3POhEk2rN9DdmMnq0bEHXBJwAtsq47QqJLsBQVFKm4+JzjLXIdQTzQpJY9CIv1lAAWfCsw+hTEDduA8FRgnAhr7YMuJ7GNmZDWsgBiXY9zAHsJRLH2asLazoFJ5myio9k79Ga2leRzXVfm482DzhMJrPAxDlCPoN7me6lSYkVmVy5A2YjW4Zitl1HIQQoyu+Hd7WXcXgZsoFfrvx3Xghe9qw7JZaHzynldocUoW/H2rAzOQ==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;dataspell-激活码&quot;&gt;DataSpell 激活码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;S94DZMOA2LL6XZM-eyJsaWNlbnNlSWQiOiJTOTREWk1PQTJMTDZYWk0iLCJsaWNlbnNlZU5hbWUiOiJtZW5vcmFoIHBhcmFwZXQiLCJsaWNlbnNlZVR5cGUiOiJQRVJTT05BTCIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IkRTIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUERCIiwiZmFsbGJhY2tEYXRlIjoiMjAyNi0wOS0xNCIsInBhaWRVcFRvIjoiMjAyNi0wOS0xNCIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQU0kiLCJmYWxsYmFja0RhdGUiOiIyMDI2LTA5LTE0IiwicGFpZFVwVG8iOiIyMDI2LTA5LTE0IiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMjIwMjQwNzAyUFNBWDAwMDAwNVgiLCJoYXNoIjoiMTIzNDU2NzgvMC0xOTY5MjM0NDIiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwidHJpYWwiOmZhbHNlLCJhaUFsbG93ZWQiOnRydWV9-dsfBHCIV5tNybzlS6ROURFKl4UbxH2lenzpEkaIU9qA+PO552l4m2Oa8S5nr3z5TAGVl/cx/IcWTj2iBpqdnBApkQmAVqlULSdF/bm5MkcVSO/+COhQ9Z6s7YcSyDc06w/94slmhSkBM4f41U2rESv7xZqNzG097Zi06r8Y7+LGRbmgChUSWAk7FNRTdCoipWVjqpiBAQOLyLxAFk2NVGzhOdhJK9K6cC32i/a1zYoefEhqQRaUVYak39PLIpNILG8E7NZd3ij+3H5cPOE1f/+x007ZrzlrFBDcJisu0gKWqzFlk7mfNPNIfaJyCw8+1WXXTMzGCwk1nqhj2xcLmrQ==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABKaDfYJk51mtYwUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD
&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>idea</category>
      <category>intelij</category>
    </item>
    <item>
      <title>谷歌浏览器插件“此扩展程序不再受支持，因此已停用”的解决办法</title>
      <link>https://blog.kdyzm.cn/post/328</link>
      <guid>https://blog.kdyzm.cn/post/328</guid>
      <pubDate>Thu, 14 Aug 2025 23:14:16 +0800</pubDate>
      <description>&lt;p&gt;&lt;strong&gt;第一步：升级谷歌浏览器到最新版本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设置-&amp;gt;关于Chrome-&amp;gt;检查升级，升级后重启浏览器&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：打开浏览器，将以下选项修改为Enabled&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chrome://flags/#temporary-unexpire-flags-m137
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后重启浏览器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：将以下选项都修改为Disabled&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chrome://flags/#extension-manifest-v2-deprecation-warning
chrome://flags/#extension-manifest-v2-deprecation-disabled
chrome://flags/#extension-manifest-v2-deprecation-unsupported
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后重启浏览器&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步：将以下选项修改为Enabled&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chrome://flags/#allow-legacy-mv2-extensions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后重启浏览器。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>谷歌浏览器</category>
    </item>
    <item>
      <title>Redis（十二）：Java客户端之Spring Data Redis</title>
      <link>https://blog.kdyzm.cn/post/327</link>
      <guid>https://blog.kdyzm.cn/post/327</guid>
      <pubDate>Wed, 13 Aug 2025 18:52:49 +0800</pubDate>
      <description>&lt;p&gt;在上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/325&quot;&gt;Redis（十一）：Java客户端之Jedis&lt;/a&gt;》中已经介绍了使用Jedis作为Redis客户端操作Redis的方法，实际上Redis的客户端有很多，Jedis只是其中之一，比较有名的还有&lt;strong&gt;Lettuce&lt;/strong&gt;、&lt;strong&gt;Redisson&lt;/strong&gt;，以及本篇文章将要介绍的&lt;strong&gt;Spring Data Redis&lt;/strong&gt;。SpringBoot通过&lt;code&gt;spring-boot-starter-data-redis&lt;/code&gt;模块提供了对Redis的完美支持，极大简化了Java开发者使用Redis的复杂度。本文将依次讲解如何在SpringBoot项目中集成盖组件，涵盖基础配置、数据操作、缓存实现等。&lt;/p&gt;
&lt;p&gt;Spring Data Redis官方文档：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/index.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/index.html&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;一环境搭建&quot;&gt;一、环境搭建&lt;/h2&gt;
&lt;p&gt;首先，我们需要redis环境，可以参考文章：《&lt;a href=&quot;https://blog.kdyzm.cn/post/28&quot;&gt;CentOS安装Redis&lt;/a&gt;》、《&lt;a href=&quot;https://blog.kdyzm.cn/post/318&quot;&gt;Redis（七）：多机部署之主从复制模式&lt;/a&gt;》、《&lt;a href=&quot;https://blog.kdyzm.cn/post/319&quot;&gt;Redis（八）：多机部署之Sentinel（哨兵）模式&lt;/a&gt;》、《&lt;a href=&quot;https://blog.kdyzm.cn/post/320&quot;&gt;Redis（九）：多机部署之Cluster（集群）模式&lt;/a&gt;》四篇文章分别搭建好主从复制模式、哨兵模式、集群模式三种模式的redis服务，有人问为什么不搭建standalone模式的Redis服务，实际上主从复制模式就是，只是多了几个从节点而已。部署好之后可以通过&lt;code&gt;ps -aux | grep redis&lt;/code&gt;命令查询：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/010/8e2ce5cdf9d84802b39ff93d96f20fcd.png&quot; alt=&quot;image-20250810165158460&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;code&gt;spring-boot-starter-data-redis&lt;/code&gt;组件是springboot官方的组件，引入类似组件要注意版本号应当与springboot版本号保持一致。上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/326&quot;&gt;Java8升级的一点思考&lt;/a&gt;》说过了java8已经过时了，要使用java21，接下来就基于java21+最新版SpringBoot3.5.4搭建开发环境。&lt;/p&gt;
&lt;p&gt;首先创建maven项目，并引入如下pom文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;
         xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;
         xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;cn.kdyzm&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-data-redis&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;21&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;21&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.5.4&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-configuration-processor&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;annotationProcessor&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.projectlombok&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;lombok&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;annotationProcessor&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后启动类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意由于没有引入web组件，所以项目启动之后会退出，这是正常现象。&lt;/p&gt;
&lt;h2 id=&quot;二底层驱动&quot;&gt;二、底层驱动&lt;/h2&gt;
&lt;p&gt;Spring Data Redis底层可以使用两种组件驱动连接Reids服务器：Jedis以及Lettuce，其中Lettuce是默认的组件，其依赖已经被自动引入，如果想替换成Jedis，则需要引入Jedis组件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;redis.clients&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jedis&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.1.5&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Jedis和Lettuce支持的操作不一样，但是总体来说Lettuce无论是功能多样性还是性能上均比Jedis强：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Supported Feature&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Lettuce&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Jedis&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Standalone Connections&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis.html#redis:write-to-master-read-from-replica&quot;&gt;Master/Replica Connections&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis.html#redis:sentinel&quot;&gt;Redis Sentinel&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Master Lookup, Sentinel Authentication, Replica Reads&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Master Lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/cluster.html&quot;&gt;Redis Cluster&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Cluster Connections, Cluster Node Connections, Replica Reads&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Cluster Connections, Cluster Node Connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Transport Channels&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;TCP, OS-native TCP (epoll, kqueue), Unix Domain Sockets&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;TCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Connection Pooling&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X (using &lt;code&gt;commons-pool2&lt;/code&gt;)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X (using &lt;code&gt;commons-pool2&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Other Connection Features&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Singleton-connection sharing for non-blocking commands&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Pipelining and Transactions mutually exclusive. Cannot use server/connection commands in pipeline/transactions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;SSL Support&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pubsub.html&quot;&gt;Pub/Sub&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&quot;&gt;Pipelining&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X (Pipelining and Transactions mutually exclusive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/transactions.html&quot;&gt;Transactions&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X (Pipelining and Transactions mutually exclusive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Datatype support&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Key, String, List, Set, Sorted Set, Hash, Server, Stream, Scripting, Geo, HyperLogLog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Reactive (non-blocking) API&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;X&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;三连接模式&quot;&gt;三、连接模式&lt;/h2&gt;
&lt;p&gt;众所周知，Redis有四种运行方式：独立运行模式、主从复制模式、哨兵模式、集群模式，在spring-boot-stater-data-redis中这四种模式的连接需要做不同的配置，具体可以参考官方文档：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/connection-modes.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/connection-modes.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以下四种模式均可以通过配置文件配置RedisConnectionFactory信息，但是需要显示声明RedisTemplate Bean对象。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;1独立运行模式&quot;&gt;1、独立运行模式&lt;/h3&gt;
&lt;p&gt;独立运行模式只需要连接一个Redis实例就可以了，新建RedisConfiguration类，其内容如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configurable
public class RedisConfiguration {

    public static final String HOST = &amp;quot;192.168.203.130&amp;quot;;

    /**
     * Lettuce
     */
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(HOST, 6379));
    }

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(factory);
        // 必须设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }

//    /**
//     * Jedis
//     */
//    @Bean
//    public RedisConnectionFactory jedisConnectionFactory() {
//        return new JedisConnectionFactory(new RedisStandaloneConfiguration(HOST, 6379));
//    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了上述方法之外，还可以配置文件设置连接信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  data:
    redis:
      host: 192.168.203.130
      port: 6379
      database: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，只需要在代码里配置RedisTemplate信息就可以了，RedisConnectionFactory则会根据配置信息自动创建：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
    template.setConnectionFactory(factory);
    // 必须设置序列化器
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new StringRedisSerializer());
    return template;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2主从复制模式&quot;&gt;2、主从复制模式&lt;/h3&gt;
&lt;p&gt;主从复制模式的好处就是可以写主读从，但是没有哨兵加持的主从复制模式，无法实现自动故障转移。通过使用Lettuce相关功能，可以实现写主读从：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import io.lettuce.core.ReadFrom;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
public class WriteToMasterReadFromReplicaConfiguration {

    public static final String HOST = &amp;quot;192.168.203.130&amp;quot;;

	@Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(factory);
        // 必须设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .readFrom(ReadFrom.REPLICA_PREFERRED)//读操作优先考虑从节点，如果从节点不可用，从主节点读取
                .build();
        RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(
                HOST, 6379
        );
        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，不需要配置所有的节点，只需要配置主节点即可，从主节点可以自动发现其余从节点。代码中的&lt;code&gt;readFrom&lt;/code&gt;方法只是设置了个读取的策略，并非用于设置从节点。&lt;/p&gt;
&lt;p&gt;除了上述代码方法之外，还可以通过配置文件的方式配置RedisConnnectionFactory信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  data:
    redis:
      host: 192.168.203.130
      port: 6379
      database: 0
      lettuce:
        read-from: replica-preferred
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3哨兵模式&quot;&gt;3、哨兵模式&lt;/h3&gt;
&lt;p&gt;哨兵模式的配置非常简单，只需要提供master的名字以及所有哨兵节点的地址即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
public class ReidsSentinelConfiguration1 {

    public static final String HOST = &amp;quot;192.168.203.130&amp;quot;;

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(factory);
        // 必须设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }
    
    /**
     * Lettuce
     */
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master(&amp;quot;mymaster&amp;quot;)
                .sentinel(HOST, 26379)
                .sentinel(HOST, 26380)
                .sentinel(HOST, 26381);
        return new LettuceConnectionFactory(sentinelConfig);
    }

//    /**
//     * Jedis
//     */
//    @Bean
//    public RedisConnectionFactory jedisConnectionFactory() {
//        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
//                .master(&amp;quot;mymaster&amp;quot;)
//                .sentinel(HOST, 26379)
//                .sentinel(HOST, 26380)
//                .sentinel(HOST, 26381);
//        return new JedisConnectionFactory(sentinelConfig);
//    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然也可以通过配置方式定义RedisConnectionFactory信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  data:
    redis:
      database: 0
      lettuce:
        read-from: replica-preferred
      sentinel:
        master: mymaster
        nodes:
          - 192.168.203.130:26379
          - 192.168.203.130:26380
          - 192.168.203.130:26381
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4集群模式&quot;&gt;4、集群模式&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.assertj.core.util.Lists;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.List;

@Configuration
public class RedisClusterConfiguration1 {

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(factory);
        // 必须设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }

    @Bean
    public RedisConnectionFactory connectionFactory() {
        List&amp;lt;String&amp;gt; nodes = Lists.list(
                &amp;quot;192.168.203.130:30001&amp;quot;,
                &amp;quot;192.168.203.130:30002&amp;quot;,
                &amp;quot;192.168.203.130:30003&amp;quot;,
                &amp;quot;192.168.203.130:30004&amp;quot;,
                &amp;quot;192.168.203.130:30005&amp;quot;,
                &amp;quot;192.168.203.130:30006&amp;quot;,
                &amp;quot;192.168.203.130:30007&amp;quot;,
                &amp;quot;192.168.203.130:30008&amp;quot;,
                &amp;quot;192.168.203.130:30009&amp;quot;,
                &amp;quot;192.168.203.130:30010&amp;quot;,
                &amp;quot;192.168.203.130:30011&amp;quot;
        );
        return new LettuceConnectionFactory(
                new RedisClusterConfiguration(nodes));
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以通过配置文件方式定义RedisConnectionFactory信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  data:
    redis:
      lettuce:
        read-from: replica-preferred
      cluster:
        nodes:
          - 192.168.203.130:30001
          - 192.168.203.130:30002
          - 192.168.203.130:30003
          - 192.168.203.130:30004
          - 192.168.203.130:30005
          - 192.168.203.130:30006
          - 192.168.203.130:30007
          - 192.168.203.130:30008
          - 192.168.203.130:30009
          - 192.168.203.130:30010
          - 192.168.203.130:30011
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意集群配置不要和哨兵配置一起配置使用，如果一起配置使用，哨兵模式优先生效；另外集群模式配置中的数据库号设置实际上不会生效，因为集群模式只能使用db0。&lt;/p&gt;
&lt;h2 id=&quot;四使用redistemplate操作redis&quot;&gt;四、使用RedisTemplate操作Redis&lt;/h2&gt;
&lt;p&gt;RedisTemplate是操作Reids最核心的类，操作Redis基本数据类型只需要记住这一个类就可以了。&lt;/p&gt;
&lt;h3 id=&quot;1配置redistemplate&quot;&gt;1、配置RedisTemplate&lt;/h3&gt;
&lt;p&gt;从上面的案例中已经可以看得出RedisTemplate的配置方式了。如果在配置文件中配置了redis链接信息，比如如下单体Reids：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:
  data:
    redis:
      host: 192.168.203.130
      port: 6379
      database: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以直接配置RedisTemplate实例Bean：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
    template.setConnectionFactory(factory);
    // 必须设置序列化器
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new StringRedisSerializer());
    return template;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是如果没有配置文件，需要完整的配置RedisConnectionFactory之后再配置RedisTemplaet：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configurable
public class RedisConfiguration {

    public static final String HOST = &amp;quot;192.168.203.130&amp;quot;;

    /**
     * Lettuce
     */
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(HOST, 6379));
    }

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(factory);
        // 必须设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }

//    /**
//     * Jedis
//     */
//    @Bean
//    public RedisConnectionFactory jedisConnectionFactory() {
//        return new JedisConnectionFactory(new RedisStandaloneConfiguration(HOST, 6379));
//    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2配置序列化方式&quot;&gt;2、配置序列化方式&lt;/h3&gt;
&lt;p&gt;我们写入Redis的时候发生了一次序列化操作，如何把我们的输入序列化之后传给Redis？序列化操作涉及到了Key的序列化以及Value的序列化，比较简单的就是都实用StringRedisSerializer：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
    template.setConnectionFactory(factory);
    // 必须设置序列化器
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new StringRedisSerializer());
    return template;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然还可以使用自定义的序列化方式，比如存储的值类型是Json格式，可以使用Json序列化工具。&lt;/p&gt;
&lt;p&gt;如果使用第三方组件fastjson，先引入依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;fastjson&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.0.58&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用GenericFastJsonRedisSerializer类作为value的序列化工具：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
    template.setConnectionFactory(factory);
    // 使用FastJson作为value的序列化工具
    GenericFastJsonRedisSerializer valueSerializer = new GenericFastJsonRedisSerializer();
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(valueSerializer);
    return template;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，对于Jackson，可以做如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
    template.setConnectionFactory(factory);
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.WRAPPER_ARRAY);
    objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    objectMapper.registerModule(new JavaTimeModule());
    Jackson2JsonRedisSerializer&amp;lt;Object&amp;gt; jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer&amp;lt;&amp;gt;(
            objectMapper,
            Object.class);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(jackson2JsonRedisSerializer);
    return template;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3使用redistemplate&quot;&gt;3、使用RedisTemplate&lt;/h3&gt;
&lt;p&gt;首先看一个操作字符串的案例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.Collection;

@Slf4j
@SpringBootTest
public class RedisConnectionTest {

    @Autowired
    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    @Test
    public void lettuceConnectionTest() {
        String result = (String) redisTemplate.opsForValue().get(&amp;quot;lisi&amp;quot;);
        log.info(result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用RedisTemplate操作某种数据类型，需要先调用&lt;code&gt;opsFor[X]&lt;/code&gt;方法获取Operation实例：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/11/f2164642b8b642b4b87786338eb73831.png&quot; alt=&quot;image-20250811234750772&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;其中操作方法和获取的Operation对象对应如下：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;返回值&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForValue()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/ValueOperations.html&quot;&gt;&lt;code&gt;ValueOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;字符串操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForHash()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/HashOperations.html&quot;&gt;&lt;code&gt;HashOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;hash操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForList()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/ListOperations.html&quot;&gt;&lt;code&gt;ListOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;list操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForSet()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/SetOperations.html&quot;&gt;&lt;code&gt;SetOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;set操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForZSet()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/ZSetOperations.html&quot;&gt;&lt;code&gt;ZSetOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;sorted set操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForGeo()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/GeoOperations.html&quot;&gt;&lt;code&gt;GeoOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;geo操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForHyperLogLog()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/HyperLogLogOperations.html&quot;&gt;&lt;code&gt;HyperLogLogOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;hyperloglog操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redisTemplate.opsForCluster()&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/ClusterOperations.html&quot;&gt;&lt;code&gt;ClusterOperations&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;集群操作对象&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有些奇怪，为什么没有bitmap的操作对象？以为bitmap本身实际上是一个字符串，所以其操作合并到了ValueOperations中了：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/11/9ae36b481f454b869a960b1fce50cf7c.png&quot; alt=&quot;image-20250811235901485&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;另外，&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/api/java/org/springframework/data/redis/core/ClusterOperations.html&quot;&gt;&lt;code&gt;ClusterOperations&lt;/code&gt;&lt;/a&gt; 集成了一些集群管理命令，可以进行集群管理的一些操作，比如想查询某个节点的副本可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Collection&amp;lt;RedisClusterNode&amp;gt; replicas = redisTemplate
        .opsForCluster()
        .getReplicas(new RedisClusterNode(&amp;quot;192.168.203.130&amp;quot;, 30001));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总之，拿到操作对象之后就可以操作Reids了，如果熟悉redis命令，则可以毫不费力的使用相关api：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/12/8a3834c56f014c4d93164e69a0259a40.png&quot; alt=&quot;image-20250812000600369&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;更详细的api使用不再赘述。&lt;/p&gt;
&lt;h2 id=&quot;五消息发布订阅&quot;&gt;五、消息发布/订阅&lt;/h2&gt;
&lt;p&gt;关于redis发布订阅的基本使用，可以参考我之前的文章：《&lt;a href=&quot;https://blog.kdyzm.cn/post/313&quot;&gt;Redis（二）：Redis发布订阅模式&lt;/a&gt;》，接下来看看在Spring Data Redis中如何实现发布订阅模式。官方文档可参考：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pubsub.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/pubsub.html&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;1消息发布&quot;&gt;1、消息发布&lt;/h3&gt;
&lt;p&gt;消息发布最简单的方式就是使用RedisTemplate：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void publishMessageTest() {
    redisTemplate.convertAndSend(&amp;quot;channel_kdyzm&amp;quot;, &amp;quot;Hello,World&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用RedisConnection对象发送消息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void publishMessageByConnection(){
    RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    connection.publish(
            &amp;quot;channel_kdyzm&amp;quot;.getBytes(StandardCharsets.UTF_8),
            &amp;quot;Hello,World&amp;quot;.getBytes(StandardCharsets.UTF_8)
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2消息接收&quot;&gt;2、消息接收&lt;/h3&gt;
&lt;p&gt;消息的接收稍微有些复杂，首先，我们定义一个类&lt;code&gt;MessageReceiver&lt;/code&gt;用于接收消息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@AllArgsConstructor
public class MessageReceiver {

    public void receive(String message) {
        log.info(&amp;quot;receive message:{}&amp;quot;, message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后配置一些Bean：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class PubSubConfig {

    /**
     * 创建自定义的接收消息的Bean
     */
    @Bean
    MessageReceiver listener() {
        return new MessageReceiver();
    }

    @Bean
    MessageListenerAdapter messageListenerAdapter(MessageReceiver listener) {
        //接收到消息的时候将会通过反射调用MessageReceiver对象的receive方法处理消息
        return new MessageListenerAdapter(listener, &amp;quot;receive&amp;quot;);
    }

    @Bean
    RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, MessageListenerAdapter listener) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //监听channel_kdyzm channel
        container.addMessageListener(listener, ChannelTopic.of(&amp;quot;channel_kdyzm&amp;quot;));
        return container;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;六执行lua脚本&quot;&gt;六、执行lua脚本&lt;/h2&gt;
&lt;p&gt;官方操作文档：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/scripting.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/scripting.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;比如我们有个lua脚本在resources资源目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;-- checkandset.lua
local current = redis.call(&apos;GET&apos;, KEYS[1])
if current == ARGV[1]
  then redis.call(&apos;SET&apos;, KEYS[1], ARGV[2])
  return true
end
return false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先需要加载lua脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public RedisScript&amp;lt;Boolean&amp;gt; script() {
  ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource(&amp;quot;META-INF/scripts/checkandset.lua&amp;quot;));
  return RedisScript.of(scriptSource, Boolean.class);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后调用脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Example {


  @Autowired
  private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

  @Autowired
  RedisScript&amp;lt;Boolean&amp;gt; script;

  public boolean checkAndSet(String expectedValue, String newValue) {
    return redisTemplate.execute(script, List.of(&amp;quot;key&amp;quot;), expectedValue, newValue);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;七reids-cache&quot;&gt;七、Reids Cache&lt;/h2&gt;
&lt;p&gt;Spring Data Reids组件除了提供了上述的直接操作Reids的功能外，还提供了封装好的缓存功能。官方文档地址：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;实际上SpringBoot要使用缓存功能，需要引入Spring Cache模块：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-cache&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们已经引入了Spring Data Redis，就不需要再额外引入Spring Cache Starter了。&lt;/p&gt;
&lt;p&gt;在成功集成了Spring Data Redis的情况下，只需要使用一个注解就可以开启缓存功能：&lt;code&gt;@EnableCaching&lt;/code&gt;，开启缓存功能以后，会自动发现当前系统的缓存组件并启用。&lt;/p&gt;
&lt;h3 id=&quot;cacheable&quot;&gt;@Cacheable&lt;/h3&gt;
&lt;p&gt;@Cacheable将方法的返回值缓存起来，后续相同参数的调用直接从缓存返回结果，避免重复执行方法体。该注解通常加到查询方法上，以减轻对数据库的查询压力。&lt;/p&gt;
&lt;p&gt;该注解有两个非常重要的属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;cacheNames&lt;/strong&gt;：指定缓存存储的&lt;strong&gt;逻辑区域&lt;/strong&gt;（即缓存组件的名称），类似于数据库中的“表名”或“命名空间”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;：定义缓存项在&lt;strong&gt;同一区域内的唯一标识&lt;/strong&gt;（即缓存键），类似于数据库中的“主键”，一般需要Spel表达式定义改值。可以使用&lt;code&gt;#p0&lt;/code&gt;、&lt;code&gt;#a0&lt;/code&gt;、&lt;code&gt;#root.args[0]&lt;/code&gt;表示第一个参数。需要注意的是，也可以使用&lt;code&gt;#参数名&lt;/code&gt;指定参数，但是在编译的时候一定要加上&lt;code&gt;-parameters&lt;/code&gt;，否则这种方法将会因为找不到key的值而报错，查看文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/113&quot;&gt;【转载】JDK8新特性:将参数名带到字节码文件&lt;/a&gt;》了解更多。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，在一个查询接口中，可以这么设置缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Cacheable(cacheNames = &amp;quot;users&amp;quot;,key = &amp;quot;#p0&amp;quot;)
public String getUser(String id){
    log.info(&amp;quot;查询用户信息&amp;quot;);
    return &amp;quot;zhangsan&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上代码中调用该方法之后，会向redis中插入一个key：&lt;code&gt;users::1&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/13/6b1d18c7ed904a82909b76c81c738070.png&quot; alt=&quot;image-20250813171934140&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;code&gt;::&lt;/code&gt;是默认的分隔符。&lt;/p&gt;
&lt;p&gt;我认为该注解缺少一个功能，那就是没有办法设置缓存有效期。&lt;/p&gt;
&lt;h3 id=&quot;cacheput&quot;&gt;@CachePut&lt;/h3&gt;
&lt;p&gt;该注解用于强制更新缓存，每次调用都会执行方法体并将结果存入缓存。该注解通常加到更新或者新增的方法上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@CachePut(value = &amp;quot;users&amp;quot;, key = &amp;quot;#user.id&amp;quot;)
public User updateUser(User user) {
    return userRepository.save(user);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;cacheevict&quot;&gt;@CacheEvict&lt;/h3&gt;
&lt;p&gt;该注解用于删除缓存，通常加到删除操作的方法上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@CacheEvict(value = &amp;quot;users&amp;quot;, key = &amp;quot;#id&amp;quot;)  // 清除单条
public void deleteUser(Long id) {
    userRepository.deleteById(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;八其它&quot;&gt;八、其它&lt;/h2&gt;
&lt;p&gt;关于Redis的其它操作，Spring Data Redis还支持Streams、Transactions、Pipelining，由于用的比较少就不再介绍了，可以查看官方文档：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Streams&lt;/strong&gt;：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/redis-streams.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/redis-streams.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transactions&lt;/strong&gt;：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/transactions.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/transactions.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Piplining&lt;/strong&gt;：&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&quot;&gt;https://docs.spring.io/spring-data/redis/reference/redis/pipelining.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
      <category>java</category>
      <category>spring</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Java8升级的一点思考</title>
      <link>https://blog.kdyzm.cn/post/326</link>
      <guid>https://blog.kdyzm.cn/post/326</guid>
      <pubDate>Sun, 10 Aug 2025 13:12:37 +0800</pubDate>
      <description>&lt;p&gt;Oracle 对 Java 8 的公开免费更新已于2019年终止，但是直到现在2025年，Java8还是非常流行，但是随着SpringBoot、SpringFramework等框架逐渐不再支持Java8环境下运行，升级Java8到新版本已经势在必行。&lt;/p&gt;
&lt;h2 id=&quot;一新版springboot的要求&quot;&gt;一、新版SpringBoot的要求&lt;/h2&gt;
&lt;p&gt;最近翻了翻SpringBoot的官方文档，突然发现SpringBoot已经发展到&lt;code&gt;3.5.4&lt;/code&gt;了，而我还在用&lt;code&gt;2.3.12&lt;/code&gt;，真的落后了好大一截。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://spring.io/projects/spring-boot#learn&quot;&gt;https://spring.io/projects/spring-boot#learn&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/07/83cb1b6f944542f6a1dd00d93611858c.png&quot; alt=&quot;image-20250807153836530&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;点开3.5.4文档：&lt;a href=&quot;https://docs.spring.io/spring-boot/system-requirements.html&quot;&gt;https://docs.spring.io/spring-boot/system-requirements.html&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/07/bb710ad0b92840b092f2f3dfccbfcac6.png&quot; alt=&quot;image-20250807154107218&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;从官方文档可以看到，SpringBoot3.5.4需要&lt;strong&gt;Java17&lt;/strong&gt;到最新版本的&lt;strong&gt;Java24&lt;/strong&gt;之间的版本，不管是&lt;strong&gt;Java8&lt;/strong&gt;还是&lt;strong&gt;Java11&lt;/strong&gt;都已经不在支持的范围内。&lt;/p&gt;
&lt;h2 id=&quot;二新版springframework的要求&quot;&gt;二、新版SpringFramework的要求&lt;/h2&gt;
&lt;p&gt;新版SpringFramework已经到6.2.9版本，而我还在用5.2.15，同样落后了一个大版本号：&lt;a href=&quot;https://spring.io/projects/spring-framework#learn&quot;&gt;https://spring.io/projects/spring-framework#learn&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/07/bb1e4694692a43bb8eb935d038718883.png&quot; alt=&quot;image-20250807154934664&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;打开6.2.9版本的文档，可以看到该版本对Java版本的要求：&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/overview.html&quot;&gt;https://docs.spring.io/spring-framework/reference/overview.html&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/07/a75e29bac6374303aa50fe19e9eae5ec.png&quot; alt=&quot;image-20250807155049633&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;同样的，新版本SpringFramework要求Java也至少得是&lt;strong&gt;Java17&lt;/strong&gt;版本。&lt;/p&gt;
&lt;h2 id=&quot;三java版本的选择&quot;&gt;三、Java版本的选择&lt;/h2&gt;
&lt;p&gt;从SpringBoot以及SpringFramework的最新版本的要求来看，他们的运行环境至少得是Java17，那么我们应该升级到Java17吗？&lt;/p&gt;
&lt;p&gt;打开Java官网看看：&lt;a href=&quot;https://www.oracle.com/java/technologies/&quot;&gt;https://www.oracle.com/java/technologies/&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/08/07/d846ef04b1e6440f8bb102db59dc59dd.png&quot; alt=&quot;image-20250807155426370&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，Java11、Java17、Java21是LTS（Long Term Support）版本，即长期支持版。Java11比较特殊，它虽然是LTS，但是一直被人诟病，最后成了一个Java8升级的“过渡版本”，而且SpringBoot以及SpringFramework都从Java17开始支持，所以Java11不做考虑了。考虑到Java17之上还有Java21也是LTS版本，那么毫无疑问，直接升级到Java21会是更好的选择。&lt;/p&gt;
&lt;p&gt;Oracle JDK下载地址：&lt;a href=&quot;https://www.oracle.com/java/technologies/downloads/#jdk21-windows&quot;&gt;https://www.oracle.com/java/technologies/downloads/#jdk21-windows&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Open JDK下载地址： &lt;a href=&quot;https://jdk.java.net/archive/&quot;&gt;https://jdk.java.net/archive/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;四java21新特性&quot;&gt;四、Java21新特性&lt;/h2&gt;
&lt;p&gt;写到这里我有些无语，因为Java21我也没用它开发过项目，我不知道它有啥新特性。。。。等抽空再整理整理吧。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>java</category>
      <category>spring</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Redis（十一）：Java客户端之Jedis</title>
      <link>https://blog.kdyzm.cn/post/325</link>
      <guid>https://blog.kdyzm.cn/post/325</guid>
      <pubDate>Sat, 02 Aug 2025 22:39:29 +0800</pubDate>
      <description>&lt;p&gt;Redis的Java客户端主要有Jedis和Lettuce，本篇文章将会讲解使用Jedis操作Reids。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/redis/jedis&quot;&gt;Jedis&lt;/a&gt; 是一个同步的 Redis Java 客户端。如果需要一个更高级的 Java 客户端，同时支持异步和响应式连接，请使用 &lt;a href=&quot;https://redis.ac.cn/docs/latest/develop/clients/lettuce/&quot;&gt;Lettuce&lt;/a&gt; 。Github地址：&lt;a href=&quot;https://github.com/redis/jedis&quot;&gt;https://github.com/redis/jedis&lt;/a&gt; 。Jedis版本和Redis版本的对应关系图如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/28/7f4f86e1400144288c09775e23eeb8f5.png&quot; alt=&quot;image-20250728143509846&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这里我使用了6.2.1版本的redis，所以需要选择&amp;gt;=5.0版本的jedis，这里选择5.1.5版本（具体参照maven中央仓库中的版本号）。&lt;/p&gt;
&lt;h2 id=&quot;一helloworld&quot;&gt;一、Hello，World&lt;/h2&gt;
&lt;p&gt;先看Hello，World级别的案例：下面建立和Reids Server的连接，并执行set命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import redis.clients.jedis.UnifiedJedis;

/**
 * @author kdyzm
 * @date 2025/7/28
 */
@Slf4j
public class Jedis_demo {

    @Test
    public void hello() {
        UnifiedJedis jedis = new UnifiedJedis(&amp;quot;redis://192.168.203.130:6379&amp;quot;);
        jedis.set(&amp;quot;result&amp;quot;, &amp;quot;Hello,World&amp;quot;);
        String result = jedis.get(&amp;quot;result&amp;quot;);
        log.info(&amp;quot;get result={}&amp;quot;, result);
        jedis.close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;14:57:55.008 [main] INFO cn.kdyzm.jedis.demo.Jedis_demo - get result=Hello,World
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到使用jedis非常简单，它的命令和redis操作命令几乎一模一样。&lt;/p&gt;
&lt;h2 id=&quot;二连接到服务器&quot;&gt;二、连接到服务器&lt;/h2&gt;
&lt;p&gt;Redis部署有多种模式：单机版、主从复制模式、哨兵模式、集群模式，因此连接方式也有多种方式。&lt;/p&gt;
&lt;h3 id=&quot;1单机连接&quot;&gt;1、单机连接&lt;/h3&gt;
&lt;p&gt;很长时间以来，Jedis一直使用Jedis类作为客户端连接服务器，使用方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void jedis_demo() {
    Jedis jedis = new Jedis(new HostAndPort(&amp;quot;192.168.203.130&amp;quot;,6379));
    jedis.set(&amp;quot;result&amp;quot;, &amp;quot;Hello,World&amp;quot;);
    String result = jedis.get(&amp;quot;result&amp;quot;);
    log.info(&amp;quot;get result={}&amp;quot;, result);
    jedis.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Jedis4.0的时候引入了新客户端：UnifiedJedis，&lt;strong&gt;统一了同步（阻塞）和异步（非阻塞）操作&lt;/strong&gt;的API，支持更灵活的连接管理（如单连接或多连接模式）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void hello() {
    UnifiedJedis jedis = new UnifiedJedis(&amp;quot;redis://192.168.203.130:6379&amp;quot;);
    jedis.set(&amp;quot;result&amp;quot;, &amp;quot;Hello,World&amp;quot;);
    String result = jedis.get(&amp;quot;result&amp;quot;);
    log.info(&amp;quot;get result={}&amp;quot;, result);
    jedis.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到两者在使用上几乎没有区别，只是在初始化的时候有些不同。&lt;/p&gt;
&lt;p&gt;当然就像数据库连接一样，我们不推荐直接使用jdbc创建Connection，而是通过Reids连接池来统一管理连接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 经典的jedis连接池
 */
@Test
public void jedis_pool_test() {
    JedisPool jedisPool = new JedisPool(&amp;quot;192.168.203.130&amp;quot;, 6379);
    Jedis jedis = jedisPool.getResource();
    jedis.set(&amp;quot;result&amp;quot;, &amp;quot;Hello,World&amp;quot;);
    String result = jedis.get(&amp;quot;result&amp;quot;);
    log.info(&amp;quot;get result={}&amp;quot;, result);
    jedis.close();
}


/**
 * jedis4.0引入的新连接池
 */
@Test
public void jedis_pooled_test() {
    JedisPooled jedisPool = new JedisPooled(&amp;quot;192.168.203.130&amp;quot;, 6379);
    jedisPool.set(&amp;quot;result&amp;quot;, &amp;quot;Hello,World&amp;quot;);
    String result = jedisPool.get(&amp;quot;result&amp;quot;);
    log.info(&amp;quot;get result={}&amp;quot;, result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中jedis4.0引入的JedisPooled连接池使用起来更简单一些，不需要手动释放资源，也不需要显式获取Reids客户端。&lt;/p&gt;
&lt;h3 id=&quot;2哨兵模式&quot;&gt;2、哨兵模式&lt;/h3&gt;
&lt;p&gt;jedis没有单独的为主从复制模式增加API，但是对哨兵模式的连接有专门的&lt;code&gt;JedisSentinelPool&lt;/code&gt;类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void jedis_pool_sentinel() {
    JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(
            &amp;quot;mymaster&amp;quot;,
            new HashSet&amp;lt;&amp;gt;(Arrays.asList(
                    &amp;quot;192.168.203.130:26379&amp;quot;,
                    &amp;quot;192.168.203.130:26380&amp;quot;,
                    &amp;quot;192.168.203.130:26381&amp;quot;
            ))
    );
    try (Jedis resource = jedisSentinelPool.getResource()) {
        resource.set(&amp;quot;result&amp;quot;, &amp;quot;Hello,World&amp;quot;);
        String result = resource.get(&amp;quot;result&amp;quot;);
        log.info(&amp;quot;get result={}&amp;quot;, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;15:54:37.102 [main] INFO redis.clients.jedis.JedisSentinelPool - Trying to find master from available Sentinels...
15:54:37.102 [main] DEBUG redis.clients.jedis.JedisSentinelPool - Connecting to Sentinel 192.168.203.130:26380
15:54:37.143 [main] DEBUG redis.clients.jedis.JedisSentinelPool - Found Redis master at 192.168.203.130:6379
15:54:37.143 [main] INFO redis.clients.jedis.JedisSentinelPool - Redis master running at 192.168.203.130:6379, starting Sentinel listeners...
15:54:37.147 [main] INFO redis.clients.jedis.JedisSentinelPool - Created JedisSentinelPool to master at 192.168.203.130:6379
15:54:37.151 [main] INFO cn.kdyzm.jedis.demo.Jedis_demo - get result=Hello,World
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，sentinel集群中的配置一定不要写错了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel monitor mymaster 192.168.203.130 6379 2 #这里的ip地址一定不要写成了127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3集群模式&quot;&gt;3、集群模式&lt;/h3&gt;
&lt;p&gt;jedis中使用JedisCluster 连接Redis集群模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void jedis_cluster() {
    JedisCluster jedisCluster = new JedisCluster(
            new HostAndPort(&amp;quot;192.168.203.130&amp;quot;, 30001)
    );
    jedisCluster.set(&amp;quot;result&amp;quot;, &amp;quot;Hello,World&amp;quot;);
    String result = jedisCluster.get(&amp;quot;result&amp;quot;);
    log.info(&amp;quot;get result={}&amp;quot;, result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集群模式的连接也很简单，只需要传入集群任意节点的ip和端口号即可。&lt;/p&gt;
&lt;p&gt;需要注意的是，在创建集群的时候ip地址一定不要写错了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster create 192.168.203.130:30001 192.168.203.130:30002 192.168.203.130:30003 192.168.203.130:30004 192.168.203.130:30005 192.168.203.130:30006 192.168.203.130:30007 192.168.203.130:30008 192.168.203.130:30009 192.168.203.130:30010 192.168.203.130:30011 --cluster-replicas 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果写错了的话会连接不上集群，修复集群中每个端口号会比较麻烦。我的修复方案比较暴力：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;杀掉集群中全部reids进程，删除node配置文件，之后全部重新启动；&lt;/li&gt;
&lt;li&gt;在集群中任意节点上使用&lt;code&gt;cluster meet&lt;/code&gt;命令将节点一个一个加入集群&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;./reids-cli --cluster check&lt;/code&gt;命令检查集群状态，会发现此时cluster集群状态异常，不是所有的槽都分配了：使用&lt;code&gt;./redis-cli --cluster fix&lt;/code&gt;命令修复集群问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;三常用api&quot;&gt;三、常用API&lt;/h2&gt;
&lt;p&gt;jedis的api设计和redis命令格式高度一致，所以基本上会使用redis命令，就知道怎么使用jedis了。&lt;/p&gt;
&lt;h3 id=&quot;1string类型操作&quot;&gt;1、string类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.Test;
import redis.clients.jedis.JedisPooled;

import java.util.List;

import static org.junit.Assert.*;

public class StringDemo {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);

    @Test
    public void testSetAndGetOperation() {
        // 设置键值
        String setResult = jedis.set(&amp;quot;testKey&amp;quot;, &amp;quot;testValue&amp;quot;);
        assertEquals(&amp;quot;OK&amp;quot;, setResult);
        System.out.println(&amp;quot;SET operation successful&amp;quot;);

        // 获取值
        String value = jedis.get(&amp;quot;testKey&amp;quot;);
        assertEquals(&amp;quot;testValue&amp;quot;, value);
        System.out.println(&amp;quot;GET operation successful - Value: &amp;quot; + value);
    }

    @Test
    public void testSetexOperation() {
        // 设置带过期时间的键值
        String setexResult = jedis.setex(&amp;quot;tempKey&amp;quot;, 10, &amp;quot;tempValue&amp;quot;);
        assertEquals(&amp;quot;OK&amp;quot;, setexResult);
        System.out.println(&amp;quot;SETEX operation successful&amp;quot;);

        // 验证TTL
        long ttl = jedis.ttl(&amp;quot;tempKey&amp;quot;);
        assertTrue(ttl &amp;gt; 0 &amp;amp;&amp;amp; ttl &amp;lt;= 10);
        System.out.println(&amp;quot;TTL verification successful - Remaining: &amp;quot; + ttl + &amp;quot; seconds&amp;quot;);
    }

    @Test
    public void testIncrAndDecrOperations() {
        // 重置计数器
        jedis.set(&amp;quot;counter&amp;quot;, &amp;quot;0&amp;quot;);

        // 测试递增
        long incrResult = jedis.incr(&amp;quot;counter&amp;quot;);
        assertEquals(1, incrResult);
        System.out.println(&amp;quot;INCR operation successful - New value: &amp;quot; + incrResult);

        // 测试递减
        long decrResult = jedis.decr(&amp;quot;counter&amp;quot;);
        assertEquals(0, decrResult);
        System.out.println(&amp;quot;DECR operation successful - New value: &amp;quot; + decrResult);
    }

    @Test
    public void testAppendOperation() {
        // 设置初始值
        jedis.set(&amp;quot;appendKey&amp;quot;, &amp;quot;initial&amp;quot;);

        // 测试追加
        long appendLength = jedis.append(&amp;quot;appendKey&amp;quot;, &amp;quot;_appended&amp;quot;);
        assertTrue(appendLength &amp;gt; 7); // &amp;quot;initial&amp;quot;.length() == 7
        System.out.println(&amp;quot;APPEND operation successful - New length: &amp;quot; + appendLength);

        // 验证追加结果
        String appendedValue = jedis.get(&amp;quot;appendKey&amp;quot;);
        assertEquals(&amp;quot;initial_appended&amp;quot;, appendedValue);
        System.out.println(&amp;quot;Appended value verification successful: &amp;quot; + appendedValue);
    }

    @Test
    public void testStrlenOperation() {
        // 设置测试值
        jedis.set(&amp;quot;lengthKey&amp;quot;, &amp;quot;123456789&amp;quot;);

        // 测试字符串长度
        long length = jedis.strlen(&amp;quot;lengthKey&amp;quot;);
        assertEquals(9, length);
        System.out.println(&amp;quot;STRLEN operation successful - Length: &amp;quot; + length);
    }

    @Test
    public void testMsetAndMgetOperations() {
        // 测试批量设置
        String msetResult = jedis.mset(&amp;quot;multi1&amp;quot;, &amp;quot;value1&amp;quot;, &amp;quot;multi2&amp;quot;, &amp;quot;value2&amp;quot;);
        assertEquals(&amp;quot;OK&amp;quot;, msetResult);
        System.out.println(&amp;quot;MSET operation successful&amp;quot;);

        // 测试批量获取
        List&amp;lt;String&amp;gt; values = jedis.mget(&amp;quot;multi1&amp;quot;, &amp;quot;multi2&amp;quot;);
        assertNotNull(values);
        assertEquals(2, values.size());
        assertEquals(&amp;quot;value1&amp;quot;, values.get(0));
        assertEquals(&amp;quot;value2&amp;quot;, values.get(1));
        System.out.println(&amp;quot;MGET operation successful - Values: &amp;quot; + values);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2hash类型操作&quot;&gt;2、hash类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Test;
import redis.clients.jedis.JedisPooled;

import java.util.Map;
import java.util.Set;
import java.util.List;

import static org.junit.Assert.*;

public class RedisHashOperationsTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);
    private static final String HASH_KEY = &amp;quot;user:1&amp;quot;;

    @Test
    public void testHsetAndHget() {
        // 设置字段值
        long hsetResult1 = jedis.hset(HASH_KEY, &amp;quot;name&amp;quot;, &amp;quot;Alice&amp;quot;);
        long hsetResult2 = jedis.hset(HASH_KEY, &amp;quot;age&amp;quot;, &amp;quot;30&amp;quot;);
        assertTrue(hsetResult1 &amp;gt;= 0);
        assertTrue(hsetResult2 &amp;gt;= 0);
        System.out.println(&amp;quot;HSET operations successful&amp;quot;);

        // 获取字段值
        String name = jedis.hget(HASH_KEY, &amp;quot;name&amp;quot;);
        assertEquals(&amp;quot;Alice&amp;quot;, name);
        System.out.println(&amp;quot;HGET operation successful - Name: &amp;quot; + name);
    }

    @Test
    public void testHgetAll() {
        // 准备测试数据
        jedis.hset(HASH_KEY, &amp;quot;name&amp;quot;, &amp;quot;Alice&amp;quot;);
        jedis.hset(HASH_KEY, &amp;quot;age&amp;quot;, &amp;quot;30&amp;quot;);

        // 获取所有字段和值
        Map&amp;lt;String, String&amp;gt; user = jedis.hgetAll(HASH_KEY);
        assertNotNull(user);
        assertEquals(2, user.size());
        assertEquals(&amp;quot;Alice&amp;quot;, user.get(&amp;quot;name&amp;quot;));
        assertEquals(&amp;quot;30&amp;quot;, user.get(&amp;quot;age&amp;quot;));
        System.out.println(&amp;quot;HGETALL operation successful - User data: &amp;quot; + user);
    }

    @Test
    public void testHdel() {
        // 准备测试数据
        jedis.hset(HASH_KEY, &amp;quot;age&amp;quot;, &amp;quot;30&amp;quot;);

        // 删除字段
        long deleted = jedis.hdel(HASH_KEY, &amp;quot;age&amp;quot;);
        assertEquals(1, deleted);
        System.out.println(&amp;quot;HDEL operation successful - Fields deleted: &amp;quot; + deleted);

        // 验证字段已删除
        boolean exists = jedis.hexists(HASH_KEY, &amp;quot;age&amp;quot;);
        assertFalse(exists);
        System.out.println(&amp;quot;Field deletion verification successful&amp;quot;);
    }

    @Test
    public void testHexists() {
        // 准备测试数据
        jedis.hset(HASH_KEY, &amp;quot;name&amp;quot;, &amp;quot;Alice&amp;quot;);

        // 判断字段是否存在
        boolean exists = jedis.hexists(HASH_KEY, &amp;quot;name&amp;quot;);
        assertTrue(exists);
        System.out.println(&amp;quot;HEXISTS operation successful - Field exists: &amp;quot; + exists);

        // 验证不存在的字段
        boolean notExists = jedis.hexists(HASH_KEY, &amp;quot;nonexistent&amp;quot;);
        assertFalse(notExists);
        System.out.println(&amp;quot;HEXISTS operation successful - Nonexistent field check passed&amp;quot;);
    }

    @Test
    public void testHkeys() {
        // 准备测试数据
        jedis.hset(HASH_KEY, &amp;quot;name&amp;quot;, &amp;quot;Alice&amp;quot;);
        jedis.hset(HASH_KEY, &amp;quot;age&amp;quot;, &amp;quot;30&amp;quot;);
        jedis.hset(HASH_KEY, &amp;quot;email&amp;quot;, &amp;quot;alice@example.com&amp;quot;);

        // 获取所有字段名
        Set&amp;lt;String&amp;gt; fields = jedis.hkeys(HASH_KEY);
        assertNotNull(fields);
        assertEquals(3, fields.size());
        assertTrue(fields.contains(&amp;quot;name&amp;quot;));
        assertTrue(fields.contains(&amp;quot;age&amp;quot;));
        assertTrue(fields.contains(&amp;quot;email&amp;quot;));
        System.out.println(&amp;quot;HKEYS operation successful - Fields: &amp;quot; + fields);
    }

    @Test
    public void testHvals() {
        // 准备测试数据
        jedis.hset(HASH_KEY, &amp;quot;name&amp;quot;, &amp;quot;Alice&amp;quot;);
        jedis.hset(HASH_KEY, &amp;quot;age&amp;quot;, &amp;quot;30&amp;quot;);

        // 获取所有值
        List&amp;lt;String&amp;gt; vals = jedis.hvals(HASH_KEY);
        assertNotNull(vals);
        assertEquals(2, vals.size());
        assertTrue(vals.contains(&amp;quot;Alice&amp;quot;));
        assertTrue(vals.contains(&amp;quot;30&amp;quot;));
        System.out.println(&amp;quot;HVALS operation successful - Values: &amp;quot; + vals);
    }

    @Test
    public void testHincrBy() {
        // 准备测试数据
        jedis.hset(HASH_KEY, &amp;quot;age&amp;quot;, &amp;quot;30&amp;quot;);

        // 递增字段值
        long newAge = jedis.hincrBy(HASH_KEY, &amp;quot;age&amp;quot;, 1);
        assertEquals(31, newAge);
        System.out.println(&amp;quot;HINCRBY operation successful - New age: &amp;quot; + newAge);

        // 验证新值
        String updatedAge = jedis.hget(HASH_KEY, &amp;quot;age&amp;quot;);
        assertEquals(&amp;quot;31&amp;quot;, updatedAge);
        System.out.println(&amp;quot;Incremented value verification successful&amp;quot;);
    }

    @After
    public void cleanup() {
        // 清理测试数据
        jedis.del(HASH_KEY);
        System.out.println(&amp;quot;Test data cleaned up&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3list类型操作&quot;&gt;3、list类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Test;
import redis.clients.jedis.JedisPooled;
import java.util.List;
import static org.junit.Assert.*;

public class RedisListOperationsTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);
    private static final String LIST_KEY = &amp;quot;testList&amp;quot;;

    @Test
    public void testLpushAndRpush() {
        // 从左边插入元素
        long lpushResult = jedis.lpush(LIST_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;);
        assertEquals(2, lpushResult);
        System.out.println(&amp;quot;LPUSH operation successful - Items added: &amp;quot; + lpushResult);

        // 从右边插入元素
        long rpushResult = jedis.rpush(LIST_KEY, &amp;quot;item3&amp;quot;);
        assertEquals(3, rpushResult);
        System.out.println(&amp;quot;RPUSH operation successful - Items count: &amp;quot; + rpushResult);
    }

    @Test
    public void testLlen() {
        // 准备测试数据
        jedis.lpush(LIST_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;, &amp;quot;item3&amp;quot;);

        // 获取列表长度
        long length = jedis.llen(LIST_KEY);
        assertEquals(3, length);
        System.out.println(&amp;quot;LLEN operation successful - List length: &amp;quot; + length);
    }

    @Test
    public void testLrange() {
        // 准备测试数据
        jedis.rpush(LIST_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;, &amp;quot;item3&amp;quot;);

        // 获取列表元素
        List&amp;lt;String&amp;gt; items = jedis.lrange(LIST_KEY, 0, -1);
        assertNotNull(items);
        assertEquals(3, items.size());
        assertEquals(&amp;quot;item1&amp;quot;, items.get(0));
        assertEquals(&amp;quot;item2&amp;quot;, items.get(1));
        assertEquals(&amp;quot;item3&amp;quot;, items.get(2));
        System.out.println(&amp;quot;LRANGE operation successful - All items: &amp;quot; + items);
    }

    @Test
    public void testLpopAndRpop() {
        // 准备测试数据
        jedis.rpush(LIST_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;, &amp;quot;item3&amp;quot;);

        // 弹出左边元素
        String leftItem = jedis.lpop(LIST_KEY);
        assertEquals(&amp;quot;item1&amp;quot;, leftItem);
        System.out.println(&amp;quot;LPOP operation successful - Popped item: &amp;quot; + leftItem);

        // 弹出右边元素
        String rightItem = jedis.rpop(LIST_KEY);
        assertEquals(&amp;quot;item3&amp;quot;, rightItem);
        System.out.println(&amp;quot;RPOP operation successful - Popped item: &amp;quot; + rightItem);

        // 验证剩余元素
        long remaining = jedis.llen(LIST_KEY);
        assertEquals(1, remaining);
        System.out.println(&amp;quot;Remaining items count: &amp;quot; + remaining);
    }

    @Test
    public void testLindex() {
        // 准备测试数据
        jedis.rpush(LIST_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;, &amp;quot;item3&amp;quot;);

        // 根据索引获取元素
        String item = jedis.lindex(LIST_KEY, 1);
        assertEquals(&amp;quot;item2&amp;quot;, item);
        System.out.println(&amp;quot;LINDEX operation successful - Item at index 1: &amp;quot; + item);

        // 测试不存在的索引
        String nonExistItem = jedis.lindex(LIST_KEY, 5);
        assertNull(nonExistItem);
        System.out.println(&amp;quot;LINDEX operation successful - Non-existent index returns null&amp;quot;);
    }

    @Test
    public void testLtrim() {
        // 准备测试数据
        jedis.rpush(LIST_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;, &amp;quot;item3&amp;quot;, &amp;quot;item4&amp;quot;);

        // 修剪列表
        String ltrimResult = jedis.ltrim(LIST_KEY, 0, 1);
        assertEquals(&amp;quot;OK&amp;quot;, ltrimResult);
        System.out.println(&amp;quot;LTRIM operation successful&amp;quot;);

        // 验证修剪结果
        List&amp;lt;String&amp;gt; items = jedis.lrange(LIST_KEY, 0, -1);
        assertEquals(2, items.size());
        assertEquals(&amp;quot;item1&amp;quot;, items.get(0));
        assertEquals(&amp;quot;item2&amp;quot;, items.get(1));
        System.out.println(&amp;quot;List after trim: &amp;quot; + items);
    }

    @After
    public void cleanup() {
        // 清理测试数据
        jedis.del(LIST_KEY);
        System.out.println(&amp;quot;Test data cleaned up&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4set类型操作&quot;&gt;4、set类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.JedisPooled;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import static org.junit.Assert.*;

public class RedisSetOperationsTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);
    private static final String SET_KEY = &amp;quot;testSet&amp;quot;;
    private static final String SET1_KEY = &amp;quot;set1&amp;quot;;
    private static final String SET2_KEY = &amp;quot;set2&amp;quot;;

    @Before
    public void setUp() {
        // 清空测试用key
        jedis.del(SET_KEY, SET1_KEY, SET2_KEY);
    }

    @After
    public void tearDown() {
        // 清理测试数据
        jedis.del(SET_KEY, SET1_KEY, SET2_KEY);
        System.out.println(&amp;quot;Test data cleaned up&amp;quot;);
    }

    @Test
    public void testSaddAndSmembers() {
        // 添加元素
        long added = jedis.sadd(SET_KEY, &amp;quot;member1&amp;quot;, &amp;quot;member2&amp;quot;);
        assertEquals(2, added);
        System.out.println(&amp;quot;SADD operation successful - Members added: &amp;quot; + added);

        // 获取所有成员
        Set&amp;lt;String&amp;gt; members = jedis.smembers(SET_KEY);
        assertNotNull(members);
        assertEquals(2, members.size());
        assertTrue(members.contains(&amp;quot;member1&amp;quot;));
        assertTrue(members.contains(&amp;quot;member2&amp;quot;));
        System.out.println(&amp;quot;SMEMBERS operation successful - Members: &amp;quot; + members);
    }

    @Test
    public void testSismember() {
        // 准备测试数据
        jedis.sadd(SET_KEY, &amp;quot;member1&amp;quot;, &amp;quot;member2&amp;quot;);

        // 判断是否是成员
        boolean isMember = jedis.sismember(SET_KEY, &amp;quot;member1&amp;quot;);
        assertTrue(isMember);
        System.out.println(&amp;quot;SISMEMBER operation successful - &apos;member1&apos; exists: &amp;quot; + isMember);

        // 测试不存在的成员
        boolean notMember = jedis.sismember(SET_KEY, &amp;quot;nonMember&amp;quot;);
        assertFalse(notMember);
        System.out.println(&amp;quot;SISMEMBER operation successful - &apos;nonMember&apos; does not exist&amp;quot;);
    }

    @Test
    public void testSrem() {
        // 准备测试数据
        jedis.sadd(SET_KEY, &amp;quot;member1&amp;quot;, &amp;quot;member2&amp;quot;, &amp;quot;member3&amp;quot;);

        // 移除成员
        long removed = jedis.srem(SET_KEY, &amp;quot;member1&amp;quot;, &amp;quot;member3&amp;quot;);
        assertEquals(2, removed);
        System.out.println(&amp;quot;SREM operation successful - Members removed: &amp;quot; + removed);

        // 验证移除结果
        Set&amp;lt;String&amp;gt; remaining = jedis.smembers(SET_KEY);
        assertEquals(1, remaining.size());
        assertTrue(remaining.contains(&amp;quot;member2&amp;quot;));
        System.out.println(&amp;quot;Remaining members: &amp;quot; + remaining);
    }

    @Test
    public void testScard() {
        // 准备测试数据
        jedis.sadd(SET_KEY, &amp;quot;member1&amp;quot;, &amp;quot;member2&amp;quot;, &amp;quot;member3&amp;quot;, &amp;quot;member4&amp;quot;);

        // 获取集合大小
        long size = jedis.scard(SET_KEY);
        assertEquals(4, size);
        System.out.println(&amp;quot;SCARD operation successful - Set size: &amp;quot; + size);
    }

    @Test
    public void testSrandmember() {
        // 准备测试数据
        jedis.sadd(SET_KEY, &amp;quot;member1&amp;quot;, &amp;quot;member2&amp;quot;, &amp;quot;member3&amp;quot;);

        // 随机获取一个成员
        String randomMember = jedis.srandmember(SET_KEY);
        assertNotNull(randomMember);
        assertTrue(randomMember.startsWith(&amp;quot;member&amp;quot;));
        System.out.println(&amp;quot;SRANDMEMBER operation successful - Random member: &amp;quot; + randomMember);
    }

    @Test
    public void testSetOperations() {
        // 准备测试数据
        jedis.sadd(SET1_KEY, &amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;c&amp;quot;);
        jedis.sadd(SET2_KEY, &amp;quot;b&amp;quot;, &amp;quot;c&amp;quot;, &amp;quot;d&amp;quot;);

        // 交集
        Set&amp;lt;String&amp;gt; intersection = jedis.sinter(SET1_KEY, SET2_KEY);
        assertNotNull(intersection);
        assertEquals(2, intersection.size());
        assertTrue(intersection.contains(&amp;quot;b&amp;quot;));
        assertTrue(intersection.contains(&amp;quot;c&amp;quot;));
        System.out.println(&amp;quot;SINTER operation successful - Intersection: &amp;quot; + intersection);

        // 并集
        Set&amp;lt;String&amp;gt; union = jedis.sunion(SET1_KEY, SET2_KEY);
        assertNotNull(union);
        assertEquals(4, union.size());
        assertTrue(union.containsAll(new HashSet&amp;lt;&amp;gt;(Arrays.asList(&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;c&amp;quot;, &amp;quot;d&amp;quot;))));
        System.out.println(&amp;quot;SUNION operation successful - Union: &amp;quot; + union);

        // 差集
        Set&amp;lt;String&amp;gt; difference = jedis.sdiff(SET1_KEY, SET2_KEY);
        assertNotNull(difference);
        assertEquals(1, difference.size());
        assertTrue(difference.contains(&amp;quot;a&amp;quot;));
        System.out.println(&amp;quot;SDIFF operation successful - Difference: &amp;quot; + difference);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5zset类型操作&quot;&gt;5、zset类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.resps.Tuple;

import java.util.List;

import static org.junit.Assert.*;

public class RedisZSetOperationsTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);
    private static final String ZSET_KEY = &amp;quot;testZSet&amp;quot;;

    @Before
    public void setUp() {
        // 清空测试用key
        jedis.del(ZSET_KEY);
    }

    @After
    public void tearDown() {
        // 清理测试数据
        jedis.del(ZSET_KEY);
        System.out.println(&amp;quot;Test data cleaned up&amp;quot;);
    }

    @Test
    public void testZaddAndZscore() {
        // 添加成员
        long added1 = jedis.zadd(ZSET_KEY, 1.0, &amp;quot;member1&amp;quot;);
        assertEquals(1, added1);

        long added2 = jedis.zadd(ZSET_KEY, 2.0, &amp;quot;member2&amp;quot;);
        assertEquals(1, added2);

        System.out.println(&amp;quot;ZADD operations successful&amp;quot;);

        // 获取成员分数
        Double score = jedis.zscore(ZSET_KEY, &amp;quot;member1&amp;quot;);
        assertEquals(1.0, score, 0.001);
        System.out.println(&amp;quot;ZSCORE operation successful - Score: &amp;quot; + score);
    }

    @Test
    public void testZrankAndZrevrank() {
        // 准备测试数据
        jedis.zadd(ZSET_KEY, 1.0, &amp;quot;member1&amp;quot;);
        jedis.zadd(ZSET_KEY, 2.0, &amp;quot;member2&amp;quot;);
        jedis.zadd(ZSET_KEY, 3.0, &amp;quot;member3&amp;quot;);

        // 获取排名(从小到大)
        long rank = jedis.zrank(ZSET_KEY, &amp;quot;member1&amp;quot;);
        assertEquals(0, rank);
        System.out.println(&amp;quot;ZRANK operation successful - Rank: &amp;quot; + rank);

        // 获取排名(从大到小)
        long revRank = jedis.zrevrank(ZSET_KEY, &amp;quot;member1&amp;quot;);
        assertEquals(2, revRank);
        System.out.println(&amp;quot;ZREVRANK operation successful - Reverse rank: &amp;quot; + revRank);
    }

    @Test
    public void testZrangeAndZrevrange() {
        // 准备测试数据
        jedis.zadd(ZSET_KEY, 1.0, &amp;quot;member1&amp;quot;);
        jedis.zadd(ZSET_KEY, 2.0, &amp;quot;member2&amp;quot;);
        jedis.zadd(ZSET_KEY, 3.0, &amp;quot;member3&amp;quot;);

        // 获取范围内的成员(从小到大)
        List&amp;lt;String&amp;gt; members = jedis.zrange(ZSET_KEY, 0, -1);
        assertArrayEquals(new String[]{&amp;quot;member1&amp;quot;, &amp;quot;member2&amp;quot;, &amp;quot;member3&amp;quot;},
                members.toArray(new String[0]));
        System.out.println(&amp;quot;ZRANGE operation successful - Members: &amp;quot; + members);

        // 获取范围内的成员(从大到小)
        List&amp;lt;String&amp;gt; revMembers = jedis.zrevrange(ZSET_KEY, 0, -1);
        assertArrayEquals(new String[]{&amp;quot;member3&amp;quot;, &amp;quot;member2&amp;quot;, &amp;quot;member1&amp;quot;},
                revMembers.toArray(new String[0]));
        System.out.println(&amp;quot;ZREVRANGE operation successful - Reverse members: &amp;quot; + revMembers);
    }

    @Test
    public void testZrangeWithScores() {
        // 准备测试数据
        jedis.zadd(ZSET_KEY, 1.0, &amp;quot;member1&amp;quot;);
        jedis.zadd(ZSET_KEY, 2.0, &amp;quot;member2&amp;quot;);

        // 获取范围内的成员(带分数)
        List&amp;lt;Tuple&amp;gt; tuples = jedis.zrangeWithScores(ZSET_KEY, 0, -1);
        assertEquals(2, tuples.size());

        for (Tuple tuple : tuples) {
            String element = tuple.getElement();
            double score = tuple.getScore();
            System.out.println(&amp;quot;Member: &amp;quot; + element + &amp;quot;, Score: &amp;quot; + score);

            if (element.equals(&amp;quot;member1&amp;quot;)) {
                assertEquals(1.0, score, 0.001);
            } else if (element.equals(&amp;quot;member2&amp;quot;)) {
                assertEquals(2.0, score, 0.001);
            }
        }
        System.out.println(&amp;quot;ZRANGE WITHSCORES operation successful&amp;quot;);
    }

    @Test
    public void testZrangeByScore() {
        // 准备测试数据
        jedis.zadd(ZSET_KEY, 1.0, &amp;quot;member1&amp;quot;);
        jedis.zadd(ZSET_KEY, 2.0, &amp;quot;member2&amp;quot;);
        jedis.zadd(ZSET_KEY, 3.0, &amp;quot;member3&amp;quot;);

        // 根据分数范围获取成员
        List&amp;lt;String&amp;gt; byScore = jedis.zrangeByScore(ZSET_KEY, 1.0, 2.0);
        assertEquals(2, byScore.size());
        assertTrue(byScore.contains(&amp;quot;member1&amp;quot;));
        assertTrue(byScore.contains(&amp;quot;member2&amp;quot;));
        System.out.println(&amp;quot;ZRANGEBYSCORE operation successful - Members: &amp;quot; + byScore);
    }

    @Test
    public void testZrem() {
        // 准备测试数据
        jedis.zadd(ZSET_KEY, 1.0, &amp;quot;member1&amp;quot;);
        jedis.zadd(ZSET_KEY, 2.0, &amp;quot;member2&amp;quot;);

        // 移除成员
        long removed = jedis.zrem(ZSET_KEY, &amp;quot;member1&amp;quot;);
        assertEquals(1, removed);
        System.out.println(&amp;quot;ZREM operation successful - Members removed: &amp;quot; + removed);

        // 验证移除结果
        Double score = jedis.zscore(ZSET_KEY, &amp;quot;member1&amp;quot;);
        assertNull(score);
        System.out.println(&amp;quot;Member removal verification successful&amp;quot;);
    }

    @Test
    public void testZcard() {
        // 准备测试数据
        jedis.zadd(ZSET_KEY, 1.0, &amp;quot;member1&amp;quot;);
        jedis.zadd(ZSET_KEY, 2.0, &amp;quot;member2&amp;quot;);
        jedis.zadd(ZSET_KEY, 3.0, &amp;quot;member3&amp;quot;);

        // 获取集合大小
        long size = jedis.zcard(ZSET_KEY);
        assertEquals(3, size);
        System.out.println(&amp;quot;ZCARD operation successful - Set size: &amp;quot; + size);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6bitmap类型操作&quot;&gt;6、bitmap类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.args.BitOP;

import static org.junit.Assert.*;

public class RedisBitmapOperationsTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);
    private static final String BITMAP_KEY = &amp;quot;testBitmap&amp;quot;;
    private static final String BITMAP1_KEY = &amp;quot;bitmap1&amp;quot;;
    private static final String BITMAP2_KEY = &amp;quot;bitmap2&amp;quot;;
    private static final String RESULT_KEY = &amp;quot;bitResult&amp;quot;;

    @Before
    public void setUp() {
        // 清空测试用key
        jedis.del(BITMAP_KEY, BITMAP1_KEY, BITMAP2_KEY, RESULT_KEY);
    }

    @After
    public void tearDown() {
        // 清理测试数据
        jedis.del(BITMAP_KEY, BITMAP1_KEY, BITMAP2_KEY, RESULT_KEY);
        System.out.println(&amp;quot;Test data cleaned up&amp;quot;);
    }

    @Test
    public void testSetbitAndGetbit() {
        // 设置位
        boolean prevBit1 = jedis.setbit(BITMAP_KEY, 0, true);
        assertFalse(prevBit1); // 之前该位应该是0
        System.out.println(&amp;quot;SETBIT operation successful - Set bit 0 to 1&amp;quot;);

        boolean prevBit2 = jedis.setbit(BITMAP_KEY, 1, false);
        assertFalse(prevBit2); // 之前该位应该是0
        System.out.println(&amp;quot;SETBIT operation successful - Set bit 1 to 0&amp;quot;);

        // 获取位
        boolean bit0 = jedis.getbit(BITMAP_KEY, 0);
        assertTrue(bit0);
        System.out.println(&amp;quot;GETBIT operation successful - Bit 0: &amp;quot; + bit0);

        boolean bit1 = jedis.getbit(BITMAP_KEY, 1);
        assertFalse(bit1);
        System.out.println(&amp;quot;GETBIT operation successful - Bit 1: &amp;quot; + bit1);

        // 测试未设置的位
        boolean bit100 = jedis.getbit(BITMAP_KEY, 100);
        assertFalse(bit100);
        System.out.println(&amp;quot;GETBIT operation successful - Unset bit 100: &amp;quot; + bit100);
    }

    @Test
    public void testBitcount() {
        // 准备测试数据
        jedis.setbit(BITMAP_KEY, 0, true);
        jedis.setbit(BITMAP_KEY, 1, false);
        jedis.setbit(BITMAP_KEY, 2, true);
        jedis.setbit(BITMAP_KEY, 3, true);
        jedis.setbit(BITMAP_KEY, 4, false);

        // 统计设置为1的位数
        long count = jedis.bitcount(BITMAP_KEY);
        assertEquals(3, count);
        System.out.println(&amp;quot;BITCOUNT operation successful - Count: &amp;quot; + count);

        // 测试指定范围内的bitcount
        long rangeCount = jedis.bitcount(BITMAP_KEY, 0, 1); // 前两个字节
        assertEquals(3, rangeCount); 
        System.out.println(&amp;quot;BITCOUNT with range operation successful - Count: &amp;quot; + rangeCount);
    }

    @Test
    public void testBitop() {
        // 准备测试数据
        jedis.setbit(BITMAP1_KEY, 0, true);
        jedis.setbit(BITMAP1_KEY, 1, true);
        jedis.setbit(BITMAP1_KEY, 2, false);

        jedis.setbit(BITMAP2_KEY, 0, true);
        jedis.setbit(BITMAP2_KEY, 1, false);
        jedis.setbit(BITMAP2_KEY, 2, true);

        // 位运算 AND
        long resultLength = jedis.bitop(BitOP.AND, RESULT_KEY, BITMAP1_KEY, BITMAP2_KEY);
        assertTrue(resultLength &amp;gt; 0);
        System.out.println(&amp;quot;BITOP AND operation successful - Result length: &amp;quot; + resultLength);

        // 验证结果
        assertTrue(jedis.getbit(RESULT_KEY, 0));  // 1 AND 1 = 1
        assertFalse(jedis.getbit(RESULT_KEY, 1)); // 1 AND 0 = 0
        assertFalse(jedis.getbit(RESULT_KEY, 2)); // 0 AND 1 = 0
        System.out.println(&amp;quot;BITOP AND verification successful&amp;quot;);

        // 位运算 OR
        resultLength = jedis.bitop(BitOP.OR, RESULT_KEY, BITMAP1_KEY, BITMAP2_KEY);
        assertTrue(resultLength &amp;gt; 0);
        System.out.println(&amp;quot;BITOP OR operation successful - Result length: &amp;quot; + resultLength);

        // 验证结果
        assertTrue(jedis.getbit(RESULT_KEY, 0));  // 1 OR 1 = 1
        assertTrue(jedis.getbit(RESULT_KEY, 1));  // 1 OR 0 = 1
        assertTrue(jedis.getbit(RESULT_KEY, 2));  // 0 OR 1 = 1
        System.out.println(&amp;quot;BITOP OR verification successful&amp;quot;);

        // 位运算 XOR
        resultLength = jedis.bitop(BitOP.XOR, RESULT_KEY, BITMAP1_KEY, BITMAP2_KEY);
        assertTrue(resultLength &amp;gt; 0);
        System.out.println(&amp;quot;BITOP XOR operation successful - Result length: &amp;quot; + resultLength);

        // 验证结果
        assertFalse(jedis.getbit(RESULT_KEY, 0)); // 1 XOR 1 = 0
        assertTrue(jedis.getbit(RESULT_KEY, 1));  // 1 XOR 0 = 1
        assertTrue(jedis.getbit(RESULT_KEY, 2));  // 0 XOR 1 = 1
        System.out.println(&amp;quot;BITOP XOR verification successful&amp;quot;);

        // 位运算 NOT (只需要一个源key)
        resultLength = jedis.bitop(BitOP.NOT, RESULT_KEY, BITMAP1_KEY);
        assertTrue(resultLength &amp;gt; 0);
        System.out.println(&amp;quot;BITOP NOT operation successful - Result length: &amp;quot; + resultLength);

        // 验证结果
        assertFalse(jedis.getbit(RESULT_KEY, 0)); // NOT 1 = 0
        assertFalse(jedis.getbit(RESULT_KEY, 1)); // NOT 1 = 0
        assertTrue(jedis.getbit(RESULT_KEY, 2));  // NOT 0 = 1
        System.out.println(&amp;quot;BITOP NOT verification successful&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;7hyperloglog类型操作&quot;&gt;7、HyperLogLog类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.JedisPooled;
import static org.junit.Assert.*;

public class RedisHyperLogLogTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);
    private static final String HLL_KEY = &amp;quot;testHLL&amp;quot;;
    private static final String HLL2_KEY = &amp;quot;testHLL2&amp;quot;;
    private static final String MERGED_KEY = &amp;quot;mergedHLL&amp;quot;;

    @Before
    public void setUp() {
        // 清空测试用key
        jedis.del(HLL_KEY, HLL2_KEY, MERGED_KEY);
    }

    @After
    public void tearDown() {
        // 清理测试数据
        jedis.del(HLL_KEY, HLL2_KEY, MERGED_KEY);
        System.out.println(&amp;quot;Test data cleaned up&amp;quot;);
    }

    @Test
    public void testPfaddAndPfcount() {
        // 添加元素
        long added = jedis.pfadd(HLL_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;, &amp;quot;item3&amp;quot;);
        assertEquals(1, added);
        System.out.println(&amp;quot;PFADD operation successful - Elements added&amp;quot;);

        // 获取基数估计
        long count = jedis.pfcount(HLL_KEY);
        assertEquals(3, count);
        System.out.println(&amp;quot;PFCOUNT operation successful - Estimated count: &amp;quot; + count);

        // 添加重复元素
        long dupAdded = jedis.pfadd(HLL_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;);
        assertEquals(0, dupAdded);
        System.out.println(&amp;quot;PFADD with duplicates - No new elements added&amp;quot;);

        // 验证基数不变
        long sameCount = jedis.pfcount(HLL_KEY);
        assertEquals(3, sameCount);
        System.out.println(&amp;quot;PFCOUNT after duplicates - Same count: &amp;quot; + sameCount);
    }

    @Test
    public void testPfmerge() {
        // 准备测试数据
        jedis.pfadd(HLL_KEY, &amp;quot;item1&amp;quot;, &amp;quot;item2&amp;quot;, &amp;quot;item3&amp;quot;);
        jedis.pfadd(HLL2_KEY, &amp;quot;item3&amp;quot;, &amp;quot;item4&amp;quot;, &amp;quot;item5&amp;quot;);

        // 合并多个HLL
        String mergeResult = jedis.pfmerge(MERGED_KEY, HLL_KEY, HLL2_KEY);
        assertEquals(&amp;quot;OK&amp;quot;, mergeResult);
        System.out.println(&amp;quot;PFMERGE operation successful&amp;quot;);

        // 获取合并后的基数估计
        long mergedCount = jedis.pfcount(MERGED_KEY);
        assertTrue(mergedCount &amp;gt;= 5 &amp;amp;&amp;amp; mergedCount &amp;lt;= 6); // HyperLogLog有约0.81%误差
        System.out.println(&amp;quot;PFCOUNT after merge - Estimated count: &amp;quot; + mergedCount);

        // 验证原始HLL不变
        long originalCount = jedis.pfcount(HLL_KEY);
        assertEquals(3, originalCount);
        System.out.println(&amp;quot;Original HLL count unchanged: &amp;quot; + originalCount);
    }

    @Test
    public void testEmptyHLL() {
        // 测试空HLL
        long emptyCount = jedis.pfcount(HLL_KEY);
        assertEquals(0, emptyCount);
        System.out.println(&amp;quot;PFCOUNT on empty HLL returns 0&amp;quot;);

        // 添加空元素
        long emptyAdd = jedis.pfadd(HLL_KEY);
        assertEquals(1, emptyAdd);
        System.out.println(&amp;quot;PFADD with no elements returns 1&amp;quot;);
    }

    @Test
    public void testHLLAccuracy() {
        // 测试HLL的基数估计准确性
        int testSize = 10000;
        String[] elements = new String[testSize];

        // 生成10000个唯一元素
        for (int i = 0; i &amp;lt; testSize; i++) {
            elements[i] = &amp;quot;element-&amp;quot; + i;
        }

        // 添加元素到HLL
        jedis.pfadd(HLL_KEY, elements);

        // 获取基数估计
        long estimatedCount = jedis.pfcount(HLL_KEY);
        System.out.println(&amp;quot;Actual count: &amp;quot; + testSize + &amp;quot;, Estimated count: &amp;quot; + estimatedCount);

        // 计算误差率 (HLL标准误差约0.81%)
        double errorRate = Math.abs(estimatedCount - testSize) * 100.0 / testSize;
        System.out.printf(&amp;quot;Error rate: %.2f%%\n&amp;quot;, errorRate);

        // 验证误差在合理范围内
        assertTrue(errorRate &amp;lt; 2.0); // 放宽到2%以应对测试波动
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;8geo类型操作&quot;&gt;8、GEO类型操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.*;
import redis.clients.jedis.args.GeoUnit;
import redis.clients.jedis.params.GeoRadiusParam;
import redis.clients.jedis.resps.GeoRadiusResponse;

import java.util.List;

import static org.junit.Assert.*;

public class RedisGeoOperationsTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private JedisPooled jedis = new JedisPooled(HOST, PORT);
    private static final String GEO_KEY = &amp;quot;cities&amp;quot;;

    // 城市坐标常量
    private static final double BEIJING_LON = 116.405285;
    private static final double BEIJING_LAT = 39.904989;
    private static final double SHANGHAI_LON = 121.474490;
    private static final double SHANGHAI_LAT = 31.230416;
    private static final double GUANGZHOU_LON = 113.264385;
    private static final double GUANGZHOU_LAT = 23.129112;

    @Before
    public void setUp() {
        // 清空测试数据
        jedis.del(GEO_KEY);
    }

    @After
    public void tearDown() {
        // 清理测试数据
        jedis.del(GEO_KEY);
        System.out.println(&amp;quot;Test data cleaned up&amp;quot;);
    }

    @Test
    public void testGeoaddAndGeopos() {
        // 添加地理位置
        long addedBeijing = jedis.geoadd(GEO_KEY, BEIJING_LON, BEIJING_LAT, &amp;quot;Beijing&amp;quot;);
        assertEquals(1, addedBeijing);

        long addedShanghai = jedis.geoadd(GEO_KEY, SHANGHAI_LON, SHANGHAI_LAT, &amp;quot;Shanghai&amp;quot;);
        assertEquals(1, addedShanghai);

        System.out.println(&amp;quot;GEOADD operations successful&amp;quot;);

        // 获取地理位置
        List&amp;lt;GeoCoordinate&amp;gt; coordinates = jedis.geopos(GEO_KEY, &amp;quot;Beijing&amp;quot;, &amp;quot;Shanghai&amp;quot;, &amp;quot;NonExistCity&amp;quot;);
        assertNotNull(coordinates);
        assertEquals(3, coordinates.size());

        // 验证北京坐标
        GeoCoordinate beijingCoord = coordinates.get(0);
        assertNotNull(beijingCoord);
        assertEquals(BEIJING_LON, beijingCoord.getLongitude(), 0.00001);
        assertEquals(BEIJING_LAT, beijingCoord.getLatitude(), 0.00001);

        // 验证上海坐标
        GeoCoordinate shanghaiCoord = coordinates.get(1);
        assertNotNull(shanghaiCoord);
        assertEquals(SHANGHAI_LON, shanghaiCoord.getLongitude(), 0.00001);
        assertEquals(SHANGHAI_LAT, shanghaiCoord.getLatitude(), 0.00001);

        // 验证不存在的城市
        assertNull(coordinates.get(2));

        System.out.println(&amp;quot;GEOPOS operation successful&amp;quot;);
        System.out.println(&amp;quot;Beijing coordinates: &amp;quot; + beijingCoord);
        System.out.println(&amp;quot;Shanghai coordinates: &amp;quot; + shanghaiCoord);
    }

    @Test
    public void testGeodist() {
        // 准备测试数据
        jedis.geoadd(GEO_KEY, BEIJING_LON, BEIJING_LAT, &amp;quot;Beijing&amp;quot;);
        jedis.geoadd(GEO_KEY, SHANGHAI_LON, SHANGHAI_LAT, &amp;quot;Shanghai&amp;quot;);
        jedis.geoadd(GEO_KEY, GUANGZHOU_LON, GUANGZHOU_LAT, &amp;quot;Guangzhou&amp;quot;);

        // 计算距离(千米)
        Double distance = jedis.geodist(GEO_KEY, &amp;quot;Beijing&amp;quot;, &amp;quot;Shanghai&amp;quot;, GeoUnit.KM);
        assertNotNull(distance);
        assertTrue(distance &amp;gt; 1000 &amp;amp;&amp;amp; distance &amp;lt; 1200); // 北京到上海实际距离约1064公里
        System.out.println(&amp;quot;GEODIST operation successful - Distance: &amp;quot; + distance + &amp;quot; km&amp;quot;);

        // 计算距离(米)
        Double distanceMeters = jedis.geodist(GEO_KEY, &amp;quot;Beijing&amp;quot;, &amp;quot;Shanghai&amp;quot;, GeoUnit.M);
        assertEquals(distance * 1000, distanceMeters, 100); // 允许100米误差

        // 测试不存在的城市
        Double nonExistDist = jedis.geodist(GEO_KEY, &amp;quot;Beijing&amp;quot;, &amp;quot;NonExistCity&amp;quot;, GeoUnit.KM);
        assertNull(nonExistDist);
        System.out.println(&amp;quot;GEODIST with non-existent city returns null&amp;quot;);
    }

    @Test
    public void testGeoradius() {
        // 准备测试数据
        jedis.geoadd(GEO_KEY, BEIJING_LON, BEIJING_LAT, &amp;quot;Beijing&amp;quot;);
        jedis.geoadd(GEO_KEY, SHANGHAI_LON, SHANGHAI_LAT, &amp;quot;Shanghai&amp;quot;);
        jedis.geoadd(GEO_KEY, GUANGZHOU_LON, GUANGZHOU_LAT, &amp;quot;Guangzhou&amp;quot;);

        // 查找附近的位置(500公里范围内)
        List&amp;lt;GeoRadiusResponse&amp;gt; nearby = jedis.georadius(GEO_KEY, BEIJING_LON, BEIJING_LAT, 500, GeoUnit.KM);
        assertNotNull(nearby);
        assertEquals(1, nearby.size()); // 只有北京自己在500公里范围内
        assertEquals(&amp;quot;Beijing&amp;quot;, nearby.get(0).getMemberByString());
        System.out.println(&amp;quot;GEORADIUS operation successful (500km) - Nearby cities: &amp;quot; + nearby.size());

        // 查找附近的位置(1500公里范围内)
        List&amp;lt;GeoRadiusResponse&amp;gt; widerNearby = jedis.georadius(GEO_KEY, BEIJING_LON, BEIJING_LAT, 1500, GeoUnit.KM);
        assertNotNull(widerNearby);
        assertEquals(2, widerNearby.size()); // 北京和上海
        System.out.println(&amp;quot;GEORADIUS operation successful (1500km) - Nearby cities: &amp;quot; + widerNearby.size());

        // 测试带选项的GEORADIUS
        GeoRadiusParam param = new GeoRadiusParam()
                .withCoord()
                .withDist()
                .sortAscending()
                .count(2);

        List&amp;lt;GeoRadiusResponse&amp;gt; nearbyWithParams = jedis.georadius(
                GEO_KEY, BEIJING_LON, BEIJING_LAT, 2000, GeoUnit.KM, param);

        assertNotNull(nearbyWithParams);
        assertEquals(2, nearbyWithParams.size());
        assertTrue(nearbyWithParams.get(0).getDistance() &amp;lt;= nearbyWithParams.get(1).getDistance());
        System.out.println(&amp;quot;GEORADIUS with params successful - Results: &amp;quot; + nearbyWithParams);
    }

    @Test
    public void testGeoradiusByMember() {
        // 准备测试数据
        jedis.geoadd(GEO_KEY, BEIJING_LON, BEIJING_LAT, &amp;quot;Beijing&amp;quot;);
        jedis.geoadd(GEO_KEY, SHANGHAI_LON, SHANGHAI_LAT, &amp;quot;Shanghai&amp;quot;);
        jedis.geoadd(GEO_KEY, GUANGZHOU_LON, GUANGZHOU_LAT, &amp;quot;Guangzhou&amp;quot;);

        // 以北京为中心查找附近位置
        List&amp;lt;GeoRadiusResponse&amp;gt; nearby = jedis.georadiusByMember(
                GEO_KEY, &amp;quot;Beijing&amp;quot;, 1500, GeoUnit.KM);

        assertNotNull(nearby);
        assertEquals(2, nearby.size()); // 北京和上海
        System.out.println(&amp;quot;GEORADIUSBYMEMBER operation successful - Nearby cities: &amp;quot; + nearby.size());

        // 测试带选项的GEORADIUSBYMEMBER
        GeoRadiusParam param = new GeoRadiusParam()
                .withCoord()
                .withDist()
                .sortDescending();

        List&amp;lt;GeoRadiusResponse&amp;gt; nearbyWithParams = jedis.georadiusByMember(
                GEO_KEY, &amp;quot;Beijing&amp;quot;, 2000, GeoUnit.KM, param);

        assertNotNull(nearbyWithParams);
        assertEquals(3, nearbyWithParams.size()); // 北京、上海、广州
        assertTrue(nearbyWithParams.get(0).getDistance() &amp;gt;= nearbyWithParams.get(1).getDistance());
        System.out.println(&amp;quot;GEORADIUSBYMEMBER with params successful - Results: &amp;quot; + nearbyWithParams);
    }

    @Test
    public void testGeoHash() {
        // 准备测试数据
        jedis.geoadd(GEO_KEY, BEIJING_LON, BEIJING_LAT, &amp;quot;Beijing&amp;quot;);
        jedis.geoadd(GEO_KEY, SHANGHAI_LON, SHANGHAI_LAT, &amp;quot;Shanghai&amp;quot;);

        // 获取Geohash
        List&amp;lt;String&amp;gt; hashes = jedis.geohash(GEO_KEY, &amp;quot;Beijing&amp;quot;, &amp;quot;Shanghai&amp;quot;);
        assertNotNull(hashes);
        assertEquals(2, hashes.size());

        // 验证Geohash格式
        assertTrue(hashes.get(0).matches(&amp;quot;^[0-9a-z]+$&amp;quot;)); // 北京
        assertTrue(hashes.get(1).matches(&amp;quot;^[0-9a-z]+$&amp;quot;)); // 上海
        System.out.println(&amp;quot;GEOHASH operation successful&amp;quot;);
        System.out.println(&amp;quot;Beijing geohash: &amp;quot; + hashes.get(0));
        System.out.println(&amp;quot;Shanghai geohash: &amp;quot; + hashes.get(1));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;9消息队列操作&quot;&gt;9、消息队列操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;

public class RedisPubSubTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private Jedis publisherJedis;
    private Jedis subscriberJedis;
    private static final String CHANNEL = &amp;quot;testChannel&amp;quot;;
    private static final String MESSAGE = &amp;quot;testMessage&amp;quot;;
    
    @Before
    public void setUp() {
        // 创建独立的连接用于发布和订阅
        publisherJedis = new Jedis(HOST, PORT);
        subscriberJedis = new Jedis(HOST, PORT);
    }
    
    @After
    public void tearDown() {
        if (publisherJedis != null) {
            publisherJedis.close();
        }
        if (subscriberJedis != null) {
            subscriberJedis.close();
        }
        System.out.println(&amp;quot;Resources cleaned up&amp;quot;);
    }

    @Test
    public void testPubSub() throws InterruptedException {
        // 使用CountDownLatch等待消息接收
        CountDownLatch messageReceived = new CountDownLatch(1);
        
        // 创建订阅者
        JedisPubSub jedisPubSub = new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
                System.out.println(&amp;quot;Received message: &amp;quot; + message + &amp;quot; from channel: &amp;quot; + channel);
                assertEquals(CHANNEL, channel);
                assertEquals(MESSAGE, message);
                messageReceived.countDown();
            }
            
            @Override
            public void onSubscribe(String channel, int subscribedChannels) {
                System.out.println(&amp;quot;Subscribed to channel: &amp;quot; + channel);
            }
        };
        
        // 在单独线程中订阅
        new Thread(() -&amp;gt; {
            System.out.println(&amp;quot;Starting subscriber...&amp;quot;);
            subscriberJedis.subscribe(jedisPubSub, CHANNEL);
        }).start();
        
        // 等待订阅成功
        Thread.sleep(1000);
        
        // 发布消息
        long subscribers = publisherJedis.publish(CHANNEL, MESSAGE);
        assertTrue(subscribers &amp;gt; 0);
        System.out.println(&amp;quot;Published message to &amp;quot; + subscribers + &amp;quot; subscribers&amp;quot;);
        
        // 等待消息接收（最多等待5秒）
        boolean received = messageReceived.await(5, TimeUnit.SECONDS);
        assertTrue(&amp;quot;Message not received within timeout&amp;quot;, received);
        
        // 取消订阅
        jedisPubSub.unsubscribe();
    }

    @Test
    public void testPatternSubscribe() throws InterruptedException {
        // 使用CountDownLatch等待消息接收
        CountDownLatch messageReceived = new CountDownLatch(1);
        String patternChannel = &amp;quot;test.*&amp;quot;;
        String specificChannel = &amp;quot;test.pattern&amp;quot;;
        
        // 创建模式订阅者
        JedisPubSub jedisPubSub = new JedisPubSub() {
            @Override
            public void onPMessage(String pattern, String channel, String message) {
                System.out.println(&amp;quot;Received message: &amp;quot; + message + 
                                 &amp;quot; from channel: &amp;quot; + channel + 
                                 &amp;quot; matching pattern: &amp;quot; + pattern);
                assertEquals(patternChannel, pattern);
                assertEquals(specificChannel, channel);
                assertEquals(MESSAGE, message);
                messageReceived.countDown();
            }
            
            @Override
            public void onPSubscribe(String pattern, int subscribedChannels) {
                System.out.println(&amp;quot;Subscribed to pattern: &amp;quot; + pattern);
            }
        };
        
        // 在单独线程中订阅模式
        new Thread(() -&amp;gt; {
            System.out.println(&amp;quot;Starting pattern subscriber...&amp;quot;);
            subscriberJedis.psubscribe(jedisPubSub, patternChannel);
        }).start();
        
        // 等待订阅成功
        Thread.sleep(1000);
        
        // 发布消息到匹配模式的频道
        long subscribers = publisherJedis.publish(specificChannel, MESSAGE);
        assertTrue(subscribers &amp;gt; 0);
        System.out.println(&amp;quot;Published message to &amp;quot; + subscribers + &amp;quot; subscribers&amp;quot;);
        
        // 等待消息接收（最多等待5秒）
        boolean received = messageReceived.await(5, TimeUnit.SECONDS);
        assertTrue(&amp;quot;Message not received within timeout&amp;quot;, received);
        
        // 取消订阅
        jedisPubSub.punsubscribe();
    }

    @Test
    public void testMultipleChannels() throws InterruptedException {
        String channel1 = &amp;quot;channel1&amp;quot;;
        String channel2 = &amp;quot;channel2&amp;quot;;
        CountDownLatch messagesReceived = new CountDownLatch(2);
        
        // 创建订阅者
        JedisPubSub jedisPubSub = new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
                System.out.println(&amp;quot;Received message from channel: &amp;quot; + channel);
                messagesReceived.countDown();
            }
        };
        
        // 在单独线程中订阅多个频道
        new Thread(() -&amp;gt; {
            System.out.println(&amp;quot;Starting multi-channel subscriber...&amp;quot;);
            subscriberJedis.subscribe(jedisPubSub, channel1, channel2);
        }).start();
        
        // 等待订阅成功
        Thread.sleep(1000);
        
        // 发布消息到两个频道
        publisherJedis.publish(channel1, MESSAGE);
        publisherJedis.publish(channel2, MESSAGE);
        
        // 等待消息接收（最多等待5秒）
        boolean received = messagesReceived.await(5, TimeUnit.SECONDS);
        assertTrue(&amp;quot;Not all messages received within timeout&amp;quot;, received);
        
        // 取消订阅
        jedisPubSub.unsubscribe();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;10事务操作&quot;&gt;10、事务操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package cn.kdyzm.jedis.demo;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisDataException;

import java.util.List;

import static org.junit.Assert.*;

public class RedisTransactionTest {

    private static final String HOST = &amp;quot;192.168.203.130&amp;quot;;
    private static final int PORT = 6379;
    private Jedis jedis;
    private static final String KEY1 = &amp;quot;txKey1&amp;quot;;
    private static final String KEY2 = &amp;quot;txKey2&amp;quot;;
    private static final String COUNTER = &amp;quot;txCounter&amp;quot;;

    @Before
    public void setUp() {
        jedis = new Jedis(HOST, PORT);
        // 清空测试数据
        jedis.del(KEY1, KEY2, COUNTER);
    }

    @After
    public void tearDown() {
        if (jedis != null) {
            jedis.close();
        }
        System.out.println(&amp;quot;Resources cleaned up&amp;quot;);
    }

    @Test
    public void testSuccessfulTransaction() {
        // 开启事务
        Transaction tx = jedis.multi();
        System.out.println(&amp;quot;Transaction started&amp;quot;);

        // 执行命令
        tx.set(KEY1, &amp;quot;value1&amp;quot;);
        tx.set(KEY2, &amp;quot;value2&amp;quot;);
        tx.incr(COUNTER);
        System.out.println(&amp;quot;Commands added to transaction&amp;quot;);

        // 执行事务
        List&amp;lt;Object&amp;gt; results = tx.exec();
        assertNotNull(results);
        assertEquals(3, results.size());
        System.out.println(&amp;quot;Transaction executed successfully&amp;quot;);

        // 验证结果
        assertEquals(&amp;quot;OK&amp;quot;, results.get(0));
        assertEquals(&amp;quot;OK&amp;quot;, results.get(1));
        assertEquals(1L, results.get(2));

        // 验证数据
        assertEquals(&amp;quot;value1&amp;quot;, jedis.get(KEY1));
        assertEquals(&amp;quot;value2&amp;quot;, jedis.get(KEY2));
        assertEquals(&amp;quot;1&amp;quot;, jedis.get(COUNTER));
        System.out.println(&amp;quot;Data verification successful&amp;quot;);
    }

    @Test
    public void testDiscardedTransaction() {
        // 开启事务
        Transaction tx = jedis.multi();
        System.out.println(&amp;quot;Transaction started&amp;quot;);

        // 执行命令
        tx.set(KEY1, &amp;quot;value1&amp;quot;);
        tx.set(KEY2, &amp;quot;value2&amp;quot;);
        System.out.println(&amp;quot;Commands added to transaction&amp;quot;);

        // 放弃事务
        String discardResult = tx.discard();
        assertEquals(&amp;quot;OK&amp;quot;, discardResult);
        System.out.println(&amp;quot;Transaction discarded successfully&amp;quot;);

        // 验证数据未被修改
        assertNull(jedis.get(KEY1));
        assertNull(jedis.get(KEY2));
        System.out.println(&amp;quot;Data remains unchanged after discard&amp;quot;);
    }

    @Test(expected = IllegalStateException.class)
    public void testWatchAndConflict() {
        // 监视键
        jedis.watch(KEY1);
        System.out.println(&amp;quot;Watching key: &amp;quot; + KEY1);

        // 开启事务
        Transaction tx = jedis.multi();
        System.out.println(&amp;quot;Transaction started&amp;quot;);

        // 尝试修改被监视的键
        tx.set(KEY1, &amp;quot;transactionValue&amp;quot;);
        System.out.println(&amp;quot;Attempting to modify watched key in transaction&amp;quot;);

        // 在事务外修改被监视的键，此处应该抛出IllegalStateException异常
        jedis.set(KEY1, &amp;quot;externalChange&amp;quot;);
        System.out.println(&amp;quot;Key modified outside transaction&amp;quot;);

        // 执行事务（应该失败）
        tx.exec();
        System.out.println(&amp;quot;This line should not be reached&amp;quot;);
    }

    @Test
    public void testWatchAndNoConflict() {
        // 监视键
        jedis.watch(KEY1);
        System.out.println(&amp;quot;Watching key: &amp;quot; + KEY1);

        // 开启事务
        Transaction tx = jedis.multi();
        System.out.println(&amp;quot;Transaction started&amp;quot;);

        // 修改被监视的键
        tx.set(KEY1, &amp;quot;transactionValue&amp;quot;);
        System.out.println(&amp;quot;Modifying watched key in transaction&amp;quot;);

        // 执行事务（应该成功，因为没有冲突）
        List&amp;lt;Object&amp;gt; results = tx.exec();
        assertNotNull(results);
        assertEquals(1, results.size());
        assertEquals(&amp;quot;OK&amp;quot;, results.get(0));
        System.out.println(&amp;quot;Transaction executed successfully with no conflicts&amp;quot;);

        // 验证数据
        assertEquals(&amp;quot;transactionValue&amp;quot;, jedis.get(KEY1));
        System.out.println(&amp;quot;Data verification successful&amp;quot;);
    }

    @Test
    public void testTransactionWithError() {
        // 设置一个非数字值
        jedis.set(COUNTER, &amp;quot;notANumber&amp;quot;);

        // 开启事务
        Transaction tx = jedis.multi();
        System.out.println(&amp;quot;Transaction started&amp;quot;);

        // 执行命令（包含一个会失败的命令）
        tx.set(KEY1, &amp;quot;value1&amp;quot;);
        tx.incr(COUNTER); // 这会失败，但是会继续执行剩下的命令
        tx.set(KEY2, &amp;quot;value2&amp;quot;);
        System.out.println(&amp;quot;Commands added to transaction (one will fail)&amp;quot;);

        // 执行事务
        try {
            tx.exec();
        } catch (JedisDataException e) {
            System.out.println(&amp;quot;Transaction failed as expected: &amp;quot; + e.getMessage());
        }

        // 验证部分命令未执行
        assertNotNull(jedis.get(KEY1));
        assertNotNull(jedis.get(KEY2));
        assertEquals(&amp;quot;notANumber&amp;quot;, jedis.get(COUNTER));
        System.out.println(&amp;quot;No commands were executed due to transaction failure&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;四注意事项&quot;&gt;四、注意事项&lt;/h2&gt;
&lt;p&gt;jedis在实际开发中直接使用的可能性比较低，由于springboot官方有redis spring boot starter，我们一般习惯性的基于该组件操作redis。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
      <category>jedis</category>
    </item>
    <item>
      <title>DiskGenius磁盘扩容报错“$Bitmap中有标记为已使用的未用簇”解决方法</title>
      <link>https://blog.kdyzm.cn/post/324</link>
      <guid>https://blog.kdyzm.cn/post/324</guid>
      <pubDate>Mon, 28 Jul 2025 22:47:37 +0800</pubDate>
      <description>&lt;p&gt;使用DiskGenius进行磁盘扩容的时候报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$Bitmap中有标记为已使用的未用簇。簇号:1391898
$MFT位图中有标记为已使用的未用文件记录。文件:1340
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/28/cc05e570dd8f40f2abe7dcdc8dde18fd.jpg&quot; alt=&quot;399de7adf0d1c0ae4ebe17b81a4026a&quot; style=&quot;zoom: 50%;&quot; /&gt;
&lt;p&gt;解决方法就是在命令行运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chkdsk /f /x e:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;e:&lt;/code&gt; 需要根据提示换成自己的盘符。修复完成之后重新进行磁盘扩容，就能成功了。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>windows</category>
      <category>win10</category>
    </item>
    <item>
      <title>Linux命令系列：vim命令</title>
      <link>https://blog.kdyzm.cn/post/323</link>
      <guid>https://blog.kdyzm.cn/post/323</guid>
      <pubDate>Sat, 26 Jul 2025 19:55:18 +0800</pubDate>
      <description>&lt;h2 id=&quot;一vim命令的三种模式&quot;&gt;一、vim命令的三种模式&lt;/h2&gt;
&lt;p&gt;Vim中有三种模式：编辑模式、输入模式、末行模式。使用vim命令打开文件之后默认处于编辑模式下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编辑模式--&amp;gt;输入模式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;i：在当前光标所在字符的前面，转换成为输入模式&lt;/p&gt;
&lt;p&gt;a：在当前光标所在字符的后面，转换为输入模式&lt;/p&gt;
&lt;p&gt;o：在当前光标所在行的下方新建一行，并转换为输入模式&lt;/p&gt;
&lt;p&gt;I：在当前光标所在行的行首，转换为输入模式&lt;/p&gt;
&lt;p&gt;A：在当前光标所在行的行尾，转换为输入模式&lt;/p&gt;
&lt;p&gt;O：在当前光标所在行的上方新建一行，并转换成为输入模式&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入模式--&amp;gt;编辑模式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ESC&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编辑模式--&amp;gt;末行模式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;末行模式--&amp;gt;编辑模式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ESC,ESC&lt;/p&gt;
&lt;p&gt;输入模式不能直接转换到末行模式，必须先转换成为编辑模式再转换到末行模式。&lt;/p&gt;
&lt;h2 id=&quot;二文件的打开和关闭&quot;&gt;二、文件的打开和关闭&lt;/h2&gt;
&lt;h3 id=&quot;1打开文件&quot;&gt;1、打开文件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;vim +# FILENAME&lt;/strong&gt;：打开文件，并定位到文件的第#行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;vim + FILENAME&lt;/strong&gt;：打开文件，并定位到最后一行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;vim +/PATTERN&lt;/strong&gt;    ：打开文件，并定位到第一次被PATTERN匹配到的行的行首&lt;/p&gt;
&lt;p&gt;打开文件之后默认处于编辑模式&lt;/p&gt;
&lt;h3 id=&quot;2关闭文件&quot;&gt;2、关闭文件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;末行模式下关闭文件：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;输入&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:q&lt;/td&gt;
&lt;td&gt;退出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:q!&lt;/td&gt;
&lt;td&gt;不保存并退出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:wq或者:x&lt;/td&gt;
&lt;td&gt;保存并退出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:w&lt;/td&gt;
&lt;td&gt;保存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:w!&lt;/td&gt;
&lt;td&gt;强行保存&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;编辑模式下退出：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ZZ：保存并退出&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;强行退出：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ctrl+C，使用这种方式的结果就是会产生.swp文件，这种文件会保存未保存的内容以便于下一次编辑文件的时候进行恢复，所以比较麻烦，强烈不建议直接Ctrl+C退出，按照正常流程退出的话就不会产生这种文件了。&lt;/p&gt;
&lt;h2 id=&quot;三编辑模式下的操作&quot;&gt;三、编辑模式下的操作&lt;/h2&gt;
&lt;p&gt;使用vim大多数情况下都是在编辑模式下操作，编辑模式是vim中使用率最高的模式。&lt;/p&gt;
&lt;h3 id=&quot;1光标移动&quot;&gt;1、光标移动&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;逐字符移动光标：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;h&lt;/td&gt;
&lt;td&gt;向左移动一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;j&lt;/td&gt;
&lt;td&gt;向下移动一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k&lt;/td&gt;
&lt;td&gt;向上移动一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;l&lt;/td&gt;
&lt;td&gt;向右移动一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#h | j | k | l&lt;/td&gt;
&lt;td&gt;向 左|下|上|右 移动一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;逐单词移动光标：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;w&lt;/td&gt;
&lt;td&gt;跳转到下一个单词的首部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;跳转到上一个单词的首部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e&lt;/td&gt;
&lt;td&gt;跳转到下一个单词的尾部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#w&lt;/td&gt;
&lt;td&gt;跳转到下#个单词的首部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#b&lt;/td&gt;
&lt;td&gt;跳转到上#个单词的首部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#e&lt;/td&gt;
&lt;td&gt;跳转到下#个单词的尾部&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;行内跳转：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;跳转到绝对行首&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$&lt;/td&gt;
&lt;td&gt;跳转到绝对行尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;^&lt;/td&gt;
&lt;td&gt;跳转到行首第一个非空白字符处&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;行间跳转：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gg&lt;/td&gt;
&lt;td&gt;跳转到第一行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G | :$&lt;/td&gt;
&lt;td&gt;跳转到最后一行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#G | :#&lt;/td&gt;
&lt;td&gt;跳转到第#行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;翻屏跳转：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+f&lt;/td&gt;
&lt;td&gt;向下翻一屏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+b&lt;/td&gt;
&lt;td&gt;向上翻一屏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+d&lt;/td&gt;
&lt;td&gt;向下翻半屏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+u&lt;/td&gt;
&lt;td&gt;向上翻半屏&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;2删除操作&quot;&gt;2、删除操作&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;删除字符：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;x&lt;/td&gt;
&lt;td&gt;删除光标所在处的字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#x&lt;/td&gt;
&lt;td&gt;删除光标所在处及向后的共#个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;删除单词：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;[#]dw&lt;/td&gt;
&lt;td&gt;删除光标所在处的#个单词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[#]db&lt;/td&gt;
&lt;td&gt;删除光标所在处之前的#个单词&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[#]de&lt;/td&gt;
&lt;td&gt;删除光标所在处之后的#个单词，注意和dw之间的区别&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;删除行：&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dd&lt;/td&gt;
&lt;td&gt;删除光标所在处的行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#dd&lt;/td&gt;
&lt;td&gt;删除当前光标所在行以及向下共#行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;3复制粘贴&quot;&gt;3、复制粘贴&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;复制：&lt;/strong&gt; 使用y命令，使用方法和d命令完全相同，比如&lt;code&gt;yw&lt;/code&gt;表示复制光标所在处的单词,&lt;code&gt;yy&lt;/code&gt;复制当前行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粘贴：&lt;/strong&gt; 使用d命令删除的内容和使用y命令复制的内容都保存到了系统粘贴板上，使用p或者P命令能够粘贴到指定的位置上。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;p命令：如果复制或删除的是非整行则粘贴至当前光标所在字符的后面，如果复制或删除的是整行，则会粘贴到当前光标所在行的下方。&lt;/li&gt;
&lt;li&gt;P命令：如果复制或删除的是非整行则粘贴至当前光标所在字符的前面，如果复制或删除的是整行，则会粘贴到当前光标所在行的上方。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;4撤销和反撤销&quot;&gt;4、撤销和反撤销&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u&lt;/td&gt;
&lt;td&gt;撤销上一次操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#u&lt;/td&gt;
&lt;td&gt;撤销最近#次操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ctrl+r&lt;/td&gt;
&lt;td&gt;反撤销最近一次操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;5可视化&quot;&gt;5、可视化&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;v&lt;/td&gt;
&lt;td&gt;按照字符选取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;按照行选取&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;6查找&quot;&gt;6、查找&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;向下查找&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;向上查找&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;7替换&quot;&gt;7、替换&lt;/h3&gt;
&lt;p&gt;替换的命令语法和sed命令语法完全相同&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ADDR1,ADDR2 s/PATTERN/string/gi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​    可以使用1,$指定全文范围，但是可以使用%代替之。&lt;/p&gt;
&lt;h2 id=&quot;四末行模式下的操作&quot;&gt;四、末行模式下的操作&lt;/h2&gt;
&lt;h3 id=&quot;1显示或者取消显示行号&quot;&gt;1.显示或者取消显示行号&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:set number | set nu&lt;/td&gt;
&lt;td&gt;显示行号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:set nonumber | set nonu&lt;/td&gt;
&lt;td&gt;不显示行号&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;2显示忽略或者区分字符大小写&quot;&gt;2.显示忽略或者区分字符大小写&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:set ignorecase | set ic&lt;/td&gt;
&lt;td&gt;忽略大小写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:set noignorecase | set noic&lt;/td&gt;
&lt;td&gt;区分大小写&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;3设定自动缩进&quot;&gt;3.设定自动缩进&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:set autoindent | set ai&lt;/td&gt;
&lt;td&gt;设置自动缩进&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:set noautoindent | set noai&lt;/td&gt;
&lt;td&gt;取消自动缩进&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;4查找到的文本高亮显示或者取消&quot;&gt;4.查找到的文本高亮显示或者取消&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:set hlsearch&lt;/td&gt;
&lt;td&gt;查找到的文本高亮显示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:set nohlsearch&lt;/td&gt;
&lt;td&gt;取消查找到的文本高亮显示&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;5语法高亮&quot;&gt;5.语法高亮&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:syntax on&lt;/td&gt;
&lt;td&gt;语法高亮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:syntax off&lt;/td&gt;
&lt;td&gt;取消语法高亮&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;五配置文件&quot;&gt;五、配置文件&lt;/h2&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/etc/vimrc&lt;/td&gt;
&lt;td&gt;全局配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~/.vimrc&lt;/td&gt;
&lt;td&gt;个人配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
      <category>linux</category>
      <category>linux命令</category>
    </item>
    <item>
      <title>Linux命令系列：man命令</title>
      <link>https://blog.kdyzm.cn/post/322</link>
      <guid>https://blog.kdyzm.cn/post/322</guid>
      <pubDate>Fri, 25 Jul 2025 16:05:10 +0800</pubDate>
      <description>&lt;p&gt;介绍man命令之前，先运行如下命令以保证文档包可用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;yum install man-pages shadow-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;man&lt;/code&gt; 命令是 Linux/Unix 系统中用于查看命令、函数或配置文件手册页（manual pages）的核心工具。其基本语法格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;man [选项] [章节号] &amp;lt;命令名/函数名/配置文件&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;man命令将输入分为了九类，被称为九个章节：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;1   可执行程序或 shell 命令
2   系统调用(内核提供的函数)
3   库调用(程序库中的函数)
4   特殊文件(通常位于 /dev)
5   文件格式和规范，如 /etc/passwd
6   游戏
7   杂项(包括宏包和规范，如 man(7)，groff(7))
8   系统管理命令(通常只针对 root 用户)
9   内核例程 [非标准
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入的命令可能在这九个章节中的某几个章节中有内容，比如&lt;code&gt;passwd&lt;/code&gt;命令，使用&lt;code&gt;man -wa passwd&lt;/code&gt;可以看到它属于哪几个章节，即哪几个章节中会有内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@localhost ~]# man -wa passwd
/usr/share/man/man1/passwd.1.gz
/usr/share/man/man1/sslpasswd.1ssl.gz
/usr/share/man/man5/passwd.5.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到passwd命令在章节1和章节5有内容，则可以使用&lt;code&gt;man 1 passwd&lt;/code&gt;或者&lt;code&gt;man 5 passwd&lt;/code&gt;命令查看相关文档。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/25/f9cb9c73aeae445799d4107995e8fea9.png&quot; alt=&quot;image-20250725155113018&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;常见选项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt;：显示与指定关键字相关的手册页面。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-k&lt;/code&gt;：搜索手册页中与关键字匹配的条目。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-a&lt;/code&gt;：显示所有匹配的手册页面。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-w&lt;/code&gt;：仅显示手册页的位置，而不显示其内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如我们想查看匹配的手册页面，可以使用&lt;code&gt;-wa&lt;/code&gt;选项，正如之前说的查看passwd的手册页面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@localhost ~]# man -wa passwd
/usr/share/man/man1/passwd.1.gz
/usr/share/man/man1/sslpasswd.1ssl.gz
/usr/share/man/man5/passwd.5.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;交互快捷键&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;翻页&lt;/strong&gt;：&lt;code&gt;空格 / d&lt;/code&gt;（下一页）、&lt;code&gt;b / u&lt;/code&gt;（上一页）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐行&lt;/strong&gt;：&lt;code&gt;j / 回车&lt;/code&gt;（下一行）、&lt;code&gt;k&lt;/code&gt;（上一行）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索&lt;/strong&gt;：&lt;code&gt;/keyword&lt;/code&gt;（向前搜索）、&lt;code&gt;?keyword&lt;/code&gt;（向后搜索）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转&lt;/strong&gt;：&lt;code&gt;g&lt;/code&gt;（开头）、&lt;code&gt;G&lt;/code&gt;（末尾）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;退出&lt;/strong&gt;：&lt;code&gt;q&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <category>linux</category>
      <category>linux命令</category>
    </item>
    <item>
      <title>Redis（十）：使用redis-cli工具操作redis集群</title>
      <link>https://blog.kdyzm.cn/post/321</link>
      <guid>https://blog.kdyzm.cn/post/321</guid>
      <pubDate>Thu, 24 Jul 2025 23:28:22 +0800</pubDate>
      <description>&lt;p&gt;上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/320&quot;&gt;Redis（九）：多机部署之Cluster（集群）模式&lt;/a&gt;》中，已经介绍过Redis集群的搭建以及在redis-cli中使用命令操作redis集群。实际上可以不用进入redis-cli，而是直接通过redis-cli程序使用--cluster功能项直接操作集群，其底层实际上也是使用了redis命令。那为什么还要使用redis-cli --cluster功能项呢？因为其更简单，功能更强大。在上一篇文章介绍&lt;code&gt;cluster setslot&lt;/code&gt;命令时，使用redis cluster命令需要经过四步才能将槽成功迁移；使用--cluster功能项，则使用一条语句就能实现类似的槽迁移功能。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster reshard 127.0.0.1:30002 --cluster-from 9d148120512f85f29dd246e515243fd6019c7dcc --cluster-to d85dc362370f81fe627305e9b246365bd51a02c2 --cluster-slots 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显，使用redis-cli的--cluster子命令可以大幅度提高集群的操作效率。&lt;/p&gt;
&lt;h2 id=&quot;一redis-cli简介&quot;&gt;一、redis-cli简介&lt;/h2&gt;
&lt;p&gt;一般我们使用redis-cli的目的是进入某个服务器节点交互式执行一些命令，比如我们可以使用&lt;code&gt;./redis-cli -h 127.0.0.1 -p 6379&lt;/code&gt;命令进入6379服务的交互控制台。除了进入控制台，redis-cli还可以不进入交互控制台，直接执行一些命令，其支持的命令可以使用&lt;code&gt;./redis-cli --help&lt;/code&gt;查看。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;分类&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;选项&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;描述&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;默认值/示例&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;连接相关&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-h &amp;lt;hostname&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;指定 Redis 服务器主机名&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-p &amp;lt;port&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;指定 Redis 服务器端口&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;6379&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-s &amp;lt;socket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;使用 Unix 套接字连接（覆盖主机名和端口）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-a &amp;lt;password&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;连接服务器的密码（也可用 &lt;code&gt;REDISCLI_AUTH&lt;/code&gt; 环境变量）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--user &amp;lt;username&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;使用 ACL 风格认证（需配合 &lt;code&gt;-a&lt;/code&gt;）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--askpass&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;强制从 STDIN 输入密码（忽略 &lt;code&gt;-a&lt;/code&gt; 和环境变量）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-u &amp;lt;uri&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;使用 URI 格式连接（如 &lt;code&gt;redis://user:pass@host:port&lt;/code&gt;）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-n &amp;lt;db&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;选择数据库编号&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;命令执行&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-r &amp;lt;repeat&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;重复执行命令 N 次&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;redis-cli -r 100 INCR counter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-i &amp;lt;interval&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;配合 &lt;code&gt;-r&lt;/code&gt; 设置命令间隔时间（支持小数秒）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-i 0.1&lt;/code&gt;（0.1秒间隔）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-x&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;从 STDIN 读取最后一个参数&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;`cat file.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-e&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;命令失败时返回错误退出码&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--eval &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;执行 Lua 脚本文件&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--eval script.lua key1 key2 , arg1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--ldb&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;启用 Lua 调试器（异步模式）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;输出格式&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--raw&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;原始格式输出（非终端时默认）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--no-raw&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;强制格式化输出（即使是非终端）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--csv&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;CSV 格式输出&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-d &amp;lt;delimiter&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;设置原始格式的批量响应分隔符&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;默认 &lt;code&gt;\n&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--show-pushes &amp;lt;yn&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;控制是否显示 RESP3 PUSH 消息&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;默认根据终端类型决定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;监控与诊断&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--stat&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;显示服务器滚动统计（内存、客户端等）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--latency&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;实时延迟监控模式&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--latency-history&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;延迟历史记录（默认15秒间隔）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-i 5&lt;/code&gt; 修改间隔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--bigkeys&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;查找包含大量元素的键&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--hotkeys&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;查找热点键（需 LFU 策略）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--scan --pattern &amp;lt;pat&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;扫描匹配模式的键&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;默认 &lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;特殊模式&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;-c&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;集群模式（跟随重定向）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--pipe&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;将原始协议从 STDIN 传输到服务器&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;`cat data.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--replica&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;模拟副本显示主服务器命令&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;数据操作&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--rdb &amp;lt;filename&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;将远程服务器的 RDB 转储保存到本地文件&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--rdb dump.rdb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;集群管理&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--cluster &amp;lt;command&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;执行集群管理命令（如 &lt;code&gt;--cluster help&lt;/code&gt;）&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;其他&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--help&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;显示帮助信息&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;--version&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;显示版本信息&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;比如我们使用命令&lt;code&gt;./redis-cli -p 6379&lt;/code&gt;进入控制台之后使用get命令获取键值，如果键值中有中文，则可能会出现中文乱码，解决方法就是使用使用&lt;code&gt;--raw&lt;/code&gt;选项，使用&lt;code&gt;./redis-cli -p 6379 --raw&lt;/code&gt;重新进入控制台就可以解决问题了。&lt;/p&gt;
&lt;p&gt;接下来讲解redis-cli集群管理相关的功能。&lt;/p&gt;
&lt;h2 id=&quot;二redis-cli集群管理工具&quot;&gt;二、redis-cli集群管理工具&lt;/h2&gt;
&lt;p&gt;redis-cli客户端附带的集群管理程序使用&lt;code&gt;--cluster&lt;/code&gt;选项开启。先使用help命令查看其有多少子命令可用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@localhost demo_redis_6.2.1]# ./redis-cli --cluster help
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN
                 --cluster-replicas &amp;lt;arg&amp;gt;
  check          host:port
                 --cluster-search-multiple-owners
  info           host:port
  fix            host:port
                 --cluster-search-multiple-owners
                 --cluster-fix-with-unreachable-masters
  reshard        host:port
                 --cluster-from &amp;lt;arg&amp;gt;
                 --cluster-to &amp;lt;arg&amp;gt;
                 --cluster-slots &amp;lt;arg&amp;gt;
                 --cluster-yes
                 --cluster-timeout &amp;lt;arg&amp;gt;
                 --cluster-pipeline &amp;lt;arg&amp;gt;
                 --cluster-replace
  rebalance      host:port
                 --cluster-weight &amp;lt;node1=w1...nodeN=wN&amp;gt;
                 --cluster-use-empty-masters
                 --cluster-timeout &amp;lt;arg&amp;gt;
                 --cluster-simulate
                 --cluster-pipeline &amp;lt;arg&amp;gt;
                 --cluster-threshold &amp;lt;arg&amp;gt;
                 --cluster-replace
  add-node       new_host:new_port existing_host:existing_port
                 --cluster-slave
                 --cluster-master-id &amp;lt;arg&amp;gt;
  del-node       host:port node_id
  call           host:port command arg arg .. arg
                 --cluster-only-masters
                 --cluster-only-replicas
  set-timeout    host:port milliseconds
  import         host:port
                 --cluster-from &amp;lt;arg&amp;gt;
                 --cluster-from-user &amp;lt;arg&amp;gt;
                 --cluster-from-pass &amp;lt;arg&amp;gt;
                 --cluster-from-askpass
                 --cluster-copy
                 --cluster-replace
  backup         host:port backup_directory
  help           

For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

Cluster Manager Options:
  --cluster-yes  Automatic yes to cluster commands prompts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到有很多子命令，接下来看看每个子命令的用法。&lt;/p&gt;
&lt;h3 id=&quot;create创建集群&quot;&gt;create：创建集群&lt;/h3&gt;
&lt;p&gt;create子命令允许用户根据已有的节点创建出一个集群。用户只需要在命令中依次给出各个节点的IP地址和端口号，命令就会将它们聚合到同一个集群中，并根据节点的数量将槽平均地指派给它们负责，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster create host1:port1 ... hostN:portN --cluster-replicas &amp;lt;arg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--cluster-replicas&lt;/code&gt; 选项可选，用于指定每个master节点有多少个副本。&lt;/p&gt;
&lt;p&gt;比如我们之前创建了包含5个主节点，5个父节点的集群：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster create \  #创建集群命令
127.0.0.1:30001 127.0.0.1:30002 127.0.0.1:30003 127.0.0.1:30004 127.0.0.1:30005 127.0.0.1:30006 127.0.0.1:30007 127.0.0.1:30008 127.0.0.1:30009 127.0.0.1:30010 \ #指定10个节点的ip和端口号
--cluster-replicas 1  #每个主节点1个副本
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;check检查集群&quot;&gt;check：检查集群&lt;/h3&gt;
&lt;p&gt;通过cluster选项的check子命令，用户可以检查集群的配置是否正确，以及全部16384个槽是否已经全部指派给了主节点。该命令接受集群中任意一个节点的ip和端口号作为参数，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster check host:port --cluster-search-multiple-owners
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--cluster-search-multiple-owners&lt;/code&gt;选项可选，用于着重验证每个哈希槽是否只被一个节点拥有。运行示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster check 127.0.0.1:30001 --cluster-search-multiple-owners
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/073eb87d2206406d8eefcf095bf46e6c.png&quot; alt=&quot;image-20250724094215030&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;info查看集群信息&quot;&gt;info：查看集群信息&lt;/h3&gt;
&lt;p&gt;用户可以通过cluster选项的info子命令查看集群的相关信息。完整命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster info host:port
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;host:port是集群中任意节点的ip和端口号。&lt;/p&gt;
&lt;p&gt;其输出包括：有多少个主节点，每个主节点有几个副本节点，分配了多少槽以及节点中有多少key。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster info 127.0.0.1:30001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/4a18aff5c1a74cb28f3ed3eb2a9c6103.png&quot; alt=&quot;image-20250724094953065&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;fix修复集群状态错误&quot;&gt;fix：修复集群状态错误&lt;/h3&gt;
&lt;p&gt;当集群在重分片、负载均衡或者槽迁移的过程中出现错误时或者集群状态错误时，执行cluster选项的fix子命令，可以让操作涉及的槽重新回到正常状态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster fix host:port #集群中任意节点的地址和端口号
--cluster-search-multiple-owners #检查并修复哈希槽（hash slot）被多个节点同时声明的冲突问题
--cluster-fix-with-unreachable-masters #即使某些主节点不可达，仍然尝试修复集群状态。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用该命令前应当先试用&lt;code&gt;check&lt;/code&gt;子命令检查集群问题，确认问题后再运行该命令修复。如果集群没问题，运行该命令不会做任何操作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster fix 127.0.0.1:30001 --cluster-search-multiple-owners --cluster-fix-with-unreachable-masters
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/0001868125f547f283b94c150d011281.png&quot; alt=&quot;image-20250724100203249&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到运行fix命令后，内部应该先调用了check命令检查了集群状态，如果没发现错误则不会进行修复操作。&lt;/p&gt;
&lt;h3 id=&quot;reshard重分片&quot;&gt;reshard：重分片&lt;/h3&gt;
&lt;p&gt;通过cluster选项的reshard子命令，用户可以将指定数量的槽从原节点迁移至目标节点，被迁移的槽将交由后者负责，并且槽中已有的数据也会陆续从原节点转移至目标节点，该命令常用于集群扩容/缩容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster reshard host:port  #集群中任意一个节点的地址和端口
 --cluster-from &amp;lt;node-id&amp;gt;			#源节点     	
 --cluster-to &amp;lt;node-id&amp;gt;				#目标节点
 --cluster-slots &amp;lt;num-slots&amp;gt;			#要迁移的槽数量
 --cluster-yes					#跳过确认提示
 --cluster-timeout &amp;lt;milliseconds&amp;gt;		#超时时间（默认 60000ms）
 --cluster-pipeline &amp;lt;num&amp;gt;		#批量迁移 key 的数量（默认 10）
 --cluster-replace				#强制替换目标节点上的现有数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令的作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;重新分配哈希槽&lt;/strong&gt;（reshard），调整数据分布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持手动指定源节点和目标节点&lt;/strong&gt;，或让 Redis 自动选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批量迁移 key&lt;/strong&gt;，提高迁移效率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用于集群扩容或缩容&lt;/strong&gt;（如新增节点后需要均衡数据）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;该命令有两种模式：&lt;strong&gt;手动交互模式&lt;/strong&gt;以及&lt;strong&gt;全自动模式&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 id=&quot;手动交互模式&quot;&gt;手动交互模式&lt;/h4&gt;
&lt;p&gt;手动交互模式只需要执行&lt;code&gt;./redis-cli --cluster reshard host:port&lt;/code&gt;就会进入交互模式，根据提示输入信息，比如待迁移的节点id、槽数量等最后提交即可完成迁移。比如现在30001节点下有0到3276槽，我们现在将30001节点下的一个槽迁移到30011：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@localhost demo_redis_6.2.1]# ./redis-cli --cluster reshard 127.0.0.1:30001
&amp;gt;&amp;gt;&amp;gt; Performing Cluster Check (using node 127.0.0.1:30001)
M: d85dc362370f81fe627305e9b246365bd51a02c2 127.0.0.1:30001
   slots:[0-3276] (3277 slots) master
   1 additional replica(s)
S: d5f1adb8939529abcfe24e657bd01787b7d7928c 127.0.0.1:30003
   slots: (0 slots) slave
   replicates 32661eb91b3be79b9a9d9ba873e1066bd64916a7
M: 32661eb91b3be79b9a9d9ba873e1066bd64916a7 127.0.0.1:30010
   slots:[6554-9829] (3276 slots) master
   1 additional replica(s)
M: adbbff5339b083b1e4fb641951e6fb9085ffb25c 127.0.0.1:30005
   slots:[13107-16383] (3277 slots) master
   1 additional replica(s)
S: 1728a0fb708c776303f25e33c0add3297332cd64 127.0.0.1:30007
   slots: (0 slots) slave
   replicates 6d2c221d2ff15ae7fb9d5a5b7551ae5b493e791f
S: 0db0c21f6f9b075b887c0ef1548cf35cb0505b0b 127.0.0.1:30009
   slots: (0 slots) slave
   replicates adbbff5339b083b1e4fb641951e6fb9085ffb25c
S: c33e236a553d9e658a0bb549d933567de5a756f8 127.0.0.1:30006
   slots: (0 slots) slave
   replicates 9d148120512f85f29dd246e515243fd6019c7dcc
M: 9d148120512f85f29dd246e515243fd6019c7dcc 127.0.0.1:30002
   slots:[3277-6553] (3277 slots) master
   1 additional replica(s)
S: dd7079d8486119fecc4e71f10fb705889ea8668b 127.0.0.1:30008
   slots: (0 slots) slave
   replicates d85dc362370f81fe627305e9b246365bd51a02c2
M: 6d2c221d2ff15ae7fb9d5a5b7551ae5b493e791f 127.0.0.1:30004
   slots:[9830-13106] (3277 slots) master
   1 additional replica(s)
M: 79a28e2df5e13dd726c892c42ee35a15423b00fb 127.0.0.1:30011
   slots: (0 slots) master
[OK] All nodes agree about slots configuration.
&amp;gt;&amp;gt;&amp;gt; Check for open slots...
&amp;gt;&amp;gt;&amp;gt; Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 1
What is the receiving node ID? 79a28e2df5e13dd726c892c42ee35a15423b00fb
Please enter all the source node IDs.
  Type &apos;all&apos; to use all the nodes as source nodes for the hash slots.
  Type &apos;done&apos; once you entered all the source nodes IDs.
Source node #1: d85dc362370f81fe627305e9b246365bd51a02c2
Source node #2: done

Ready to move 1 slots.
  Source nodes:
    M: d85dc362370f81fe627305e9b246365bd51a02c2 127.0.0.1:30001
       slots:[0-3276] (3277 slots) master
       1 additional replica(s)
  Destination node:
    M: 79a28e2df5e13dd726c892c42ee35a15423b00fb 127.0.0.1:30011
       slots: (0 slots) master
  Resharding plan:
    Moving slot 0 from d85dc362370f81fe627305e9b246365bd51a02c2
Do you want to proceed with the proposed reshard plan (yes/no)? yes
Moving slot 0 from 127.0.0.1:30001 to 127.0.0.1:30011: 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;迁移完成之后，运行命令&lt;code&gt;./redis-cli --cluster info 127.0.0.1:30001 &lt;/code&gt; 查看集群槽分布情况：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/caef985f6b6344febdf1e447b7c99679.png&quot; alt=&quot;image-20250724102933828&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到30011节点上多了一个槽。&lt;/p&gt;
&lt;h4 id=&quot;全自动模式&quot;&gt;全自动模式&lt;/h4&gt;
&lt;p&gt;全自动模式就是将交互模式下需要输入的参数全放到了命令中，接下来使用全自动模式将30011节点上的1个槽迁移回30001：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster reshard 127.0.0.1:30001  --cluster-from 79a28e2df5e13dd726c892c42ee35a15423b00fb --cluster-to d85dc362370f81fe627305e9b246365bd51a02c2  --cluster-slots 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/c968402d37e04abeae1bc2706dc534ea.png&quot; alt=&quot;image-20250724104116820&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;看似失败了，实际上已经成功了，可以运行&lt;code&gt;./redis-cli --cluster info 127.0.0.1:30001&lt;/code&gt;命令检查：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/1be95d65bafc441ab15ff60bdd195e24.png&quot; alt=&quot;image-20250724104322780&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到30001节点的槽已经恢复，而且还&lt;strong&gt;多了一个从节点&lt;/strong&gt;，同时，30011节点&lt;strong&gt;不见了&lt;/strong&gt;。。可以猜想到发生了什么事情了：30011节点变成了30001节点的从节点了，更详细的信息可以通过&lt;code&gt;./redis-cli --cluster check 127.0.0.1:30001&lt;/code&gt;命令查看。&lt;/p&gt;
&lt;p&gt;为什么会发生这种事情呢？我们只是迁移了一个槽，为什会发生主节点降级成从节点的事情？&lt;/p&gt;
&lt;p&gt;Redis集群有个特殊的机制，如果一个 &lt;strong&gt;主节点失去了所有槽位&lt;/strong&gt;，Redis Cluster &lt;strong&gt;可能会自动将其降级为从节点&lt;/strong&gt;，在 Redis 6.2+ 中，如果一个主节点不再管理任何槽位，它可能会被 &lt;strong&gt;自动转换为从节点&lt;/strong&gt;，以避免集群中出现无用的主节点。&lt;/p&gt;
&lt;p&gt;我们的30001节点上只有一个槽，这最后一个槽迁移到30001节点上以后，它就失去了所有槽，然后就被自动降级成了从节点。&lt;/p&gt;
&lt;h3 id=&quot;rebalance负载均衡&quot;&gt;rebalance：负载均衡&lt;/h3&gt;
&lt;p&gt;该命令用于&lt;strong&gt;重新平衡Redis 集群的哈希槽（slots）分布&lt;/strong&gt;，确保各个主节点的槽位数量尽可能均匀：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster relalance host:port #集群中的任意一个节点地址
-cluster-weight &amp;lt;node1=w1...nodeN=wN&amp;gt; #指定节点的权重（影响槽位分配比例），node1=2 node2=1 表示 node1 分配 2 倍于 node2 的槽位
 --cluster-use-empty-masters 		#允许对没有槽位的主节点进行重新分配（默认会忽略它们）
 --cluster-timeout &amp;lt;arg&amp;gt;			#设置操作超时时间（单位：毫秒，默认 60000）
 --cluster-simulate					#模拟 rebalance，只显示计划，不实际执行
 --cluster-pipeline &amp;lt;arg&amp;gt;			#设置每次迁移的 key 数量（默认 10）
 --cluster-threshold &amp;lt;arg&amp;gt;			#设置触发 rebalance 的最小槽位差异（默认 2）。例如：如果节点间槽位数差异 ≤ 2，则不会触发 rebalance
 --cluster-replace					#强制替换故障节点（如果某些节点不可用）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我们的30011节点是新增的节点，就可以使用该命令将槽位重新均衡：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster rebalance 127.0.0.1:30001 --cluster-use-empty-masters
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/4d87bd733c134bcd8bb0616cb8b99b33.png&quot; alt=&quot;image-20250724130502411&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，五个主节点都分别把自己的一部分槽分给了新增加的30011节点，使用&lt;code&gt;./redis-cli --cluster info 127.0.0.1:30001&lt;/code&gt;命令查看集群节点信息：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/84cd00c1f05547bcb100f436963d7667.png&quot; alt=&quot;image-20250724130634051&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到集群中全部槽已经平均分配到了六个主节点中。&lt;/p&gt;
&lt;h3 id=&quot;delnode删除节点&quot;&gt;delnode：删除节点&lt;/h3&gt;
&lt;p&gt;当用户不再需要集群中的某个节点时，可以通过cluster选项的del-node子命令来移除该节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster del-node host:port node_id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先看看集群中的节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster info 127.0.0.1:30001
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/e6603e0d0858457f92431aa13e9e4ba2.png&quot; alt=&quot;image-20250724224317100&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;集群中现在有6个节点，我想要把30011节点移除掉，运行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster del-node 127.0.0.1:30001 79a28e2df5e13dd726c892c42ee35a15423b00fb
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/3314ef3a5aec4bddb5e61473c8413181.png&quot; alt=&quot;image-20250724224459564&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;移除节点失败了，因为&lt;strong&gt;redis集群不允许移除有数据的节点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;先使用reshard命令将全部的槽转移出去，再尝试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster reshard 127.0.0.1:30001  #使用交互式方式进行槽迁移，从30011转移全部的槽到30001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转移完成之后，重新查看集群信息：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/af54b4c1f3464b35a6205e83f70ac1bd.png&quot; alt=&quot;image-20250724224907895&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到30011已经没有槽了，此时再删除30011节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;[root@localhost demo_redis_6.2.1]# ./redis-cli --cluster del-node 127.0.0.1:30001 79a28e2df5e13dd726c892c42ee35a15423b00fb
&amp;gt;&amp;gt;&amp;gt; Removing node 79a28e2df5e13dd726c892c42ee35a15423b00fb from cluster 127.0.0.1:30001
&amp;gt;&amp;gt;&amp;gt; Sending CLUSTER FORGET messages to the cluster...
&amp;gt;&amp;gt;&amp;gt; Sending CLUSTER RESET SOFT to the deleted node.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从日志中可以看到，实际上底层还是使用了cluster的内部集群命令&lt;code&gt;cluster forget&lt;/code&gt;以及&lt;code&gt;cluster reset&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;删除完成之后在查看集群信息：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/1db61bc6d7ae4f3b863e7bd6ff080a97.png&quot; alt=&quot;image-20250724225102226&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;六个节点只剩了五个，30011节点已经被成功移除。但是槽1的槽数量比较多，可以使用rebalance进行负载均分下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster rebalance 127.0.0.1:30001 
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/9dbd092a33d84010aad3231dd8980c32.png&quot; alt=&quot;image-20250724225241422&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;再次查看集群状态，槽数量已经均分到了五个节点：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/21fe67530eea424da1be032dc8315be3.png&quot; alt=&quot;image-20250724225320620&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;add-node添加节点&quot;&gt;add-node：添加节点&lt;/h3&gt;
&lt;p&gt;该命令的完整格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster add-node 
    new_host:new_port 				#新节点地址
    existing_host:existing_port		#集群中现有任意节点地址，用于连接集群
    --cluster-slave					#指定新节点为从节点
    --cluster-master-id &amp;lt;master-id&amp;gt;		#指定主节点ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新节点默认以主节点身份加入集群，使用&lt;code&gt;--cluster-slave&lt;/code&gt;选项可以使其以从节点身份加入集群，如果指定了&lt;code&gt;--cluster-master-id&lt;/code&gt;，则会被当做指定主节点的从节点；否则redis会自动为新节点选择一个主节点。&lt;/p&gt;
&lt;p&gt;比如我们现在要将30011节点添加到集群，运行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster add-node 127.0.0.1:30011 127.0.0.1:30001
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/e8d583c638f849cc8b179b0a9b42c927.png&quot; alt=&quot;image-20250724230138994&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，实际上底层还是使用了cluster的内置命令meet将新节点加入集群的。查看集群状态：&lt;code&gt;./redis-cli --cluster info 127.0.0.1:30001&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/591b81799f354bf58c0de1b578a26a96.png&quot; alt=&quot;image-20250724230252405&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到新节点已经加入了集群，但是没有任何槽分配，可以使用rebalance命令进行槽均衡分配下：&lt;code&gt;./redis-cli --cluster rebalance 127.0.0.1:30001 --cluster-use-empty-masters&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/ddfa3c75c20a4a449b6218bcb7869327.png&quot; alt=&quot;image-20250724230537950&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;再次查看集群状态，可以看到新节点30011已经分配了槽：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/e3dce770e3474e95b4529e07882ce0ce.png&quot; alt=&quot;image-20250724230516358&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;call执行命令&quot;&gt;call：执行命令&lt;/h3&gt;
&lt;p&gt;通过cluster选项的call子命令，用户可以在整个集群的所有节点上执行给定的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster call           
host:port 					#集群任意节点，用于连接集群
command arg arg .. arg		#执行的命令
--cluster-only-masters		#只在master节点执行
--cluster-only-replicas		#只在从节点执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令没什么好说的，尝试运行下get命令：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/1c759cbb92154687b0f4b6bfc74e6424.png&quot; alt=&quot;image-20250724231030579&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;set-timeout设置超时时间&quot;&gt;set-timeout：设置超时时间&lt;/h3&gt;
&lt;p&gt;通过cluster选项的set-timeout子命令，用户可以为集群的所有节点重新设置cluster-node-timeout选项的值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster set-timeout host:port milliseconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;超时时间单位是毫秒。&lt;/p&gt;
&lt;p&gt;运行如下命令，将超时时间设置为5秒钟：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster set-timeout 127.0.0.1:30001 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/c113fc23e3384ddaa9567264f153d4b1.png&quot; alt=&quot;image-20250724231325786&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;import导入数据&quot;&gt;import：导入数据&lt;/h3&gt;
&lt;p&gt;import选项用于将某单机上的数据导入集群：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster import 
host:port						#目标集群的任意节点地址
 --cluster-from &amp;lt;arg&amp;gt;			#源Redis实例地址
 --cluster-from-user &amp;lt;arg&amp;gt;		#源实例用户名（如果需要认证）
 --cluster-from-pass &amp;lt;arg&amp;gt;		#源实例密码（如果需要认证）
 --cluster-from-askpass			#交互式输入源实例密码
 --cluster-copy					#复制模式（保留源数据）
 --cluster-replace				#替换模式（覆盖冲突键）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我们将6379单机redis的数据迁移到redis集群，运行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster import 127.0.0.1:30001 --cluster-from 127.0.0.1:6379 --cluster-copy --cluster-replace
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;backup备份&quot;&gt;backup：备份&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster backup host:port backup_directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令用于将集群数据备份到本地目录，比如我们现在将集群备份到data目录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster backup 127.0.0.1:30001 ./data
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/a0efe1e934864857937ac953ebfd0ff1.png&quot; alt=&quot;image-20250724232618135&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;看看data文件夹：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/24/40511cec4161481dac43f50acd2ce7f8.png&quot; alt=&quot;image-20250724232722604&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到该选项将每个主节点的数据都存到了单独的rdb文件。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（九）：多机部署之Cluster（集群）模式</title>
      <link>https://blog.kdyzm.cn/post/320</link>
      <guid>https://blog.kdyzm.cn/post/320</guid>
      <pubDate>Tue, 22 Jul 2025 11:14:36 +0800</pubDate>
      <description>&lt;p&gt;无论是主从复制模式，还是哨兵模式的主从复制模式，实际上都可以认为是“单机”部署的Redis实例，因为无论有多少从节点，主节点都只有一个，所有的数据写入都是写入主节点，从节点只是复制主节点而已。主从复制模式的缺点很明显，那就是无法水平扩展，如果数据量越来越大，对于单台机器的性能要求比较高。自Redis3.0开始，真正引入了Redis“集群”的概念，自此，Redis有了水平扩展的能力。&lt;/p&gt;
&lt;h2 id=&quot;一集群模式简介&quot;&gt;一、集群模式简介&lt;/h2&gt;
&lt;p&gt;与单机版Redis（包括主从复制模式）将整个数据库放在同一台服务器上的做法不同，Redis集群通过将数据库分散存储到多个节点上来平衡各个节点的负载压力。&lt;/p&gt;
&lt;p&gt;具体来说，Redis集群会将整个数据库空间划分为&lt;strong&gt;16384&lt;/strong&gt;个槽（slot）来实现数据分片（sharding），而集群中的各个主节点则会分别负责处理其中的一部分槽。当用户尝试将一个键存储到集群中时，客户端会先计算出键所属的槽，接着在记录集群节点槽分布的映射表中找出处理该槽的节点，最后再将键存储到相应的节点中，如下图所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/48f72bdab76f4894bb62dfe766dd9b28.png&quot; alt=&quot;Redis集群的分片实现&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;Redis数据迁移的基本单位是槽（slot）。如果我们想新增一个节点，可以向集群发送几个命令，让集群把相应的槽以及槽内的数据迁移到新节点上；如果我们想删除一个节点，则集群会将被删除节点的槽迁移到其它节点上去。上述伴随着向集群新增或者删除节点导致槽迁移的过程被称为“&lt;strong&gt;重分片（reshard）&lt;/strong&gt;”。无论是向集群中新增节点还是删除节点，整个重分片（reshard）过程都可以在线进行，Redis集群无须因此而停机。&lt;/p&gt;
&lt;p&gt;集群模式有以下几个特点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.高可用。&lt;/strong&gt; Redis集群模式可为每个主节点配置副本节点，而且自带高可用功能，其机制和Sentinel类似，在主节点故障时自动实施故障转移。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.高性能。&lt;/strong&gt; 客户端发送的命令在绝大部分情况下都不需要实施转向，或者仅需要一次转向，因此在Redis集群中执行命令的性能与在单机Redis服务器上执行命令的性能非常接近，在海量数据的情况下Redis集群模式比Redis单机模式性能大幅度领先。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.可扩展。&lt;/strong&gt; Redis集群通过槽机制让数据均匀分布或者根据权重分布在所有的节点上，在需要扩展时可以很方便的添加新的Redis实例，甚至不需要重启集群。&lt;/p&gt;
&lt;h2 id=&quot;二集群模式部署&quot;&gt;二、集群模式部署&lt;/h2&gt;
&lt;p&gt;接下来部署有十个Redis节点，包含五主五从的Redis集群，端口号设置从30001到30010。其架构图如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/6efca67002da47449fd2af10ab402319.png&quot; alt=&quot;image-20250718154457488&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;先创建10个文件夹，编号从30001到30010，然后将&lt;code&gt;redis-server&lt;/code&gt;以及&lt;code&gt;redis.conf&lt;/code&gt;文件挨个复制进去（redis编译参考：《&lt;a href=&quot;https://blog.kdyzm.cn/post/28&quot;&gt;CentOS安装Redis&lt;/a&gt;》），目录结构如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;├── 30001
│   ├── redis.conf
│   └── redis-server
├── 30002
│   ├── redis.conf
│   └── redis-server
├── 30003
│   ├── redis.conf
│   └── redis-server
......
├── 30009
│   ├── redis.conf
│   └── redis-server
└── 30010
    ├── redis.conf
    └── redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1配置文件修改&quot;&gt;1、配置文件修改&lt;/h3&gt;
&lt;p&gt;修改每个节点的&lt;code&gt;redis.conf&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;port 30001  #修改该配置为正确的端口号，从30001到30010
daemonize yes #后台运行
logfile &amp;quot;server.log&amp;quot; #日志文件
dir &amp;quot;./&amp;quot; #工作目录设置在当前目录，即运行命令的目录，方便查看日志等
cluster-enabled yes  #开启集群功能
cluster-config-file nodes-30001.conf #集群运行时配置文件，会自动创建
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2运行10个redis服务&quot;&gt;2、运行10个Redis服务&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;进入&lt;/strong&gt;每个文件夹，使用命令&lt;code&gt;./redis-server ./redis.conf&lt;/code&gt;运行服务。&lt;/p&gt;
&lt;p&gt;10个服务都启动起来之后，它们相互之间不知道彼此的存在，只是单纯的redis服务，并没有组成集群，此时要使用redis-cli工具创建集群让它们彼此关联起来。&lt;/p&gt;
&lt;h3 id=&quot;3创建集群&quot;&gt;3、创建集群&lt;/h3&gt;
&lt;p&gt;使用redis-cli运行如下命令创建集群：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster create \  #创建集群命令
127.0.0.1:30001 127.0.0.1:30002 127.0.0.1:30003 127.0.0.1:30004 127.0.0.1:30005 127.0.0.1:30006 127.0.0.1:30007 127.0.0.1:30008 127.0.0.1:30009 127.0.0.1:30010 \ #指定10个节点的ip和端口号
--cluster-replicas 1  #每个主节点1个副本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行完会提示如下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;gt;&amp;gt;&amp;gt; Performing hash slots allocation on 10 nodes...
Master[0] -&amp;gt; Slots 0 - 3276
Master[1] -&amp;gt; Slots 3277 - 6553
Master[2] -&amp;gt; Slots 6554 - 9829
Master[3] -&amp;gt; Slots 9830 - 13106
Master[4] -&amp;gt; Slots 13107 - 16383
Adding replica 127.0.0.1:30007 to 127.0.0.1:30001
Adding replica 127.0.0.1:30008 to 127.0.0.1:30002
Adding replica 127.0.0.1:30009 to 127.0.0.1:30003
Adding replica 127.0.0.1:30010 to 127.0.0.1:30004
Adding replica 127.0.0.1:30006 to 127.0.0.1:30005
&amp;gt;&amp;gt;&amp;gt; Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: d85dc362370f81fe627305e9b246365bd51a02c2 127.0.0.1:30001
   slots:[0-3276] (3277 slots) master
M: 9d148120512f85f29dd246e515243fd6019c7dcc 127.0.0.1:30002
   slots:[3277-6553] (3277 slots) master
M: d5f1adb8939529abcfe24e657bd01787b7d7928c 127.0.0.1:30003
   slots:[6554-9829] (3276 slots) master
M: 6d2c221d2ff15ae7fb9d5a5b7551ae5b493e791f 127.0.0.1:30004
   slots:[9830-13106] (3277 slots) master
M: adbbff5339b083b1e4fb641951e6fb9085ffb25c 127.0.0.1:30005
   slots:[13107-16383] (3277 slots) master
S: c33e236a553d9e658a0bb549d933567de5a756f8 127.0.0.1:30006
   replicates 9d148120512f85f29dd246e515243fd6019c7dcc
S: 1728a0fb708c776303f25e33c0add3297332cd64 127.0.0.1:30007
   replicates 6d2c221d2ff15ae7fb9d5a5b7551ae5b493e791f
S: dd7079d8486119fecc4e71f10fb705889ea8668b 127.0.0.1:30008
   replicates d85dc362370f81fe627305e9b246365bd51a02c2
S: 0db0c21f6f9b075b887c0ef1548cf35cb0505b0b 127.0.0.1:30009
   replicates adbbff5339b083b1e4fb641951e6fb9085ffb25c
S: 32661eb91b3be79b9a9d9ba873e1066bd64916a7 127.0.0.1:30010
   replicates d5f1adb8939529abcfe24e657bd01787b7d7928c
Can I set the above configuration? (type &apos;yes&apos; to accept): 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上是一段提示性信息，提示我们将要进行集群创建了，有哪些节点是主节点，每个主节点将分配多少个槽，每个主节点的副本节点是谁，主节点的id和副本节点的id是什么。。可以说尽可能详尽的告诉我们如果创建了集群，它将会是什么样子的。输入yes表示同意按照这个配置创建集群：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Can I set the above configuration? (type &apos;yes&apos; to accept): yes
&amp;gt;&amp;gt;&amp;gt; Nodes configuration updated
&amp;gt;&amp;gt;&amp;gt; Assign a different config epoch to each node
&amp;gt;&amp;gt;&amp;gt; Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
&amp;gt;&amp;gt;&amp;gt; Performing Cluster Check (using node 127.0.0.1:30001)
M: d85dc362370f81fe627305e9b246365bd51a02c2 127.0.0.1:30001
   slots:[0-3276] (3277 slots) master
   1 additional replica(s)
S: c33e236a553d9e658a0bb549d933567de5a756f8 127.0.0.1:30006
   slots: (0 slots) slave
   replicates 9d148120512f85f29dd246e515243fd6019c7dcc
S: 0db0c21f6f9b075b887c0ef1548cf35cb0505b0b 127.0.0.1:30009
   slots: (0 slots) slave
   replicates adbbff5339b083b1e4fb641951e6fb9085ffb25c
S: 32661eb91b3be79b9a9d9ba873e1066bd64916a7 127.0.0.1:30010
   slots: (0 slots) slave
   replicates d5f1adb8939529abcfe24e657bd01787b7d7928c
S: 1728a0fb708c776303f25e33c0add3297332cd64 127.0.0.1:30007
   slots: (0 slots) slave
   replicates 6d2c221d2ff15ae7fb9d5a5b7551ae5b493e791f
M: adbbff5339b083b1e4fb641951e6fb9085ffb25c 127.0.0.1:30005
   slots:[13107-16383] (3277 slots) master
   1 additional replica(s)
S: dd7079d8486119fecc4e71f10fb705889ea8668b 127.0.0.1:30008
   slots: (0 slots) slave
   replicates d85dc362370f81fe627305e9b246365bd51a02c2
M: 6d2c221d2ff15ae7fb9d5a5b7551ae5b493e791f 127.0.0.1:30004
   slots:[9830-13106] (3277 slots) master
   1 additional replica(s)
M: d5f1adb8939529abcfe24e657bd01787b7d7928c 127.0.0.1:30003
   slots:[6554-9829] (3276 slots) master
   1 additional replica(s)
M: 9d148120512f85f29dd246e515243fd6019c7dcc 127.0.0.1:30002
   slots:[3277-6553] (3277 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
&amp;gt;&amp;gt;&amp;gt; Check for open slots...
&amp;gt;&amp;gt;&amp;gt; Check slots coverage...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就创建成功了。&lt;/p&gt;
&lt;h3 id=&quot;4集群验证&quot;&gt;4、集群验证&lt;/h3&gt;
&lt;p&gt;我们用redis-cli工具验证，运行如下命令进入集群中的30001节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli -c -p 30001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;-c&lt;/code&gt;参数的使用，表示是&lt;strong&gt;集群模式连接&lt;/strong&gt;，如果没有这个参数，后续运行命令可能会报错。&lt;/p&gt;
&lt;p&gt;运行命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster info
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster_state:ok				#集群状态正常
cluster_slots_assigned:16384	#16384个槽均已分配
cluster_slots_ok:16384			#16384个槽状态都正常
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:10			#集群中有10个节点
cluster_size:5
cluster_current_epoch:10
cluster_my_epoch:1
cluster_stats_messages_ping_sent:13777
cluster_stats_messages_pong_sent:13518
cluster_stats_messages_fail_sent:12
cluster_stats_messages_sent:27307
cluster_stats_messages_ping_received:13518
cluster_stats_messages_pong_received:13767
cluster_stats_messages_fail_received:6
cluster_stats_messages_received:27291
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从输出信息上来看，集群状态是正常的。&lt;/p&gt;
&lt;p&gt;接下来运行如下命令查看所有节点信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/8f800677baef4f918ab334f37d121004.png&quot; alt=&quot;image-20250718164144192&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;从输出上来看，5主5从10个节点都是正常的。还能看到一些主从关系信息，比如30010是30003的副本节点。&lt;/p&gt;
&lt;p&gt;当然，可以运行如下命令确认一下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;CLUSTER REPLICAS d5f1adb8939529abcfe24e657bd01787b7d7928c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/f0b06483c9b84d8d90208d5cbd0285e5.png&quot; alt=&quot;image-20250718164917435&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到30010确实是30003的副本节点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接下来看看其高可用机制是否生效，我们把30003主节点下掉，看看30010是否会成为主节点实现自动故障转移。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用命令&lt;code&gt;./redis-cli -p 30003&lt;/code&gt; 进入30003节点，运行命令&lt;code&gt;shutdown&lt;/code&gt;退出reids进程，之后查看30003的副本节点30010的日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/895923a65e98442a8cb539cb96850212.png&quot; alt=&quot;image-20250718170804841&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;从节点日志上来看，30010副本节点发现无法联系上30003主节点之后，发起了一次投票选举主节点，最后自己成为了新的主节点。&lt;/p&gt;
&lt;p&gt;重新运行&lt;code&gt;cluster nodes&lt;/code&gt;命令：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/56367613513f4f1a81ecb495861a3237.png&quot; alt=&quot;image-20250718171115226&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到30003节点已经被标记为下线状态，30010副本节点已经被提级为主节点了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接下来再把30003节点启动起来，看看能否自动加入集群。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;先看下其启动日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/b92b5ff9c0024fd5877438704a0d872c.png&quot; alt=&quot;image-20250718171607907&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;从启动日志上来看，30003节点启动之后把自己变成了30010的副本，并且发起了一次全量数据同步，30003节点应该变成了30010的副本。运行命令&lt;code&gt;cluster nodes&lt;/code&gt;命令查看：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/18/dfe2f2d3fc524118806fb9192f3dbdec.png&quot; alt=&quot;image-20250718171935424&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到重新启动起来的30003节点变成了30010的副本节点，这个和sentinel+主从复制模式中的处理策略是相同的。&lt;/p&gt;
&lt;p&gt;至此，我们验证了集群已经成功搭建完成。&lt;/p&gt;
&lt;h2 id=&quot;三集群管理命令&quot;&gt;三、集群管理命令&lt;/h2&gt;
&lt;p&gt;与sentinel模式相似，cluster模式也有一套管理命令，使用&lt;code&gt;./redis-cli -c -p 30001&lt;/code&gt;进入集群任意一节点以后，使用命令&lt;code&gt;cluster help&lt;/code&gt;可以查看所有相关的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; cluster help
 1) CLUSTER &amp;lt;subcommand&amp;gt; [&amp;lt;arg&amp;gt; [value] [opt] ...]. Subcommands are:
 2) ADDSLOTS &amp;lt;slot&amp;gt; [&amp;lt;slot&amp;gt; ...]
 3)     Assign slots to current node.
 4) BUMPEPOCH
 5)     Advance the cluster config epoch.
 6) COUNT-FAILURE-REPORTS &amp;lt;node-id&amp;gt;
 7)     Return number of failure reports for &amp;lt;node-id&amp;gt;.
 8) COUNTKEYSINSLOT &amp;lt;slot&amp;gt;
 9)     Return the number of keys in &amp;lt;slot&amp;gt;.
10) DELSLOTS &amp;lt;slot&amp;gt; [&amp;lt;slot&amp;gt; ...]
11)     Delete slots information from current node.
12) FAILOVER [FORCE|TAKEOVER]
13)     Promote current replica node to being a master.
14) FORGET &amp;lt;node-id&amp;gt;
15)     Remove a node from the cluster.
16) GETKEYSINSLOT &amp;lt;slot&amp;gt; &amp;lt;count&amp;gt;
17)     Return key names stored by current node in a slot.
18) FLUSHSLOTS
19)     Delete current node own slots information.
20) INFO
21)     Return information about the cluster.
22) KEYSLOT &amp;lt;key&amp;gt;
23)     Return the hash slot for &amp;lt;key&amp;gt;.
24) MEET &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt; [&amp;lt;bus-port&amp;gt;]
25)     Connect nodes into a working cluster.
26) MYID
27)     Return the node id.
28) NODES
29)     Return cluster configuration seen by node. Output format:
30)     &amp;lt;id&amp;gt; &amp;lt;ip:port&amp;gt; &amp;lt;flags&amp;gt; &amp;lt;master&amp;gt; &amp;lt;pings&amp;gt; &amp;lt;pongs&amp;gt; &amp;lt;epoch&amp;gt; &amp;lt;link&amp;gt; &amp;lt;slot&amp;gt; ...
31) REPLICATE &amp;lt;node-id&amp;gt;
32)     Configure current node as replica to &amp;lt;node-id&amp;gt;.
33) RESET [HARD|SOFT]
34)     Reset current node (default: soft).
35) SET-CONFIG-EPOCH &amp;lt;epoch&amp;gt;
36)     Set config epoch of current node.
37) SETSLOT &amp;lt;slot&amp;gt; (IMPORTING|MIGRATING|STABLE|NODE &amp;lt;node-id&amp;gt;)
38)     Set slot state.
39) REPLICAS &amp;lt;node-id&amp;gt;
40)     Return &amp;lt;node-id&amp;gt; replicas.
41) SAVECONFIG
42)     Force saving cluster configuration on disk.
43) SLOTS
44)     Return information about slots range mappings. Each range is made of:
45)     start, end, master and replicas IP addresses, ports and ids
46) HELP
47)     Prints this help.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;差不多有20来个命令。下面逐一介绍各个命令。&lt;/p&gt;
&lt;h3 id=&quot;cluster-meet&quot;&gt;cluster meet&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cluster meet&lt;/code&gt;命令用于将某个节点加入到当前集群（运行redis-cli所在的节点所属的集群）中，其完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster meet &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt; [&amp;lt;bus-port&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ip和port是待加入集群的ip地址和端口号，bus-port则似乎第一次见：bus-port是节点的集群通信端口号，默认值是&lt;code&gt;&amp;lt;port&amp;gt;+10000&lt;/code&gt;，比如待加入集群的节点端口号是30011，那么集群通信端口号就是30011+10000=40011。集群通信端口号不处理业务请求，仅用于集群内部通信，例如故障检测、配置更新、数据迁移等关键操作。&lt;/p&gt;
&lt;p&gt;比如，我们在30001节点进入redis-cli，使用cluster meet命令加入一个新节点30011，将30011加入到集群中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster meet 127.0.0.1 30011  #等价于 cluster meet 127.0.0.1 30011 40011
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察30011的日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;2797:M 18 Jul 2025 18:20:51.333 # IP address for this node updated to 127.0.0.1
2797:M 18 Jul 2025 18:20:57.197 # Cluster state changed: ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;cluster nodes&lt;/code&gt;命令观察新加入的节点：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/e8eb24e75ad54d0bb5220faf04a55bb4.png&quot; alt=&quot;image-20250721094228198&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到30011作为master节点已经加入到了集群中。&lt;/p&gt;
&lt;h3 id=&quot;cluster-nodes&quot;&gt;cluster nodes&lt;/h3&gt;
&lt;p&gt;该命令用于查看集群内所有节点的相关信息，其完整命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回值是一个列表，形式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;lt;id&amp;gt; &amp;lt;ip:port&amp;gt; &amp;lt;flags&amp;gt; &amp;lt;master&amp;gt; &amp;lt;pings&amp;gt; &amp;lt;pongs&amp;gt; &amp;lt;epoch&amp;gt; &amp;lt;link&amp;gt; &amp;lt;slot&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各个字段的意思如下所示：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;信息项&lt;/th&gt;
&lt;th&gt;意义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;节点ID&lt;/td&gt;
&lt;td&gt;记录节点的运行ID。每个节点的运行ID在集群中都是唯一的，用户可以在执行集群操作时将其用作节点的标识符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;ip:port&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;地址和端口&lt;/td&gt;
&lt;td&gt;记录节点的IP地址以及端口号。位于@符号左边的是节点的客户端端口，而位于@符号右边的则是节点的集群端口，前者用于与客户端通信，而后者则用于与集群中的其他节点通信&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;flags&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;角色和状态&lt;/td&gt;
&lt;td&gt;记录节点当前担任的角色以及节点目前所处的状态。表20-2记录了这个项可能出现的值，以及各个值代表的意思。当节点的角色和状态同时出现时，这个项会使用逗号去分隔多个值，比如显示myself,master表示这个节点是客户端目前正在连接的节点，并且它是一个主节点，而显示slave,fail?则表示这个节点是一个从节点，并且它正处于疑似下线状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;master&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;主节点ID&lt;/td&gt;
&lt;td&gt;如果节点是一个从节点，那么这里显示的就是它正在复制的主节点的ID；如果节点本身就是一个主节点，那么它在这个项中只会显示一个-符号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;pings&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发送PING消息的时间&lt;/td&gt;
&lt;td&gt;节点最近一次向其他节点发送PING消息时的UNIX时间戳，格式为毫秒。如果该节点与其他节点的连接正常，并且它发送的PING消息也没有被阻塞，那么这个值将被设置为0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;pongs&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;收到PONG消息的时间&lt;/td&gt;
&lt;td&gt;节点最近一次接收到其他节点发送的PONG消息时的UNIX时间戳，格式为毫秒。对于客户端正在连接的节点来说，这个项的值总是为0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;epoch&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;配置纪元&lt;/td&gt;
&lt;td&gt;节点所处的配置纪元&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接状态&lt;/td&gt;
&lt;td&gt;节点集群总线的连接状态。connected表示连接正常，disconnected表示连接已断开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;负责的槽&lt;/td&gt;
&lt;td&gt;显示节点目前负责处理的槽以及这些槽所处的状态。表20-3记录了这个项可能出现的值以及各个值代表的含义。如果节点是一个从节点，或者是一个没有负责任何槽的主节点，那么这一项的值将为空&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;节点的角色和状态&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;myself&lt;/td&gt;
&lt;td&gt;这是客户端目前正在连接的节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;master&lt;/td&gt;
&lt;td&gt;这是一个主节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slave&lt;/td&gt;
&lt;td&gt;这是一个从节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fail?&lt;/td&gt;
&lt;td&gt;这个节点正处于疑似下线状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fail&lt;/td&gt;
&lt;td&gt;这个节点已经下线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nofailover&lt;/td&gt;
&lt;td&gt;这个节点开启了 cluster-replica-no-failover 配置选项，带有这个标志的从节点即使在主服务器下线的情况下，也不会主动执行故障转移操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;handshake&lt;/td&gt;
&lt;td&gt;集群正在与这个节点握手，尚未确认它的状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noaddr&lt;/td&gt;
&lt;td&gt;目前尚不清楚这个节点的具体地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noflags&lt;/td&gt;
&lt;td&gt;目前尚不清楚这个节点担任的角色以及它所处的状态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;槽的数字以及状态&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;槽的类型&lt;/th&gt;
&lt;th&gt;打印方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;连续的槽&lt;/td&gt;
&lt;td&gt;每当CLUSTER NODES命令遇到连续的槽号时，它就会以&lt;code&gt;start_slot-end_slot&lt;/code&gt;格式打印节点负责的槽。比如，打印&lt;code&gt;0-5460&lt;/code&gt;代表节点负责从槽0直到槽5460在内的连续多个槽&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不连续的槽&lt;/td&gt;
&lt;td&gt;每当CLUSTER NODES命令遇到不连续的槽号时，就会单独地打印出不连续的槽号。比如，如果一个节点只负责了1、3、5这3个槽，那么命令在这个节点的槽号部分将打印出&lt;code&gt;1 3 5&lt;/code&gt;。因为CLUSTER NODES总是会将节点负责的每一个不连续槽号都打印出来，所以如果一个节点负责了大量不连续的槽，那么它的槽号部分可能会非常庞大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;正在导入的槽&lt;/td&gt;
&lt;td&gt;如果节点正在从另一个节点导入某个槽，那么CLUSTER NODES命令将以&lt;code&gt;[slot_number-&amp;lt;-node_id]&lt;/code&gt;的格式打印出被导入的槽以及该槽原来所在的节点。比如，打印&lt;code&gt;[123-&amp;lt;-47b7ea54965875c3bf1316071584e842342c6fa3]&lt;/code&gt;就代表节点正在从ID为47b7ea54965875c3bf1316071584e842342c6fa3的节点中导入槽123&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;正在迁移的槽&lt;/td&gt;
&lt;td&gt;如果节点正在将自己的某个槽迁移至另一个节点，那么CLUSTER NODES命令将以&lt;code&gt;[slot_number-&amp;gt;-node_id]&lt;/code&gt;格式打印出被迁移的槽以及该槽正在迁移的目标节点。比如，如果节点正在将自己的槽255迁移至ID为47b7ea54965875c3bf1316071584e842342c6fa3的节点，那么命令将打印出&lt;code&gt;[255-&amp;gt;-47b7ea54965875c3bf1316071584e842342c6fa3]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以我们新增的30011节点为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;79a28e2df5e13dd726c892c42ee35a15423b00fb 127.0.0.1:30011@40011 master - 0 1752834070495 0 connected
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;节点id是79a28e2df5e13dd726c892c42ee35a15423b00fb&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点ip和端口号是127.0.0.1:30011，集群通信端口号是40011&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点角色是master&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点是master节点，没有master，以-表示&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于节点连接正常，pings是0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;收到PONG消息的时间是1752834070495&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置纪元是0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;连接状态是connected，表示连接正常&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;没有slots字段，因为这个节点是新加入集群的节点，还没有分配槽。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;cluster-myid&quot;&gt;cluster myid&lt;/h3&gt;
&lt;p&gt;该命令用于查看当前节点的运行ID。因为不少集群命令都需要使用节点的运行ID作为参数，所以当我们需要对正在连接的节点执行某个使用运行ID作为参数的操作时，就可以使用CLUSTER MYID命令快速地获得节点的ID。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/7e181c292fc84e02b2556e3d633c248a.png&quot; alt=&quot;image-20250721102314180&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;cluster-info&quot;&gt;cluster info&lt;/h3&gt;
&lt;p&gt;该命令用于查看与集群以及当前节点有关的状态信息。&lt;/p&gt;
&lt;p&gt;以下是一个对节点30001执行CLUSTER INFO命令的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; cluster info
cluster_state:ok						#集群目前处于在线状态
cluster_slots_assigned:16384			#有16384个槽已经被指派
cluster_slots_ok:16384					#有16384个槽处于在线状态
cluster_slots_pfail:0					#没有槽处于疑似下线状态
cluster_slots_fail:0					#没有槽处于已下线状态
cluster_known_nodes:11					#集群包含11个节点
cluster_size:5							#集群中有5个节点被指派了槽
cluster_current_epoch:11				#集群当前所处的纪元为11				
cluster_my_epoch:1						#节点当前所处的配置纪元为1
cluster_stats_messages_ping_sent:25124	#节点发送PING消息的数量
cluster_stats_messages_pong_sent:24625	#节点发送PONG消息的数量
cluster_stats_messages_meet_sent:1		#节点发送的meet消息的数量
cluster_stats_messages_fail_sent:20		#表示当前节点向其他节点发送的FAIL消息数量为20次。FAIL消息用于通知集群其他节点某个节点已被标记为下线（故障状态）
cluster_stats_messages_auth-ack_sent:1		#表示节点发送的认证确认消息（AUTH-ACK）数量为1次。这类消息用于集群节点间的身份验证响应，通常在安全认证场景中出现
cluster_stats_messages_sent:49771			#当前节点通过集群总线（node-to-node二进制总线）发送的所有消息总量，包括PING、PONG、FAIL等各类消息
cluster_stats_messages_ping_received:24625	#节点接收到的PING消息数量
cluster_stats_messages_pong_received:25114	#节点接收到的PONG消息数量
cluster_stats_messages_fail_received:7		#节点接收到的FAIL消息数量，表示其他节点通知本节点有7次关于某个节点故障的广播
cluster_stats_messages_auth-req_received:1 #节点接收到的认证请求消息（AUTH-REQ）数量，通常用于集群安全认证的初始化
cluster_stats_messages_received:49747      #节点通过集群总线接收的所有消息总量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从输出上来看，可以总结出来关于message的重要特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cluster_stats_messages_&amp;lt;type&amp;gt;_received&lt;/code&gt;：表示某种类型消息接收的数量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cluster_stats_messages_&amp;lt;type&amp;gt;_sent&lt;/code&gt;：表示某种类型消息发送的数量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;cluster-forget&quot;&gt;cluster forget&lt;/h3&gt;
&lt;p&gt;该命令用于从集群中移除某个节点，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;CLUSTER FORGET &amp;lt;node-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与&lt;code&gt;CLUSTER MEET&lt;/code&gt;命令引发的节点添加消息不一样，&lt;code&gt;CLUSTER FORGET&lt;/code&gt;命令引发的节点移除消息并不会通过Gossip协议传播至集群中的其他节点：当用户向一个节点发送&lt;code&gt;CLUSTER FORGET&lt;/code&gt;命令，让它去移除集群中的另一个节点时，接收到命令的节点只是暂时屏蔽了用户指定的节点，但这个被屏蔽的节点对于集群中的其他节点仍然是可见的。为此，要让集群真正地移除一个节点，用户必须向集群中的所有节点都发送相同的&lt;code&gt;CLUSTER FORGET&lt;/code&gt;命令，并且这一动作必须在60s之内完成，否则被暂时屏蔽的节点就会因为Gossip协议的作用而被重新添加到集群中。&lt;/p&gt;
&lt;p&gt;可以使用&lt;code&gt;redis-cli&lt;/code&gt;工具对集群中所有节点发送相同的命令移除刚新增的30011节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster call 127.0.0.1:30001 cluster forget 79a28e2df5e13dd726c892c42ee35a15423b00fb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;cluster-replicate&quot;&gt;cluster replicate&lt;/h3&gt;
&lt;p&gt;该命令用于将执行命令的当前节点转换为某个主节点的从节点，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster replicate &amp;lt;node-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户给定的主节点必须与当前节点位于相同的集群当中。此外，根据当前节点角色的不同，CLUSTER REPLICATE命令在执行时的情况也会有所不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果当前节点是一个主节点，那么它必须是一个没有被指派任何槽的主节点，并且它的数据库中也不能有任何数据，这样它才可以转换成一个从节点。&lt;/li&gt;
&lt;li&gt;如果当前节点已经是一个从节点，那么它将清空数据库中已有的数据，并开始复制用户给定的节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CLUSTER REPLICATE&lt;/code&gt;命令在成功执行时将返回OK作为结果。与单机版本的&lt;code&gt;REPLICAOF&lt;/code&gt;命令一样，CLUSTER REPLICATE命令引发的复制操作也是异步执行的。&lt;/p&gt;
&lt;p&gt;需要注意的是，该命令和单机版本的&lt;code&gt;replicaof&lt;/code&gt;不同，在使用单机版本的Redis时，用户可以让一个从服务器去复制另一个从服务器，以此来构建一系列链式复制的服务器；而Redis集群只允许节点对主节点而不是从节点进行复制，如果用户尝试使用CLUSTER REPLICATE命令让一个节点去复制一个从节点，那么命令将返回一个错误：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/06ffe782ab8c4839a4e67ff1f4f3cc10.png&quot; alt=&quot;image-20250721133149620&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;cluster-replicas&quot;&gt;cluster replicas&lt;/h3&gt;
&lt;p&gt;该命令用于查询某个节点的所有从节点的信息，其完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster replicas &amp;lt;node-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令的输出可以认为是&lt;code&gt;cluster nodes&lt;/code&gt;命令的一部分，其输出表头是一样的。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/a1502c0f314a4e8bb1314eab890efd07.png&quot; alt=&quot;image-20250721133521991&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;cluster-failover&quot;&gt;cluster failover&lt;/h3&gt;
&lt;p&gt;该命令作用对象是从节点，用于对从节点强制故障转移，将从节点提升为主节点，其完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster failover [force|takeover]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，&lt;code&gt;CLUSTER FAILOVER&lt;/code&gt;执行安全的主从切换，流程包括&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slave通知Master停止处理客户端请求。&lt;/li&gt;
&lt;li&gt;Master返回当前复制偏移量（replication offset）。&lt;/li&gt;
&lt;li&gt;Slave等待数据同步完成后再晋升为Master。&lt;/li&gt;
&lt;li&gt;新配置需获得集群中半数以上Master的投票认可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用force和takeover可改变这个过程以适应不同情况下的集群恢复：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;force：当Master宕机无法响应Slave的协商请求，但集群中半数以上Master仍存活时使用；跳过与旧Master的数据同步协商（即跳过标准流程的前3步），直接发起选举，但仍需获得多数Master的投票认可才能晋升；在Master不可达但集群仍健康时&lt;strong&gt;加速切换&lt;/strong&gt;，避免因等待超时而延长服务中断时间&lt;/li&gt;
&lt;li&gt;takeover：当半数以上Master节点故障（如机房断网），集群无法达成多数投票时使用；完全绕过选举机制，Slave直接生成最大的&lt;code&gt;config epoch&lt;/code&gt;，强制接管所有Slot并广播新配置。其他节点收到后会无条件接受新主节点；可能导致&lt;strong&gt;配置冲突&lt;/strong&gt;（如脑裂），仅用于极端故障恢复（如多活机房容灾）或测试环境。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;cluster-reset&quot;&gt;cluster reset&lt;/h3&gt;
&lt;p&gt;该命令用于重置节点，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster reset [hard|soft]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令接受SOFT和HARD两个可选项作为参数，用于指定重置操作的具体行为（软重置和硬重置）。如果用户在执行CLUSTER RESET命令的时候没有显式地指定重置方式，那么命令&lt;strong&gt;默认将使用SOFT&lt;/strong&gt;选项。&lt;/p&gt;
&lt;p&gt;CLUSTER RESET命令在执行时，将对节点执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;遗忘该节点已知的其他所有节点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;撤销指派给该节点的所有槽，并清空节点内部的槽-节点映射表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果执行该命令的节点是一个从节点，那么将它转换成一个主节点。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果执行的是硬重置，那么为节点创建一个新的运行ID。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果执行的是硬重置，那么将节点的纪元和配置纪元都设置为0。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过集群节点配置文件的方式，将新的配置持久化到硬盘上。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要注意的是，执行该命令的节点不能有数据，如果节点的数据库非空，那么该命令将执行失败。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/fca6dba280c64e1e9ace1f9d9804520f.png&quot; alt=&quot;image-20250721142842969&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;cluster-slots&quot;&gt;cluster slots&lt;/h3&gt;
&lt;p&gt;该命令用于查看槽与节点之间的关联信息，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster slots
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令会返回一个嵌套数组，数组中的每个项记录了一个槽范围（slot range）及其处理者的相关信息，其中包括&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;槽范围的起始槽。&lt;/li&gt;
&lt;li&gt;槽范围的结束槽。&lt;/li&gt;
&lt;li&gt;负责处理这些槽的主节点信息。&lt;/li&gt;
&lt;li&gt;零个或任意多个主节点属下从节点的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，每一项节点信息都由以下3项信息组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;节点的IP地址。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点的端口号。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点的运行ID。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如以下输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; CLUSTER SLOTS
1) 1) (integer) 6554			#起始槽
   2) (integer) 9829			#结束槽
   3) 1) &amp;quot;127.0.0.1&amp;quot;			#主节点ip地址
      2) (integer) 30010		#主节点端口号
      3) &amp;quot;32661eb91b3be79b9a9d9ba873e1066bd64916a7&amp;quot; #主节点id
   4) 1) &amp;quot;127.0.0.1&amp;quot;			#从节点ip地址
      2) (integer) 30003		#从节点端口号
      3) &amp;quot;d5f1adb8939529abcfe24e657bd01787b7d7928c&amp;quot; #从节点id
......
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;cluster-keyslot&quot;&gt;cluster keyslot&lt;/h3&gt;
&lt;p&gt;该命令用于查看键所属的槽，完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster keyslot &amp;lt;key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如30001节点有key f，想知道f存储在哪个槽了，可以使用该命令查询：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/65734b1bdc144d6abbd28e776a6ecb1a.png&quot; alt=&quot;image-20250721172533921&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;cluster-addslots&quot;&gt;cluster addslots&lt;/h3&gt;
&lt;p&gt;该命令用于把指定的槽分配给当前节点，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster addslots &amp;lt;slot&amp;gt; [&amp;lt;slot&amp;gt; ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，CLUSTER ADDSLOTS只能对&lt;strong&gt;尚未被指派的槽&lt;/strong&gt;执行指派操作，如果用户给定的槽已经被指派，那么命令将报错：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/f3cc632118b84109a0812c067c25356c.png&quot; alt=&quot;image-20250721170235521&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;cluster-delslots&quot;&gt;cluster delslots&lt;/h3&gt;
&lt;p&gt;该命令用于撤销当前节点的槽指派，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster delslots &amp;lt;slot&amp;gt; [&amp;lt;slot&amp;gt; ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如，我们想要撤销30001节点的3168槽，可以使用如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster delslots 3168
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行cluster info命令，可以看到集群状态为失败状态，而且槽指派的数量本应是16384，现在是16383，减少了1&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/d036347cc2ec4232b79115437dec39be.png&quot; alt=&quot;image-20250721172950028&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以使用&lt;code&gt;cluster slots&lt;/code&gt;查看槽分配的详情。&lt;/p&gt;
&lt;p&gt;需要注意的是，使用 &lt;code&gt;CLUSTER DELSLOTS&lt;/code&gt; 命令删除槽指派后，&lt;strong&gt;对应槽中的数据不会立即被清除，但会变为不可访问状态&lt;/strong&gt;。使用get命令获取key会提示如下信息：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/38ac3674d6df4fa0a1cedfd8efe7dc56.png&quot; alt=&quot;image-20250721173315552&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;如何恢复数据呢？&lt;/p&gt;
&lt;p&gt;有两种情况，一是槽直接分配给原节点，这种情况可以使用&lt;code&gt;cluster addslots&lt;/code&gt;命令将槽分配下就可以了；另外一种情况是槽要迁移到别的节点上，这种情况需要使用&lt;code&gt;cluster setslots&lt;/code&gt;命令做槽迁移。&lt;/p&gt;
&lt;p&gt;先用&lt;code&gt;cluster addslots&lt;/code&gt;尝试在原节点恢复槽分配：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/21/7a673f51717b4d54bc6140f1aa6f7283.png&quot; alt=&quot;image-20250721231149436&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;正常获取到了f的key值，可以确定30001的3168槽已经恢复槽分配。&lt;/p&gt;
&lt;h3 id=&quot;cluster-setslot&quot;&gt;cluster setslot&lt;/h3&gt;
&lt;p&gt;该命令可以改变给定槽在节点中的状态，从而实现节点之间的槽迁移以及集群重分片。该命令完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster setslot &amp;lt;slot&amp;gt; (IMPORTING|MIGRATING|STABLE|NODE &amp;lt;node-id&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令有四个子命令，通过四个命令的相互搭配，可以实现不同节点之间的槽迁移。我们接下来将30001（id为d85dc362370f81fe627305e9b246365bd51a02c2）节点上的3168槽迁移到30002（id为9d148120512f85f29dd246e515243fd6019c7dcc）节点上，看看如何操作。&lt;/p&gt;
&lt;p&gt;在正式操作前，需要确保3168槽归30001节点所有，这点和addslots命令的要求完全相反的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：30001节点设置为MIGRATING状态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; cluster setslot 3168 MIGRATING d85dc362370f81fe627305e9b246365bd51a02c2
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行完晒命令，30001节点将自己状态设置为“迁移中”状态，准备发送3168槽的数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：30002节点设置为IMPORTING状态&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;运行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30002&amp;gt; cluster setslot 3168 IMPORTING 9d148120512f85f29dd246e515243fd6019c7dcc
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行命令后，节点30002的状态被设置为“导入中”状态，准备接收3168槽的数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：key迁移&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;获取3168槽中的key：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; CLUSTER GETKEYSINSLOT 3168 10
1) &amp;quot;f&amp;quot;
127.0.0.1:30001&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到只有一个key。&lt;/p&gt;
&lt;p&gt;然后使用migrate命令将key迁移到30002节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; migrate 127.0.0.1 30002 &amp;quot;&amp;quot; 0 3000 keys f
OK
127.0.0.1:30001&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第四步：将槽指派给节点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;迁移完成数据以后，在集群中任意节点将3168槽指派给30002节点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster setslot 3168 node 9d148120512f85f29dd246e515243fd6019c7dcc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;集群的其中一个节点在执行了NODE子命令之后，对给定槽的新指派信息将被传播至整个集群，目标节点在接收到这一信息之后将移除给定槽的“导入中”状态，而源节点在接收到这一信息之后将移除给定槽的“迁移中”状态。&lt;/p&gt;
&lt;p&gt;30001节点再次查询f的键值，将会重定向到30002节点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; get f
-&amp;gt; Redirected to slot [3168] located at 127.0.0.1:30002
&amp;quot;6&amp;quot;
127.0.0.1:30002&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，使用&lt;code&gt;cluster setslot&lt;/code&gt;命令迁移槽节点是比较麻烦的，为了精简这个步骤，可以使用&lt;code&gt;cluster-cli&lt;/code&gt;的工具命令，一个命令就可以解决了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli --cluster reshard 127.0.0.1:30002 --cluster-from 9d148120512f85f29dd246e515243fd6019c7dcc --cluster-to d85dc362370f81fe627305e9b246365bd51a02c2 --cluster-slots 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令的确定是不能精准控制哪几个槽的迁移，只能确定迁移的数量。部分输出如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/22/758ca8cec49646548ffa0858764bf21a.png&quot; alt=&quot;image-20250722111139512&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到挑选需要迁移的节点也可能是有策略的，并没有破坏了其它连续的槽节点，而是挑选了单独的3168槽迁移。&lt;/p&gt;
&lt;p&gt;关于redis-cli工具的具体使用，后续会有介绍。&lt;/p&gt;
&lt;h3 id=&quot;cluster-keyslot-1&quot;&gt;cluster keyslot&lt;/h3&gt;
&lt;p&gt;该命令用于查看键所属的槽，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster keyslot &amp;lt;key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我们想查看f键所属的槽，可以运行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; CLUSTER KEYSLOT f
(integer) 3168
127.0.0.1:30001&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;cluster-countkeysinslot&quot;&gt;cluster countkeysinslot&lt;/h3&gt;
&lt;p&gt;该命令用于查询槽包含的键数量，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster countkeysinslot &amp;lt;slot&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例，我们想查看3168槽中包含的键数量，可以使用如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30002&amp;gt; cluster countkeysinslot 3168
(integer) 1
127.0.0.1:30002&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意运行该命令必须在3168槽所属的节点上运行，否则会返回0。&lt;/p&gt;
&lt;h3 id=&quot;cluster-getkeysinslot&quot;&gt;cluster getkeysinslot&lt;/h3&gt;
&lt;p&gt;该命令用于获取槽包含的键，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster getkeysinslot &amp;lt;slot&amp;gt; &amp;lt;count&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我想从3168槽中获取10条数据，可以运行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30002&amp;gt; cluster getkeysinslot 3168 10
1) &amp;quot;f&amp;quot;
127.0.0.1:30002&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意该命令必须在3168槽所属的节点上运行，否则查询不到数据。&lt;/p&gt;
&lt;h3 id=&quot;cluster-flushslots&quot;&gt;cluster flushslots&lt;/h3&gt;
&lt;p&gt;该命令用于撤销对节点的所有槽指派，相当于对节点上的每个槽执行&lt;code&gt;cluster delslots&lt;/code&gt;命令。完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cluster flushslots
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户在执行CLUSTER FLUSHSLOTS命令之前，必须确保节点的数据库为空，否则节点将拒绝执行命令并返回一个错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30002&amp;gt; cluster flushslots
(error) ERR DB must be empty to perform CLUSTER FLUSHSLOTS.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;四注意事项&quot;&gt;四、注意事项&lt;/h2&gt;
&lt;h3 id=&quot;1集群模式对部分命令的影响&quot;&gt;1、集群模式对部分命令的影响&lt;/h3&gt;
&lt;p&gt;由于集群模式比较特殊，每个节点分管部分槽，这导致每个节点都不知道全部节点的key都是什么，所以使用&lt;code&gt;keys&lt;/code&gt;命令只能查看自己节点的键。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; set a 1
-&amp;gt; Redirected to slot [15495] located at 127.0.0.1:30005
OK
127.0.0.1:30005&amp;gt; set b 2
-&amp;gt; Redirected to slot [3300] located at 127.0.0.1:30002
OK
127.0.0.1:30002&amp;gt; set c 3
-&amp;gt; Redirected to slot [7365] located at 127.0.0.1:30010
OK
127.0.0.1:30010&amp;gt; set f 4
-&amp;gt; Redirected to slot [3168] located at 127.0.0.1:30001
OK
127.0.0.1:30001&amp;gt; keys *
1) &amp;quot;f&amp;quot;
127.0.0.1:30001&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，&lt;code&gt;cluster countkeysinslot&lt;/code&gt;以及&lt;code&gt;cluster getkeysinslot&lt;/code&gt; 命令都有类似的问题，需要注意。&lt;/p&gt;
&lt;h3 id=&quot;2强制键放在同槽的方法&quot;&gt;2、强制键放在同槽的方法&lt;/h3&gt;
&lt;p&gt;在默认情况下，Redis将根据用户输入的整个键计算出该键所属的槽，然后将键存储到相应的槽中，但是有些时候，需要将键放到同一个槽中，这时候可以使用&lt;strong&gt;散列标签&lt;/strong&gt;实现该功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;散列标签&lt;/strong&gt;功能会找出键中第一个被大括号{}包围并且非空的字符串子串（sub string），然后根据子串计算出该键所属的槽。这样一来，即使两个键原本不属于同一个槽，但只要它们拥有相同的被包围子串，那么程序计算出的散列值就是一样的，因此Redis集群就会把它们存储到同一个槽中。&lt;/p&gt;
&lt;p&gt;举个例子，默认情况下我们存储&lt;code&gt;user:1&lt;/code&gt;以及&lt;code&gt;user:2&lt;/code&gt;结果如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30001&amp;gt; set user:1 1
-&amp;gt; Redirected to slot [10778] located at 127.0.0.1:30004
OK
127.0.0.1:30004&amp;gt; set user:2 2
-&amp;gt; Redirected to slot [6777] located at 127.0.0.1:30010
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到两个键被分别存储到了3004节点以及30010节点上不同的槽。&lt;/p&gt;
&lt;p&gt;但是如果使用散列标签方式，可以让user开头的key都存到同一个槽中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:30010&amp;gt; set {user}:1 1
-&amp;gt; Redirected to slot [5474] located at 127.0.0.1:30002
OK
127.0.0.1:30002&amp;gt; set {user}:2 2
OK
127.0.0.1:30002&amp;gt; cluster getkeysinslot 5474 10
1) &amp;quot;{user}:1&amp;quot;
2) &amp;quot;{user}:2&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到两个键都被存到了30002节点上的5474槽位。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（八）：多机部署之Sentinel（哨兵）模式</title>
      <link>https://blog.kdyzm.cn/post/319</link>
      <guid>https://blog.kdyzm.cn/post/319</guid>
      <pubDate>Tue, 15 Jul 2025 22:49:12 +0800</pubDate>
      <description>&lt;p&gt;在上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/318&quot;&gt;Redis（七）：多机部署之主从复制模式&lt;/a&gt;》中讲过在主从复制模式下从节点发生了故障之后重连数据重新同步的问题，但是并没有提过主节点发生了故障会发生什么。实际上从节点挂掉并不会影响什么，但是主节点挂掉影响就大了，如果没有高可用机制，则需要人工干预手动选择其他节点作为主节点，整个过程从发现到处理结束不仅耗时处理还繁琐。如何让这个过程自动化呢？这就是本篇文章讨论的主题：基于Redis6.2.1使用sentinel实现Redis主从复制模式高可用。&lt;/p&gt;
&lt;h2 id=&quot;一手动处理主节点故障&quot;&gt;一、手动处理主节点故障&lt;/h2&gt;
&lt;p&gt;主从服务器拥有相同的数据，所以它们在理论上是可以互相替换的：如果我们把主服务器的某个从服务器转换为主服务器，让它代替原来的主服务器处理命令请求，那么它得出的结果应该与原来的主服务器处理相同请求时得出的结果一致。基于这个原理，我们可以在主服务器因故下线时，将它的其中一个从服务器转换为主服务器，并使用新的主服务器继续处理命令请求，这样整个系统就可以继续运转，不必仅因为主服务器的下线而停机。这种使用正常服务器替换下线服务器以维持系统正常运转的操作，一般被称为&lt;strong&gt;故障转移（failover）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为Redis支持主从复制特性，所以我们同样可以对下线的Redis主服务器实施故障转移。举个例子，假设现在有一个主服务器127.0.0.1：6379（简称6379），它有两个从服务器127.0.0.1：6380（简称6380）和127.0.0.1：6381（简称6381），这3个服务器分别处理一些客户端的命令请求。如果在某一时刻，主服务器6379因为故障而下线，那么我们可以向6380发送命令&lt;code&gt;REPLICAOF no one&lt;/code&gt;，将它转换为主服务器，然后向另一个从服务器6381发送命令&lt;code&gt;REPLICAOF 127.0.0.6380&lt;/code&gt;，让它去复制新的主服务器6380，这样就可以重新建立起一个能够正常运作的主从服务器连接，并继续处理客户端发送的命令请求。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/14/765896765fc940f79e45ad92e89b9344.png&quot; alt=&quot;image-20250714161926560&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;简单来说，如果我们在前面的例子中，使用了Redis sentinel来监视主服务器6379以及它的两个从服务器6380、6381，那么在6379因为故障而下线时，Redis sentinel就会把6380和6381的其中一个转换为主服务器，并让另一个从服务器去复制新的主服务器，这个过程完全不需要人工介入。&lt;/p&gt;
&lt;h2 id=&quot;二部署主从复制sentinel&quot;&gt;二、部署主从复制+sentinel&lt;/h2&gt;
&lt;p&gt;组建sentinel网络的方法非常简单，与启动单个sentinel时的方法一样：用户只需要启动多个sentinel，并使用sentinel monitor配置选项指定sentinel要监视的主服务器，那些监视相同主服务器的sentinel就会自动发现对方，并组成相应的sentinel网络。&lt;/p&gt;
&lt;p&gt;在启动sentinel之前，我们先把主从复制模式的一主两从启动起来，详情可看文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/318&quot;&gt;Redis（七）：多机部署之主从复制模式&lt;/a&gt;》部署完成之后的架构图：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/25ae012d32da4966ac93a85eaed4a0ae.png&quot; alt=&quot;主从复制模式架构图&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;接下来在同一台机器上启动三个sentinel实例。&lt;/p&gt;
&lt;p&gt;源码构建redis（参考&lt;a href=&quot;https://blog.kdyzm.cn/post/28&quot;&gt;CentOS安装Redis&lt;/a&gt;）后，sentinel的程序文件和redis-server同一个文件夹，文件名为&lt;code&gt;redis-sentinel&lt;/code&gt;，配置文件则和redis.conf文件夹同一个目录，文件名为&lt;code&gt;sentinel.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;redis-6.2.1
	│
    ├── redis.conf
    ├── sentinel.conf
    └── src
        ├── redis-server
        └── redis-sentinel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后创建三个文件夹sentinel.1、sentinel.2、sentinel.3分别将redis-sentinel、sentinel.conf文件复制进去，目录结构最终如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;├── sentinel.1
│   ├── redis-sentinel
│   └── sentinel.conf
├── sentinel.2
│   ├── redis-sentinel
│   └── sentinel.conf
└── sentinel.3
    ├── redis-sentinel
    └── sentinel.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的目标架构如下：&lt;/p&gt;
&lt;h3 id=&quot;1配置文件修改&quot;&gt;1、配置文件修改&lt;/h3&gt;
&lt;p&gt;启动sentinel的命令是&lt;code&gt;./redis-sentinel ./sentinel.conf&lt;/code&gt;，形式和redis-server的启动方式很像。sentinel的配置文件很重要，sentinel程序不仅读，而且还写该配置文件，所以在运行sentinel程序的时候一定要确保当前用户对该文件拥有写权限。下面说下配置文件的重要配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;port 26379 #默认端口号26379，运行多个实例需要改动端口号
daemonize no #是否在后台运行
pidfile /var/run/redis-sentinel.pid #后台运行时创建的pid文件名
logfile &amp;quot;&amp;quot;  #日志文件的名称
dir /tmp #工作目录，最好是./这样可以方便查看日志文件
sentinel monitor mymaster 127.0.0.1 6379 2 #最重要的配置文件，监听的主服务器名称、ip、端口号以及quorum
sentinel down-after-milliseconds mymaster 30000  #多少毫秒内主服务器无响应考虑其主观下线
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件中最重要的配置就是&lt;code&gt;sentinel monitor mymaster 127.0.0.1 6379 2&lt;/code&gt;，其中最后的2是&lt;code&gt;quorum&lt;/code&gt;，当认为主节点主观下线的哨兵数量到达quorum的时候，就会执行故障转移操作。&lt;/p&gt;
&lt;p&gt;修改配置文件成如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bind 0.0.0.0
port 26379  #另外两个sentinel实例修改为26380
daemonize yes
pidfile /var/run/redis-sentinel1.pid #另外两个sentinel实例修改成redis-sentinel2.pid、redis-sentinel3.pid
logfile &amp;quot;sentinel.log&amp;quot;
dir ./
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外两个配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则不需要动，特别注意此处为主服务器命名&amp;quot;mymaster&amp;quot;，在配置文件中有多处配置都使用了该名字，如果修改改名字，一定要全改掉。&lt;/p&gt;
&lt;h3 id=&quot;2启动sentinel&quot;&gt;2、启动sentinel&lt;/h3&gt;
&lt;p&gt;修改完配置文件以后，在三个文件夹中依次运行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-sentinel ./sentinel.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动后查看26379日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;2561:X 15 Jul 2025 13:26:06.421 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2561:X 15 Jul 2025 13:26:06.422 # Redis version=6.2.1, bits=64, commit=00000000, modified=0, pid=2561, just started
2561:X 15 Jul 2025 13:26:06.422 # Configuration loaded
2561:X 15 Jul 2025 13:26:06.422 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2561:X 15 Jul 2025 13:26:06.422 * monotonic clock: POSIX clock_gettime
2561:X 15 Jul 2025 13:26:06.423 * Running mode=sentinel, port=26379.
2561:X 15 Jul 2025 13:26:06.423 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2561:X 15 Jul 2025 13:26:06.423 # Sentinel ID is 3a03b019736dfe3fe57e78863b8039d2c84b7a3a
2561:X 15 Jul 2025 13:26:06.423 # +monitor master mymaster 127.0.0.1 6379 quorum 2
2561:X 15 Jul 2025 13:30:37.763 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
2561:X 15 Jul 2025 13:30:37.764 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后26380和26381逐个启动起来之后，26379会打印日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;2561:X 15 Jul 2025 13:39:23.767 * +sentinel sentinel 928560c4df01bf5b19ad19751fbeae80db947c18 127.0.0.1 26380 @ mymaster 127.0.0.1 6379
2561:X 15 Jul 2025 13:40:36.276 * +sentinel sentinel 9a84c1618c95ae74ed8000efee50be6a7bd4a73c 127.0.0.1 26381 @ mymaster 127.0.0.1 6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样即表示sentinel集群已经正确监控redis的master节点以及两个slave节点。现在我们有了三个sentinel实例以及一主两从三个redis实例，架构图如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/fbbf95ac3c2b4aea91c9b99b4dd28208.png&quot; alt=&quot;sentinel网络架构图&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;3部署验证&quot;&gt;3、部署验证&lt;/h3&gt;
&lt;p&gt;先使用命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ps -aux | grep redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看三个sentinel和三个redis实例进程是否都在线：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/2afe9b8232ba4db2a038d3248669f9b6.png&quot; alt=&quot;image-20250715141157285&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;没问题的话我们进行一个实验：突然把master干掉，看看sentinel会怎么做。&lt;/p&gt;
&lt;p&gt;第一步：打开三个窗口，使用&lt;code&gt;tail -f -n 200 sentinel.log&lt;/code&gt;命令实时查看日志&lt;/p&gt;
&lt;p&gt;第二步：使用&lt;code&gt;kill 1795&lt;/code&gt;杀掉redis master实例的进程&lt;/p&gt;
&lt;p&gt;查看三个sentinel实例的日志（可能得等待几秒钟才能看到日志）：&lt;/p&gt;
&lt;p&gt;sentinel1的日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/88785baebdc34539b75bb862d0c99145.png&quot; alt=&quot;image-20250715151601961&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;sentinel2的日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/5d5ba41b9083473e81d04059c7db7213.png&quot; alt=&quot;image-20250715151630188&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;sentinel3的日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/fb74a85688ff4c50812e5dc6896cf88f.png&quot; alt=&quot;image-20250715151655034&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;从日志中可以看到（日志详解参考附录部分），哨兵最终选举了sentinel3作为执行者执行了故障转移，6381被选择成为新的主节点，原来的6379被标记为主观下线了。&lt;/p&gt;
&lt;p&gt;使用redis-cli进入6381redis实例，查看其角色信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6381&amp;gt; role
1) &amp;quot;master&amp;quot;
2) (integer) 2655664
3) 1) 1) &amp;quot;127.0.0.1&amp;quot;
      2) &amp;quot;6380&amp;quot;
      3) &amp;quot;2655664&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，6381节点已经变成了master节点，而且它只有一个副本。&lt;/p&gt;
&lt;p&gt;接下来我们将6379重新上线，看看sentinel做了什么：&lt;/p&gt;
&lt;p&gt;三个sentinel实例打印了如下日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;2561:X 15 Jul 2025 15:20:42.236 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示消除了6379的主观下线状态，sentinel实例多了一行日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;2561:X 15 Jul 2025 15:20:52.245 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表示将6379变成6381实例的副本。&lt;/p&gt;
&lt;p&gt;再次进入6381 redis-cli，查看角色信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6381&amp;gt; role
1) &amp;quot;master&amp;quot;
2) (integer) 2729754
3) 1) 1) &amp;quot;127.0.0.1&amp;quot;
      2) &amp;quot;6380&amp;quot;
      3) &amp;quot;2729621&amp;quot;
   2) 1) &amp;quot;127.0.0.1&amp;quot;
      2) &amp;quot;6379&amp;quot;
      3) &amp;quot;2729754&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到6379确实已经成为了6381的副本。&lt;/p&gt;
&lt;p&gt;至此，我们验证了sentinel确实发挥了作用。&lt;/p&gt;
&lt;h2 id=&quot;三sentinel故障转移原理&quot;&gt;三、sentinel故障转移原理&lt;/h2&gt;
&lt;p&gt;我们的哨兵网络中有三个哨兵实例，每个实例都会独立检测除了自己之外的所有实例，包括redis实例以及sentinel实例，一旦主节点下线，每个哨兵都会发现，然后将其标记为“&lt;strong&gt;主观下线&lt;/strong&gt;”，表示当前哨兵认为主节点不可用，同时通知其它哨兵。当认为主节点主观下线状态的哨兵数量大于配置的&lt;code&gt;quorum&lt;/code&gt;时，会将该主节点标记为“&lt;strong&gt;客观下线&lt;/strong&gt;”。这时，将开始正式的故障转移流程。&lt;/p&gt;
&lt;p&gt;由于哨兵节点有多个，不可能都去执行故障转移操作，它们会先选举出来一个代表，具体的操作就是每个哨兵节点都从所有哨兵节点中&lt;strong&gt;投票&lt;/strong&gt;选举一个，票多者成为leader，成为leader的哨兵将具体执行故障转移。&lt;/p&gt;
&lt;p&gt;成为leader的哨兵先从所有的slave节点中选择合适的节点向其发送&lt;code&gt;slaveof no one&lt;/code&gt;命令，使其成为&lt;strong&gt;主节点&lt;/strong&gt;；之后重新设置其它节点，让其它节点成为该主节点的&lt;strong&gt;从节点&lt;/strong&gt;（包括已下线的原主节点）。&lt;/p&gt;
&lt;p&gt;最后，所有哨兵更新主节点和从节点信息，完成故障转移流程。&lt;/p&gt;
&lt;p&gt;我们着重看下这个流程中的选举哨兵leader以及选举新的master节点的细节。&lt;/p&gt;
&lt;h3 id=&quot;1选举哨兵leader&quot;&gt;1、选举哨兵leader&lt;/h3&gt;
&lt;p&gt;Redis哨兵模式中的Leader选举是一个基于Raft算法的分布式一致性过程，用于在主节点故障时选举出一个负责协调故障转移的哨兵领导者。&lt;/p&gt;
&lt;p&gt;哨兵Leader选举的触发条件是主节点被确认为**客观下线(ODown)**状态。这需要先经过：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主观下线(SDown)&lt;/strong&gt;：单个哨兵通过心跳检测(PING命令)发现主节点无响应，该哨兵将主节点状态标记为主观下线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客观下线(ODown)&lt;/strong&gt;：当quorum数量的哨兵(通常为多数)都认为主节点不可用，达成共识，所有哨兵将该节点状态标记为客观下线。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选举采用了Raft算法，其核心要点包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;**纪元(epoch)**机制：类似于Raft的term概念，每次选举会递增epoch值，确保选举过程有序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;先到先得&lt;/strong&gt;原则：最先发起投票请求的哨兵有更大几率成为Leader&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多数决原则&lt;/strong&gt;：候选人需要获得超过半数的哨兵投票才能当选&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哨兵确认主节点ODown后，将自己的current_epoch加1，然后将leader字段指向自己，表明要参与竞选；随后向其他所有哨兵发送&lt;code&gt;sentinel is-master-down-by-addr&lt;/code&gt;命令，附带自己的epoch和runID。**每个哨兵节点只能投一票，遵循&amp;quot;先到先投&amp;quot;原则。**收到请求的哨兵会比较请求中的epoch与自己的current_epoch：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果请求epoch更大，则更新自己的epoch并将票投给请求方&lt;/li&gt;
&lt;li&gt;如果epoch相同，则投给自己已记录的leader(可能是自己)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;已投过票的哨兵会拒绝后续请求。&lt;/p&gt;
&lt;p&gt;最终，得票最多的哨兵将成为leader。&lt;/p&gt;
&lt;p&gt;在我们的例子中，有三个哨兵节点实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel1：3a03b019736dfe3fe57e78863b8039d2c84b7a3a
sentinel2：928560c4df01bf5b19ad19751fbeae80db947c18
sentinel3：9a84c1618c95ae74ed8000efee50be6a7bd4a73c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个哨兵节点均发现了主节点主观下线的事实，然后将该消息传播给了其它哨兵节点。&lt;/p&gt;
&lt;p&gt;哨兵1最先得到消息，但是并没有得到所有消息，它只知道包括自己在内，有两个哨兵已经发现主节点主观下线了，并且触达了&lt;code&gt;quorum&lt;/code&gt;阈值，主节点已经达到了客观下线的条件。所以它立即将纪元值自增1并发起投票，由于它觉得是自己发现主节点客观下线的，所以它将票投给了自己：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/df202692b9aa427d8ee9c7ca6308f628.png&quot; alt=&quot;image-20250715164507820&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;其实哨兵1不知道的是，哨兵3则知道3个哨兵的主节点主观下线状态了，所以哨兵3和哨兵1一样，都以为自己是第一个发现主节点客观下线的，所以它也将纪元新增1并且发起了投票，同样的，它将票投给了自己：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/da12b90f209446b79dbbe66c7ec02d3a.png&quot; alt=&quot;image-20250715164839378&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;所以得胜票在哨兵2手里，哨兵2先收到谁的投票请求，它就会将票投给谁：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/b7a251f4be5c4580978852088cc5528e.png&quot; alt=&quot;image-20250715165040352&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;它将票投给了哨兵3，这意味着它先收到了节点3的投票请求，节点3顺利成章成为了哨兵leader。&lt;/p&gt;
&lt;h3 id=&quot;2选择新的主节点&quot;&gt;2、选择新的主节点&lt;/h3&gt;
&lt;p&gt;选择新的主节点按照&lt;strong&gt;优先级 &amp;gt; offset &amp;gt; run id&lt;/strong&gt;的次序依次筛选。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优先级最高的从节点胜出。每个redis数据节点都会在配置文件中有一个优先级配置（slave-priority，默认情况下都相同）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复制偏移量offset最大的从节点胜出。offset代表从节点从主节点这里同步数据的进度。数值越大，说明从节点的数据和主节点就越接近。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;run id值更小的胜出。run id是每个redis节点启动的时候随机生成的一串数字（大小全凭缘分）。此时意味着优先级和offset都一样，那么选谁都可以，其实就是随便挑一个。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在我们的例子中，由于两个副本的优先级相同，有可能是6381的复制偏移量更大，也有可能是随机被选择到的，说不好是什么原因被选择到的。&lt;/p&gt;
&lt;h2 id=&quot;四sentinel管理命令&quot;&gt;四、sentinel管理命令&lt;/h2&gt;
&lt;p&gt;使用redis-cli可以像进入redis-server一样进入redis-sentinel执行各种命令，实际上sentinel的启动命令&lt;code&gt;./redis-sentinel ./sentinel.conf&lt;/code&gt;可以替换为&lt;code&gt;./redis-server ./sentinel.conf --sentinel&lt;/code&gt;，两者是等价的。&lt;/p&gt;
&lt;p&gt;进入sentinel控制台命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli -p 26379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入控制台以后，能运行哪些命令呢？可以使用&lt;code&gt;sentinel help&lt;/code&gt;命令查看有哪些子命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel help
 1) SENTINEL &amp;lt;subcommand&amp;gt; [&amp;lt;arg&amp;gt; [value] [opt] ...]. Subcommands are:
 2) CKQUORUM &amp;lt;master-name&amp;gt;
 3)     Check if the current Sentinel configuration is able to reach the quorum
 4)     needed to failover a master and the majority needed to authorize the
 5)     failover.
 6) CONFIG SET &amp;lt;param&amp;gt; &amp;lt;value&amp;gt;
 7)     Set a global Sentinel configuration parameter.
 8) CONFIG GET &amp;lt;param&amp;gt;
 9)     Get global Sentinel configuration parameter.
10) GET-MASTER-ADDR-BY-NAME &amp;lt;master-name&amp;gt;
11)     Return the ip and port number of the master with that name.
12) FAILOVER &amp;lt;master-name&amp;gt;
13)     Manually failover a master node without asking for agreement from other
14)     Sentinels
15) FLUSHCONFIG
16)     Force Sentinel to rewrite its configuration on disk, including the current
17)     Sentinel state.
18) INFO-CACHE &amp;lt;master-name&amp;gt;
19)     Return last cached INFO output from masters and all its replicas.
20) IS-MASTER-DOWN-BY-ADDR &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt; &amp;lt;current-epoch&amp;gt; &amp;lt;runid&amp;gt;
21)     Check if the master specified by ip:port is down from current Sentinel&apos;s
22)     point of view.
23) MASTER &amp;lt;master-name&amp;gt;
24)     Show the state and info of the specified master.
25) MASTERS
26)     Show a list of monitored masters and their state.
27) MONITOR &amp;lt;name&amp;gt; &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt; &amp;lt;quorum&amp;gt;
28)     Start monitoring a new master with the specified name, ip, port and quorum.
29) MYID
30)     Return the ID of the Sentinel instance.
31) PENDING-SCRIPTS
32)     Get pending scripts information.
33) REMOVE &amp;lt;master-name&amp;gt;
34)     Remove master from Sentinel&apos;s monitor list.
35) REPLICAS &amp;lt;master-name&amp;gt;
36)     Show a list of replicas for this master and their state.
37) RESET &amp;lt;pattern&amp;gt;
38)     Reset masters for specific master name matching this pattern.
39) SENTINELS &amp;lt;master-name&amp;gt;
40)     Show a list of Sentinel instances for this master and their state.
41) SET &amp;lt;master-name&amp;gt; &amp;lt;option&amp;gt; &amp;lt;value&amp;gt;
42)     Set configuration paramters for certain masters.
43) SIMULATE-FAILURE (CRASH-AFTER-ELECTION|CRASH-AFTER-PROMOTION|HELP)
44)     Simulate a Sentinel crash.
45) HELP
46)     Prints this help.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sentinel-masters&quot;&gt;sentinel masters&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sentinel masters&lt;/code&gt;命令用于获取所有被监视主服务器的信息。从命令形式中就可以看出，实际上sentinel网络可以监控多个master实例，只需要在配置文件中配置好就可以。&lt;code&gt;sentinel masters&lt;/code&gt;命令输出如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel masters
1)  1) &amp;quot;name&amp;quot;
    2) &amp;quot;mymaster&amp;quot;
    3) &amp;quot;ip&amp;quot;
    4) &amp;quot;127.0.0.1&amp;quot;
    5) &amp;quot;port&amp;quot;
    6) &amp;quot;6381&amp;quot;
    7) &amp;quot;runid&amp;quot;
    8) &amp;quot;a63914648545d8922778cc3a6bfcc5eefc383260&amp;quot;
    9) &amp;quot;flags&amp;quot;
   10) &amp;quot;master&amp;quot;
   11) &amp;quot;link-pending-commands&amp;quot;
   12) &amp;quot;0&amp;quot;
   13) &amp;quot;link-refcount&amp;quot;
   14) &amp;quot;1&amp;quot;
   15) &amp;quot;last-ping-sent&amp;quot;
   16) &amp;quot;0&amp;quot;
   17) &amp;quot;last-ok-ping-reply&amp;quot;
   18) &amp;quot;730&amp;quot;
   19) &amp;quot;last-ping-reply&amp;quot;
   20) &amp;quot;730&amp;quot;
   21) &amp;quot;down-after-milliseconds&amp;quot;
   22) &amp;quot;30000&amp;quot;
   23) &amp;quot;info-refresh&amp;quot;
   24) &amp;quot;6813&amp;quot;
   25) &amp;quot;role-reported&amp;quot;
   26) &amp;quot;master&amp;quot;
   27) &amp;quot;role-reported-time&amp;quot;
   28) &amp;quot;11141987&amp;quot;
   29) &amp;quot;config-epoch&amp;quot;
   30) &amp;quot;1&amp;quot;
   31) &amp;quot;num-slaves&amp;quot;
   32) &amp;quot;2&amp;quot;
   33) &amp;quot;num-other-sentinels&amp;quot;
   34) &amp;quot;2&amp;quot;
   35) &amp;quot;quorum&amp;quot;
   36) &amp;quot;2&amp;quot;
   37) &amp;quot;failover-timeout&amp;quot;
   38) &amp;quot;180000&amp;quot;
   39) &amp;quot;parallel-syncs&amp;quot;
   40) &amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形成表格如下所示：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;字段名&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;示例值&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;含义解释&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;mymaster&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;主服务器（master）的名称，由用户在 sentinel 配置中定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;ip&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;127.0.0.1&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;主服务器的 IP 地址。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;port&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;6381&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;主服务器的端口号。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;runid&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;a63914648545d...&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;主服务器的唯一运行 ID，用于标识实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;flags&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;master&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;当前角色标识，&lt;code&gt;master&lt;/code&gt; 表示主节点；可能包含其他状态（如 &lt;code&gt;s_down&lt;/code&gt;、&lt;code&gt;o_down&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;link-pending-commands&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;sentinel向服务器发送了命令之后，仍在等待回复的命令数量，通常为 0 表示无阻塞。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;link-refcount&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;1&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;当前连接到主服务器的引用计数（如 sentinel 或客户端连接数）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;last-ping-sent&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;距离 sentinel最后一次向服务器发送PING 命令之后消逝的毫秒数，0表示已发送&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;last-ok-ping-reply&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;730&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;服务器最后一次向sentinel 返回有效PING 命令回复之后消逝的毫秒数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;last-ping-reply&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;730&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;服务器最后一次向 sentinel 返回 PING 命令回复之后消逝的毫秒数，一般与last-ok-ping-reply相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;down-after-milliseconds&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;30000&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;判定主节点不可用的超时时间（毫秒），超过此时间未响应则标记为主观下线（SDOWN）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;info-refresh&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;6813&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;上一次从主节点获取 INFO 信息的时间间隔（毫秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;role-reported&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;master&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;主节点自身报告的角色（应与 &lt;code&gt;flags&lt;/code&gt; 一致）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;role-reported-time&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;11141987&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;上一次角色报告的时间戳（毫秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;config-epoch&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;1&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;配置版本号，用于故障转移时标识新主节点的唯一性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;num-slaves&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;2&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;当前主节点的从服务器（slave）数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;num-other-sentinels&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;2&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;监控此主节点的其他 sentinel 实例数量（不包含当前 sentinel）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;quorum&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;2&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;达成客观下线（ODOWN）所需的最小 sentinel 同意数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;failover-timeout&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;180000&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;故障转移超时时间（毫秒），超过此时间未完成则放弃本次故障转移&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;parallel-syncs&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;&amp;quot;1&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;故障转移后，允许同时向新主节点同步数据的从节点数量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;sentinel-master&quot;&gt;sentinel master&lt;/h3&gt;
&lt;p&gt;该命令的完整格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel master &amp;lt;master-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令只会返回用户指定主服务器的相关信息。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel master mymaster
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询的是mymaster主服务器的相关信息。返回数据格式和sentinel masters相同。&lt;/p&gt;
&lt;h3 id=&quot;sentinel-sentinels&quot;&gt;sentinel sentinels&lt;/h3&gt;
&lt;p&gt;该命令用于获取其他sentinel的相关信息，其完整命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel sentinels &amp;lt;master-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令输出如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel sentinels mymaster
1)  1) &amp;quot;name&amp;quot;
    2) &amp;quot;928560c4df01bf5b19ad19751fbeae80db947c18&amp;quot;
    3) &amp;quot;ip&amp;quot;
    4) &amp;quot;127.0.0.1&amp;quot;
    5) &amp;quot;port&amp;quot;
    6) &amp;quot;26380&amp;quot;
    7) &amp;quot;runid&amp;quot;
    8) &amp;quot;928560c4df01bf5b19ad19751fbeae80db947c18&amp;quot;
    9) &amp;quot;flags&amp;quot;
   10) &amp;quot;sentinel&amp;quot;
   11) &amp;quot;link-pending-commands&amp;quot;
   12) &amp;quot;0&amp;quot;
   13) &amp;quot;link-refcount&amp;quot;
   14) &amp;quot;1&amp;quot;
   15) &amp;quot;last-ping-sent&amp;quot;
   16) &amp;quot;0&amp;quot;
   17) &amp;quot;last-ok-ping-reply&amp;quot;
   18) &amp;quot;813&amp;quot;
   19) &amp;quot;last-ping-reply&amp;quot;
   20) &amp;quot;813&amp;quot;
   21) &amp;quot;down-after-milliseconds&amp;quot;
   22) &amp;quot;30000&amp;quot;
   23) &amp;quot;last-hello-message&amp;quot;
   24) &amp;quot;145&amp;quot;
   25) &amp;quot;voted-leader&amp;quot;
   26) &amp;quot;?&amp;quot;
   27) &amp;quot;voted-leader-epoch&amp;quot;
   28) &amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该输出和sentinel masters基本相同，有三项新增：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;last-hello-message&lt;/td&gt;
&lt;td&gt;距离当前 sentinel 最后一次从这个 sentinel那里收到问候信息之后，逝去的毫秒数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;voted-leader&lt;/td&gt;
&lt;td&gt;sentinel网络当前票选出来的sentinel首领(leader)，?表示目前无首领&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;voted-leader-epoch&lt;/td&gt;
&lt;td&gt;sentinel首领当前所处的配置纪元&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;sentinel-get-master-addr-by-name&quot;&gt;sentinel get-master-addr-by-name&lt;/h3&gt;
&lt;p&gt;该命令可以通过给定主服务器的名字来获取该服务器的IP地址以及端口号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel get-master-addr-by-name &amp;lt;master-name&amp;gt;        
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如想获取mymaster节点的ip地址和端口号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel GET-MASTER-ADDR-BY-NAME mymaster
1) &amp;quot;127.0.0.1&amp;quot;
2) &amp;quot;6381&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sentinel-reset&quot;&gt;sentinel reset&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sentinel reset&lt;/code&gt;命令可以让sentinel忘掉主服务器之前的记录，并重新开始对主服务器进行监视，其完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel reset &amp;lt;pattern&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pattern可以使用通配符一次选定多个被监视的master节点。&lt;/p&gt;
&lt;p&gt;sentinel reset命令我觉得是个最有用的命令了，sentinel会将运行中的数据实时写入sentinel.conf文件中，下次重启的时候会利用这些数据恢复上次运行时的状态，要命的是这时候往往很多节点的状态已经发生了变化，最主要的是有可能部分节点已经下线或者id已经发生了变化，这时候运行该命令可以让sentinel删除所有数据，重新开始对主服务器监视，相当于“关机重启”了。&lt;/p&gt;
&lt;p&gt;运行该命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel reset mymaster
(integer) 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sentinel日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/a1dd5949ace349419b3f34d1a68cd5fc.png&quot; alt=&quot;image-20250715180146802&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;运行了sentinel reset命令的哨兵节点会重新建立和其他哨兵的联系，并重新监视主节点，添加从节点。&lt;/p&gt;
&lt;h3 id=&quot;sentinel-failover&quot;&gt;sentinel failover&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sentinel failover&lt;/code&gt;的作用是使用当前哨兵节点强制执行故障转移，其完整命令为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel failover &amp;lt;master-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般来说，sentinel网络需要主节点故障之后触发故障转移流程，哨兵节点还需要先投票选举leader最后才能执行故障转移。使用&lt;code&gt;sentinel failover&lt;/code&gt;命令则不需要主节点发生故障，也不需要选举，它将指定当前哨兵节点强制执行故障转移。&lt;/p&gt;
&lt;p&gt;比如我们想使用sentinel1对mymaster实行强制故障转移，可以使用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel failover mymaster
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sentinel1的日志：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/0a44f4fe6ccf458a9cc60bcb481f1257.png&quot; alt=&quot;image-20250715221110643&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，开启了新纪元2，并且6379服务重新变成了主节点。&lt;/p&gt;
&lt;h3 id=&quot;sentinel-ckquorum&quot;&gt;sentinel ckquorum&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sentinel ckquorum&lt;/code&gt;命令用于检查sentinel网络当前可用的sentinel数量是否达到了判断主服务器客观下线并实施故障转移所需的数量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel ckquorum &amp;lt;master-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel ckquorum mymaster
OK 3 usable sentinels. Quorum and failover authorization can be reached
127.0.0.1:26379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从结果可以看到，sentinel网络目前有3个sentinel可用，这已经满足了判断mymaster客观下线所需的sentinel数量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sentinel ckquorum&lt;/code&gt;命令一般用于检查sentinel网络的部署是否成功。比如，如果我们在部署了3个sentinel之后，却发现&lt;code&gt;sentinel ckquorum&lt;/code&gt;只能识别到2个可用的sentinel，那就说明有什么地方出错了。&lt;/p&gt;
&lt;h3 id=&quot;sentinel-flushconfig&quot;&gt;sentinel flushconfig&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sentinel flushconfig&lt;/code&gt;命令用于将配置文件重新写入硬盘。&lt;/p&gt;
&lt;p&gt;因为sentinel在被监视服务器的状态发生变化时就会自动重写配置文件，所以这个命令的作用就是在配置文件基于某些原因或错误而丢失时，立即生成一个新的配置文件。此外，当sentinel的配置选项发生变化时，sentinel内部也会使用这个命令创建新的配置文件来替换原有的配置文件。&lt;/p&gt;
&lt;p&gt;这个命令在误删除配置文件的时候非常有用。&lt;/p&gt;
&lt;h3 id=&quot;sentinel-monitor&quot;&gt;sentinel monitor&lt;/h3&gt;
&lt;p&gt;该命令用于监视一个新的主服务器，完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel monitor &amp;lt;master-name&amp;gt; &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt; &amp;lt;quorum&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sentinel monitor命令本质上就是sentinel monitor配置选项的命令版本，当我们想要让sentinel监视一个新的主服务器，但是又不想重启sentinel并手动修改sentinel配置文件时就可以使用这个命令。&lt;/p&gt;
&lt;p&gt;比如，我们想监控127.0.0.1:6379主服务器，而且达成客观下线所需的最小 sentinel 同意数为2，则可以运行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel monitor mymaster 127.0.0.1 6379 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sentinel-remove&quot;&gt;sentinel remove&lt;/h3&gt;
&lt;p&gt;该命令用于取消对指定主服务器的监控，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel remove &amp;lt;masters-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收到这个命令的sentinel会停止对给定主服务器的监视，并删除sentinel内部以及sentinel配置文件中与给定主服务器有关的所有信息，然后返回OK表示操作执行成功。&lt;/p&gt;
&lt;p&gt;举例：移除对mymaster主节点的监控&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel remove mymaster
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sentinel-set&quot;&gt;sentinel set&lt;/h3&gt;
&lt;p&gt;该命令用于在线修改sentinel配置文件中与主服务器相关的配置选项值，完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel set &amp;lt;master-name&amp;gt; &amp;lt;option&amp;gt; &amp;lt;value&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要是sentinel配置文件中与主服务器有关的配置选项，都可以使用&lt;code&gt;sentinel set&lt;/code&gt;命令在线进行配置。命令在成功修改给定的配置选项值之后将返回OK作为结果。&lt;/p&gt;
&lt;p&gt;可以先使用&lt;code&gt;sentinel masters&lt;/code&gt;命令查看有哪些参数以及值是多少，然后使用该命令修改指定的参数值。&lt;/p&gt;
&lt;p&gt;比如，我要修改&lt;code&gt;failover-timeout&lt;/code&gt;的值，可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel set mymaster failover-timeout 120000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意该修改并不会传播给其它哨兵节点，也就是说每个哨兵节点都需要执行一次这个命令才能实现配置同步。&lt;/p&gt;
&lt;h3 id=&quot;sentinel-myid&quot;&gt;sentinel myid&lt;/h3&gt;
&lt;p&gt;该命令用于查询当前哨兵节点的id号码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel myid
&amp;quot;3a03b019736dfe3fe57e78863b8039d2c84b7a3a&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sentinel-replicas&quot;&gt;sentinel replicas&lt;/h3&gt;
&lt;p&gt;该命令用于展示被监控的主节点的所有副本信息，完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentinel replicas &amp;lt;master-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:26379&amp;gt; sentinel replicas mymaster
1)  1) &amp;quot;name&amp;quot;
    2) &amp;quot;127.0.0.1:6381&amp;quot;
    3) &amp;quot;ip&amp;quot;
    4) &amp;quot;127.0.0.1&amp;quot;
    5) &amp;quot;port&amp;quot;
    6) &amp;quot;6381&amp;quot;
    7) &amp;quot;runid&amp;quot;
    8) &amp;quot;a63914648545d8922778cc3a6bfcc5eefc383260&amp;quot;
    9) &amp;quot;flags&amp;quot;
   10) &amp;quot;slave&amp;quot;
   11) &amp;quot;link-pending-commands&amp;quot;
   12) &amp;quot;0&amp;quot;
   13) &amp;quot;link-refcount&amp;quot;
   14) &amp;quot;1&amp;quot;
   15) &amp;quot;last-ping-sent&amp;quot;
   16) &amp;quot;0&amp;quot;
   17) &amp;quot;last-ok-ping-reply&amp;quot;
   18) &amp;quot;501&amp;quot;
   19) &amp;quot;last-ping-reply&amp;quot;
   20) &amp;quot;501&amp;quot;
   21) &amp;quot;down-after-milliseconds&amp;quot;
   22) &amp;quot;30000&amp;quot;
   23) &amp;quot;info-refresh&amp;quot;
   24) &amp;quot;502&amp;quot;
   25) &amp;quot;role-reported&amp;quot;
   26) &amp;quot;slave&amp;quot;
   27) &amp;quot;role-reported-time&amp;quot;
   28) &amp;quot;2029212&amp;quot;
   29) &amp;quot;master-link-down-time&amp;quot;
   30) &amp;quot;0&amp;quot;
   31) &amp;quot;master-link-status&amp;quot;
   32) &amp;quot;ok&amp;quot;
   33) &amp;quot;master-host&amp;quot;
   34) &amp;quot;127.0.0.1&amp;quot;
   35) &amp;quot;master-port&amp;quot;
   36) &amp;quot;6379&amp;quot;
   37) &amp;quot;slave-priority&amp;quot;
   38) &amp;quot;100&amp;quot;
   39) &amp;quot;slave-repl-offset&amp;quot;
   40) &amp;quot;5259427&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;五附录&quot;&gt;五、附录&lt;/h2&gt;
&lt;h3 id=&quot;哨兵故障转移日志详解&quot;&gt;哨兵故障转移日志详解&lt;/h3&gt;
&lt;p&gt;在上面的案例中，sentinel3完成了第一次故障转移：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/15/fb74a85688ff4c50812e5dc6896cf88f.png&quot; alt=&quot;image-20250715151655034&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;让我们挨个分析每行日志：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;故障检测阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;+sdown master mymaster 127.0.0.1 6379&lt;/code&gt;：哨兵将主节点标记为&lt;strong&gt;主观下线&lt;/strong&gt;(Subjectively Down)，表示当前哨兵认为主节点不可用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;+odown master mymaster 127.0.0.1 6379 #quorum 3/2&lt;/code&gt;：哨兵集群达成共识，将主节点标记为&lt;strong&gt;客观下线&lt;/strong&gt;(Objectively Down)，表示多个哨兵(3个中的2个)都认为主节点不可用&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;故障转移准备阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+new-epoch 1&lt;/code&gt;：开启新的配置纪元(epoch)，这是一个递增的计数器，用于标识集群配置的版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+try-failover master mymaster 127.0.0.1 6379&lt;/code&gt;：开始尝试进行故障转移&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;领导者选举阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;+vote-for-leader&lt;/code&gt;：哨兵节点开始投票选举领导者(leader)来执行故障转移操作&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;+elected-leader master mymaster 127.0.0.1 6379&lt;/code&gt;：成功选举出负责故障转移的哨兵领导者&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;从节点选择与提升阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+failover-state-select-slave&lt;/code&gt;：开始寻找合适的从节点进行提升&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+selected-slave slave 127.0.0.1:6381&lt;/code&gt;：选择了6381端口的从节点作为新的主节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+failover-state-send-slaveof-noone&lt;/code&gt;：向选中的从节点发送&lt;code&gt;SLAVEOF NO ONE&lt;/code&gt;命令，使其成为主节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+failover-state-wait-promotion&lt;/code&gt;：等待从节点被提升为主节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+promoted-slave slave 127.0.0.1:6381&lt;/code&gt;：从节点6381成功被提升为新的主节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重新配置从节点阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+failover-state-reconf-slaves&lt;/code&gt;：开始重新配置其他从节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+slave-reconf-sent&lt;/code&gt;：向其他从节点(6380)发送命令，使其复制新的主节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+slave-reconf-inprog&lt;/code&gt;：从节点正在重新配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+slave-reconf-done&lt;/code&gt;：从节点完成重新配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;故障转移完成阶段&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-odown master mymaster 127.0.0.1 6379&lt;/code&gt;：清除主节点的客观下线状态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+failover-end master mymaster 127.0.0.1 6379&lt;/code&gt;：故障转移顺利完成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+switch-master mymaster 127.0.0.1 6379 127.0.0.1 6381&lt;/code&gt;：更新主节点信息，从6379切换到6381&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;后续处理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+slave slave 127.0.0.1:6380&lt;/code&gt;：将6380节点添加为新主节点的从节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+slave slave 127.0.0.1:6379&lt;/code&gt;：原主节点6379恢复后，被添加为新主节点的从节点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+sdown slave 127.0.0.1:6379&lt;/code&gt;：原主节点6379因为尚未完全恢复被标记为主观下线&lt;/li&gt;
&lt;/ul&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（七）：多机部署之主从复制模式</title>
      <link>https://blog.kdyzm.cn/post/318</link>
      <guid>https://blog.kdyzm.cn/post/318</guid>
      <pubDate>Mon, 14 Jul 2025 10:29:18 +0800</pubDate>
      <description>&lt;p&gt;Redis多机部署有三种模式：主从复制模式、Sentinel（哨兵）模式、Cluster模式，本篇文章将基于Redis6.2.1讲解主从复制模式的部署、使用、常见问题等。&lt;/p&gt;
&lt;h2 id=&quot;一主从复制模式的部署&quot;&gt;一、主从复制模式的部署&lt;/h2&gt;
&lt;p&gt;一般来说主从复制模式要用多台机器部署，由于资源有限，下面在一台机器上部署一主两从的主从复制架构。其架构图如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/91f591d1e9404bc98d8db93e00fcb2ed.png&quot; alt=&quot;redis主从复制模式架构图&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;1独立启动三个服务&quot;&gt;1、独立启动三个服务&lt;/h3&gt;
&lt;p&gt;创建3个文件夹：6379、6380、6381，参考单台Redis的安装教程：&lt;a href=&quot;https://blog.kdyzm.cn/post/28&quot;&gt;CentOS安装Redis&lt;/a&gt; （只看前三步到运行即可），将redis.conf配置文件以及redis-server、redis-cli配置文件复制到每个文件夹&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tree&quot;&gt;├── 6379
│   ├── redis-cli
│   ├── redis.conf
│   ├── redis-server
├── 6380
│   ├── redis-cli
│   ├── redis.conf
│   ├── redis-server
└── 6381
    ├── redis-cli
    ├── redis.conf
    └── redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后逐个使用命令&lt;code&gt;./reids-server ./redis.conf&lt;/code&gt;启动服务，每个服务的启动日志类似如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;2520:C 11 Jul 2025 11:11:08.314 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2520:C 11 Jul 2025 11:11:08.314 # Redis version=6.2.1, bits=64, commit=00000000, modified=0, pid=2520, just started
2520:C 11 Jul 2025 11:11:08.314 # Configuration loaded
2520:M 11 Jul 2025 11:11:08.316 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2520:M 11 Jul 2025 11:11:08.316 * monotonic clock: POSIX clock_gettime
2520:M 11 Jul 2025 11:11:08.317 * Running mode=standalone, port=6379.
2520:M 11 Jul 2025 11:11:08.317 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2520:M 11 Jul 2025 11:11:08.317 # Server initialized
2520:M 11 Jul 2025 11:11:08.317 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add &apos;vm.overcommit_memory = 1&apos; to /etc/sysctl.conf and then reboot or run the command &apos;sysctl vm.overcommit_memory=1&apos; for this to take effect.
2520:M 11 Jul 2025 11:11:08.318 * Ready to accept connections
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这时候启动的三个服务全都是主服务器，即master节点。&lt;/strong&gt; 它们都是独立的server，相互之间并没有任何关系。&lt;/p&gt;
&lt;h3 id=&quot;2主从模式架构部署&quot;&gt;2、主从模式架构部署&lt;/h3&gt;
&lt;p&gt;上一步已经启动了三个独立的Redis Server，现在将6379端口号的服务作为master节点，将6380、6381端口号的服务作为从节点做主从复制模式配置，其结构图如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/25ae012d32da4966ac93a85eaed4a0ae.png&quot; alt=&quot;image-20250711162825265&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h4 id=&quot;方法一replicaof命令&quot;&gt;方法一：replicaof命令&lt;/h4&gt;
&lt;p&gt;第一种方式就是使用redis-cli连接6380、6381服务，运行 &lt;code&gt;replicaof&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;REPLICAOF localhost 6379 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以6380服务运行该命令为例，运行该命令的时候，6379服务的server.log日志会打印如下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;2520:M 11 Jul 2025 11:22:13.107 * Replica [::1]:6380 asks for synchronization
2520:M 11 Jul 2025 11:22:13.107 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for &apos;a1659b41814b4c2efffd56d4e1d4eb175a66f458&apos;, my replication IDs are &apos;2cd002f50c115cfad4235840a1d16b047d05c097&apos; and &apos;0000000000000000000000000000000000000000&apos;)
2520:M 11 Jul 2025 11:22:13.107 * Replication backlog created, my new replication IDs are &apos;63830caba228eb487be0aaaf561cee0258e21c3e&apos; and &apos;0000000000000000000000000000000000000000&apos;
2520:M 11 Jul 2025 11:22:13.107 * Starting BGSAVE for SYNC with target: disk
2520:M 11 Jul 2025 11:22:13.141 * Background saving started by pid 2580
2580:C 11 Jul 2025 11:22:13.169 * DB saved on disk
2580:C 11 Jul 2025 11:22:13.169 * RDB: 4 MB of memory used by copy-on-write
2520:M 11 Jul 2025 11:22:13.198 * Background saving terminated with success
2520:M 11 Jul 2025 11:22:13.198 * Synchronization with replica [::1]:6380 succeeded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从日志中可以看到，6380运行&lt;code&gt;preplicaof&lt;/code&gt;命令后，6379服务首先运行&lt;code&gt;BGSAVE&lt;/code&gt;命令将自己的完整数据以RDB的格式备份到了硬盘上，然后将该文件传输给了6380服务。&lt;/p&gt;
&lt;p&gt;看看6380服务的日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;2529:S 11 Jul 2025 11:22:13.105 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
2529:S 11 Jul 2025 11:22:13.105 * Connecting to MASTER localhost:6379
2529:S 11 Jul 2025 11:22:13.106 * MASTER &amp;lt;-&amp;gt; REPLICA sync started
2529:S 11 Jul 2025 11:22:13.106 * REPLICAOF localhost:6379 enabled (user request from &apos;id=3 addr=127.0.0.1:32840 laddr=127.0.0.1:6380 fd=8 name= age=63 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=44 qbuf-free=40910 argv-mem=22 obl=0 oll=0 omem=0 tot-mem=61486 events=r cmd=replicaof user=default redir=-1&apos;)
2529:S 11 Jul 2025 11:22:13.106 * Non blocking connect for SYNC fired the event.
2529:S 11 Jul 2025 11:22:13.106 * Master replied to PING, replication can continue...
2529:S 11 Jul 2025 11:22:13.107 * Trying a partial resynchronization (request a1659b41814b4c2efffd56d4e1d4eb175a66f458:1).
2529:S 11 Jul 2025 11:22:13.167 * Full resync from master: 63830caba228eb487be0aaaf561cee0258e21c3e:0
2529:S 11 Jul 2025 11:22:13.167 * Discarding previously cached master state.
2529:S 11 Jul 2025 11:22:13.198 * MASTER &amp;lt;-&amp;gt; REPLICA sync: receiving 175 bytes from master to disk
2529:S 11 Jul 2025 11:22:13.198 * MASTER &amp;lt;-&amp;gt; REPLICA sync: Flushing old data
2529:S 11 Jul 2025 11:22:13.198 * MASTER &amp;lt;-&amp;gt; REPLICA sync: Loading DB in memory
2529:S 11 Jul 2025 11:22:13.200 * Loading RDB produced by version 6.2.1
2529:S 11 Jul 2025 11:22:13.200 * RDB age 0 seconds
2529:S 11 Jul 2025 11:22:13.200 * RDB memory usage when created 1.83 Mb
2529:S 11 Jul 2025 11:22:13.200 * MASTER &amp;lt;-&amp;gt; REPLICA sync: Finished with success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6380进行了一次全量数据同步：6380接收到主服务器6379发送过来的数据保存到了硬盘上，之后清空自己所有的旧数据，然后从硬盘加载了RDB数据文件。&lt;/p&gt;
&lt;h4 id=&quot;方法二配置文件&quot;&gt;方法二：配置文件&lt;/h4&gt;
&lt;p&gt;使用上述方法一可以暂时实现主从模式架构的部署，但是服务器一重启就失效了，为了能永久有效，需要将该命令写入配置文件：修改6380和6381服务的redis.conf配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;replicaof localhost 6379
masterauth &amp;lt;master-password&amp;gt;  #如果主服务器开启password需要新增这项配置
masteruser &amp;lt;username&amp;gt;		#如果主服务器开启了ACL则需要新增这项配置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该配置项和命令是一样的。&lt;/p&gt;
&lt;p&gt;这样修改完成后，再重新启动redis就不会丢失主从模式架构的配置了。&lt;/p&gt;
&lt;h3 id=&quot;3主从模式架构验证&quot;&gt;3、主从模式架构验证&lt;/h3&gt;
&lt;p&gt;上面已经将一主两从的主从复制模式架构搭建好了，现在验证下是否已经成功，如果我们在主服务器上设置了一个key，在从服务器上能查询到该key，则表示主从模式架构已经生效了。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/758bad581e62465fb9ef0b87209b31b8.gif&quot; alt=&quot;redis主从复制模式&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到主服务器新增的key，已经顺利同步到了两个从服务器上，表示我们主从复制模式的架构已经搭建好了。&lt;/p&gt;
&lt;h4 id=&quot;role查看服务器角色&quot;&gt;role：查看服务器角色&lt;/h4&gt;
&lt;p&gt;用户可以通过执行role命令查看服务的当前角色。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主服务器执行role命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; role
1) &amp;quot;master&amp;quot;         
2) (integer) 12577
3) 1) 1) &amp;quot;::1&amp;quot;
      2) &amp;quot;6380&amp;quot;
      3) &amp;quot;12563&amp;quot;
   2) 1) &amp;quot;::1&amp;quot;
      2) &amp;quot;6381&amp;quot;
      3) &amp;quot;12577&amp;quot;
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个元素master表示是主服务器角色&lt;/li&gt;
&lt;li&gt;第二个元素12577是复制偏移量（replication offset），它是一个整数，记录了主服务器目前向复制数据流发送的数据数量&lt;/li&gt;
&lt;li&gt;第三个元素是一个数组，表示主服务器属下的从服务器。每个元素都由三个子元素组成：
&lt;ul&gt;
&lt;li&gt;第1个子元素为从服务器的IP地址&lt;/li&gt;
&lt;li&gt;第2个子元素为从服务器的端口号&lt;/li&gt;
&lt;li&gt;第3个子元素则为从服务器的复制偏移量。从服务器的复制偏移量记录了从服务器通过复制数据流接收到的复制数据数量，当从服务器的复制偏移量与主服务器的复制偏移量保持一致时，它们的数据就是一致的。在这个例子中并不一致，这是正常的，因为从服务器发送数据给从服务器，从服务器需要时间同步，再者也存在网络延迟。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;从服务器执行role命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;localhost:6381&amp;gt; role
1) &amp;quot;slave&amp;quot;
2) &amp;quot;localhost&amp;quot;
3) (integer) 6379
4) &amp;quot;connected&amp;quot;
5) (integer) 13095
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数组的第1个元素是字符串&amp;quot;slave&amp;quot;，它表示这个服务器的角色是从服务器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组的第2个元素和第3个元素记录了这个从服务器正在复制的主服务器的IP地址和端口号。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组的第4个元素是主服务器与从服务器当前的连接状态，这个状态的值及其表示的意义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;none&amp;quot;：主从服务器尚未建立连接。&lt;/li&gt;
&lt;li&gt;&amp;quot;connect&amp;quot;：主从服务器正在握手。&lt;/li&gt;
&lt;li&gt;&amp;quot;connecting&amp;quot;：主从服务器成功建立了连接。&lt;/li&gt;
&lt;li&gt;&amp;quot;sync&amp;quot;：主从服务器正在进行数据同步。&lt;/li&gt;
&lt;li&gt;&amp;quot;connected&amp;quot;：主从服务器已经进入在线更新状态。&lt;/li&gt;
&lt;li&gt;&amp;quot;unknown&amp;quot;：主从服务器连接状态未知。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数组的第5个元素是从服务器当前的复制偏移量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;info-replication&quot;&gt;info replication&lt;/h4&gt;
&lt;p&gt;在之前的文章&lt;a href=&quot;https://blog.kdyzm.cn/post/312&quot;&gt;Redis（一）：Redis数据类型和常用命令&lt;/a&gt; 介绍过info命令，其中info命令有个子命令&lt;code&gt;info replication&lt;/code&gt;可以查看主从复制信息，包括角色（master/slave）、从节点列表、复制偏移量、延迟等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在主服务器上执行&lt;code&gt;info replication&lt;/code&gt;命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; info replication
# Replication
role:master
connected_slaves:2
slave0:ip=::1,port=6380,state=online,offset=14201,lag=1
slave1:ip=::1,port=6381,state=online,offset=14201,lag=1
master_failover_state:no-failover
master_replid:63830caba228eb487be0aaaf561cee0258e21c3e
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14201
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14201
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从该输出上可以看出该节点的角色信息、从节点数量以及从节点的ip和端口号、状态、延迟等信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在从服务器上执行&lt;code&gt;info replication&lt;/code&gt;命令：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;localhost:6381&amp;gt; info replication
# Replication
role:slave
master_host:localhost
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:14901
slave_priority:100
slave_read_only:1
connected_slaves:0
master_failover_state:no-failover
master_replid:63830caba228eb487be0aaaf561cee0258e21c3e
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14901
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:10235
repl_backlog_histlen:4667
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;replicaof-no-one&quot;&gt;replicaof no one&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;replicaof no one&lt;/code&gt; 在从服务器上执行之后，会将从服务器停止复制，并变回主服务器。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/9dea2cfb504c4a26a6b87f49202e885c.gif&quot; alt=&quot;replicaof no one命令&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;二数据同步机制&quot;&gt;二、数据同步机制&lt;/h2&gt;
&lt;p&gt;当用户将一个服务器设置为从服务器，让它去复制另一个服务器的时候，主从服务器需要通过数据同步机制来让两个服务器的数据库状态保持一致。&lt;/p&gt;
&lt;h3 id=&quot;1完整同步&quot;&gt;1、完整同步&lt;/h3&gt;
&lt;p&gt;当一个Redis服务器接收到REPLICAOF命令，开始对另一个服务器进行复制的时候，主从服务器会执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;主服务器执行BGSAVE命令，生成一个RDB文件，并使用缓冲区存储起在BGSAVE命令之后执行的所有写命令。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当RDB文件创建完毕，主服务器会通过套接字将RDB文件传送给从服务器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从服务器在接收完主服务器传送过来的RDB文件之后，就会载入这个RDB文件，从而获得主服务器在执行BGSAVE命令时的所有数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当从服务器完成RDB文件载入操作，并开始上线接受命令请求时，主服务器就会把之前存储在缓存区中的所有写命令发送给从服务器执行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为主服务器存储的写命令都是在执行BGSAVE命令之后执行的，所以当从服务器载入完RDB文件，并执行完主服务器存储在缓冲区中的所有写命令之后，主从服务器包含的数据库数据将完全相同。&lt;/p&gt;
&lt;p&gt;这个通过创建、传送并载入RDB文件来达成数据一致的步骤，我们称之为完整同步操作。每个从服务器在刚开始进行复制的时候，都需要与主服务器进行一次完整同步。&lt;/p&gt;
&lt;p&gt;完整流程图如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/dbee64df9f3848f3ba8fa66bf35ff540.png&quot; alt=&quot;redis主从复制模式全量同步时序图&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;2部分同步&quot;&gt;2、部分同步&lt;/h3&gt;
&lt;p&gt;主从服务器在执行完完整同步操作之后，它们的数据就达到了一致状态，但这种一致并不是永久的：每当主服务器执行了新的写命令之后，它的数据库就会被改变，这时主从服务器的数据一致性就会被破坏。&lt;/p&gt;
&lt;p&gt;为了让主从服务器的数据一致性可以保持下去，让它们一直拥有相同的数据，Redis会对从服务器进行在线更新：每当主服务器执行完一个写命令之后，它就会将相同的写命令或者具有相同效果的写命令发送给从服务器执行。&lt;/p&gt;
&lt;p&gt;只要从服务器一直与主服务器保持连接，在线更新操作就会不断进行，使得从服务器的数据库可以一直被更新，并与主服务器的数据库保持一致。&lt;/p&gt;
&lt;h4 id=&quot;数据一致性问题&quot;&gt;数据一致性问题&lt;/h4&gt;
&lt;p&gt;每当主服务器执行完一个写命令之后，它就会将相同的写命令或者具有相同效果的写命令发送给从服务器执行以保持主从数据的一致性。但是由于网络延迟以及数据处理需要时间，在主服务器执行完写命令之后，直到从服务器也执行完相同写命令的这段时间里，主从服务器的数据库将出现短暂的不一致。&lt;/p&gt;
&lt;p&gt;此外，主服务器也可能因故障下线，这时候可能还没来得及将同步命令发送出去，这同样可能会导致出现主从数据不一致的情况。&lt;/p&gt;
&lt;p&gt;可以看到，数据一致性问题在主从复制模式中是没有办法杜绝的，对于数据一致性要求比较高的场景，最好直接读主服务器。&lt;/p&gt;
&lt;p&gt;redis提供了两个参数设置，可以尽可能的降低出现数据不一致情况发生的概率：&lt;code&gt;min-replicas-to-write&lt;/code&gt;、&lt;code&gt;min-replicas-max-lag&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;min-replicas-to-write&lt;/code&gt; 和 &lt;code&gt;min-replicas-max-lag&lt;/code&gt; 搭配使用可以实现&lt;strong&gt;主节点写入熔断&lt;/strong&gt;，确保数据同步的可靠性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;min-replicas-to-write&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：定义主节点（master）在执行写操作时，要求至少连接的从节点（replica）数量。如果当前连接的从节点数少于该值，主节点会拒绝写请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：例如，设置为&lt;code&gt;3&lt;/code&gt;时，主节点会检查是否有至少3个从节点处于正常连接状态。若不满足条件，写操作将被拒绝，从而避免数据写入后无法同步到足够多的从节点，降低数据丢失风险&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认值&lt;/strong&gt;：未显式配置时，主节点不会检查从节点数量，可能导致数据不一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;min-replicas-max-lag&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：设置从节点与主节点之间的最大复制延迟（秒数）。若从节点的延迟超过该阈值，主节点会将其视为“不可用”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：例如，设置为&lt;code&gt;10&lt;/code&gt;时，主节点会检查所有从节点的复制延迟是否在10秒内。若延迟超过10秒的从节点数量不满足&lt;code&gt;min-replicas-to-write&lt;/code&gt;的要求，主节点同样会拒绝写请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认值&lt;/strong&gt;：未配置时，主节点不会检查延迟&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个参数通常配合使用，主要解决以下问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;异步复制的数据丢失&lt;/strong&gt;：Redis默认异步复制，主节点不会等待从节点确认写操作。通过这两个参数，可以确保写入时至少有N个从节点处于低延迟状态，提高数据可靠性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脑裂问题&lt;/strong&gt;：当网络分区导致主节点与部分从节点断开时，旧主节点可能继续接收写请求，但新主节点已被选举。通过限制写条件，旧主节点会因不满足从节点数量或延迟要求而拒绝写入，减少数据不一致&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;min-replicas-to-write 2
min-replicas-max-lag 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示：至少需要2个从节点连接且延迟不超过10秒，主节点才允许写入。&lt;/p&gt;
&lt;h4 id=&quot;从节点重连&quot;&gt;从节点重连&lt;/h4&gt;
&lt;p&gt;在主从复制模式中，如果从节点突然挂掉了，在挂掉的期间主节点还做了很多写操作，从节点重新上线后会发生什么？&lt;/p&gt;
&lt;p&gt;当一个Redis服务器成为另一个服务器的主服务器时，它会把每个被执行的写命令都记录到一个特定长度的先进先出队列（复制积压缓冲区）中。当断线的从服务器尝试重新连接主服务器的时候，主服务器将检查从服务器断线期间，被执行的那些写命令是否仍然保存在队列里面。如果是，那么主服务器就会直接把从服务器缺失的那些写命令发送给从服务器执行，从服务器通过执行这些写命令就可以重新与主服务器保持一致，这样就避免了重新进行完整同步的麻烦。&lt;/p&gt;
&lt;p&gt;现在模拟下这个过程，已知现在的架构如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/25ae012d32da4966ac93a85eaed4a0ae.png&quot; alt=&quot;image-20250711162825265&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;我现在模拟这个过程，将6380从节点使用&lt;code&gt;shutdown&lt;/code&gt;命令退出redis进程，之后6379服务执行一个写命令，看看6380服务重新上线之后会发生什么。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/11/e24ba371a7e544cc9dbebcf3ea7c8d49.gif&quot; alt=&quot;redis主从处模式从节点重连&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;主服务的日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;1842:M 11 Jul 2025 16:43:38.816 # Connection with replica [::1]:6380 lost.
1842:M 11 Jul 2025 16:43:57.618 * Replica [::1]:6380 asks for synchronization
1842:M 11 Jul 2025 16:43:57.618 * Partial resynchronization request from [::1]:6380 accepted. Sending 47 bytes of backlog starting from offset 1810.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从服务的日志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;1951:S 11 Jul 2025 16:43:57.616 * Loading RDB produced by version 6.2.1
1951:S 11 Jul 2025 16:43:57.616 * RDB age 19 seconds
1951:S 11 Jul 2025 16:43:57.617 * RDB memory usage when created 1.85 Mb
1951:S 11 Jul 2025 16:43:57.617 * DB loaded from disk: 0.001 seconds
1951:S 11 Jul 2025 16:43:57.617 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
1951:S 11 Jul 2025 16:43:57.617 * Ready to accept connections
1951:S 11 Jul 2025 16:43:57.617 * Connecting to MASTER localhost:6379
1951:S 11 Jul 2025 16:43:57.618 * MASTER &amp;lt;-&amp;gt; REPLICA sync started
1951:S 11 Jul 2025 16:43:57.618 * Non blocking connect for SYNC fired the event.
1951:S 11 Jul 2025 16:43:57.618 * Master replied to PING, replication can continue...
1951:S 11 Jul 2025 16:43:57.618 * Trying a partial resynchronization (request 6cb93c7859fe76e9e1e334fe799eda6fdb55fffc:1810).
1951:S 11 Jul 2025 16:43:57.619 * Successful partial resynchronization with master.
1951:S 11 Jul 2025 16:43:57.619 * MASTER &amp;lt;-&amp;gt; REPLICA sync: Master accepted a Partial Resynchronization.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从节点服务重启的时候先从本地加载RDB文件恢复之前的数据，连接主服务器成功以后尝试发起部分同步请求，主服务器接受了部分同步请求发送从服务器断线以后丢失的命令数据，从服务器最终同步成功。&lt;/p&gt;
&lt;h3 id=&quot;3复制积压缓冲区&quot;&gt;3、复制积压缓冲区&lt;/h3&gt;
&lt;p&gt;上面的部分同步功能很依赖于主服务器的复制积压缓冲区，复制积压缓冲区越大，意味着从节点可以离线的时间越长。复制积压缓冲区的设置可以在主服务器的redis.conf配置文件中设置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;repl-backlog-size 1mb  
repl-backlog-ttl 3600  
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;repl-backlog-size&quot;&gt;repl-backlog-size&lt;/h4&gt;
&lt;p&gt;设置复制积压缓冲区的大小。积压缓冲区是一个缓存区，用于在从节点（replica）因某些原因断开连接时累积主节点的数据。这样，当从节点重新连接时，通常不需要执行完整的全量同步（full resync），而只需通过部分同步（partial resync）传递从节点在断开期间缺失的数据部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;复制积压缓冲区越大，从节点能容忍的断开时间越长&lt;/strong&gt;，后续也更可能通过部分同步恢复数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;该缓冲区仅在至少有一个从节点连接时才会分配内存空间&lt;/strong&gt;，默认值是1Mb。&lt;/p&gt;
&lt;h4 id=&quot;repl-backlog-ttl&quot;&gt;repl-backlog-ttl&lt;/h4&gt;
&lt;p&gt;当主节点（master）在一段时间内没有任何连接的从节点（replica）时，积压缓冲区（backlog）将被释放。该配置项用于设定从最后一个从节点断开连接开始，经过多少秒后积压缓冲区会被自动释放。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：从节点（replica）永远不会因超时而释放积压缓冲区，因为它们后续可能被提升为主节点，必须确保能够与其他从节点正确进行“部分重新同步”（partial resynchronization）。因此，从节点会始终保留积压缓冲区。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;值为0表示永不释放积压缓冲区&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;默认值：&lt;code&gt;3600&lt;/code&gt;（即缓冲区在最后一个从节点断开后1小时释放）&lt;/p&gt;
&lt;h3 id=&quot;4无需硬盘的复制&quot;&gt;4、无需硬盘的复制&lt;/h3&gt;
&lt;p&gt;主服务器在进行完整同步的时候，需要在本地创建RDB文件，然后通过套接字将这个RDB文件传送给从服务器。但是，如果主服务器所在宿主机器的硬盘负载非常大或者性能不佳，那么创建RDB文件引起的大量硬盘写入将对主服务器的性能造成影响，并导致复制进程变慢。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，Redis从2.8.18版本开始引入无须硬盘的复制特性（diskless replication）：启用了这个特性的主服务器在接收到REPLICAOF命令时将不会再在本地创建RDB文件，而是会派生出一个子进程，然后由子进程通过套接字直接将RDB文件写入从服务器。这样主服务器就可以在不创建RDB文件的情况下，完成与从服务器的数据同步。&lt;/p&gt;
&lt;p&gt;要使用无须硬盘的复制特性，可以使用如下配置&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;repl-diskless-sync no #核心配置
repl-diskless-sync-delay 5
repl-diskless-load disabled
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;repl-diskless-sync&quot;&gt;repl-diskless-sync&lt;/h4&gt;
&lt;p&gt;该配置项的值为&lt;code&gt;no&lt;/code&gt;时使用rdb传输，为&lt;code&gt;yes&lt;/code&gt;时使用无盘传输。&lt;/p&gt;
&lt;p&gt;该配置项为节点，如果无法通过增量同步继续复制过程，则需要执行&lt;strong&gt;全量同步（full synchronization）&lt;/strong&gt;。此时，主节点会生成RDB文件并传输给从节点。&lt;/p&gt;
&lt;p&gt;RDB文件的传输可通过两种方式实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;磁盘备份模式（Disk-backed）
&lt;ul&gt;
&lt;li&gt;主节点创建一个子进程，将RDB文件写入磁盘。&lt;/li&gt;
&lt;li&gt;父进程随后将磁盘中的RDB文件增量传输给从节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无盘模式（Diskless）：
&lt;ul&gt;
&lt;li&gt;主节点创建一个子进程，直接通过套接字（socket）将RDB文件流式传输给从节点，&lt;strong&gt;完全不经过磁盘&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;两种模式的差异&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;磁盘备份模式&lt;/strong&gt;：生成RDB文件期间，新连接的从节点会排队等待，当前子进程完成后即可共享同一RDB文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无盘模式&lt;/strong&gt;：传输一旦开始，新到达的从节点需等待当前传输完成后才能启动新一轮传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;无盘模式的延迟配置&lt;/strong&gt;：
启用无盘复制时，主节点会等待一段可配置的时间（秒），以期望更多从节点加入，从而实现并行传输（默认值：&lt;code&gt;repl-diskless-sync-delay 5&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;：在&lt;strong&gt;磁盘性能较差但网络带宽较高&lt;/strong&gt;的环境下，无盘复制的性能更优。&lt;/p&gt;
&lt;h4 id=&quot;repl-diskless-sync-delay&quot;&gt;repl-diskless-sync-delay&lt;/h4&gt;
&lt;p&gt;该配置项的值为整数，单位秒，表示当启用无盘复制（diskless replication）时，主节点在启动RDB传输子进程前等待的延迟时间（单位为秒）。这一延迟的目的是让更多从节点能够加入当前的同步批次，从而避免重复生成RDB文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要性&lt;/strong&gt;：一旦RDB传输开始，新连接的从节点将无法立即加入同步，只能排队等待下一次RDB传输。因此，主节点通过延迟等待，尽可能让更多从节点在同一批次完成同步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;默认值&lt;/strong&gt;：5秒。若设置为0秒，则主节点会立即开始传输RDB，不等待任何从节点加入。&lt;/p&gt;
&lt;h4 id=&quot;repl-diskless-load&quot;&gt;repl-diskless-load&lt;/h4&gt;
&lt;p&gt;该配置项用于从服务器节点RDB误判加载功能的开启。它有三种值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;disabled&lt;/code&gt;：禁用无盘加载，优先将RDB写入磁盘再加载（默认安全选项）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;on-empty-db&lt;/code&gt;：仅在数据库为空时启用无盘加载（完全安全时使用）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;swapdb&lt;/code&gt;：解析套接字数据时，在内存中保留当前数据库的副本。&lt;strong&gt;需确保内存充足，否则可能触发OOM&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们知道使用&lt;code&gt;repl-diskless-sync&lt;/code&gt;配置项可以让主服务器实现无盘复制，但是从服务器默认还是需要先下载rdb文件再从rdb文件恢复数据的。&lt;code&gt;repl-diskless-load&lt;/code&gt;配置项则可以让从服务器节点实现无盘加载，即从从服务器获取到rdb文件后不下载到本地，直接从内存恢复数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：RDB无盘加载是实验性功能&lt;/strong&gt;。
在此配置下，从节点（replica）不会立即将RDB文件存储到磁盘，可能导致故障转移期间的数据丢失。此外，若Redis模块未处理I/O读取，在与主节点初始同步阶段出现I/O错误时，Redis可能会中止。&lt;strong&gt;如果能接受该风险，就可以使用该项功能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优劣分析&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无盘加载的优势&lt;/strong&gt;：
磁盘速度通常低于网络，存储和加载RDB文件可能增加复制时间（甚至增加主节点的写时复制内存和从节点缓冲区压力）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无盘加载的风险&lt;/strong&gt;：
直接从套接字解析RDB文件时，可能需要在完整接收RDB前清空当前数据库内容，导致数据不一致或内存不足（OOM）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;三可写的从服务器&quot;&gt;三、可写的从服务器&lt;/h2&gt;
&lt;p&gt;从Redis 2.6版本开始，Redis的从服务器在默认状态下只允许执行读命令。如果用户尝试对一个只读从服务器执行写命令，那么服务器将拒绝写入。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/14/cf1ae91143224869aa5daa2707ab8dd2.png&quot; alt=&quot;image-20250714093231175&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;Redis之所以将从服务器默认设置为只读服务器，是为了确保从服务器只能通过与主服务器进行数据同步来得到更新，从而保证主从服务器之间的数据一致性。&lt;/p&gt;
&lt;p&gt;但在某些情况下，我们可能想要将一些不太重要或者临时性的数据存储在从服务器中，或者不得不在从服务器中执行一些带有写性质的命令（比如ZINTERSTORE命令，它只能将计算结果存储在数据库中，不能直接返回计算结果）。这时我们可以通过将replica-read-only配置选项的值设置为no来打开从服务器的写功能：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;replica-read-only &amp;lt;yes|no&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意该配置是作用在从服务器上的。&lt;/p&gt;
&lt;p&gt;使用从服务器写功能可能会导致和主服务器写的key冲突，这点需要注意，在从服务器上写入key时尽量设置ttl从而保证和主服务器的数据一致性。&lt;/p&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（六）：Redis整合Lua</title>
      <link>https://blog.kdyzm.cn/post/317</link>
      <guid>https://blog.kdyzm.cn/post/317</guid>
      <pubDate>Tue, 08 Jul 2025 14:08:33 +0800</pubDate>
      <description>&lt;p&gt;lua是一种轻量小巧的脚本语言，用标准C语言编写并以源代码形式开放， 其设计目的是为了嵌入应用程序中，从而为应用程序提供灵活的扩展和定制功能。&lt;/p&gt;
&lt;h2 id=&quot;一使用lua脚本的好处&quot;&gt;一、使用Lua脚本的好处&lt;/h2&gt;
&lt;p&gt;Redis与Lua脚本的整合为开发者提供了一种在Redis服务器端执行复杂逻辑的强大能力，这种组合不仅保证了操作的原子性，还能显著减少网络开销，提升系统性能。关于Lua，可以参考 &lt;a href=&quot;https://www.runoob.com/lua/lua-tutorial.html&quot;&gt;Lua教程&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.原子性保障&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Redis执行Lua脚本时具有天然的原子性，脚本中的所有命令会作为一个整体执行，要么全部成功，要么全部失败。这对于需要多步操作的业务场景至关重要。相比传统的&lt;code&gt;WATCH/MULTI/EXEC&lt;/code&gt;事务机制，Lua脚本能更好地规避网络波动导致的部分命令执行失败问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.性能优化&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;减少网络开销&lt;/strong&gt;：通过将多个命令打包成一个脚本执行，有效减少网络通信次数。测试表明，对于需要连续执行5个命令的操作，使用脚本可将网络延迟降低80%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;脚本缓存&lt;/strong&gt;：Lua脚本在Redis中会被缓存，后续调用直接使用SHA1摘要执行即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lua脚本支持条件判断、循环等复杂控制结构，能够实现超出单一Redis命令能力的业务逻辑，如分布式锁、限流算法、复杂事务等。&lt;/p&gt;
&lt;h2 id=&quot;二redis中的脚本命令&quot;&gt;二、Redis中的脚本命令&lt;/h2&gt;
&lt;p&gt;redis中关于脚本相关的命令一共分为两类：&lt;code&gt;EVAL&lt;/code&gt;开头的脚本执行相关的命令以及&lt;code&gt;SCRIPT&lt;/code&gt;相关的子命令系列。&lt;/p&gt;
&lt;h3 id=&quot;1redis调用lua脚本&quot;&gt;1、redis调用lua脚本&lt;/h3&gt;
&lt;p&gt;eval命令用于redis执行lua脚本，一共有两个命令：eval命令以及evalsha命令。&lt;/p&gt;
&lt;h4 id=&quot;eval&quot;&gt;eval&lt;/h4&gt;
&lt;p&gt;eval命令用于直接执行lua脚本，其语法格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;EVAL script numkeys key [key ...] arg [arg ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相关参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;script&lt;/code&gt;&lt;/strong&gt;：必填参数，是一段Lua脚本代码。脚本无需（也不应）定义为Lua函数，直接编写逻辑即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;numkeys&lt;/code&gt;&lt;/strong&gt;：指定后续键名参数（&lt;code&gt;key&lt;/code&gt;）的数量，必须为非负整数。若为0，则表示脚本不操作任何键。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;key [key ...]&lt;/code&gt;&lt;/strong&gt;：从第三个参数开始，按&lt;code&gt;numkeys&lt;/code&gt;指定的数量传递键名。这些键在Lua脚本中通过全局数组&lt;code&gt;KEYS&lt;/code&gt;访问，索引从1开始（如&lt;code&gt;KEYS[1]&lt;/code&gt;、&lt;code&gt;KEYS[2]&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;arg [arg ...]&lt;/code&gt;&lt;/strong&gt;：附加参数，在Lua脚本中通过全局数组&lt;code&gt;ARGV&lt;/code&gt;访问（如&lt;code&gt;ARGV[1]&lt;/code&gt;、&lt;code&gt;ARGV[2]&lt;/code&gt;，注意索引是从1开始），用于传递动态值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; eval &amp;quot;return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}&amp;quot; 2 key1 key2 arg1 arg2
key1
key2
arg1
arg2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;evalsha&quot;&gt;evalsha&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;EVALSHA&lt;/code&gt; 是 Redis 中用于执行预加载 Lua 脚本的命令，通过脚本的 SHA1 摘要值调用已缓存的脚本，避免重复传输脚本内容，提升执行效率。其语法格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;EVALSHA sha1 numkeys key [key ...] arg [arg ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相关参数如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sha1&lt;/code&gt;&lt;/strong&gt;：Lua 脚本的 SHA1 校验和，通过 &lt;code&gt;SCRIPT LOAD&lt;/code&gt; 命令预先加载脚本后生成&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;numkeys&lt;/code&gt;&lt;/strong&gt;：指定后续键名参数（&lt;code&gt;key&lt;/code&gt;）的数量，必须为非负整数。若为 0，表示脚本不操作任何键。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;key [key ...]&lt;/code&gt;&lt;/strong&gt;：脚本中使用的 Redis 键名，通过 &lt;code&gt;KEYS&lt;/code&gt; 数组在 Lua 脚本中访问（如 &lt;code&gt;KEYS[1]&lt;/code&gt;、&lt;code&gt;KEYS[2]&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;arg [arg ...]&lt;/code&gt;&lt;/strong&gt;：附加参数，通过 &lt;code&gt;ARGV&lt;/code&gt; 数组在 Lua 脚本中访问（如 &lt;code&gt;ARGV[1]&lt;/code&gt;、&lt;code&gt;ARGV[2]&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到evalsha命令和eval命令很相似，唯一的区别就是第二个参数上，eval命令操作的是script本身，而evalsha操作的是script的sha1校验和。因此，使用evalsha命令需要先获取script的sha1校验和。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; SCRIPT LOAD &amp;quot;return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}&amp;quot;
a42059b356c875f0717db19a51f6aaca9ae659ea
127.0.0.1:6379&amp;gt; evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 key1 key2 arg1 arg2
key1
key2
arg1
arg2
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2lua脚本调用redis&quot;&gt;2、lua脚本调用redis&lt;/h3&gt;
&lt;p&gt;上面介绍了在redis中如何调用lua脚本，那么在lua脚本中如何调用redis命令实现与redis server交互呢？&lt;/p&gt;
&lt;p&gt;在lua脚本中使用&lt;code&gt;redis.call&lt;/code&gt;和&lt;code&gt;redis.pcall&lt;/code&gt;执行redis命令。&lt;/p&gt;
&lt;h4 id=&quot;rediscall&quot;&gt;redis.call&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;redis.call()&lt;/code&gt; 是 Redis Lua 脚本中用于执行 Redis 命令的核心函数，其语法格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;redis.call(&amp;quot;命令名称&amp;quot;, 参数1, 参数2, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;‌&lt;strong&gt;命令名称&lt;/strong&gt;‌（字符串）：
Redis 命令的名称（如 &lt;code&gt;&amp;quot;SET&amp;quot;&lt;/code&gt;、&lt;code&gt;&amp;quot;GET&amp;quot;&lt;/code&gt;、&lt;code&gt;&amp;quot;HMSET&amp;quot;&lt;/code&gt; 等），需严格遵循 Redis 命令规范。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;‌参数&lt;/p&gt;
&lt;p&gt;命令所需的参数，数量和类型取决于具体命令。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redis.call(&amp;quot;SET&amp;quot;, &amp;quot;key&amp;quot;, &amp;quot;value&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redis.call(&amp;quot;HGETALL&amp;quot;, &amp;quot;user:1001&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;返回值&lt;/strong&gt;：‌&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回 Redis 命令的执行结果（类型取决于命令）：字符串（如 &lt;code&gt;GET&lt;/code&gt;）、整数（如 &lt;code&gt;INCR&lt;/code&gt;）、数组（如 &lt;code&gt;HGETALL&lt;/code&gt;）、nil（键不存在时）等。&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;若命令执行失败&lt;/strong&gt;‌（如语法错误、键不存在等），会抛出 Lua 错误并中断脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;举例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; eval &amp;quot;return redis.call(&apos;set&apos;,KEYS[1],ARGV[1])&amp;quot; 1 name zhangsan
OK
127.0.0.1:6379&amp;gt; get name
zhangsan
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;redispcall&quot;&gt;redis.pcall&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;redis.pcall&lt;/code&gt;中的p指的是&amp;quot;&lt;strong&gt;protected&lt;/strong&gt;&amp;quot;，即&lt;strong&gt;受保护的调用&lt;/strong&gt;，它的语法格式和redis.call一模一样，但是和redis.call不同的是，redis.pcall命令遇到错误会继续执行脚本，类似于java中的&amp;quot;try catch&amp;quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#redis.call正常调用的返回
127.0.0.1:6379&amp;gt; eval &amp;quot;return redis.call(&apos;hset&apos;,&apos;zhagnsan&apos;,&apos;name&apos;,&apos;zhangsan&apos;)&amp;quot; 0
1
127.0.0.1:6379&amp;gt; 
#redis.call报错的返回
127.0.0.1:6379&amp;gt; eval &amp;quot;return redis.call(&apos;hset&apos;,&apos;zhagnsan&apos;)&amp;quot; 0
ERR Error running script (call to f_9f8009f869f9b829b0aa06a921626f6e8312fb97): @user_script:1: @user_script: 1: Wrong number of args calling Redis command From Lua script
#redis.pcall的返回
127.0.0.1:6379&amp;gt; eval &amp;quot;return redis.pcall(&apos;hset&apos;,&apos;zhagnsan&apos;)&amp;quot; 0
@user_script: 1: Wrong number of args calling Redis command From Lua script
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3script命令&quot;&gt;3、SCRIPT命令&lt;/h3&gt;
&lt;p&gt;redis中script命令用于管理lua脚本，其有如下子命令：&lt;code&gt;script load&lt;/code&gt;、&lt;code&gt;script exists&lt;/code&gt;、&lt;code&gt;script flush&lt;/code&gt;、&lt;code&gt;script kill&lt;/code&gt;、&lt;code&gt;script debug&lt;/code&gt;、&lt;code&gt;script help&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&quot;script-load&quot;&gt;script load&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;script load&lt;/code&gt;命令用于将lua脚本预加载到redis服务器并返回脚本的sha1校验和，其命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;script LOAD 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; script load &apos;return {1,2,3,4}&apos;
b70175b4152c3db555480fca35498644b6b16f01
127.0.0.1:6379&amp;gt; evalsha b70175b4152c3db555480fca35498644b6b16f01 0
1
2
3
4
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;script-exists&quot;&gt;script exists&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;script exists&lt;/code&gt;命令用于检查某个脚本是否存在，其命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;script EXISTS &amp;lt;sha1&amp;gt; [&amp;lt;sha1&amp;gt; ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; script exists b70175b4152c3db555480fca35498644b6b16f01
1
127.0.0.1:6379&amp;gt; script exists b70175b4152c3db555480fca35498644b6b16f00
0 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;script-flush&quot;&gt;script flush&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;script flush&lt;/code&gt;命令用于清空redis预加载的脚本，其命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;script FLUSH [ASYNC|SYNC]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; script exists b70175b4152c3db555480fca35498644b6b16f01
1
127.0.0.1:6379&amp;gt; script flush
OK
127.0.0.1:6379&amp;gt; script exists b70175b4152c3db555480fca35498644b6b16f01
0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;script-kill&quot;&gt;script kill&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;script kill&lt;/code&gt;命令用于终止正在执行的脚本（仅当脚本未执行写操作时有效），其命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;script kill
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; script kill
NOTBUSY No scripts in execution right now.

127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;script-debug&quot;&gt;script debug&lt;/h4&gt;
&lt;p&gt;Redis的&lt;code&gt;SCRIPT DEBUG&lt;/code&gt;命令用于调试Lua脚本，是开发者排查脚本问题的重要工具。&lt;/p&gt;
&lt;p&gt;其命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SCRIPT DEBUG (YES|SYNC|NO)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;YES&lt;/code&gt;：开启非阻塞异步调试模式（输出到客户端）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SYNC&lt;/code&gt;：开启阻塞同步调试模式（会暂停脚本执行）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NO&lt;/code&gt;：关闭调试模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调试输出会包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;执行的Lua代码行号&lt;/li&gt;
&lt;li&gt;局部/全局变量值变化&lt;/li&gt;
&lt;li&gt;Redis命令调用详情&lt;/li&gt;
&lt;li&gt;返回值信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于debug命令对服务端影响较大，在生产环境务必保证&lt;code&gt;debug NO&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&quot;三分布式锁实现原理&quot;&gt;三、分布式锁实现原理&lt;/h2&gt;
&lt;p&gt;这里简单说下分布式锁的设计，先不说具体的实现了。&lt;/p&gt;
&lt;h3 id=&quot;1加锁&quot;&gt;1、加锁&lt;/h3&gt;
&lt;p&gt;在分布式锁的使用场景中，我们要求每个客户端在执行临界区代码之前要先获取锁，获取锁成功了才能进入临界区。使用&lt;code&gt;setnx&lt;/code&gt;命令似乎能完美解决这个问题：先判断key存不存在，不存在则设置key和value；如果存在则不作任何操作。但是我们加锁是需要超时时间的，setnx命令并不支持添加超时时间。&lt;/p&gt;
&lt;p&gt;可以使用set命令代替setNx命令，一般我们使用set命令都是简单使用&lt;code&gt;set key value&lt;/code&gt;设置一个值，实际上该命令的完整语法如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到set命令本身支持NX操作，实际上setNX是set命令中的一个特殊快捷方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;setNX key value = set key value NX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且set命令支持有效期参数，所以最好使用set命令实现加锁操作，比如，我们要加锁10秒钟，可以使用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;set lock:business:1 zhangsan EX 10 NX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;key是lock:business:1，value值是zhangsan表示是zhangsan进行的加锁（这里是zhangsan，代表的是锁的持有者，也可以是任意uuid，只要以后能解锁就行），加锁时间10秒钟，10秒钟之内如果无人解锁将自动解锁。&lt;/p&gt;
&lt;p&gt;set命令一个命令就完成了以下几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;key存不存在&lt;/li&gt;
&lt;li&gt;为key赋值&lt;/li&gt;
&lt;li&gt;添加有效期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而且&lt;strong&gt;set命令是原子操作不存在线程安全性问题，所以不需要使用lua脚本实现加锁&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&quot;2解锁&quot;&gt;2、解锁&lt;/h3&gt;
&lt;p&gt;解锁步骤分为两步：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：&lt;/strong&gt; 判断前来请求解锁的人是否是当前锁的持有者，如果不是锁的持有者来解锁应当不执行解锁操作&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：&lt;/strong&gt; 解锁，即删除key&lt;/p&gt;
&lt;p&gt;将redis解锁的过程翻译成需要执行的命令，可以等价于如下步骤：&lt;/p&gt;
&lt;p&gt;输入需要解锁的key以及key的值value（锁的持有者），redis根据输入的key使用&lt;code&gt;get&lt;/code&gt;命令获取到value值和输入的value比对，如果不相同，则不做任何操作；如果相同，则调用&lt;code&gt;del&lt;/code&gt;命令删除key实现解锁。&lt;/p&gt;
&lt;p&gt;可以看到，解锁涉及到多个步骤，客户端执行这些步骤无法保证其原子性，这时候就可以使用lua脚本了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;--如果key的值相等，则释放锁，这样保证：谁加的锁，谁释放。
if redis.call(&apos;GET&apos;,KEYS[1]) == ARGV[1]
then
	return redis.call(&apos;DEL&apos;,KEYS[1])
else
	return 0
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用eval命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;eval script 1 key value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以实现解锁了。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
      <category>分布式锁</category>
    </item>
    <item>
      <title>Redis（五）：Redis Stream</title>
      <link>https://blog.kdyzm.cn/post/316</link>
      <guid>https://blog.kdyzm.cn/post/316</guid>
      <pubDate>Thu, 03 Jul 2025 14:00:08 +0800</pubDate>
      <description>&lt;p&gt;在之前的文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/313&quot;&gt;Redis（二）：Redis消息队列&lt;/a&gt;》中已经介绍过Redis中使用List以及发布订阅两种模式实现的消息队列，其中发布订阅模式比List模式功能更强大，但是有很大的缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;消息没有持久化，如果消费者断线重连，消息会丢失&lt;/li&gt;
&lt;li&gt;没有ack机制，无法保证消息被成功消费。&lt;/li&gt;
&lt;li&gt;Redis服务重启后消息会丢失。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Redis Stream是在5.0版本引入的新消息队列，它能持久化消息并且带ACK机制，甚至还有类似Kafka一样的ConsumerGroup（消费者组）协同消费消息，可以说完全弥补了发布订阅模式的所有缺点。&lt;/p&gt;
&lt;p&gt;这篇文章单独把Redis Stream拿出来说，是因为我认为到目前为止，Redis Stream是Redis最复杂的类型了。Stream类型不像BitMap或者GEO操作底层存储还是五种基本数据类型之一，Stream类型的key使用type命令得到的类型就是&amp;quot;stream&amp;quot;，它是一个独立的类型。&lt;/p&gt;
&lt;p&gt;以下内容均基于Redis 6.2.1。&lt;/p&gt;
&lt;h2 id=&quot;一stream基本命令&quot;&gt;一、Stream基本命令&lt;/h2&gt;
&lt;h3 id=&quot;1xadd&quot;&gt;1、xadd&lt;/h3&gt;
&lt;p&gt;xadd命令用于向Stream中添加新消息，其完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XADD key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold [LIMIT count]] *|ID field value [field value ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令比较复杂，下面一部分一部分来看：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;key：&lt;/strong&gt; 指定要添加消息的 Stream 名称&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOMKSTREAM：&lt;/strong&gt; 可选参数，如果指定，当 key 不存在时不会自动创建新的 Stream&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;长度限制参数：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAXLEN [=|~] threshold&lt;/strong&gt;：限制 Stream 的最大长度，超过时会删除旧消息
&lt;ul&gt;
&lt;li&gt;= 表示精确限制（性能较低）&lt;/li&gt;
&lt;li&gt;~ 表示近似限制（性能更高，默认）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MINID [=|~] threshold&lt;/strong&gt;：只保留 ID 大于等于指定值的消息（Redis 6.2+）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LIMIT count&lt;/strong&gt;：与 MAXLEN/MINID 配合使用，限制每次修剪的消息数量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;消息ID&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;*：让 Redis 自动生成 ID（格式为&amp;lt;毫秒时间戳&amp;gt;-&amp;lt;序列号&amp;gt;，如1717020000000-5）&lt;/li&gt;
&lt;li&gt;手动指定 ID：必须大于 Stream 中已有最大 ID，否则会报错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;field value [field value ...]&lt;/strong&gt;：键值对形式的消息内容，可以包含多个字段&lt;/p&gt;
&lt;p&gt;举例：向Stream新增一条数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xadd stream * name zhangsan age 15 sex man
1751425687108-0  #返回消息id
127.0.0.1:6379&amp;gt; type stream
stream
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这里添加了多个field value，但是在Stream中还是一个元素。&lt;/p&gt;
&lt;h3 id=&quot;2xrange&quot;&gt;2、xrange&lt;/h3&gt;
&lt;p&gt;xrange用于按照消息 ID 的范围查询 Stream 中的消息。其具体命令格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xrange key start end [COUNT count]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;：要查询的 Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;start&lt;/strong&gt;：起始消息 ID（必填）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特殊值 &lt;code&gt;-&lt;/code&gt; 表示最小 ID（即 Stream 中的第一条消息）&lt;/li&gt;
&lt;li&gt;可以指定具体的消息 ID（如 &lt;code&gt;1526919030474-55&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;end&lt;/strong&gt;：结束消息 ID（必填）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特殊值 &lt;code&gt;+&lt;/code&gt; 表示最大 ID（即 Stream 中的最新消息）&lt;/li&gt;
&lt;li&gt;可以指定具体的消息 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[COUNT count]&lt;/strong&gt;：可选参数，限制返回的消息数量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不指定，默认返回范围内的所有消息&lt;/li&gt;
&lt;li&gt;指定后只返回最多 count 条消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：查询stream中的所有消息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xrange stream - +
1751425687108-0   #消息id，之后是消息体
name
zhangsan
age
15
sex
man
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3xrevrange&quot;&gt;3、xrevrange&lt;/h3&gt;
&lt;p&gt;xrevrange命令用于按照消息 ID 的反向顺序（从大到小）查询 Stream 中的消息。它与 xrange命令功能相似但遍历方向相反，特别适合获取最新的消息。其命令格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xrevrange key end start [COUNT count]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;：要查询的 Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;end&lt;/strong&gt;：结束消息 ID（必填）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特殊值 &lt;code&gt;+&lt;/code&gt; 表示最大 ID（即 Stream 中的最新消息）&lt;/li&gt;
&lt;li&gt;可以指定具体的消息 ID（如 &lt;code&gt;1526919030474-55&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;start&lt;/strong&gt;：起始消息 ID（必填）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特殊值 &lt;code&gt;-&lt;/code&gt; 表示最小 ID（即 Stream 中的第一条消息）&lt;/li&gt;
&lt;li&gt;可以指定具体的消息 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[COUNT count]&lt;/strong&gt;：可选参数，限制返回的消息数量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不指定，默认返回范围内的所有消息&lt;/li&gt;
&lt;li&gt;指定后只返回最多 count 条消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：查询stream中的数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xrevrange stream + -
1751425687108-0
name
zhangsan
age
15
sex
man
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4xlen&quot;&gt;4、xlen&lt;/h3&gt;
&lt;p&gt;xlen命令用于查询stream的长度，命令格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xlen key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;xlen命令比较简单，举例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xlen stream
1
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5xread&quot;&gt;5、xread&lt;/h3&gt;
&lt;p&gt;xread是 Redis Stream 数据结构中用于读取消息的核心命令，支持阻塞和非阻塞模式，能够从一个或多个 Stream 中获取消息。其命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xread [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;[COUNT count]&lt;/strong&gt;：可选参数，限制从每个 Stream 中返回的消息数量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[BLOCK milliseconds]&lt;/strong&gt;：可选参数，设置阻塞模式及超时时间（毫秒）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置为0表示无限期阻塞&lt;/li&gt;
&lt;li&gt;不设置此参数则为非阻塞模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;STREAMS key [key ...]&lt;/strong&gt;：指定要读取的一个或多个 Stream 名称&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ID [ID ...]&lt;/strong&gt;：指定每个 Stream 的起始消息ID&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt; 表示只读取最新消息（阻塞模式下常用）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;0-0&lt;/code&gt; 表示从第一条消息开始读取&lt;/li&gt;
&lt;li&gt;指定具体ID则读取该ID之后的消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;举例：非阻塞监听&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xread streams stream 0-0  #从第一条消息开始读取
stream
1751425687108-0 #第一条消息id
name
zhangsan
age
15
sex
man
1751436694812-0 #第二条消息id
zhangsan
age
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;举例：阻塞监听&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;阻塞监听有个问题，它和pub/sub模式不一样的是，pub/sub模式下的&lt;code&gt;subscribe&lt;/code&gt;命令会持续监听，而xread命令监听到一条消息之后命令就结束了。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/02/a741976a84ad410988888784d711a6f9.gif&quot; alt=&quot;redis的xread命令阻塞式案例&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;在实际使用中，为了能持续监听消费消息，而且不漏掉消息，需要记住消费的消息id，伪代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String lastId = &amp;quot;0-0&amp;quot;; // 初始从第一条消息开始
while(true) {
    // 从上次消费的位置继续读取
    Object msg = redis.execute(&amp;quot;XREAD COUNT 10 BLOCK 5000 STREAMS users &amp;quot; + lastId);
    if (msg == null) continue;
    // 更新最后消费的ID
    lastId = getLastIdFrom(msg); 
    handleMessage(msg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6xtrim&quot;&gt;6、xtrim&lt;/h3&gt;
&lt;p&gt;xtrim是 Redis Stream 数据结构中用于修剪(trim)流长度的命令，它可以限制 Stream 中保存的消息数量或基于消息ID进行裁剪。Stream的使用过程中，其消息长度会越来越大，应当定期使用xtrim命令对其进行修剪。&lt;/p&gt;
&lt;p&gt;xtrim命令的完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XTRIM key MAXLEN|MINID [=|~] threshold [LIMIT count]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;：Stream 的名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;策略选择&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAXLEN&lt;/strong&gt;：基于消息数量进行修剪&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MINID&lt;/strong&gt;：基于消息ID进行修剪（Redis 6.2+）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[=|~]&lt;/strong&gt;：精确或近似修剪模式&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt;：精确修剪（默认）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~&lt;/code&gt;：近似修剪（提高性能）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;threshold&lt;/strong&gt;：阈值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 MAXLEN：保留的最大消息数量&lt;/li&gt;
&lt;li&gt;对于 MINID：保留的最小消息ID（所有小于此ID的消息将被删除）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[LIMIT count]&lt;/strong&gt;：每次迭代删除的消息数量限制（Redis 6.2+）&lt;/p&gt;
&lt;h3 id=&quot;7xdel&quot;&gt;7、xdel&lt;/h3&gt;
&lt;p&gt;XDEL 是 Redis Stream 数据结构中用于删除指定消息的命令。其语法结构如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XDEL key ID [ID ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;XDEL 命令会从指定的 Stream 中删除给定的消息ID，删除的消息会从内存中释放，如果被删除的消息存在于任何消费者组的 Pending Entries List (PEL) 中，它也会从这些PEL中移除，但不会影响消费者组的 last_delivered_id。&lt;/p&gt;
&lt;h2 id=&quot;二消费者组&quot;&gt;二、消费者组&lt;/h2&gt;
&lt;p&gt;前面介绍了xread命令已经能够从消息队列中读取消息了，但是xread命令有几个很大的缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无法记住已经消费的消息。它每次消费必须指定id；若是指定$，则实际上还是和发布订阅模式有一样的问题，若是客户端连接中断则有可能漏掉部分消息。&lt;/li&gt;
&lt;li&gt;无ACK机制。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用消费者组则能解决以上问题。消费者组消费消息的示意图如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/03/463f41d4958849c9adb1a16cd7330902.png&quot; alt=&quot;Redis消费者组协同消费消息示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每个消费者组都有若干个消费者，这些消费者协同消费消息。若是开启ACK，每个消费者接收到消息之后会将消息id放入自己维护的pending_ids列表中，表示还未ACK，待ACK后就将处理完的消息从pending_ids列表中移除。另外，每个消费者组还维护了last_deliverred_id表示最后一个投递给消费者的id，下次再消费则从此处开始。&lt;/p&gt;
&lt;p&gt;如果某个消费者的pending_ids列表中有消息长时间没有ack，则可以将该消息重新转移给别的消费者让其重试解决。&lt;/p&gt;
&lt;h3 id=&quot;1xgroup&quot;&gt;1、xgroup&lt;/h3&gt;
&lt;p&gt;xgroup是 Redis Stream 中用于管理消费者组(Consumer Group)的核心命令，它提供了创建、配置和销毁消费者组的功能。其完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XGROUP 
[CREATE key groupname ID|$ [MKSTREAM]] 
[SETID key groupname ID|$] 
[DESTROY key groupname] 
[CREATECONSUMER key groupname consumername] 
[DELCONSUMER key groupname consumername]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，上述命令中的&lt;code&gt;ID|$&lt;/code&gt;实际上是&lt;code&gt;id-or-$&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&quot;create子命令&quot;&gt;create子命令&lt;/h4&gt;
&lt;p&gt;create子命令用于创建消费者组，完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XGROUP [CREATE key groupname ID|$ [MKSTREAM]] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;groupname&lt;/strong&gt;: 消费者组名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;id-or-$&lt;/strong&gt;: 起始消息ID（必填）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt; 表示只消费新消息（创建后添加的消息,当前Stream中的消息会全部忽略）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;0-0&lt;/code&gt; 表示从第一条消息开始消费&lt;/li&gt;
&lt;li&gt;指定具体ID则从该ID之后开始消费&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[MKSTREAM]&lt;/strong&gt;: 可选参数，如果Stream不存在则自动创建&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xgroup create stream consumer_group 0-0 #创建consumer_group消费者组从头开始消费stream的消息
OK
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;setid子命令&quot;&gt;setid子命令&lt;/h4&gt;
&lt;p&gt;setid子命令用于重置消费者组读取位置，实际上是修改了消费者组的 &lt;code&gt;last_delivered_id&lt;/code&gt;，完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XGROUP [SETID key groupname ID|$] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;groupname&lt;/strong&gt;: 消费者组名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;id-or-$&lt;/strong&gt;: 起始消息ID（必填）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt; 表示只消费新消息（创建后添加的消息）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;0-0&lt;/code&gt; 表示从第一条消息开始消费&lt;/li&gt;
&lt;li&gt;指定具体ID则从该ID之后开始消费&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;destroy子命令&quot;&gt;destroy子命令&lt;/h4&gt;
&lt;p&gt;destroy子命令用于删除消费者组，同时组内所有消费者也会被删除。完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XGROUP [DESTROY key groupname] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;groupname&lt;/strong&gt;: 消费者组名称（必填）&lt;/p&gt;
&lt;h4 id=&quot;createconsumer子命令&quot;&gt;createconsumer子命令&lt;/h4&gt;
&lt;p&gt;createconsumer子命令用于在现有消费者组中显式创建一个消费者，通常是不需要使用该命令显式创建消费者的，使用&lt;code&gt;XREADGROUP&lt;/code&gt;命令会自动创建消费者。createconsumer子命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XGROUP [CREATECONSUMER key groupname consumername] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;groupname&lt;/strong&gt;: 消费者组名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;consumername&lt;/strong&gt;：消费者名称&lt;/p&gt;
&lt;h4 id=&quot;delconsumer子命令&quot;&gt;delconsumer子命令&lt;/h4&gt;
&lt;p&gt;delconsumer子命令用于从消费者组中删除指定的消费者。delconsumer子命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XGROUP [DELCONSUMER key groupname consumername]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;groupname&lt;/strong&gt;: 消费者组名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;consumername&lt;/strong&gt;：消费者名称&lt;/p&gt;
&lt;h3 id=&quot;2xreadgroup&quot;&gt;2、xreadgroup&lt;/h3&gt;
&lt;p&gt;xreadgroup是 Redis Stream 中用于消费者组(Consumer Group)模式读取消息的核心命令， 也是构建可靠消息系统的关键命令，通过消费者组模式实现了消息分发、负载均衡和消息确认机制，是Redis Stream最强大的特性之一。&lt;/p&gt;
&lt;p&gt;该命令会自动过滤已经消费过的消息。&lt;/p&gt;
&lt;p&gt;完整命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;GROUP group consumer&lt;/strong&gt;：指定消费者组名称和消费者名称&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;group&lt;/code&gt;：消费者组名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;consumer&lt;/code&gt;：消费者名称（自动创建不存在的消费者）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[COUNT count]&lt;/strong&gt;：限制从每个 Stream 中返回的消息数量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[BLOCK milliseconds]&lt;/strong&gt;：设置阻塞模式及超时时间（毫秒）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置为0表示无限期阻塞&lt;/li&gt;
&lt;li&gt;不设置此参数则为非阻塞模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[NOACK]&lt;/strong&gt;：不将消息添加到 PEL (Pending Entries List)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用此参数后消息会被视为已确认，无需后续 XACK&lt;/li&gt;
&lt;li&gt;适用于不需要消息确认的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;STREAMS key [key ...]&lt;/strong&gt;：指定要读取的一个或多个 Stream 名称&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ID [ID ...]&lt;/strong&gt;：指定每个 Stream 的起始消息ID&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;：特殊ID，表示只读取从未分发给该消费者的新消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; 或 &lt;code&gt;0-0&lt;/code&gt;：从消费者组的 PEL (Pending Entries List) 中重新读取未确认的消息&lt;/li&gt;
&lt;li&gt;具体ID：从指定ID之后的消息开始读取&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xreadgroup group consumer_group kdyzm_consumer_1 block 0 streams stream &amp;gt;
stream
1751425687108-0
name
zhangsan
age
15
sex
man
1751436694812-0
zhangsan
age
1751438226276-0
name
zhangsan
1751438232352-0
name
zhangsan
1751438371472-0
name
zhangsan
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3xpending&quot;&gt;3、xpending&lt;/h3&gt;
&lt;p&gt;xpending是 Redis Stream 数据结构中用于查询消费者组(Consumer Group)内待处理消息(Pending Entries List, PEL)的关键命令。其完整命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XPENDING key group [[IDLE min-idle-time] start end count [consumer]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;group&lt;/strong&gt;: 消费者组名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[IDLE min-idle-time]&lt;/strong&gt;: 筛选空闲时间超过指定毫秒数的消息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;start end&lt;/strong&gt;: 消息ID范围，支持特殊符号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-&lt;/code&gt; 表示最小ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; 表示最大ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;count&lt;/strong&gt;: 限制返回的消息数量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[consumer]&lt;/strong&gt;: 指定只查看某个消费者的待处理消息&lt;/p&gt;
&lt;p&gt;先看一个简单查询：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xpending stream consumer_group 
5 #共有五条消息未被确认
1751425687108-0   # 最小消息id
1751438371472-0		# 最大消息id
kdyzm_consumer_1	#具体消费者
5					#kdyzm_consumer_1共有五条消息未确认
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单查询结果是一个简化的统计结果，如果我们想查询所有的未ACK的消息列表，可以使用如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xpending stream consumer_group - + 10
1751425687108-0     #消息id
kdyzm_consumer_1    #消费者名称：当前持有该消息但未确认的消费者
1304133             #空闲时间（毫秒）：自该消息最后一次被交付给消费者后经过的时间（约 21.7 分钟）
1                   #投递次数：该消息被读取的次数。若大于 1，说明消息曾被多次重试或转移
1751436694812-0
kdyzm_consumer_1
1304133
1
1751438226276-0
kdyzm_consumer_1
1304133
1
1751438232352-0
kdyzm_consumer_1
1304133
1
1751438371472-0
kdyzm_consumer_1
1304133
1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4xack&quot;&gt;4、xack&lt;/h3&gt;
&lt;p&gt;xack是 Redis Stream 数据结构中用于确认消息处理完成的关键命令，它属于消费者组(Consumer Group)功能的一部分，在非消费者组消费消息的场景下无法使用该命令。该命令完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xack key group ID [ID ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;：Stream 的名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;group&lt;/strong&gt;：消费者组名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ID [ID ...]&lt;/strong&gt;：一个或多个需要确认的消息ID（必填）&lt;/p&gt;
&lt;p&gt;xack命令返回值是一个整数，表示返回成功确认的消息数量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;xack工作原理：&lt;/strong&gt; 当消费者通过 XREADGROUP 读取消息后，这些消息会被添加到 Pending Entries List (PEL) 中，表示&amp;quot;已读取但未确认&amp;quot;；XACK 命令会从消费者组的 PEL 中移除指定的消息ID，表示这些消息已被成功处理；确认后的消息会从 PEL 中删除，释放相关内存。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xpending stream consumer_group - + 10  #查询出五条消息在PEL中
1751425687108-0
kdyzm_consumer_1
1671250
1
1751436694812-0
kdyzm_consumer_1
1671250
1
1751438226276-0
kdyzm_consumer_1
1671250
1
1751438232352-0
kdyzm_consumer_1
1671250
1
1751438371472-0
kdyzm_consumer_1
1671250
1
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; xack stream consumer_group 1751425687108-0 1751436694812-0 1751438226276-0 #手动ACK三条
3
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; xpending stream consumer_group - + 10 #再查还有两条消息在PEL中
1751438232352-0
kdyzm_consumer_1
1713880
1
1751438371472-0
kdyzm_consumer_1
1713880
1
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5xclaim&quot;&gt;5、xclaim&lt;/h3&gt;
&lt;p&gt;xclaim是 Redis Stream 消费者组功能中的一个重要命令，用于将待处理消息(Pending Entries List, PEL)的所有权从一个消费者转移到另一个消费者。该命令用于某些消息长时间未ACK需要消息重投的场景。命令的完整格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XCLAIM key group consumer min-idle-time ID [ID ...] [IDLE ms] [TIME ms-unix-time] [RETRYCOUNT count] [force] [justid]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;：Stream 的名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;group&lt;/strong&gt;：消费者组名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;consumer&lt;/strong&gt;：新的消费者名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;min-idle-time&lt;/strong&gt;：消息最小空闲时间（毫秒），只有空闲时间超过此值的消息才会被转移&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ID [ID ...]&lt;/strong&gt;：一个或多个需要转移的消息ID（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[IDLE ms]&lt;/strong&gt;：设置消息的新空闲时间（毫秒），默认使用当前时间减去消息最后交付时间&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[TIME ms-unix-time]&lt;/strong&gt;：与IDLE相同，但使用绝对Unix时间戳（毫秒）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[RETRYCOUNT count]&lt;/strong&gt;：设置消息的重试计数器，通常用于监控长时间未处理的消息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[force]&lt;/strong&gt;：强制创建PEL条目，即使指定的ID尚未分配给任何消费者&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[justid]&lt;/strong&gt;：只返回成功转移的消息ID，不返回消息内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xpending stream consumer_group - + 10 #查询出两条在PEL中的消息都被kdyzm_consumer_1持有
1751438232352-0
kdyzm_consumer_1
1885915
1
1751438371472-0
kdyzm_consumer_1
1885915
1
127.0.0.1:6379&amp;gt; xclaim stream consumer_group kdyzm_consumer_2 0 1751438232352-0 #将第一条消息转移给kdyzm_consumer_2
1751438232352-0
name
zhangsan
127.0.0.1:6379&amp;gt; xpending stream consumer_group - + 10 #再次查询
1751438232352-0
kdyzm_consumer_2 #第一条消息被转移到了kdyzm_consuer_2
30169			#空闲时间被重置，从0开始重新计算
2				#投递次数变成了2
1751438371472-0
kdyzm_consumer_1
1984128
1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6xinfo&quot;&gt;6、xinfo&lt;/h3&gt;
&lt;p&gt;xinfo是 Redis Stream 中用于获取流和消费者组相关信息的命令，它提供了多种子命令来查询 Stream 的内部状态。该命令的完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XINFO [CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;consumers子命令&quot;&gt;consumers子命令&lt;/h4&gt;
&lt;p&gt;consumers子命令用于查询指定消费者组中所有消费者的详细信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XINFO CONSUMERS &amp;lt;key&amp;gt; &amp;lt;groupname&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;groupname&lt;/strong&gt;: 消费者组名称（必填）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xinfo consumers stream consumer_group

127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;groups子命令&quot;&gt;groups子命令&lt;/h4&gt;
&lt;p&gt;groups子命令用于查询指定Stream的所有消费者组信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XINFO GROUPS &amp;lt;key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xinfo groups stream
name
consumer_group
consumers
0
pending
0
last-delivered-id
0-0
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;stream子命令&quot;&gt;stream子命令&lt;/h4&gt;
&lt;p&gt;stream子命令用于查询指定Stream的信息，包括长度、最后生成id等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;XINFO STREAM &amp;lt;key&amp;gt; [FULL [COUNT &amp;lt;count&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;key&lt;/strong&gt;: Stream 名称（必填）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xinfo stream stream
length
5
radix-tree-keys
1
radix-tree-nodes
2
last-generated-id
1751438371472-0
groups
1
first-entry
1751425687108-0
name
zhangsan
age
15
sex
man
last-entry
1751438371472-0
name
zhangsan
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;help子命令&quot;&gt;help子命令&lt;/h4&gt;
&lt;p&gt;help子命令用于查询xinfo命令的使用帮助信息。其输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xinfo help
XINFO &amp;lt;subcommand&amp;gt; [&amp;lt;arg&amp;gt; [value] [opt] ...]. Subcommands are:
CONSUMERS &amp;lt;key&amp;gt; &amp;lt;groupname&amp;gt;
    Show consumers of &amp;lt;groupname&amp;gt;.
GROUPS &amp;lt;key&amp;gt;
    Show the stream consumer groups.
STREAM &amp;lt;key&amp;gt; [FULL [COUNT &amp;lt;count&amp;gt;]
    Show information about the stream.
HELP
    Prints this help.
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;7消费者组综合练习&quot;&gt;7、消费者组综合练习&lt;/h3&gt;
&lt;p&gt;使用xadd命令创建一个Stream：kdyzm_stream并添加一条消息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xadd kdyzm_stream * name kdyzm
1751469707635-0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看stream的长度：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xlen kdyzm_stream 
1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个消费者组kdyzm_consumer_group监听kdyzm_stream消息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xgroup create kdyzm_stream kdyzm_consumer_group 0-0
OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;监听消息&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xreadgroup group kdyzm_consumer_group kdyzm_consumer_1 block 0 streams kdyzm_stream &amp;gt;
kdyzm_stream
1751469707635-0
name
kdyzm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&amp;gt;符号首次监听会接收到第一条消息。&lt;/p&gt;
&lt;p&gt;使用xpending命令查看PEL中的数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xpending kdyzm_stream kdyzm_consumer_group - + 10
1751469707635-0
kdyzm_consumer_1
278229
3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用xack命令确认下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xack kdyzm_stream kdyzm_consumer_group 1751469707635-0
1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次使用xpending命令查看PEL中的数据就已经看不到数据了。&lt;/p&gt;
&lt;p&gt;接下来再创建一个消费者，共同监听kdyzm_stream&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xreadgroup group kdyzm_consumer_group kdyzm_consumer_2 block 0 streams kdyzm_stream &amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到由于kdyzm_consume_1已经消费过第一条数据了，所以kdyzm_consumer_2就没有再重复消费，而是进行了一个阻塞行为。&lt;/p&gt;
&lt;p&gt;接下来开三个窗口，两个消费者窗口，一个发送消息的窗口，演示Stream负载均衡发送和接受消息的场景。&lt;/p&gt;
&lt;p&gt;第一个窗口运行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xreadgroup group kdyzm_consumer_group kdyzm_consumer_1 block 0 streams kdyzm_stream &amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用kdyzm_consumer_group下的kdyzm_consumer_1消费者阻塞式监听kdyzm_stream&lt;/p&gt;
&lt;p&gt;第二个窗口运行相似的命令，只是使用消费者kdyzm_consumer_2：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xreadgroup group kdyzm_consumer_group kdyzm_consumer_2 block 0 streams kdyzm_stream &amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行完两个命令，两个命令都会阻塞式监听，等待接收消息了。&lt;/p&gt;
&lt;p&gt;第三个窗口发送命令发送消息数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xadd kdyzm_stream * name zhangsan
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后查看消费者的行为：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/07/03/43c8b2798251446b8fbf991631aab14a.gif&quot; alt=&quot;redis Stream负载均衡发送消息&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到Redis负载均衡将四次消息发送分别发送了给两个消费者各两次。&lt;/p&gt;
&lt;p&gt;通过xpending命令能更直观的看到各个消费者未ack的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xpending kdyzm_stream kdyzm_consumer_group
4
1751521545678-0
1751521563585-0
kdyzm_consumer_1
2
kdyzm_consumer_2
2
127.0.0.1:6379&amp;gt; xpending kdyzm_stream kdyzm_consumer_group - + 100
1751521545678-0
kdyzm_consumer_1
677247
1
1751521551737-0
kdyzm_consumer_2
671188
1
1751521558157-0
kdyzm_consumer_1
664768
1
1751521563585-0
kdyzm_consumer_2
659340
1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来将这四个消息都ack掉：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; xack kdyzm_stream kdyzm_consumer_group 1751521545678-0 1751521551737-0 1751521558157-0 1751521563585-0
4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个过程基本上就结束了。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
END.
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（四）：Redis扩展类型Bitmap、HyperLogLog、GEO</title>
      <link>https://blog.kdyzm.cn/post/315</link>
      <guid>https://blog.kdyzm.cn/post/315</guid>
      <pubDate>Tue, 01 Jul 2025 22:37:50 +0800</pubDate>
      <description>&lt;p&gt;在前面第一篇文章中介绍了redis中的string、hash、list、set、zset五种基本数据类型，除了这五种基本数据类型，还有位图(Bitmap)、基数估算(HyperLogLog)、地理位置(GEO)三种扩展数据类型。&lt;/p&gt;
&lt;h2 id=&quot;一位图bitmap&quot;&gt;一、位图Bitmap&lt;/h2&gt;
&lt;p&gt;Bitmap并非单独的一种数据类型，它操作的实际上还是string类型，只不过我们通过&lt;code&gt;set&lt;/code&gt;/&lt;code&gt;get&lt;/code&gt;命令操作的是整个字符串，而bitmap则操作字符串的某个二进制位，这样在只需要存储0或者1的状态的场景中，将会大大提高操作效率并且能更节省空间（节省空间不是绝对的，得看场景）。&lt;/p&gt;
&lt;p&gt;我们已经知道位图底层其实就是一个 Redis 字符串，而 Redis 字符串的最大存储限制是 512 MB，这换算成字节数就是 512 * 1024 * 1024 = 536870912 字节 = 2^29 字节，换算成二进制位就是 2^29 * 8 = &lt;strong&gt;2^32&lt;/strong&gt; 位，这能满足任何状态存储需求了。&lt;/p&gt;
&lt;p&gt;bitmap操作相关的命令如下所示：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setbit key offset value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对指定 key 的某一二进制位进行赋值，并返回该位置上原来的二进制值，初始值为0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getbit key offset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取指定 key 上指定位置的二进制值，若超出当前位图范围，返回0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bitcount key [start end]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取指定 key 中被设置为 1 的数量，可以通过 start 和 end 参数来指定区间，该数值是指字节数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bitop operation destkey key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对一个或多个保存二进制位的字符串 key 进行位运算操作，并将结果保存到 destkey 上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bitpos key bit [start] [end]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取指定 key 中被第一个二进制位为0或1的bit位，可以通过 start 和 end 参数来指定区间，该数值是指字节数。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在bitmap的命令中，offset是偏移量，指的是第几个bit，从0开始算起；start和end则是以字节为单位。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/8d5039ab4e6b43e486310ca65c97cf9b.png&quot; alt=&quot;redis的bitmap结构示意图&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;1操作命令&quot;&gt;1、操作命令&lt;/h3&gt;
&lt;h4 id=&quot;setbitgetbit&quot;&gt;setbit/getbit&lt;/h4&gt;
&lt;p&gt;setbit命令格式：&lt;code&gt;setbit key offset value&lt;/code&gt;  ，返回值是原bit的值。&lt;/p&gt;
&lt;p&gt;getbit命令格式：&lt;code&gt;getbit key offset&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/7120d2b47a784799a2dbf7dd625e6d34.gif&quot; alt=&quot;redis的setbit、getbit使用方法&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h4 id=&quot;bitcount&quot;&gt;bitcount&lt;/h4&gt;
&lt;p&gt;bitcount命令格式：&lt;code&gt;bitcount key [start end]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;bitcount命令用于获取指定 key 中被设置为 1 的数量，可以通过 start 和 end 参数来指定区间，该数值是指字节数。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/a74e904ec15e4b6886e47b2a62c65991.gif&quot; alt=&quot;redis的bitcount使用方法&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h4 id=&quot;bitpos&quot;&gt;bitpos&lt;/h4&gt;
&lt;p&gt;bitpos是bit postion的简写。bitpos命令格式：&lt;code&gt;bitpos key bit [start] [end]&lt;/code&gt; ，该命令用于 获取指定 key 中被第一个二进制位为0或1的bit位，可以通过 start 和 end 参数来指定区间，该数值是指字节数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;key 表示键&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bit 表示要目标二进制值是0还是1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;start 表示指定区间的起始字节数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;end 表示指定区间的终止字节数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/4c47ddd9a4c74bd995ce86b2c3835d7a.gif&quot; alt=&quot;redis的bitpos命令使用方法&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h4 id=&quot;bitop&quot;&gt;bitop&lt;/h4&gt;
&lt;p&gt;bitop是bit operation的简写。bitop命令格式：&lt;code&gt;bitop operation destkey key [key ...]&lt;/code&gt;，该命令对一个或多个保存二进制位的字符串 key 进行位运算操作，并将结果保存到 destkey 上。&lt;/p&gt;
&lt;p&gt;operation 可以是 &lt;code&gt;AND&lt;/code&gt; 、 &lt;code&gt;OR&lt;/code&gt; 、 &lt;code&gt;NOT&lt;/code&gt; 、 &lt;code&gt;XOR&lt;/code&gt; 这四种操作中的任意一种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BITOP AND destkey key [key ...]&lt;/code&gt; ，对一个或多个 key 求逻辑并，并将结果保存到destkey 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BITOP OR destkey key [key ...]&lt;/code&gt; ，对一个或多个 key 求逻辑或，并将结果保存到 destkey。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BITOP XOR destkey key [key ...]&lt;/code&gt; ，对一个或多个 key 求逻辑异或，并将结果保存到destkey 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BITOP NOT destkey key&lt;/code&gt; ，对给定 key 求逻辑非，并将结果保存到 destkey 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了 NOT 操作之外，其他操作都可以接受一个或多个 key 作为输入。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; setbit a 8 0
(integer) 0
127.0.0.1:6379&amp;gt; setbit b 8 1
(integer) 0
127.0.0.1:6379&amp;gt; bitop and a_b_and a b  #and操作
(integer) 2
127.0.0.1:6379&amp;gt; getbit a_b_and 8
(integer) 0
127.0.0.1:6379&amp;gt; bitop or a_b_or a b #or操作
(integer) 2
127.0.0.1:6379&amp;gt; getbit a_b_or 8
(integer) 1
127.0.0.1:6379&amp;gt; bitop xor a_b_xor a b #xor操作
(integer) 2
127.0.0.1:6379&amp;gt; getbit a_b_xor 8
(integer) 1
127.0.0.1:6379&amp;gt; bitop not a_not a #not操作
(integer) 2
127.0.0.1:6379&amp;gt; getbit a_not 8
(integer) 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2bitmap优劣势分析&quot;&gt;2、Bitmap优劣势分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;极低存储成本（1bit/状态）&lt;/li&gt;
&lt;li&gt;O(1)时间复杂度的查询和设置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;局限性：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅支持二值状态，无法存储多值信息&lt;/li&gt;
&lt;li&gt;稀疏数据（如少数位为1）可能浪费空间，此时集合更优&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;让我们分析一个命令&lt;code&gt;setbit asdf 10000001 1&lt;/code&gt;，该命令浪费了大量内存空间：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内存预分配机制&lt;/strong&gt;：Redis的Bitmap底层是字符串，当设置一个较大的偏移量时，Redis会预分配足够的内存来覆盖从偏移量0到目标偏移量的所有位。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;稀疏数据问题&lt;/strong&gt;：如果实际只有少数位被设置为1（例如仅第10000001 位），其余位默认填充0，这些未使用的0值仍会占用内存，导致内存利用率极低&lt;/p&gt;
&lt;p&gt;可以通过命令查看asdf占用的存户空间：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; setbit asdf 10000001 1
(integer) 0
127.0.0.1:6379&amp;gt; memory usage asdf
(integer) 1310768
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1310768字节等于1310768/1024/1024约等于1.25MB大小。&lt;/p&gt;
&lt;p&gt;如何解决这个问题呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1、避免大偏移量初始化&lt;/strong&gt;：&lt;strong&gt;连续偏移量设计&lt;/strong&gt;，将用户ID或时间戳等标识符映射为连续整数（如从0开始），避免跳跃式偏移量。比如用户ID为150000000001时，可减去基准值（如150000000000），使偏移量变为1。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2、分区或分片&lt;/strong&gt;：将大Bitmap拆分为多个小Bitmap。比如按天分片：&lt;code&gt;SETBIT user:sign:20240630 1001 1&lt;/code&gt;；按用户范围分片：&lt;code&gt;SETBIT segment:1 1001 1&lt;/code&gt;（用户ID 1-100万归到segment:1）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3、使用其他数据结构&lt;/strong&gt;：若数据极度稀疏（如仅有少量位为1），改用集合（Set）或哈希（Hash）可能更节省内存。&lt;/p&gt;
&lt;h2 id=&quot;二基数统计hyperloglog&quot;&gt;二、基数统计HyperLogLog&lt;/h2&gt;
&lt;p&gt;HyperLogLog是Redis提供的一种概率型数据结构，专门用于解决大规模数据集的基数统计（即唯一元素计数）问题。与传统方法相比，HyperLogLog能够在极低的内存消耗下（仅约12KB）提供接近线性的时间复杂度，使其成为大数据分析和实时统计领域的有力工具。&lt;/p&gt;
&lt;p&gt;HyperLogLog具有以下几个显著特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;极低内存消耗&lt;/strong&gt;：无论统计多少元素，每个HyperLogLog键通常只占用约12KB内存（极端情况下最多64KB），可以统计高达2^64个元素的基数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;固定误差率&lt;/strong&gt;：标准误差率约为0.81%，对于大多数统计场景来说是可接受的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高效合并&lt;/strong&gt;：支持多组HyperLogLog的合并去重（PFMERGE），时间复杂度为O(1)，适合分布式统计&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动去重&lt;/strong&gt;：会忽略重复元素，多次添加同一元素不会影响结果&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常数时间复杂度&lt;/strong&gt;：插入(PFADD)和查询(PFCOUNT)操作都是O(1)复杂度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;HyperLogLog算法比较复杂，但是使用起来比较简单，Redis提供了三个核心命令来操作HyperLogLog：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pfadd key element [element ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将一个或多个元素添加到指定的HyperLogLog结构中。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pfcount key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询一个或多个HyperLogLog的基数估算值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pfmerge destkey sourcekey [sourcekey ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将一个或多个源HyperLogLog合并到目标HyperLogLog中，用于合并不同数据集的基数估算&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;pfadd--pfcount&quot;&gt;pfadd / pfcount&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pfadd key element [element ...]&lt;/code&gt;：将一个或多个元素添加到指定的HyperLogLog结构中。如果至少有一个元素是新添加的（即影响了基数估算），则返回1，否则返回0&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pfcount key [key ...]&lt;/code&gt;：查询一个或多个HyperLogLog的基数估算值。当传入多个key时，返回的是这些HyperLogLog并集的基数估算值&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/73db0e7893af465aacae27c6c2a2080f.gif&quot; alt=&quot;redis的pfadd、pfcount使用方法&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;pfmerge&quot;&gt;pfmerge&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pfmerge destkey sourcekey [sourcekey ...]&lt;/code&gt;：将一个或多个源HyperLogLog合并到目标HyperLogLog中，用于合并不同数据集的基数估算&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/4054e0b0ed06409e89825fabb1b9eca8.gif&quot; alt=&quot;redis的pfmerge使用方法&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;三geo操作&quot;&gt;三、GEO操作&lt;/h2&gt;
&lt;p&gt;Redis GEO 是 Redis 3.2 版本引入的地理空间数据处理功能，基于有序集合zset（Sorted Set）实现，支持存储经纬度坐标及高效的地理位置查询。&lt;/p&gt;
&lt;p&gt;为了更好讲解geo各种命令，我们先准备好一组数据插入redis：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;地点&lt;/th&gt;
&lt;th&gt;坐标&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;北京&lt;/td&gt;
&lt;td&gt;116.41,39.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上海&lt;/td&gt;
&lt;td&gt;121.47,31.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;青岛&lt;/td&gt;
&lt;td&gt;120.31,36.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;石家庄&lt;/td&gt;
&lt;td&gt;114.51,38.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重庆&lt;/td&gt;
&lt;td&gt;106.55,29.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;香港&lt;/td&gt;
&lt;td&gt;114.17,22.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;澳门&lt;/td&gt;
&lt;td&gt;116.41,39.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;连云港&lt;/td&gt;
&lt;td&gt;119.22,34.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;乌鲁木齐&lt;/td&gt;
&lt;td&gt;87.62,43.83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;济南&lt;/td&gt;
&lt;td&gt;117.12,36.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;郑州&lt;/td&gt;
&lt;td&gt;113.66,34.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;太原&lt;/td&gt;
&lt;td&gt;112.55,37.87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;南京&lt;/td&gt;
&lt;td&gt;118.80,32.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;武汉&lt;/td&gt;
&lt;td&gt;114.30,30.59&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意，为了防止中文乱码，要使用&lt;code&gt;./redis-cli --raw&lt;/code&gt;命令进入控制台，批量插入的命令为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GEOADD cities:china 116.41 39.90 北京 121.47 31.23 上海 120.31 36.06 青岛 114.51 38.04 石家庄 106.55 29.56 重庆 114.17 22.28 香港 116.41 39.91 澳门 119.22 34.60 连云港 87.62 43.83 乌鲁木齐 117.12 36.65 济南 113.66 34.75 郑州 112.55 37.87 太原 118.80 32.06 南京 114.30 30.59 武汉
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1新增geo数据geoadd&quot;&gt;1、新增GEO数据：geoadd&lt;/h3&gt;
&lt;p&gt;geo新增命令是geoadd命令，geoadd命令完整格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;geoadd key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;geoadd命令部分格式和zadd命令有些相似，zadd命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zadd key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个命令中，[NX|XX]以及[CH]参数的意义是一样的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可选参数[NX|XX]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NX：不存在则添加，否则忽略&lt;/p&gt;
&lt;p&gt;XX：存在则更新分数，否则忽略&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可选参数[CH]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CH参数的作用：返回值改为所有被修改的成员数量（包括新增和更新的成员）。zadd的默认行为是仅统计新添加的成员。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;longitude latitude member&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;经度  维度  名字，比如 116.41 39.90 北京&lt;/p&gt;
&lt;p&gt;举例：新增一条数据（116.41,39.9,北京），如果不存在则添加，否则忽略，返回值要统计被修改的成员数量，可以使用如下命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; geoadd cities:china NX CH 116.41 39.9 北京
0
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于之前已经添加过了北京的geo数据，所以没有添加成功，返回了0。&lt;/p&gt;
&lt;h3 id=&quot;2查看geo数据&quot;&gt;2、查看GEO数据&lt;/h3&gt;
&lt;h4 id=&quot;查看zset数据&quot;&gt;查看zset数据&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;geoadd&lt;/code&gt;命令添加的数据在redis中是以zset形式存储的，&lt;code&gt;geoadd&lt;/code&gt; 命令将经纬度坐标转换为 &lt;strong&gt;52 位整数&lt;/strong&gt;作为 &lt;code&gt;score&lt;/code&gt; 存储，同时将地点名称（如 &lt;code&gt;&amp;quot;北京&amp;quot;&lt;/code&gt;）作为 &lt;code&gt;member&lt;/code&gt;，所以没有直接查询geo数据的命令，可以使用zset相关命令直接查询geo数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; zrange cities:china 0 -1
乌鲁木齐
重庆
香港
武汉
上海
郑州
济南
南京
连云港
青岛
太原
石家庄
北京
澳门
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，如果出现了中文乱码，一定要在redis-cli后加上--raw参数启动redis-cli。&lt;/p&gt;
&lt;h4 id=&quot;geopos&quot;&gt;geopos&lt;/h4&gt;
&lt;p&gt;geopos用于查询某地的经纬度，完整命令格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;geopos key member [member ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如查询北京的经纬度信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; geopos cities:china 北京
116.40999823808670044
39.90000009167092543
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里，redis并没有存储经纬度信息，geopos命令是通过动态解码score获得经纬度信息系的，所以这里产生了精度丢失。&lt;/p&gt;
&lt;h4 id=&quot;geohash&quot;&gt;geohash&lt;/h4&gt;
&lt;p&gt;Redis 的 &lt;code&gt;GEOHASH&lt;/code&gt; 命令用于获取指定地理位置的 &lt;strong&gt;GeoHash 编码&lt;/strong&gt;，这是一种将二维经纬度转换为一维字符串的算法，便于高效存储和查询地理位置数据，完整命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;geohash key member [member ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;member是地理位置名称，返回一个或多个成员的 &lt;strong&gt;Base32 编码字符串&lt;/strong&gt;（长度通常为 11 字符），例如 &lt;code&gt;&amp;quot;wx4fbzx4me0&amp;quot;&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; geohash cities:china 北京
wx4fbzx4me0
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3经纬度计算&quot;&gt;3、经纬度计算&lt;/h3&gt;
&lt;p&gt;接下来说一说geo命令中的经纬度计算相关的命令。&lt;/p&gt;
&lt;h4 id=&quot;geodist&quot;&gt;geodist&lt;/h4&gt;
&lt;p&gt;Redis 的 &lt;code&gt;geodist&lt;/code&gt; 命令用于计算两个地理位置之间的直线距离（基于经纬度），支持多种单位（如米、千米、英里等），是 Redis GEO 地理信息功能的核心命令之一，其完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;geodist key member1 member2 [m|km|ft|mi]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;m&lt;/code&gt;：米&lt;/li&gt;
&lt;li&gt;&lt;code&gt;km&lt;/code&gt;：千米&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mi&lt;/code&gt;：英里&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ft&lt;/code&gt;：英尺&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子，计算北京和上海的直线距离：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; geodist cities:china 北京 上海 km
1066.9923
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个数据和官方给的数据1088差不多，有差距是因为我是在高德地图上随意选点和官方的计算方式不同导致的。&lt;/p&gt;
&lt;h4 id=&quot;georadius&quot;&gt;georadius&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;georadius&lt;/code&gt; 是 Redis 中用于查询指定经纬度为中心、半径范围内的地理位置成员的强大命令，适用于附近地点搜索、LBS 服务等场景。georadius命令已经不再被官方推荐使用，推荐使用geosearch命令替代。georadius命令完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;georadius key longitude latitude radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 
[STORE key] 
[STOREDIST key]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;必选参数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;key&lt;/code&gt;&lt;/strong&gt;：存储地理位置的 Redis 键名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;longitude latitude&lt;/code&gt;&lt;/strong&gt;：中心点的经度和纬度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;radius&lt;/strong&gt;：搜索半径，需搭配单位：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;m&lt;/code&gt;：米&lt;/li&gt;
&lt;li&gt;&lt;code&gt;km&lt;/code&gt;：千米&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ft&lt;/code&gt;：英尺&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mi&lt;/code&gt;：英里&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;可选参数&lt;/strong&gt;&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选参数&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WITHCOORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在返回结果中包含匹配位置的经纬度坐标&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WITHDIST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在返回结果中包含匹配位置与中心点的距离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WITHHASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回位置元素经过原始geohash编码的有序集合分值(52位有符号整数)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COUNT count [ANY]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限制返回结果的数量。添加ANY选项时，命令会在找到足够结果后立即返回，可能不精确但更快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ASC|DESC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;结果排序方式，ASC表示从近到远，DESC表示从远到近。默认是ASC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STORE key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将返回结果的地理位置信息保存到指定key(以GeoHash值作为分数)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STOREDIST key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将返回结果离中心点的距离保存到指定key(以距离作为分数)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我查到临沂的坐标是（118.36,35.10），现在我以该坐标为中心，依次查询10km、100km、200km、300km范围内的城市：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; georadius cities:china 118.36 35.10 10 km

127.0.0.1:6379&amp;gt; georadius cities:china 118.36 35.10 100 km
连云港
127.0.0.1:6379&amp;gt; georadius cities:china 118.36 35.10 200 km
连云港
127.0.0.1:6379&amp;gt; georadius cities:china 118.36 35.10 300 km
连云港
青岛
济南
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加上可选参数重新运行命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; georadius cities:china 118.36 35.10 300 km withcoord withdist withhash count 3 desc 
青岛
206.1915          		#距离
4067544670361346		#score值
120.30999988317489624 	#经度
36.05999892411877994 	#纬度
济南
205.4495
4065935147208104
117.12000042200088501
36.65000089893579371
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;georadiusbymember&quot;&gt;georadiusbymember&lt;/h4&gt;
&lt;p&gt;和&lt;code&gt;georadius&lt;/code&gt;命令&lt;strong&gt;根据指定的坐标查询&lt;/strong&gt;指定范围内的地点不同的是，&lt;code&gt;georadiusbymember&lt;/code&gt;命令可以&lt;strong&gt;根据给定的成员位置&lt;/strong&gt;来查找指定范围内的其他成员。完整的命令如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;georadiusbymember key member radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 
[STORE key] 
[STOREDIST key]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可选参数都和georadius一样，不再赘述。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; georadiusbymember cities:china 青岛 300 km withcoord withdist withhash desc
济南
293.1738 				#距离
4065935147208104		#score值
117.12000042200088501	#经度
36.65000089893579371	#纬度
连云港
190.1374
4067136777713544
119.21999841928482056
34.59999953633954561
青岛
0.0000
4067544670361346
120.30999988317489624
36.05999892411877994
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;geosearch&quot;&gt;geosearch&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;geosearch &lt;/code&gt; 是 Redis 6.2 版本新增的地理空间查询命令，用于在指定的地理空间索引中搜索符合条件的成员。该命令提供了比旧版 &lt;code&gt;georadius&lt;/code&gt; 和 &lt;code&gt;georadiusbymember &lt;/code&gt; 更强大和灵活的查询功能，支持圆形和矩形两种搜索范围，并且同时支持基于指定坐标以及指定地点的坐标的查询。完整的命令格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;geosearch key 
[FROMMEMBER member] 
[FROMLONLAT longitude latitude] 
[BYRADIUS radius m|km|ft|mi] 
[BYBOX width height m|km|ft|mi] 
[ASC|DESC] 
[COUNT count [ANY]] 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然大多数的参数都是可选参数，但是实际上只是使用”geosearch key“命令直接查询会报错。可选参数大多数都必须&amp;quot;二选一&amp;quot;。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;可选参数&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROMMEMBER member&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使用已存在于地理空间索引中的成员作为中心点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FROMLONLAT longitude latitude&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;直接指定经纬度作为中心点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BYRADIUS radius m|km|ft|mi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;圆形范围查询，指定半径和单位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BYBOX width height m|km|ft|mi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;矩形范围查询，指定宽度、高度和单位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ASC|DESC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;结果排序 ASC：按距离从近到远排序；DESC：按距离从远到近排序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COUNT count [ANY]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;限制返回结果的数量，Any与COUNT配合使用，表示只要找到足够数量的匹配项就立即返回，不保证是最接近的结果，但能提高查询效率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WITHCOORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回匹配项的经度和纬度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WITHDIST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回匹配项到指定中心点的距离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WITHHASH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以52位无符号整数的形式返回原始geohash编码（主要用于调试）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以看到geosearch是个大而全的命令，功能上基本上等于&lt;code&gt;georadius&lt;/code&gt;+&lt;code&gt;georadiusbymember&lt;/code&gt;+&lt;code&gt;矩形范围查询&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;还是以临沂市（118.36,35.10）为中心查询的例子，现在我以该坐标为中心，依次查询10km、100km、200km、300km范围内的城市：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; geosearch cities:china fromlonlat 118.36 35.10 byradius 10 km desc withcoord withdist withhash

127.0.0.1:6379&amp;gt; geosearch cities:china fromlonlat 118.36 35.10 byradius 100 km desc withcoord withdist withhash
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; geosearch cities:china fromlonlat 118.36 35.10 byradius 200 km desc withcoord withdist withhash
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561
127.0.0.1:6379&amp;gt; geosearch cities:china fromlonlat 118.36 35.10 byradius 300 km desc withcoord withdist withhash
青岛
206.1915				#距离
4067544670361346		#score值
120.30999988317489624	#横坐标
36.05999892411877994	#纵坐标
济南
205.4495
4065935147208104
117.12000042200088501
36.65000089893579371
连云港
96.2017
4067136777713544
119.21999841928482056
34.59999953633954561
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;geosearchstore&quot;&gt;geosearchstore&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;geosearchstore &lt;/code&gt; 是 Redis 6.2 版本引入的地理空间命令，它结合了搜索和存储功能，允许用户执行地理空间搜索并将结果存储到一个新的键中。这个命令是 &lt;code&gt;geosearch &lt;/code&gt; 命令的变体，主要区别在于它会将搜索结果保存到指定的目标键中。该命令的完整格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;geosearchstore destination source 
[FROMMEMBER member] 
[FROMLONLAT longitude latitude] 
[BYRADIUS radius m|km|ft|mi] 
[BYBOX width height m|km|ft|mi] 
[ASC|DESC] 
[COUNT count [ANY]] 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[STOREDIST]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到geosearchstore命令的格式和geosearch的命令如出一辙。&lt;/p&gt;
&lt;p&gt;接下来我将查询临沂市方圆300km范围的城市并将其保存到新的key new_cities。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; geosearchstore new_cities cities:china fromlonlat 118.36 35.10 byradius 300 km desc
3
127.0.0.1:6379&amp;gt; zrange new_cities 0 -1 withscores
济南
4065935147208104
连云港
4067136777713544
青岛
4067544670361346
127.0.0.1:6379&amp;gt; geopos new_cities 济南
117.12000042200088501
36.65000089893579371
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，该命令在实践中发现并不能使用&lt;code&gt;[WITHCOORD]&lt;/code&gt; 、&lt;code&gt;[WITHDIST]&lt;/code&gt; 、&lt;code&gt;[WITHHASH]&lt;/code&gt; 三个可选参数，使用之后会报错。&lt;/p&gt;
&lt;p&gt;另外，&lt;code&gt;STOREDIST&lt;/code&gt;可选参数的意思是将结果存储为 &lt;code&gt;destination&lt;/code&gt; 键的 zset，并将距离作为分值（Score）。若不指定，默认存储 GeoHash 编码。&lt;/p&gt;
&lt;h4 id=&quot;georadius_ro&quot;&gt;georadius_ro&lt;/h4&gt;
&lt;p&gt;georadius命令的只读版本，完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;georadius_ro key arg arg arg arg ...options...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上可以等同于georadius命令删除store参数之后的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;georadius key longitude latitude radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是因为没有了写操作，所以更安全。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; georadius_ro cities:china 118.36 35.10 300 km
连云港
青岛
济南
127.0.0.1:6379&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;georadiusbymember_ro&quot;&gt;georadiusbymember_ro&lt;/h4&gt;
&lt;p&gt;georadiusbymember命令的只读版本，完整命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;georadiusbymember_ro key arg arg arg ...options...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上等同于georadiusbymember命令删除store参数之后的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-basn&quot;&gt;georadiusbymember key member radius m|km|ft|mi 
[WITHCOORD] 
[WITHDIST] 
[WITHHASH] 
[COUNT count [ANY]] 
[ASC|DESC] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是没有了写操作，所以更安全。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; georadiusbymember_ro cities:china 青岛 300 km desc
济南
连云港
青岛
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（三）：Redis事务</title>
      <link>https://blog.kdyzm.cn/post/314</link>
      <guid>https://blog.kdyzm.cn/post/314</guid>
      <pubDate>Mon, 30 Jun 2025 14:15:52 +0800</pubDate>
      <description>&lt;p&gt;我认为Redis的事务是一个很不完善的功能，甚至和“事务”的概念都不搭边，这里讲解Redis事务只是说它有这个东西，我想大概没有人在生产中用这个东西。&lt;/p&gt;
&lt;p&gt;我们知道Mysql事务满足ACID特性，即原子性、一致性、隔离性、持久性，但是Redis事务并不满足ACID特性，&lt;strong&gt;如果Redis事务中的一条命令执行失败它会继续执行任务而且不会回滚&lt;/strong&gt;。Redis事务实际上更像是打包好的批量脚本执行。&lt;/p&gt;
&lt;p&gt;Redis事务涉及到以下命令的使用：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标记一个事务块的开始。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;执行所有事务块内的命令。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;discard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取消事务，放弃执行事务块内的所有命令。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;watch key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;监视一个(或多个) key ，如果在事务执行之前这个(或这些) key 被其他命令所改动，那么事务将被打断。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unwatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取消 WATCH 命令对所有 key 的监视。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;先看一个比较正常的执行流程：使用&lt;code&gt;multi&lt;/code&gt;命令和&lt;code&gt;exec&lt;/code&gt;命令批量执行命令。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/8fd5c6eacb8240579a629deda2a4b6b9.gif&quot; alt=&quot;redis事务执行&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;那么watch命令是怎么用的呢？假设有个string类型的变量a为1，接下来要开启一个事务，事务内依赖变量a，如果变量a发生了变化则可能导致事务执行结果异常，这时候就要&amp;quot;watch&amp;quot; a ,防止在事务执行的过程中有其他客户端改变了a的值；如果发现a的值发生了变化，就要停止事务的执行：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/b304a76869514051aa0e37fa1832068d.gif&quot; alt=&quot;redis中的watch&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;discard命令是可以在事务内执行的命令，执行后会退出事务。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/198de6cc1fdc4a6da126fb2d7e390003.gif&quot; alt=&quot;动画4_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;好了，redis事务比较简单，由于其功能并不完善，所以建议别用。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（二）：Redis发布订阅模式</title>
      <link>https://blog.kdyzm.cn/post/313</link>
      <guid>https://blog.kdyzm.cn/post/313</guid>
      <pubDate>Mon, 30 Jun 2025 11:17:11 +0800</pubDate>
      <description>&lt;p&gt;使用Redis实现消息队列有两种方式：list队列模式以及发布订阅模式。&lt;/p&gt;
&lt;h2 id=&quot;一list队列模式&quot;&gt;一、list队列模式&lt;/h2&gt;
&lt;h3 id=&quot;1list队列的原理&quot;&gt;1、list队列的原理&lt;/h3&gt;
&lt;p&gt;在上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/312&quot;&gt;Redis（一）：Redis数据类型和常用命令&lt;/a&gt;》中已经详细讲解了Redis中list相关的操作。list底层是个双向链表，头部和尾部的操作效率非常高，其操作命令也符合队列API的行为方式。我们在使用list作为消息队列的时候，会使用&lt;code&gt;lpush&lt;/code&gt;命令将消息放入队列，使用&lt;code&gt;rpop&lt;/code&gt;命令将消息出队列。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/27/1af3bbc9e8f8426d9d181629af3b7bb1.png&quot; alt=&quot;image-20250627224658106&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;需要注意的是&lt;code&gt;rpop&lt;/code&gt;命令是一次性命令，它在执行前并不知道消息队列中是否有消息，可以预料到的是，如果队列中一直没有消息，只能通过&lt;code&gt;rpop&lt;/code&gt;命令一直轮询。为了解决这个问题，可以使用brpop命令:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brpop key [key ...] timeout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;brpop命令中的timeout参数是必传的，当它为0的时候表示无限期阻塞等待；不为零的时候计时结束就会自动放弃阻塞等待。&lt;/p&gt;
&lt;p&gt;brpop命令返回值有两个值，第一个值是key的名字，第二个值是弹出的元素值，如果从队列中取不出来数据，会一直阻塞，在一定范围内没有取出则返回null。需要注意brpop命令并不会阻塞主线程，brpop虽然是个阻塞命令，但是它会通过如下机制避免阻塞主线程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;非忙等待&lt;/strong&gt;：当 &lt;code&gt;BRPOP&lt;/code&gt; 监听列表为空时，Redis 会将客户端连接标记为“阻塞状态”，并将其从活跃客户端列表中移除，主线程不会持续轮询或等待，而是继续处理其他请求&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;事件驱动&lt;/strong&gt;：Redis 使用事件循环（如 &lt;code&gt;epoll&lt;/code&gt;）管理客户端连接。阻塞的客户端会被挂起，直到目标列表有新元素插入时，Redis 通过事件回调唤醒对应的客户端连接&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;2list队列演示&quot;&gt;2、list队列演示&lt;/h3&gt;
&lt;h4 id=&quot;第一步进入容器&quot;&gt;第一步：进入容器&lt;/h4&gt;
&lt;p&gt;我在6.2.1版本的redis下做个演示。由于我使用了docker，所以需要先进入我的redis容器（容器名为redis）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it redis /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后进入redis-cli命令所在的目录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd /usr/local/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行redis-cli命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./redis-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入redis交互命令行，执行auth命令做个认证：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;auth 123456
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依照此步骤打开三个会话页面：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/27/e14f6ec9b8bc49bf9fae03f0eab146a1.png&quot; alt=&quot;image-20250627230511980&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h4 id=&quot;第二步发送接收消息&quot;&gt;第二步：发送/接收消息&lt;/h4&gt;
&lt;p&gt;三个会话中，第一个会话用于向队列发送消息，第二个和第三个会话使用brpop命令接收消息。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/27/f372c0a133014297b1b3e666e0cff500.gif&quot; alt=&quot;动画45_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，brpop命令即使key不存在，也可以持续监听，并且当key被创建而且list中有值时会自动将值取出来。多个监听者可以同时监听同一个消息队列，但是同一个值只能被一个监听者获取到。&lt;/p&gt;
&lt;h2 id=&quot;二发布订阅模式&quot;&gt;二、发布订阅模式&lt;/h2&gt;
&lt;h3 id=&quot;1发布订阅模式简介&quot;&gt;1、发布订阅模式简介&lt;/h3&gt;
&lt;p&gt;发布订阅模式和rabbitmq消息队列的topic模式很像，多个客户端订阅同一个channel，channel发布消息的时候，每个客户端都能收到相同的消息。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/4f4a0786b65e47e18121937d74c9ea13.png&quot; alt=&quot;image-20250630102158818&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;消息订阅命令：&lt;code&gt;subscribe channel [channel ...]&lt;/code&gt;,也就是说该命令可以订阅多个channel。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/369c049566624e77afe3d955030020b0.png&quot; alt=&quot;image-20250630102728000&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;消息发送命令：&lt;code&gt;publish channel message&lt;/code&gt;该命令只能向一个channel发送命令。&lt;/p&gt;
&lt;p&gt;全部命令如下所示：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;命令&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;publish channel message&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;将信息发送到指定的频道。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;&lt;code&gt;subscribe channel [channel ...]&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;订阅给定的一个或多个频道的信息。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;pubsub subcommand [argument [argument ...]]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;查看订阅与发布系统状态。&lt;br/&gt;&lt;code&gt;PUBSUB CHANNELS&lt;/code&gt;：列出当前活跃的频道&lt;br/&gt;&lt;code&gt;PUBSUB NUMSUB [channel ...]&lt;/code&gt;：返回指定频道的订阅者数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;unsubscribe [channel [channel ...]]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;指退订给定的频道。&lt;br/&gt;目前还不知道如何在redis-cli中执行该命令，因为ctrl+c就已经实现退订了。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;psubscribe pattern [pattern ...]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;订阅一个或多个符合给定模式的频道。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;punsubscribe [pattern [pattern ...]]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;退订所有给定模式的频道。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;2发布订阅模式演示&quot;&gt;2、发布订阅模式演示&lt;/h3&gt;
&lt;p&gt;接下来打开三个客户端，一个用于发布，另外两个用来订阅。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/30/6ffd5c192bbe41acbe366cb7a80b01aa.gif&quot; alt=&quot;动画1_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;3发布订阅模式分析&quot;&gt;3、发布订阅模式分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;发布订阅模式的优点&lt;/strong&gt;：发布者和订阅者不需要知道彼此的存在，功能解耦。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发布订阅模式的缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消息没有持久化，如果消费者断线重连，消息会丢失&lt;/li&gt;
&lt;li&gt;没有ack机制，无法保证消息被成功消费。&lt;/li&gt;
&lt;li&gt;Redis服务重启后消息会丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综合来看，redis在消息队列方面功能并不完善，如果需要用到消息队列，最好使用专业的消息队列中间件，比如rabbitmq、kafka等。&lt;/p&gt;
&lt;p&gt;当然，redis5中引入了Stream，Stream做了数据的持久化处理，一定方面弥补了发布订阅模式的缺点。。。建议还是不要用。&lt;/p&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>Redis（一）：Redis数据类型和常用命令</title>
      <link>https://blog.kdyzm.cn/post/312</link>
      <guid>https://blog.kdyzm.cn/post/312</guid>
      <pubDate>Fri, 27 Jun 2025 15:32:44 +0800</pubDate>
      <description>&lt;p&gt;上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/28&quot;&gt;CentOS安装Redis&lt;/a&gt;》已经安装好了Redis，本篇文章将讲解Redis的基本使用以及五种数据类型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：本篇文章使用的redis版本是6.2.1，到今天为止，redis8.0已经GA了，但是实际上基础使用命令差别不大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;一常用命令&quot;&gt;一、常用命令&lt;/h2&gt;
&lt;h3 id=&quot;1redis命令行&quot;&gt;1、redis命令行&lt;/h3&gt;
&lt;p&gt;REDIS是一种NoSQL数据库，它和MySQL数据库一样，都有和Server端交互的命令行工具，而且在这个命令行工具下，还有非常友好的命令提示。&lt;/p&gt;
&lt;p&gt;我安装的是docker版本的redis，可以先使用命令&lt;code&gt;docker exec -it 容器名 /bin/bash&lt;/code&gt;进入容器，然后打开&lt;code&gt;/usr/local/bin&lt;/code&gt;目录，就可以看到redis安装后提供的相关工具了&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/e2fc0ec29c8c448293da3715dde8a70c.png&quot; alt=&quot;image-20250625105007776&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;直接运行&lt;code&gt;./redis-cli&lt;/code&gt;就可以进入命令行&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/7ba6139865a74694a680aa8e4fb68345.png&quot; alt=&quot;image-20250625105129229&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;2认证命令auth&quot;&gt;2、认证命令auth&lt;/h3&gt;
&lt;p&gt;进入命令行以后，如果redis设置了密码，则直接运行相关的命令会提示报错，需要先执行auth命令认证，auth命令的格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;AUTH [username] password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;username可选。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/a6b7d109d5a44ef4b82a859727a9fdfb.png&quot; alt=&quot;image-20250625105405639&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;3查看信息命令info&quot;&gt;3、查看信息命令info&lt;/h3&gt;
&lt;p&gt;info是个很有用的命令，可以使用该命令查看当前运行的redis server的所有状态，包括：redis server的版本号、CPU信息、内存信息、持久化信息、数据库信息等等&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;info [section]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用info命令默认显示如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Server
redis_version:6.2.1
redis_git_sha1:00000000
redis_git_dirty:0
......

# Clients
connected_clients:2
cluster_connections:0
maxclients:10000
......

# Memory
used_memory:894792
used_memory_human:873.82K
used_memory_rss:10809344
......

# Persistence
loading:0
current_cow_size:0
current_fork_perc:0.00%
......

# Stats
total_connections_received:3
total_commands_processed:283
instantaneous_ops_per_sec:0
......

# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
......

# CPU
used_cpu_sys:4.598728
used_cpu_user:4.760232
used_cpu_sys_children:0.003479
......

# Modules

# Errorstats
errorstat_ERR:count=1
errorstat_NOAUTH:count=2

# Cluster
cluster_enabled:0

# Keyspace
db0:keys=1,expires=0,avg_ttl=0
db1:keys=1,expires=0,avg_ttl=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;info命令的输出很长，而且输出是带注释的，注释中的内容表示“模块名”，共分为10个模块，&lt;code&gt;info [section]&lt;/code&gt;中的section正是模块名：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;查询命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;info server&lt;/td&gt;
&lt;td&gt;提供Redis服务器基本信息，如版本号、运行时间、操作系统、进程ID、配置文件路径等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clients&lt;/td&gt;
&lt;td&gt;info clients&lt;/td&gt;
&lt;td&gt;显示客户端连接状态，包括连接数、阻塞客户端数量、输入/输出缓冲区大小等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;info memory&lt;/td&gt;
&lt;td&gt;展示内存使用详情，包括已用内存、内存碎片率、峰值内存、分配器等，用于监控内存优化。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;info persistence&lt;/td&gt;
&lt;td&gt;记录RDB和AOF持久化状态，如最近保存时间、后台操作状态、AOF重写进度等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stats&lt;/td&gt;
&lt;td&gt;info stats&lt;/td&gt;
&lt;td&gt;统计命令处理数量、连接数、键过期/驱逐情况、每秒操作数等性能指标。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replication&lt;/td&gt;
&lt;td&gt;info replication&lt;/td&gt;
&lt;td&gt;主从复制信息，包括角色（master/slave）、从节点列表、复制偏移量、延迟等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;info cpu&lt;/td&gt;
&lt;td&gt;统计CPU消耗，包括用户态和系统态时间、子进程CPU占用等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modules&lt;/td&gt;
&lt;td&gt;info modules&lt;/td&gt;
&lt;td&gt;用于查看当前 Redis 服务器加载的模块信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster&lt;/td&gt;
&lt;td&gt;info cluster&lt;/td&gt;
&lt;td&gt;显示集群状态，如节点信息、槽分配情况（仅集群模式有效）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyspace&lt;/td&gt;
&lt;td&gt;info keyspace&lt;/td&gt;
&lt;td&gt;统计各数据库的键数量、过期键数量及平均TTL。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;4切换数据库命令select&quot;&gt;4、切换数据库命令select&lt;/h3&gt;
&lt;p&gt;redis数据库默认有16个，编号从0到15，对redis的读写操作实际上是针对某个redis库的读写操作，使用select命令切换使用的数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;select index
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;index是从0到15的整数。&lt;/p&gt;
&lt;p&gt;在redis-cli下，在没有选择数据库的情况下，默认读写操作都是在数据库0下操作的：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/3f8c1a82e9fe4d1db945666abfd37427.png&quot; alt=&quot;image-20250625132714896&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;使用select命令选择非0的数据库之后，将会在命令行中显示当前使用的数据库：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/fd9f6df8095f413c99573b77bddd0a07.png&quot; alt=&quot;image-20250625132927434&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;另外，可以使用&lt;code&gt;client info&lt;/code&gt;命令更直观的看到当前链接使用的客户端信息（包含使用的数据库）&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/2e0f15da0b7d4af0aaa509515ec6550a.png&quot; alt=&quot;image-20250625134716797&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;5客户端管理命令client&quot;&gt;5、客户端管理命令client&lt;/h3&gt;
&lt;p&gt;client命令用于查看和管理连接到redis server的客户端信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;client list&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;列出当前所有连接到 Redis 的客户端信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;CLIENT LIST
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要输出字段及说明：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;客户端唯一 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;addr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;客户端 IP 和端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;客户端名称（可自定义）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;当前使用的数据库编号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接已建立时间（秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;空闲时间（秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接类型（&lt;code&gt;N&lt;/code&gt;=普通客户端，&lt;code&gt;M&lt;/code&gt;=主节点，&lt;code&gt;S&lt;/code&gt;=从节点等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最近执行的命令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;client info&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;查看当前连接到Redis的客户端信息，其输出格式和client list命令一样，但是只显示当前连接的客户端信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;client setname&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为当前连接设置一个易读的名称，方便管理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;client setname kdyzm-connection
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;client getname&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;查看当前连接的客户端名称。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/6bbd02051a144d368d3577cc7b668a97.png&quot; alt=&quot;image-20250625141337063&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;client kill&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;该命令用于终止某个客户端的连接，完整命令格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;client kill [ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] [USER username] [ADDR ip:port] [SKIPME yes/no]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从命令格式中就可以看得出，该命令可以通过多种方式终止客户端的连接。&lt;/p&gt;
&lt;p&gt;比如想终止id为5的连接，可以使用命令&lt;code&gt;client kill id 5&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;6数据清理命令&quot;&gt;6、数据清理命令&lt;/h3&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flushdb [ASYNC|SYNC]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除当前数据库的所有键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;flushall [ASYNC|SYNC]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除所有数据库的所有键&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这两个命令往往会触发安全策略被禁止执行。&lt;/p&gt;
&lt;h2 id=&quot;二五种数据类型&quot;&gt;二、五种数据类型&lt;/h2&gt;
&lt;p&gt;Redis的存储是以Key-Value形式存储的，value有五种类型：string、hash、list、set、zset 以应对不同数据格式的存储。&lt;/p&gt;
&lt;h3 id=&quot;1string&quot;&gt;1、string&lt;/h3&gt;
&lt;p&gt;String类型是最简单的数据类型，它的值是字符串类型或者数字类型。&lt;/p&gt;
&lt;h4 id=&quot;赋值&quot;&gt;赋值&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SET key value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置指定 key 的值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MSET key value [key value ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同时设置一个或多个 key-value 对。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPEND key value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;APPEND 命令将指定的 value 追加到该 key 原来值（value）的末尾，返回值是追加后字符串长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;SETNX key value&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;仅当不存在时赋值，使用该命令可以实现【分布式锁】的功能&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;查询&quot;&gt;查询&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询指定 key 的值。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MGET key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;同时获取一个或多个key值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STRLEN key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回键值的长度，如果键不存在则返回0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GETRANGE key start end&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取key值的子串，坐标从0开始，start到end为闭区间；end为-1表示最后一个字符&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;更新&quot;&gt;更新&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GETSET key value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为key设置新值value，并返回旧值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INCR key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自增命令。&lt;br/&gt;当value为整数数据时，才能使用该命令，该命令是原子操作，返回值是自增后的新值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INCRBY key increment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;增加increment命令。&lt;br/&gt;当value为整数数据时，才能使用该命令，该命令是原子操作，返回值是增加后的新值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DECR key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自减命令。&lt;br/&gt;当value为整数数据时，才能使用该命令，该命令是原子操作，返回值是自减后的新值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DECRBY key decrement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;减少decrement命令。&lt;br/&gt;当value为整数数据时，才能使用该命令，该命令是原子操作，返回值是减少后的新值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;2hash&quot;&gt;2、hash&lt;/h3&gt;
&lt;p&gt;hash 类型也叫散列类型，它提供了字段和字段值的映射。字段值只能是字符串类型，不支持散列类型、集合类型等
其它类型，它的数据结构和java中的Map或者Python中的字典比较像，hash 特别适合用于存储对象。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/25/3d259e47111b48458c16326cd553494c.png&quot; alt=&quot;image-20250625221818072&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h4 id=&quot;赋值-1&quot;&gt;赋值&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hset key field value [field value ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置多个属性值，例如&lt;code&gt;hset user name zhangsan age 14&lt;/code&gt;，&lt;br/&gt;返回值是一个整数表示新增的字段数量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hmset key field value [field value ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置多个属性值，返回值为成功或者失败；&lt;br/&gt;新版本已经不推荐使用该方法设置了，推荐使用hset。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;hsetnx key field value&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;类似 HSET ，区别在于如果字段存在，该命令不执行任何操作。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;查询-1&quot;&gt;查询&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hget key field&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取一个字段值，例如：&lt;code&gt;hget user name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hmget key field [field ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取多个字段值，例如：&lt;code&gt;hmget user name age&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hgetall key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取所有字段值，例如：&lt;code&gt;hgetall user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hexists key field&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断某个属性是否存在&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hkeys key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取所有属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hvals key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取所有值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hlen key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取字段数量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;更新-1&quot;&gt;更新&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hdel key field [field ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除key的属性字段&lt;br/&gt;注意并非删除key，删除key应当使用通用的del命令。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hincrby key field increment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;增加increment命令。&lt;br/&gt;当属性值为整数数据时，才能使用该命令，该命令是原子操作，返回值是增加后的新值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hincrbyfloat key field increment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;增加increment命令。&lt;br/&gt;当属性值为数字类型时，才能使用该命令，该命令是原子操作，返回值是增加后的新值&lt;br/&gt;相对于hincrby，它支持浮点数的运算，但是可能存在精度丢失的问题。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;3list&quot;&gt;3、list&lt;/h3&gt;
&lt;p&gt;redis中的list可以存储一个有序的字符串列表，它的内部是一个双向链表所以向列表两端添加元素的时间复杂度为0(1) ，获取越接近两端的元素速度就越快。基于此特性，list数据类型可被用于消息队列操作。&lt;/p&gt;
&lt;h4 id=&quot;入队出队&quot;&gt;入队出队&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lpush key element [element ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从左侧入队。比如&lt;code&gt;lpush list 1 2 3 &lt;/code&gt;实际上在list中存储的顺序是3 2 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rpush key element [element ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从右侧入队。比如&lt;code&gt;rpush list 1 2 3&lt;/code&gt;实际上在list中存储的也是1 2 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lpop key [count]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;左侧弹出一个元素或者count个元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rpop key [count]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;右侧弹出一个元素或者count个元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;更新-2&quot;&gt;更新&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lrem key count element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除列表中指定个数的值。&lt;br/&gt;LREM 命令会删除列表中前 count 个值为 value 的元素，返回实际删除的元素个数。&lt;br/&gt;根据 count 值的不同，该命令的执行方式会有所不同：&lt;br/&gt;当count&amp;gt;0时， LREM会从列表左边开始删除。&lt;br/&gt;当count&amp;lt;0时， LREM会从列表后边开始删除。&lt;br/&gt;当count=0时， LREM删除所有值为value的元素。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lset key index element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设置index下标（正向）的数组值为element&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;linsert key BEFORE|AFTER pivot element&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向列表中插入元素。&lt;br/&gt;该命令首先会在列表中从左到右查找值为pivot的元素，然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ltrim key start stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只保留列表指定片段,指定范围和LRANGE一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rpoplpush source destination&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将source的最后一个元素转移到destination的头部&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;查询-2&quot;&gt;查询&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lrange key start stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询指定范围（闭区间）的元素列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lindex key index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询指定元素下标的值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llen key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询列表元素长度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;4set&quot;&gt;4、set&lt;/h3&gt;
&lt;p&gt;set 类型即集合类型，其中的数据是不重复且没有顺序。&lt;/p&gt;
&lt;p&gt;集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等，由于集合类型的 Redis 内部是使用值
为空的散列表实现，所有这些操作的时间复杂度都为 0(1) 。&lt;/p&gt;
&lt;p&gt;Redis 还提供了多个集合之间的交集、并集、差集的运算。&lt;/p&gt;
&lt;h4 id=&quot;基础命令&quot;&gt;基础命令&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sadd key member [member ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;向集合中添加元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;srem key member [member ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从集合中移除元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smembers key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获得集合中的所有元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sismember key member&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断元素是否在集合中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scard key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获得集合中元素的个数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spop key [count]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;随机从集合中移除count个元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;集合运算&quot;&gt;集合运算&lt;/h4&gt;
&lt;p&gt;假设我们有A集合{1,2,3}和B集合{3,4,5}&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sdiff key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取差集。&lt;br/&gt;&lt;code&gt;sdiff A B&lt;/code&gt;相当于A-B，结果为{1,2}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sinter key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取交集。&lt;br/&gt;&lt;code&gt;sinter A B&lt;/code&gt;相当于A∩B，结果为{3}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sunion key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;取并集。&lt;br/&gt;&lt;code&gt;suion A B&lt;/code&gt;相当于A∪B，结果为{1,2,3,4,5}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;5zset&quot;&gt;5、zset&lt;/h3&gt;
&lt;p&gt;zet又称为sorted set，即有序集合，它是在 set 集合类型的基础上，为集合中的每个元素都关联一个分数 ，这使得我们不仅可以完成插入、删除和判断元素是否存在在集合中，还能够获得分数最高或最低的前N个元素、获取指定分数范围内的元素等与分数有关的操作。在某些方面有序集合和列表类型有些相似：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二者都是有序的。&lt;/li&gt;
&lt;li&gt;二者都可以获得某一范围的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是，二者有着很大区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列表类型是通过链表实现的，获取靠近两端的数据速度极快，而当元素增多后，访问中间数据的速度会变慢。&lt;/li&gt;
&lt;li&gt;有序集合类型使用散列表实现，所有即使读取位于中间部分的数据也很快。&lt;/li&gt;
&lt;li&gt;列表中不能简单的调整某个元素的位置，但是有序集合可以（通过更改分数实现）&lt;/li&gt;
&lt;li&gt;有序集合要比列表类型更耗内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;zadd命令&quot;&gt;zadd命令&lt;/h4&gt;
&lt;p&gt;zadd命令用于向有序集合中新增或更新元素，它是有序集合中最重要的一个命令，其格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zadd key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到它有很多可选参数，这导致该命令比较复杂。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基础使用命令&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;刨除可选参数，zadd的基础命令就是&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zadd key score member [score member ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，分数在前，元素在后。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; zadd scoreboard 80 zhangsan 89 lisi 94 wangwu
(integer) 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;可选参数[NX|XX]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;NX：不存在则添加，否则忽略&lt;/p&gt;
&lt;p&gt;XX：存在则更新分数，否则忽略&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; zadd z nx 90 zhangsan   #zhangsan不存在则新增
(integer) 1
127.0.0.1:6379&amp;gt; zadd z nx 90 zhangsan   #zhangsan已存在则忽略
(integer) 0
127.0.0.1:6379&amp;gt; zadd z xx 80 lisi       #lisi不存在则忽略
(integer) 0
127.0.0.1:6379&amp;gt; zadd z xx 80 zhangsan   #lisi已存在则更新
(integer) 0
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; 
127.0.0.1:6379&amp;gt; zrange z 0 -1 withscores
1) &amp;quot;zhangsan&amp;quot;
2) &amp;quot;80&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;可选参数[GT|LT]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GT：当新分数大于原分数时才更新&lt;/p&gt;
&lt;p&gt;LT：新分数小于原分数时才更新&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; zrange z 0 -1 withscores
1) &amp;quot;zhangsan&amp;quot;
2) &amp;quot;80&amp;quot;
127.0.0.1:6379&amp;gt; zadd z GT 79 zhangsan   # zhangsan新分数79大于原分数80才更新，所以没更新
(integer) 0
127.0.0.1:6379&amp;gt; zrange z 0 -1 withscores
1) &amp;quot;zhangsan&amp;quot;
2) &amp;quot;80&amp;quot;
127.0.0.1:6379&amp;gt; zadd z GT 81 zhangsan  # zhangsan新分数81大于原分数80才更新，所以更新了
(integer) 0
127.0.0.1:6379&amp;gt; zrange z 0 -1 withscores
1) &amp;quot;zhangsan&amp;quot;
2) &amp;quot;81&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;可选参数[CH]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CH参数的作用：返回值改为所有被修改的成员数量（包括新增和更新的成员）。zadd的默认行为是仅统计新添加的成员。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; zrange z 0 -1 withscores
1) &amp;quot;zhangsan&amp;quot;
2) &amp;quot;90&amp;quot;
127.0.0.1:6379&amp;gt; zadd z 91 zhangsan   #修改了zhangsan的分数为91但是返回0
(integer) 0
127.0.0.1:6379&amp;gt; zadd z CH 91 zhangsan #虽然加了CH参数，但是新旧分数都一样都是91所以并没有执行修改动作
(integer) 0
127.0.0.1:6379&amp;gt; zadd z CH 92 zhangsan #加了CH参数，新分数92修改后返回了影响行数
(integer) 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;可选参数INCR&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将成员的分数累加指定值（类似 &lt;code&gt;ZINCRBY&lt;/code&gt;），此时只能操作一个成员。返回值是更新后的分数值。建议使用&lt;code&gt;zincrby&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; zrange z 0 -1 withscores
1) &amp;quot;zhangsan&amp;quot;
2) &amp;quot;95&amp;quot;
127.0.0.1:6379&amp;gt; zadd z incr 3 zhangsan #zhangsan增加3
&amp;quot;98&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;更新-3&quot;&gt;&lt;strong&gt;更新&lt;/strong&gt;&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zincrby key increment member&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;有序集合中对指定成员的分数加上增量 increment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zrem key member [member ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;移除有序集合中的一个或多个成员&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zremrangebyscore key min max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;根据分值范围删除范围内的所有成员&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zremrangebylex key min max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;移除有序集合中给定的字典区间的所有成员&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zremrangebyrank key start stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;移除有序集合中给定的排名区间的所有成员&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;查询-3&quot;&gt;&lt;strong&gt;查询&lt;/strong&gt;&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zrange key min max [BYSCOREBYLEX] [REV] [LIMIT offset count] [WITHSCORES]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取索引在min和max之间的成员。&lt;br/&gt;比如&lt;code&gt;zrange z 0 -1 withscores&lt;/code&gt;命令将查询z集合的全部成员。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zcard key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;获取有序集合的成员数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zcount key min max&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算在有序集合中指定区间分数的成员数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zrangebyscore key min max [WITHSCORES] [LIMIT offset count]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过分数返回有序集合指定区间内的成员&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zrank key member&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询member的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zscore key member&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询成员分值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;集合运算-1&quot;&gt;集合运算&lt;/h4&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zdiff numkeys key [key ...] [WITHSCORES]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求差集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zinter numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求交集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zunion numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求并集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zdiffstore destination numkeys key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求差集并将结果存入有序集合destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zinterstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求交集并将结果存入有序集合destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zunionstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求并集并将结果存入有序集合destination&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;三key操作命令&quot;&gt;三、key操作命令&lt;/h2&gt;
&lt;p&gt;以上五种数据类型都没有介绍删除命令，因为它们都使用统一的key删除命令。&lt;/p&gt;
&lt;h3 id=&quot;1常见命令&quot;&gt;1、常见命令&lt;/h3&gt;
&lt;p&gt;以下是常见的key命令：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;del key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除一个或多个key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists key [key ...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断一个key或者多个key是否存在&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expire key seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为key设置过期时间，单位秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;keys pattern&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;查询指定模式的key列表，比如常见的&lt;code&gt;keys *&lt;/code&gt;查询所有key，注意该命令可能会导致redis server被阻塞。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;move key db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将key移动到别的db&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persist key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;移除key的过期时间，key将持久保存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ttl key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以秒为单位返回key的剩余有效期事件，有效期过后key将被删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pttl key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;以毫秒为单位返回key的剩余有效期事件，有效期过后key将被删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rename key newkey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;将key重命名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renamenx key newkey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;在newkey不存在的情况下将key重命名为newkey&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;type key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查询某个key的类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;除了以上常见命令，还有一个比较重要的常见命令scan。&lt;/p&gt;
&lt;h3 id=&quot;2scan命令&quot;&gt;2、scan命令&lt;/h3&gt;
&lt;p&gt;Redis 的 &lt;code&gt;SCAN&lt;/code&gt; 命令是一种**非阻塞式、渐进式遍历键空间（key space）**的迭代器命令，用于替代阻塞式的 &lt;code&gt;KEYS&lt;/code&gt; 命令，特别适合生产环境中处理大数据量的键遍历需求。&lt;/p&gt;
&lt;p&gt;scan命令格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scan cursor [MATCH pattern] [COUNT count] [TYPE type]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cursor&lt;/code&gt;&lt;/strong&gt;：游标值，初始为 &lt;code&gt;0&lt;/code&gt;，每次调用返回新的游标，直到返回 &lt;code&gt;0&lt;/code&gt; 表示遍历完成&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;MATCH pattern&lt;/code&gt;&lt;/strong&gt;（可选）：通配符模式匹配键名（如 &lt;code&gt;user:*&lt;/code&gt; 匹配以 &lt;code&gt;user:&lt;/code&gt; 开头的键）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;COUNT count&lt;/code&gt;&lt;/strong&gt;（可选）：提示每次迭代返回的键数量（默认约 10 个，实际可能浮动）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;TYPE type&lt;/code&gt;&lt;/strong&gt;（Redis 6.0+）：按数据类型过滤（如 &lt;code&gt;string&lt;/code&gt;、&lt;code&gt;hash&lt;/code&gt; 等）&lt;/p&gt;
&lt;p&gt;来看一个例子。&lt;/p&gt;
&lt;p&gt;现在我有如下keys&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; keys *
1) &amp;quot;c&amp;quot;
2) &amp;quot;g&amp;quot;
3) &amp;quot;d&amp;quot;
4) &amp;quot;f&amp;quot;
5) &amp;quot;a&amp;quot;
6) &amp;quot;e&amp;quot;
7) &amp;quot;h&amp;quot;
8) &amp;quot;b&amp;quot;
127.0.0.1:6379&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我要用scan命令来遍历它：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;127.0.0.1:6379&amp;gt; scan 0 match * count 1 type string #初始从0开始遍历
1) &amp;quot;4&amp;quot;				#下一个游标是4
2) 1) &amp;quot;c&amp;quot;			#本次返回的结果是c
127.0.0.1:6379&amp;gt; scan 4 match * count 1 type string #从4开始下一个迭代查询
1) &amp;quot;2&amp;quot;				#下一个游标是2
2) 1) &amp;quot;h&amp;quot;			#本次返回两个值：h、b
   2) &amp;quot;b&amp;quot;
127.0.0.1:6379&amp;gt; scan 2 match * count 1 type string
1) &amp;quot;6&amp;quot;
2) 1) &amp;quot;g&amp;quot;
   2) &amp;quot;d&amp;quot;
127.0.0.1:6379&amp;gt; scan 6 match * count 1 type string
1) &amp;quot;7&amp;quot;
2) 1) &amp;quot;f&amp;quot;
   2) &amp;quot;a&amp;quot;
   3) &amp;quot;e&amp;quot;
127.0.0.1:6379&amp;gt; scan 7 match * count 1 type string
1) &amp;quot;0&amp;quot;
2) (empty array)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，虽然指定了每次返回的数量是1，但是返回的数量大部分并不是1，似乎count参数设置的并没有生效：这是因为&lt;code&gt;COUNT&lt;/code&gt; 不保证精确性，它是性能优化的提示，非强制限制。&lt;code&gt;SCAN&lt;/code&gt; 使用高位进位加法遍历哈希槽，避免因扩容/缩容遗漏数据。此算法可能导致单次遍历多个槽位，从而返回更多元素。&lt;/p&gt;
&lt;p&gt;scan命令只用于key的遍历，对于么比如hash、set、zset类型的，它们本身也是可以遍历的，所以还有scan的变种命令：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hscan key cursor [MATCH pattern] [COUNT count]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hash类型的key遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sscan key cursor [MATCH pattern] [COUNT count]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;set类型的key遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zscan key cursor [MATCH pattern] [COUNT count]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;zset类型的key遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
      <category>redis</category>
    </item>
    <item>
      <title>SpringBoot集成TOTP双因素认证（2FA）实战</title>
      <link>https://blog.kdyzm.cn/post/311</link>
      <guid>https://blog.kdyzm.cn/post/311</guid>
      <pubDate>Tue, 17 Jun 2025 17:38:54 +0800</pubDate>
      <description>&lt;h2 id=&quot;一双因素认证的概念&quot;&gt;一、双因素认证的概念&lt;/h2&gt;
&lt;p&gt;双因素认证（2FA，Two Factor Authentication）又称双因子认证、两步验证，指的是是一种安全认证过程，需要用户提供两种不同类型的认证因子来表明自己的身份，包括密码、指纹、短信验证码、智能卡、生物识别等多种因素组合，从而提高用户账户的安全性和可靠性。&lt;/p&gt;
&lt;p&gt;2FA认证流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户登录应用程序。&lt;/li&gt;
&lt;li&gt;用户输入登录凭证，通常是账号和密码，做初始身份验证。&lt;/li&gt;
&lt;li&gt;验证成功后，提示用户提交第二个身份验证因子。&lt;/li&gt;
&lt;li&gt;用户将第二个身份验证因子输入至应用程序，如果第二个身份验证因子通过，用户将通过身份验证并被授予对应的系统操作权限。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;举个简单的例子，我们使用账号密码登录微博、豆瓣等应用时，命名账号密码都对了，但是还要输入手机验证码二次验证以确保安全性，这就是双因素验证。&lt;/p&gt;
&lt;p&gt;虽然短信验证码实现简单，但是在实际场景中，一般会使用其它2FA方式替代：一则短信发送会产生费用，二则它也不是那么安全，它容易被拦截和伪造，SIM 卡也可以克隆。&lt;/p&gt;
&lt;p&gt;一般来说，安全的双因素认证不是密码 + 短消息，而是密码+ &lt;a href=&quot;https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm&quot;&gt;TOTP&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;二totp&quot;&gt;二、TOTP&lt;/h2&gt;
&lt;p&gt;TOTP 的全称是&amp;quot;基于时间的一次性密码&amp;quot;（Time-based One-time Password）。它是公认的可靠解决方案，已经写入国际标准 &lt;a href=&quot;https://tools.ietf.org/html/rfc6238&quot;&gt;RFC6238&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id=&quot;1totp步骤&quot;&gt;1、TOTP步骤&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一步&lt;/strong&gt;，用户开启双因素认证后，服务器生成一个密钥。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步&lt;/strong&gt;：服务器提示用户扫描二维码（或者使用其他方式），把密钥保存到用户的手机。也就是说，服务器和用户的手机，现在都有了同一把密钥。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/a2dbf55a60e14f7f99bb58c660edbca5.png&quot; alt=&quot;image-20250617151407567&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;注意，密钥必须跟手机绑定。一旦用户更换手机，就必须生成全新的密钥。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步&lt;/strong&gt;，用户登录时，手机客户端使用这个密钥和当前时间戳，生成一个哈希，有效期默认为30秒。用户在有效期内，把这个哈希提交给服务器。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/7e4b59a37a0a4bbd86013d4fc76f3361.jpg&quot; alt=&quot;微信图片_20250617151530&quot; style=&quot;zoom: 25%;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;第四步&lt;/strong&gt;，服务器也使用密钥和当前时间戳，生成一个哈希，跟用户提交的哈希比对。只要两者不一致，就拒绝登录。&lt;/p&gt;
&lt;h3 id=&quot;2totp原理&quot;&gt;2、TOTP原理&lt;/h3&gt;
&lt;p&gt;仔细看上面的步骤，你可能会有一个问题：手机客户端和服务器，如何保证30秒期间都得到同一个哈希呢？&lt;/p&gt;
&lt;p&gt;答案就是下面的公式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TC = floor((unixtime(now) − unixtime(T0)) / TS)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的公式中，TC 表示一个时间计数器，&lt;code&gt;unixtime(now)&lt;/code&gt;是当前 Unix 时间戳，&lt;code&gt;unixtime(T0)&lt;/code&gt;是约定的起始时间点的时间戳，默认是&lt;code&gt;0&lt;/code&gt;，也就是1970年1月1日。TS 则是哈希有效期的时间长度，默认是30秒。因此，上面的公式就变成下面的形式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TC = floor(unixtime(now) / 30)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，只要在 30 秒以内，TC 的值都是一样的。前提是服务器和手机的时间必须同步。&lt;/p&gt;
&lt;p&gt;接下来，就可以算出哈希了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;TOTP = HASH(SecretKey, TC)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，&lt;code&gt;HASH&lt;/code&gt;就是约定的哈希函数，默认是 SHA-1。&lt;/p&gt;
&lt;p&gt;接下来在SpringBoot中集成TOTP实现双因素认证。&lt;/p&gt;
&lt;h2 id=&quot;三totp双因素认证实战&quot;&gt;三、TOTP双因素认证实战&lt;/h2&gt;
&lt;h3 id=&quot;1开源项目googleauth&quot;&gt;1、开源项目：GoogleAuth&lt;/h3&gt;
&lt;p&gt;TOTP自己手写还是稍稍有些复杂，去网上找了开源项目，发现一个比较好的实现：&lt;a href=&quot;https://github.com/wstrange/GoogleAuth&quot;&gt;GoogleAuth&lt;/a&gt;，使用时需要引入Maven依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.warrenstrange&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;googleauth&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.5.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了生成图片二维码，还需要引入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;properties&amp;gt;
    &amp;lt;maven.compiler.source&amp;gt;8&amp;lt;/maven.compiler.source&amp;gt;
    &amp;lt;maven.compiler.target&amp;gt;8&amp;lt;/maven.compiler.target&amp;gt;
    &amp;lt;zxing.version&amp;gt;3.4.0&amp;lt;/zxing.version&amp;gt;
&amp;lt;/properties&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.google.zxing&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;core&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${zxing.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.google.zxing&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;javase&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${zxing.version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GoogleAuth的API比较简单，常见API如下：&lt;/p&gt;
&lt;h4 id=&quot;生成secret&quot;&gt;生成secret&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 测试获取秘钥
 */
@Test
public void testGetKey() {
    GoogleAuthenticator gAuth = new GoogleAuthenticator();
    final GoogleAuthenticatorKey key = gAuth.createCredentials();
    String key1 = key.getKey();
    log.info(key1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;生成一次性验证码&quot;&gt;生成一次性验证码&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 测试生成一次性验证码
 */
@Test
public void testGetTotpPassword() {
    GoogleAuthenticator gAuth = new GoogleAuthenticator();
    int code = gAuth.getTotpPassword(&amp;quot;PWTBUDW6OAPV6E2EVMBHX2X7LH6MXRNE&amp;quot;);
    log.info(&amp;quot;{}&amp;quot;, code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;验证码验证&quot;&gt;验证码验证&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 测试秘钥验证
 */
@Test
public void testAuthorize() {
    GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig
            .GoogleAuthenticatorConfigBuilder()
            //设置容忍度最小
            .setWindowSize(1)
            .build();
    GoogleAuthenticator gAuth = new GoogleAuthenticator(config);
    int verificationCode = 448247;
    String secretKey = &amp;quot;6VRFLPHNPQ4P2WAQWEIYPCQ43KIHVCJO&amp;quot;;
    boolean isCodeValid = gAuth.authorize(secretKey, verificationCode);
    if (isCodeValid) {
        log.info(&amp;quot;匹配&amp;quot;);
    } else {
        log.info(&amp;quot;不匹配&amp;quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;获取二维码图片链接格式&quot;&gt;获取二维码（图片链接格式）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 获取图片二维码
 */
@Test
public void testGetOtpAuthURL() {
    GoogleAuthenticator gAuth = new GoogleAuthenticator();
    final GoogleAuthenticatorKey key = gAuth.createCredentials();
    log.info(key.getKey());
    String otpAuthURL = GoogleAuthenticatorQRGenerator.getOtpAuthURL(
            ISSUER,
            userName,
            key
    );
    log.info(otpAuthURL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;获取二维码字节流&quot;&gt;获取二维码（字节流）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void testGetOtpAuthQrByteArrayOutputStream() throws WriterException, IOException {
    GoogleAuthenticator gAuth = new GoogleAuthenticator();
    final GoogleAuthenticatorKey key = gAuth.createCredentials();
    String otpAuthUri = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(
            ISSUER,
            userName,
            key);

    QRCodeWriter qrWriter = new QRCodeWriter();
    BitMatrix bitMatrix = qrWriter.encode(otpAuthUri, BarcodeFormat.QR_CODE, 200, 200);
    BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ImageIO.write(bufferedImage, &amp;quot;png&amp;quot;, outputStream);
    ImageIO.write(bufferedImage, &amp;quot;png&amp;quot;, new File(&amp;quot;temp.png&amp;quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2开源项目光年admin模板&quot;&gt;2、开源项目：光年Admin模板&lt;/h3&gt;
&lt;p&gt;为了能更直观的展示TOTP功能集成到SpringBoot的样子，我决定基于开源项目 &lt;a href=&quot;https://gitee.com/yinqi/Light-Year-Admin-Template-v5&quot;&gt;Light Year Admin v5&lt;/a&gt; 去做前端的开发。&lt;strong&gt;&lt;a href=&quot;https://gitee.com/yinqi/Light-Year-Admin-Template-v5&quot;&gt;Light Year Admin v5&lt;/a&gt;&lt;/strong&gt; 是一个管理端模板，基于Bootstrap 5.1.3。线上体验地址：&lt;a href=&quot;http://lyear.itshubao.com/v5/&quot;&gt;http://lyear.itshubao.com/v5/&lt;/a&gt; ，也可以下载下来以后使用&lt;code&gt;http-server&lt;/code&gt;快速启动查看效果：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/d4dac5664edb4c17906be417cc094ddf.jpg&quot; alt=&quot;20250617160234&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;p&gt;该项目是一个纯前端项目，为了更方便的集成到SpringBoot，我将其集成到了freemarker：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/f1092fafdbbd4f51bc3dcfa088d709f5.png&quot; alt=&quot;image-20250617161339553&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;3项目实战2fa-demo&quot;&gt;3、项目实战：2fa-demo&lt;/h3&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://gitee.com/kdyzm/2fa-demo&quot;&gt;https://gitee.com/kdyzm/2fa-demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;该项目依赖于MySQL，所以在运行前需要先准备好MySQL环境。&lt;/p&gt;
&lt;h4 id=&quot;运行前准备&quot;&gt;运行前准备&lt;/h4&gt;
&lt;p&gt;需要创建Mysql数据库，运行如下脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE DATABASE `2fa_demo` ;
USE `2fa_demo`;

CREATE TABLE `sys_user` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT &apos;主键&apos;,
  `user_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;用户名&apos;,
  `nick_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;用户昵称&apos;,
  `password` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;用户密码&apos;,
  `two_fa_secret` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;两步验证的秘钥&apos;,
  `tow_fa_enabled` tinyint(1) DEFAULT &apos;0&apos; COMMENT &apos;是否启用两步验证&apos;,
  `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT &apos;sys&apos; COMMENT &apos;创建人&apos;,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT &apos;创建时间&apos;,
  `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;更新人&apos;,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT &apos;更新时间&apos;,
  `del_flag` tinyint(1) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;删除标志，0：未删除；1：已删除&apos;,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT=&apos;用户表&apos;;

insert  into `sys_user`(`id`,`user_name`,`nick_name`,`password`,`two_fa_secret`,`tow_fa_enabled`,`create_by`,`create_time`,`update_by`,`update_time`,`del_flag`) values 
(1,&apos;kdyzm&apos;,&apos;狂盗一枝梅&apos;,&apos;123456&apos;,&apos;H5C7U7M3FJN6DGL6EAAWHF6TVAAINAGU&apos;,0,&apos;kdyzm&apos;,&apos;2025-06-16 13:51:03&apos;,NULL,&apos;2025-06-17 13:37:13&apos;,0);

CREATE TABLE `sys_user_2fa` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT &apos;主键&apos;,
  `user_id` bigint DEFAULT NULL COMMENT &apos;用户id&apos;,
  `secret_key` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;秘钥&apos;,
  `scratch_codes` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;静态验证码&apos;,
  `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT &apos;创建人&apos;,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT &apos;创建时间&apos;,
  `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT &apos;更新人&apos;,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT &apos;更新时间&apos;,
  `del_flag` tinyint(1) NOT NULL DEFAULT &apos;0&apos; COMMENT &apos;删除标志，0：未删除；1：已删除&apos;,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT=&apos;两步验证相关临时配置表&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后修改配置文件中的Mysql配置信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;JDBC_MYSQL_HOST: localhost
JDBC_MYSQL_PORT: 3306
JDBC_MYSQL_DATABASE: 2fa_demo
JDBC_MYSQL_USERNAME: root
JDBC_MYSQL_PASSWORD: &apos;123456&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;项目启动&quot;&gt;项目启动&lt;/h4&gt;
&lt;p&gt;将项目导入Intelij，运行Application，出现如下即可表示运行成功&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/a380a31e0bdd44a5a8a0589eafb35309.png&quot; alt=&quot;image-20250617165811258&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;打开链接，进入登录页面&lt;/p&gt;
&lt;h4 id=&quot;账号密码登录&quot;&gt;账号密码登录&lt;/h4&gt;
&lt;p&gt;在未登录的情况下打开链接&lt;a href=&quot;http://localhost:8024，就会进入登录页面&quot;&gt;http://localhost:8024，就会进入登录页面&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/101c6b1714874552b5613237cf183402.jpg&quot; alt=&quot;20250617170158&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;blockquote&gt;
&lt;p&gt;登录账号：kdyzm&lt;/p&gt;
&lt;p&gt;登录密码：123456&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;登录成功之后进入首页。&lt;/p&gt;
&lt;h4 id=&quot;开启两步验证&quot;&gt;开启两步验证&lt;/h4&gt;
&lt;p&gt;登录成功之后进入首页，点击首页右上角两步验证&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/1daa4471b05948acbb6852dad505e613.jpg&quot; alt=&quot;20250617170554&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;p&gt;进入两步验证页面，由于未设置过二次验证，所以会提示去设置&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/a3e03f40dd354278881db04660c35d18.png&quot; alt=&quot;20250617170716&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;p&gt;点击“开启二次验证”按钮，进入两步骤验证向导&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/5e7dde929dd24aa195abd9b32d9a730a.jpg&quot; alt=&quot;20250617170906&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;p&gt;点击下一步输入电子邮件&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/bbcdae68b8ae4cb58d496af05e082ee9.jpg&quot; alt=&quot;20250617171200&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;p&gt;点击下一步，进入关键的验证器配置步骤&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/1b475b7cc39e41b69053242b72c030ea.jpg&quot; alt=&quot;20250617171311&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;p&gt;在这一步，IOS下可以安装&lt;code&gt;Authenticator&lt;/code&gt;或者微信小程序搜索“&lt;code&gt;MFA&lt;/code&gt;”，使用“&lt;code&gt;腾讯身份验证器&lt;/code&gt;”扫描二维码完成验证器设置，注意，&lt;strong&gt;如果多次扫描相同的二维码，需要删除上次扫描的记录&lt;/strong&gt;。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/7e4b59a37a0a4bbd86013d4fc76f3361.jpg&quot; alt=&quot;微信图片_20250617151530&quot; style=&quot;zoom: 25%;&quot; /&gt;
&lt;p&gt;点击下一步，校验配置的正确性：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/41e4eab9013a43b2b10b17052d83ca3b.jpg&quot; alt=&quot;20250617172222&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;p&gt;提示配置开启成功即表示已配置成功。&lt;/p&gt;
&lt;h4 id=&quot;验证两步验证&quot;&gt;验证两步验证&lt;/h4&gt;
&lt;p&gt;退出登录，回到登录页，输入账号密码登录，登录成功后不再跳转到首页，而是跳转到二次验证页面：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/fb21a1f86dee4e46ba3db1dabc47de8a.jpg&quot; alt=&quot;20250617172603&quot; style=&quot;zoom:80%;&quot; /&gt;
&lt;p&gt;从手机上获取动态验证码，即可成功登录系统。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/2decfbd6d20647fe86efc63c8ef569a1.jpg&quot; alt=&quot;20250617172715&quot; style=&quot;zoom:80%;&quot; /&gt;
&lt;h4 id=&quot;关闭两步验证&quot;&gt;关闭两步验证&lt;/h4&gt;
&lt;p&gt;进入首页后，再次进入两步验证页面，即可看到关闭按钮&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/17/00a371a536ad404cb69fd7984ca29a4d.png&quot; alt=&quot;20250617172910&quot; style=&quot;zoom:80%;&quot; /&gt;
&lt;p&gt;关闭后再次登录系统，就不会进入两步验证页面了。&lt;/p&gt;
&lt;h3 id=&quot;4实战总结&quot;&gt;4、实战总结&lt;/h3&gt;
&lt;p&gt;由于本项目案例只关注2FA相关的内容，我在光年Admin模板的静态模板只实现了登录以及2FA相关的功能，而且时间匆忙代码比较糙。。。&lt;/p&gt;
&lt;p&gt;该项目还剩下一些问题没实现&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;二次验证的记住设备功能&lt;/li&gt;
&lt;li&gt;8位数的静态码未实现校验功能&lt;/li&gt;
&lt;li&gt;设备丢失静态码找回功能未实现&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以后有时间再补充了。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>spring</category>
      <category>springboot</category>
      <category>2fa</category>
      <category>totp</category>
    </item>
    <item>
      <title>Python正则表达式指南</title>
      <link>https://blog.kdyzm.cn/post/310</link>
      <guid>https://blog.kdyzm.cn/post/310</guid>
      <pubDate>Thu, 12 Jun 2025 14:59:06 +0800</pubDate>
      <description>&lt;p&gt;正则表达式在各种语言中都是一个复杂的主题，在Python中，正则表达式设计的尤其复杂以适应不同场景下的脚本。&lt;/p&gt;
&lt;p&gt;python官方文档提供了正则表达式使用中的各种细节：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/howto/regex.html&quot;&gt;《正则表达式指南》&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html&quot;&gt;《&lt;code&gt;re&lt;/code&gt; --- 正则表达式操作》&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果仔仔细细看完这些文档，正则表达式也就掌握的差不多了，然鹅文档太长了，而且格式排版让人相当的难受，我将其常用功能重新分类整理，以方便查询。&lt;/p&gt;
&lt;h2 id=&quot;一元字符和转义字符&quot;&gt;一、元字符和转义字符&lt;/h2&gt;
&lt;p&gt;关于元字符和转义字符，可以参考官方文档：&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#regular-expression-syntax&quot;&gt;《正则表达式语法》&lt;/a&gt; 或者我整理的手册：&lt;a href=&quot;https://blog.kdyzm.cn/post/309&quot;&gt;《Python正则表达式匹配字符手册》&lt;/a&gt;，这里重新复习下核心内容部分。&lt;/p&gt;
&lt;p&gt;大多数字母和符号都会简单地匹配自身。例如，正则表达式 &lt;code&gt;test&lt;/code&gt; 将会精确地匹配到 &lt;code&gt;test&lt;/code&gt; 。（你可以启用不区分大小写模式，让这个正则也匹配 &lt;code&gt;Test&lt;/code&gt; 或 &lt;code&gt;TEST&lt;/code&gt; ，稍后会详细介绍。）&lt;/p&gt;
&lt;p&gt;但该规则有例外。有些字符是特殊的 &lt;em&gt;元字符（metacharacters）&lt;/em&gt;，并不匹配自身。事实上，它们表示匹配一些非常规的内容，或者通过重复它们或改变它们的含义来影响正则的其他部分。本文的大部分内容都致力于讨论各种元字符及其作用。&lt;/p&gt;
&lt;p&gt;这是元字符的完整列表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;. ^ $ * + ? { } [ ] \ | ( )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先介绍的元字符是 &lt;code&gt;[&lt;/code&gt; 和 &lt;code&gt;]&lt;/code&gt; 。这两个元字符用于指定一个字符类，也就是你希望匹配的字符的一个集合。这些字符可以单独地列出，也可以用字符范围来表示（给出两个字符并用 &lt;code&gt;&apos;-&apos;&lt;/code&gt; 分隔）。例如，&lt;code&gt;[abc]&lt;/code&gt; 将匹配 &lt;code&gt;a&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;c&lt;/code&gt; 之中的任意一个字符；这与 &lt;code&gt;[a-c]&lt;/code&gt; 相同，后者使用一个范围来表达相同的字符集合。如果只想匹配小写字母，则正则表达式将是 &lt;code&gt;[a-z]&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;元字符 (除了 &lt;code&gt;\&lt;/code&gt;) 在字符类中是不起作用的。 例如，&lt;code&gt;[akm$]&lt;/code&gt; 将会匹配以下任一字符 &lt;code&gt;&apos;a&apos;&lt;/code&gt;, &lt;code&gt;&apos;k&apos;&lt;/code&gt;, &lt;code&gt;&apos;m&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;$&apos;&lt;/code&gt;；&lt;code&gt;&apos;$&apos;&lt;/code&gt; 通常是一个元字符，但在一个字符类中它的特殊性被消除了。&lt;/p&gt;
&lt;p&gt;你可以通过对集合 &lt;em&gt;取反&lt;/em&gt; 来匹配字符类中未列出的字符。方法是把 &lt;code&gt;&apos;^&apos;&lt;/code&gt; 放在字符类的最开头。 例如，&lt;code&gt;[^5]&lt;/code&gt; 将匹配除 &lt;code&gt;&apos;5&apos;&lt;/code&gt; 之外的任何字符。 如果插入符出现在字符类的其他位置，则它没有特殊含义。 例如：&lt;code&gt;[5^]&lt;/code&gt; 将匹配 &lt;code&gt;&apos;5&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;^&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;也许最重要的元字符是反斜杠，&lt;code&gt;\&lt;/code&gt; 。 与 Python 字符串字面量一样，反斜杠后面可以跟各种字符来表示各种特殊序列。它还用于转义元字符，以便可以在表达式中匹配元字符本身。例如，如果需要匹配一个 &lt;code&gt;[&lt;/code&gt; 或 &lt;code&gt;\&lt;/code&gt; ，可以在其前面加上一个反斜杠来消除它们的特殊含义：&lt;code&gt;\[&lt;/code&gt; 或 &lt;code&gt;\\&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;一些以 &lt;code&gt;&apos;\&apos;&lt;/code&gt; 开头的特殊序列表示预定义的字符集合，这些字符集通常很有用，例如数字集合、字母集合或非空白字符集合。&lt;/p&gt;
&lt;p&gt;让我们举一个例子：&lt;code&gt;\w&lt;/code&gt; 匹配任何字母数字字符，&lt;code&gt;\w&lt;/code&gt; 相当于字符类 &lt;code&gt;[a-zA-Z0-9_]&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;以下为特殊序列的不完全列表。 完整列表参见标准库参考中 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re-syntax&quot;&gt;正则表达式语法&lt;/a&gt; 部分 。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;转义字符&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配任何十进制数字，等价于字符类 &lt;code&gt;[0-9]&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配任何非数字字符，等价于字符类 &lt;code&gt;[^0-9]&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配任何空白字符，等价于字符类 &lt;code&gt;[ \t\n\r\f\v]&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配任何非空白字符，等价于字符类 &lt;code&gt;[^ \t\n\r\f\v]&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配任何字母与数字字符，等价于字符类 &lt;code&gt;[a-zA-Z0-9_]&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\W&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配任何非字母与数字字符，等价于字符类 &lt;code&gt;[^a-zA-Z0-9_]&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些序列可以包含在字符类中。 例如，&lt;code&gt;[\s,.]&lt;/code&gt; 是一个匹配任何空白字符、&lt;code&gt;&apos;,&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;.&apos;&lt;/code&gt; 的字符类。&lt;/p&gt;
&lt;h2 id=&quot;二简单正则&quot;&gt;二、简单正则&lt;/h2&gt;
&lt;p&gt;让我们从最简单的正则表达式开始吧。由于正则表达式是用来操作字符串的，我们将从最常见的任务开始：匹配字符。能够匹配各种各样的字符集合是正则表达式可以做到的第一件事。&lt;/p&gt;
&lt;p&gt;我们先来说说重复元字符 &lt;code&gt;*&lt;/code&gt; 。 &lt;code&gt;*&lt;/code&gt; 并不是匹配一个字面字符 &lt;code&gt;&apos;*&apos;&lt;/code&gt; 。实际上，它指定前一个字符可以匹配零次或更多次，而不是只匹配一次。&lt;/p&gt;
&lt;p&gt;例如，&lt;code&gt;ca*t&lt;/code&gt; 将匹配 &lt;code&gt;&apos;ct&apos;&lt;/code&gt; （ 0 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; ）、&lt;code&gt;&apos;cat&apos;&lt;/code&gt; （ 1 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; ）、 &lt;code&gt;&apos;caaat&apos;&lt;/code&gt; （ 3 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; ）等等。&lt;/p&gt;
&lt;h3 id=&quot;1贪婪匹配&quot;&gt;1、贪婪匹配&lt;/h3&gt;
&lt;p&gt;类似 &lt;code&gt;*&lt;/code&gt; 这样的重复是 &lt;em&gt;贪婪的&lt;/em&gt; 。当重复正则时，匹配引擎将尝试重复尽可能多的次数。 如果表达式的后续部分不匹配，则匹配引擎将回退并以较少的重复次数再次尝试。&lt;/p&gt;
&lt;p&gt;通过一个逐步示例更容易理解这一点。让我们分析一下表达式 &lt;code&gt;a[bcd]*b&lt;/code&gt; 。 该表达式首先匹配一个字母 &lt;code&gt;&apos;a&apos;&lt;/code&gt; ，接着匹配字符类 &lt;code&gt;[bcd]&lt;/code&gt; 中的零个或更多个字母，最后以一个 &lt;code&gt;&apos;b&apos;&lt;/code&gt; 结尾。 现在想象一下用这个正则来匹配字符串 &lt;code&gt;&apos;abcbd&apos;&lt;/code&gt; 。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;步骤&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;匹配&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;1&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;正则中的 &lt;code&gt;a&lt;/code&gt; 匹配成功。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;abcbd&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;引擎尽可能多地匹配 &lt;code&gt;[bcd]*&lt;/code&gt; ，直至字符串末尾。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;3&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;em&gt;失败&lt;/em&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;引擎尝试匹配 &lt;code&gt;b&lt;/code&gt; ，但是当前位置位于字符串末尾，所以匹配失败。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;4&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;abcb&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;回退，让 &lt;code&gt;[bcd]*&lt;/code&gt; 少匹配一个字符。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;5&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;em&gt;失败&lt;/em&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;再次尝试匹配 &lt;code&gt;b&lt;/code&gt; ， 但是当前位置上的字符是最后一个字符 &lt;code&gt;&apos;d&apos;&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;6&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;abc&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;再次回退，让 &lt;code&gt;[bcd]*&lt;/code&gt; 只匹配 &lt;code&gt;bc&lt;/code&gt; 。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;7&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;abcb&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;再次尝试匹配 &lt;code&gt;b&lt;/code&gt; 。 这一次当前位置的字符是 &lt;code&gt;&apos;b&apos;&lt;/code&gt; ，所以它成功了。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;此时正则表达式已经到达了尽头，并且匹配到了 &lt;code&gt;&apos;abcb&apos;&lt;/code&gt; 。 这个例子演示了匹配引擎一开始会尽其所能地进行匹配，如果没有找到匹配，它将逐步回退并重试正则的剩余部分，如此往复，直至 &lt;code&gt;[bcd]*&lt;/code&gt; 只匹配零次。如果随后的匹配还是失败了，那么引擎会宣告整个正则表达式与字符串匹配失败。&lt;/p&gt;
&lt;p&gt;贪婪匹配在实际业务中会存在比较严重的问题，特别是在HTML解析方面，贪婪匹配的特性会让人很困惑。&lt;/p&gt;
&lt;p&gt;看以下案例：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/dd55b1c473c048fa9fca9d095c4e7f2f.png&quot; alt=&quot;image-20250611102538873&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;我们实际上想匹配第一个&amp;lt;html&amp;gt;标签，但是由于贪婪匹配的特性，最终匹配到了整个字符串。如何解决这个问题呢？&lt;/p&gt;
&lt;p&gt;解决方案是使用 &lt;strong&gt;&lt;span style=&apos;color:red&apos;&gt;非贪婪限定符&lt;/span&gt;&lt;/strong&gt; ?，在代表重复的元字符后加上?即可限制正则表达式的贪婪匹配特性： &lt;code&gt;*?&lt;/code&gt;, &lt;code&gt;+?&lt;/code&gt;, &lt;code&gt;??&lt;/code&gt; 或 &lt;code&gt;{m,n}?&lt;/code&gt;，它们会匹配尽可能 &lt;em&gt;少的&lt;/em&gt; 文本。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/3b37c28a9cb0418e83acb913f185b92d.png&quot; alt=&quot;image-20250611103154777&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;在一开始的&lt;code&gt;a[bcd]*b&lt;/code&gt; 正则匹配中，只需要将正则表达式改成&lt;code&gt;a[bcd]*?b&lt;/code&gt;即可消除贪婪匹配特性，最终匹配&lt;code&gt;&apos;abcbd&apos;&lt;/code&gt;字符串的结果将是&lt;code&gt;ab&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&quot;2代表重复的元字符&quot;&gt;2、代表重复的元字符&lt;/h3&gt;
&lt;p&gt;上面&lt;code&gt;*&lt;/code&gt;是介绍的第一个代表重复的元字符，另一个重复元字符是 &lt;code&gt;+&lt;/code&gt; ，表示匹配一次或更多次。请注意 &lt;code&gt;*&lt;/code&gt; 与 &lt;code&gt;+&lt;/code&gt; 之间的差别。 &lt;code&gt;*&lt;/code&gt; 表示匹配 &lt;em&gt;零次&lt;/em&gt; 或更多次，也就是说它所重复的内容是可以完全不出现的。而 &lt;code&gt;+&lt;/code&gt; 则要求至少出现一次。举一个类似的例子， &lt;code&gt;ca+t&lt;/code&gt; 可以匹配 &lt;code&gt;&apos;cat&apos;&lt;/code&gt; （ 1 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; ）或 &lt;code&gt;&apos;caaat&apos;&lt;/code&gt; （ 3 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt;），但不能匹配 &lt;code&gt;&apos;ct&apos;&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;此外还有两个重复操作符或限定符。 问号 &lt;code&gt;?&lt;/code&gt; 表示匹配一次或零次；你可以认为它把某项内容变成了可选的。 例如，&lt;code&gt;home-?brew&lt;/code&gt; 可以匹配 &lt;code&gt;&apos;homebrew&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;home-brew&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;最复杂的限定符是 &lt;code&gt;{m,n}&lt;/code&gt;，其中 &lt;em&gt;m&lt;/em&gt; 和 &lt;em&gt;n&lt;/em&gt; 都是十进制整数。 该限定符表示必须至少重复 &lt;em&gt;m&lt;/em&gt; 次，至多重复 &lt;em&gt;n&lt;/em&gt; 次。 例如，&lt;code&gt;a/{1,3}b&lt;/code&gt; 将匹配 &lt;code&gt;&apos;a/b&apos;&lt;/code&gt;, &lt;code&gt;&apos;a//b&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;a///b&apos;&lt;/code&gt;。 它不能匹配 &lt;code&gt;&apos;ab&apos;&lt;/code&gt;，因为其中没有斜杠，也不能匹配 &lt;code&gt;&apos;a////b&apos;&lt;/code&gt;，因为其中有四个斜杠。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;m&lt;/em&gt; 和 &lt;em&gt;n&lt;/em&gt; 不是必填的，缺失的情况下会设定为默认值。缺失 &lt;em&gt;m&lt;/em&gt; 会解释为最少重复 0 次 ，缺失 &lt;em&gt;n&lt;/em&gt; 则解释为最多重复无限次。&lt;/p&gt;
&lt;p&gt;最简单情况 &lt;code&gt;{m}&lt;/code&gt; 将与前一项完全匹配 &lt;em&gt;m&lt;/em&gt; 次。 例如，&lt;code&gt;a/{2}b&lt;/code&gt; 将只匹配 &lt;code&gt;&apos;a//b&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;实际上&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;+&lt;/code&gt;以及&lt;code&gt;?&lt;/code&gt;都可以用{m,n}限定符表示： &lt;code&gt;{0,}&lt;/code&gt; 等同于 &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;{1,}&lt;/code&gt; 等同于 &lt;code&gt;+&lt;/code&gt;, 而 &lt;code&gt;{0,1}&lt;/code&gt; 等同于 &lt;code&gt;?&lt;/code&gt;。 在可能的情况下使用 &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt; 或 &lt;code&gt;?&lt;/code&gt; 会更好，因为它们更为简短易读。&lt;/p&gt;
&lt;h2 id=&quot;三使用正则表达式&quot;&gt;三、使用正则表达式&lt;/h2&gt;
&lt;p&gt;现在我们已经了解了一些简单的正则表达式，那么我们如何在 Python 中实际使用它们呢？ &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#module-re&quot;&gt;&lt;code&gt;re&lt;/code&gt;&lt;/a&gt; 模块提供了正则表达式引擎的接口，可以让你将正则编译为对象，然后用它们来进行匹配。&lt;/p&gt;
&lt;h3 id=&quot;1编译正则表达式&quot;&gt;1、编译正则表达式&lt;/h3&gt;
&lt;p&gt;正则表达式被编译成模式对象，模式对象具有各种操作的方法，例如搜索模式匹配或执行字符串替换。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/7f3e20fe4aec479fb71ce6b0baad4fe5.png&quot; alt=&quot;image-20250611104644606&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.compile&quot;&gt;&lt;code&gt;re.compile()&lt;/code&gt;&lt;/a&gt; 也接受一个可选的 &lt;em&gt;flags&lt;/em&gt; 参数，用于启用各种特殊功能和语法变体。 我们稍后将介绍可用的设置，但现在只需一个例子&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/949f07d4f75d4ee09fad384aa9813b66.png&quot; alt=&quot;image-20250611104708689&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;正则作为字符串传递给 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.compile&quot;&gt;&lt;code&gt;re.compile()&lt;/code&gt;&lt;/a&gt; 。 正则被处理为字符串，因为正则表达式不是核心Python语言的一部分，并且没有创建用于表达它们的特殊语法。 （有些应用程序根本不需要正则，因此不需要通过包含它们来扩展语言规范。）相反，&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#module-re&quot;&gt;&lt;code&gt;re&lt;/code&gt;&lt;/a&gt; 模块只是Python附带的C扩展模块，就类似于 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/socket.html#module-socket&quot;&gt;&lt;code&gt;socket&lt;/code&gt;&lt;/a&gt; 或 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/zlib.html#module-zlib&quot;&gt;&lt;code&gt;zlib&lt;/code&gt;&lt;/a&gt; 模块。&lt;/p&gt;
&lt;p&gt;将正则放在字符串中可以使 Python 语言更简单，但有一个缺点是下一节的主题。&lt;/p&gt;
&lt;h3 id=&quot;2原始字符串解决反斜杠灾难&quot;&gt;2、原始字符串解决反斜杠灾难&lt;/h3&gt;
&lt;p&gt;我们来思考一个问题，如何写一个正则表达式以匹配字符串&lt;code&gt;\section&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;我们知道正则表达式使用反斜杠字符 (&lt;code&gt;&apos;\&apos;&lt;/code&gt;) 来表示特殊形式，比如&lt;code&gt;\d&lt;/code&gt;表示数字，&lt;code&gt;\D&lt;/code&gt;表示非数字等；普通字符串中反斜杠也是转义符号，比如&lt;code&gt;\n&lt;/code&gt;是换行符，&lt;code&gt;\t&lt;/code&gt;是制表符，使用&lt;code&gt;\&amp;quot;&lt;/code&gt;来避免字符串提前结束等。&lt;/p&gt;
&lt;p&gt;在本案例中，正则表达式中的反斜杠必须是原始反斜杠符号，所以正则表达式可以先写成&lt;code&gt;\\s.*&lt;/code&gt;以抵消正则表达式中的反斜杠转义，然而这还不行，因为正则表达式本身要作为字符串使用，反斜杠在Python字符串中也有转义作用，所以必须要对&lt;code&gt;\\s.*&lt;/code&gt;做再次转义：&lt;code&gt;\\\\s.*&lt;/code&gt;；回到&lt;code&gt;\section&lt;/code&gt;字符串，他作为被匹配的字符串，里面的转义符号也应当取消，所以它在Python字符串中的正确写法是&lt;code&gt;\\section&lt;/code&gt;，完整的程序如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;\\\\s.*&apos;)
&amp;gt;&amp;gt;&amp;gt; p.match(&apos;\\section&apos;)
&amp;lt;re.Match object; span=(0, 8), match=&apos;\\section&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，反斜杠在正则表达式中使用的时候要非常谨慎，当作为原始字符反斜杠使用的时候正则表达式更为复杂且难读懂，如何解决这个问题呢？&lt;/p&gt;
&lt;p&gt;答案是：使用**&lt;span style=&apos;color:red&apos;&gt;原始字符串&lt;/span&gt;**，Python中的原始字符串以前缀&apos;r&apos;开头，原始字符串不处理反斜杠的转义功能，这意味着&lt;code&gt;r&amp;quot;\n&amp;quot;&lt;/code&gt; 是一个包含 &lt;code&gt;&apos;\&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;n&apos;&lt;/code&gt; 的双字符字符串，而 &lt;code&gt;&amp;quot;\n&amp;quot;&lt;/code&gt; 是一个包含换行符的单字符字符串。正则表达式通常使用这种原始字符串表示法表示。&lt;/p&gt;
&lt;p&gt;回到本节的主题，使用原始字符串写法如何写一个正则表达式以匹配字符串&lt;code&gt;\section&lt;/code&gt;？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\\s.*&apos;)
&amp;gt;&amp;gt;&amp;gt; p.match(r&apos;\section&apos;)
&amp;lt;re.Match object; span=(0, 8), match=&apos;\\section&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3匹配和查询&quot;&gt;3、匹配和查询&lt;/h3&gt;
&lt;p&gt;一旦你有一个表示编译正则表达式的对象，你用它做什么？ 模式对象有几种方法和属性。 这里只介绍最重要的内容；请参阅 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#module-re&quot;&gt;&lt;code&gt;re&lt;/code&gt;&lt;/a&gt; 文档获取完整列表。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;方法 / 属性&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;目的&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;match()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;确定正则是否从字符串的开头匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;search()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;扫描字符串，查找此正则匹配的任何位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;findall()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;找到正则匹配的所有子字符串，并将它们作为列表返回。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;finditer()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;找到正则匹配的所有子字符串，并将它们返回为一个 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/glossary.html#term-iterator&quot;&gt;iterator&lt;/a&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果没有找到匹配， &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.match&quot;&gt;&lt;code&gt;match()&lt;/code&gt;&lt;/a&gt; 和 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.search&quot;&gt;&lt;code&gt;search()&lt;/code&gt;&lt;/a&gt; 返回 &lt;code&gt;None&lt;/code&gt; 。如果它们成功， 一个 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#match-objects&quot;&gt;匹配对象&lt;/a&gt; 实例将被返回，包含匹配相关的信息：起始和终结位置、匹配的子串以及其它。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\d+&apos;)
&amp;gt;&amp;gt;&amp;gt; m = p.match(&apos;&apos;)
&amp;gt;&amp;gt;&amp;gt; print(m)
None
&amp;gt;&amp;gt;&amp;gt; m = p.match(&apos;123a&apos;)
&amp;gt;&amp;gt;&amp;gt; print(m)
&amp;lt;re.Match object; span=(0, 3), match=&apos;123&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#match-objects&quot;&gt;匹配对象&lt;/a&gt; 中有以下几个方法最为重要：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;方法 / 属性&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;目的&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;group()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;返回正则匹配的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;start()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;返回匹配的开始位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;end()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;返回匹配的结束位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;span()&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;返回包含匹配 (start, end) 位置的元组&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; m.group()
&apos;123&apos;
&amp;gt;&amp;gt;&amp;gt; m.start(),m.end()
(0, 3)
&amp;gt;&amp;gt;&amp;gt; m.span()
(0, 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.group&quot;&gt;&lt;code&gt;group()&lt;/code&gt;&lt;/a&gt; 返回正则匹配的子字符串。 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.start&quot;&gt;&lt;code&gt;start()&lt;/code&gt;&lt;/a&gt; 和 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.end&quot;&gt;&lt;code&gt;end()&lt;/code&gt;&lt;/a&gt; 返回匹配的起始和结束索引。 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.span&quot;&gt;&lt;code&gt;span()&lt;/code&gt;&lt;/a&gt; 在单个元组中返回开始和结束索引。 &lt;strong&gt;由于 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.match&quot;&gt;&lt;code&gt;match()&lt;/code&gt;&lt;/a&gt; 方法只检查正则是否在字符串的开头匹配，所以 &lt;code&gt;start()&lt;/code&gt; 将始终为零。&lt;/strong&gt; 但是，模式的 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.search&quot;&gt;&lt;code&gt;search()&lt;/code&gt;&lt;/a&gt; 方法会扫描字符串，因此在这种情况下匹配可能不会从零开始。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; m = p.match(&apos;a123&apos;)
&amp;gt;&amp;gt;&amp;gt; print(m)
None
&amp;gt;&amp;gt;&amp;gt; m = p.search(&apos;a123&apos;)
&amp;gt;&amp;gt;&amp;gt; print(m)
&amp;lt;re.Match object; span=(1, 4), match=&apos;123&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在实际程序中，最常见的样式是在变量中存储 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#match-objects&quot;&gt;匹配对象&lt;/a&gt;，然后检查它是否为 &lt;code&gt;None&lt;/code&gt;。 这通常看起来像:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;p = re.compile( ... )
m = p.match( &apos;string goes here&apos; )
if m:
    print(&apos;Match found: &apos;, m.group())
else:
    print(&apos;No match&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;match&lt;/code&gt;方法和&lt;code&gt;search&lt;/code&gt;方法返回Match对象；&lt;code&gt;findall&lt;/code&gt;返回匹配字符串的列表，即所有匹配项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\d+&apos;)
&amp;gt;&amp;gt;&amp;gt; p.findall(&apos;11 people eat 24 apples ,every people eat 2 apples.&apos;)
[&apos;11&apos;, &apos;24&apos;, &apos;2&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.findall&quot;&gt;&lt;code&gt;findall()&lt;/code&gt;&lt;/a&gt; 必须先创建整个列表才能返回结果。 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.finditer&quot;&gt;&lt;code&gt;finditer()&lt;/code&gt;&lt;/a&gt; 方法将一个 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#match-objects&quot;&gt;匹配对象&lt;/a&gt; 的序列返回为一个 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/glossary.html#term-iterator&quot;&gt;iterator&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\d+&apos;)
&amp;gt;&amp;gt;&amp;gt; iter = p.finditer(&apos;11 people eat 24 apples ,every people eat 2 apples.&apos;)
&amp;gt;&amp;gt;&amp;gt; for item in iter:
...     print(item)
...
&amp;lt;re.Match object; span=(0, 2), match=&apos;11&apos;&amp;gt;
&amp;lt;re.Match object; span=(14, 16), match=&apos;24&apos;&amp;gt;
&amp;lt;re.Match object; span=(42, 43), match=&apos;2&apos;&amp;gt;
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4分割字符串&quot;&gt;4、分割字符串&lt;/h3&gt;
&lt;p&gt;模式的 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.split&quot;&gt;&lt;code&gt;split()&lt;/code&gt;&lt;/a&gt; 方法在正则匹配的任何地方拆分字符串，返回一个片段列表。 它类似于 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.split&quot;&gt;&lt;code&gt;split()&lt;/code&gt;&lt;/a&gt; 字符串方法，但在分隔符的分隔符中提供了更多的通用性；字符串的 &lt;code&gt;split()&lt;/code&gt; 仅支持按空格或固定字符串进行拆分。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;.split(string[, maxsplit=0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;em&gt;maxsplit&lt;/em&gt; 非零，则最多执行 &lt;em&gt;maxsplit&lt;/em&gt; 次拆分，并且字符串的其余部分将作为列表的最后一个元素返回。 在以下示例中，分隔符是任何非字母数字字符序列。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\W+&apos;)
&amp;gt;&amp;gt;&amp;gt; p.split(&apos;This is a test, short and sweet, of split().&apos;)
[&apos;This&apos;, &apos;is&apos;, &apos;a&apos;, &apos;test&apos;, &apos;short&apos;, &apos;and&apos;, &apos;sweet&apos;, &apos;of&apos;, &apos;split&apos;, &apos;&apos;]
&amp;gt;&amp;gt;&amp;gt; p.split(&apos;This is a test, short and sweet, of split().&apos;, 3)
[&apos;This&apos;, &apos;is&apos;, &apos;a&apos;, &apos;test, short and sweet, of split().&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在正则中使用捕获括号，则它们的值也将作为列表的一部分返回。 比较以下调用:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\W+&apos;)
&amp;gt;&amp;gt;&amp;gt; p2 = re.compile(r&apos;(\W+)&apos;)
&amp;gt;&amp;gt;&amp;gt; p.split(&apos;This... is a test.&apos;)
[&apos;This&apos;, &apos;is&apos;, &apos;a&apos;, &apos;test&apos;, &apos;&apos;]
&amp;gt;&amp;gt;&amp;gt; p2.split(&apos;This... is a test.&apos;)
[&apos;This&apos;, &apos;... &apos;, &apos;is&apos;, &apos; &apos;, &apos;a&apos;, &apos; &apos;, &apos;test&apos;, &apos;.&apos;, &apos;&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5替换字符串&quot;&gt;5、替换字符串&lt;/h3&gt;
&lt;p&gt;另一个常见任务是找到模式的所有匹配项，并用不同的字符串替换它们。 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.sub&quot;&gt;&lt;code&gt;sub()&lt;/code&gt;&lt;/a&gt; 方法接受一个替换值，可以是字符串或函数，也可以是要处理的字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;.sub(replacement, string[, count=0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回通过替换 &lt;em&gt;replacement&lt;/em&gt; 替换 &lt;em&gt;string&lt;/em&gt; 中正则的最左边非重叠出现而获得的字符串。 如果未找到模式，则 &lt;em&gt;string&lt;/em&gt; 将保持不变。&lt;/p&gt;
&lt;p&gt;可选参数 &lt;em&gt;count&lt;/em&gt; 是要替换的模式最大的出现次数；&lt;em&gt;count&lt;/em&gt; 必须是非负整数。 默认值 0 表示替换所有。&lt;/p&gt;
&lt;p&gt;这是一个使用 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.sub&quot;&gt;&lt;code&gt;sub()&lt;/code&gt;&lt;/a&gt; 方法的简单示例。 它用 &lt;code&gt;colour&lt;/code&gt; 这个词取代颜色名称:&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/333b1b875ec04ff9adcd4f85a60d0f13.png&quot; alt=&quot;image-20250611162048871&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Pattern.subn&quot;&gt;&lt;code&gt;subn()&lt;/code&gt;&lt;/a&gt; 方法完成相同的工作，但返回一个包含新字符串值和已执行的替换次数的 2 元组:&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/13ae24a635454a65949a3f805a477506.png&quot; alt=&quot;image-20250611162142732&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h4 id=&quot;空匹配陷阱&quot;&gt;空匹配陷阱&lt;/h4&gt;
&lt;p&gt;要注意当正则表达式能匹配空字符串的时候会在每个字符之间以及字符串首尾都添加替换的字符串&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/fcaa1833146a4f76bed423fbfc193c10.png&quot; alt=&quot;image-20250611163226731&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;空匹配在分割字符串的场景下也会发生：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;x*&apos;)
&amp;gt;&amp;gt;&amp;gt; p.split(&apos;apple&apos;)
[&apos;&apos;, &apos;a&apos;, &apos;p&apos;, &apos;p&apos;, &apos;l&apos;, &apos;e&apos;, &apos;&apos;]
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;后向引用&quot;&gt;后向引用&lt;/h4&gt;
&lt;p&gt;如果 &lt;em&gt;replacement&lt;/em&gt; 是一个字符串，则处理其中的任何反斜杠转义。 也就是说，&lt;code&gt;\n&lt;/code&gt; 被转换为单个换行符，&lt;code&gt;\r&lt;/code&gt; 被转换为回车符，依此类推。 诸如 &lt;code&gt;\&amp;amp;&lt;/code&gt; 之类的未知转义是孤立的。 &lt;strong&gt;后向引用&lt;/strong&gt;，例如 &lt;code&gt;\6&lt;/code&gt;，被替换为正则中相应组匹配的子字符串。 这使你可以在生成的替换字符串中合并原始文本的部分内容。&lt;/p&gt;
&lt;p&gt;这个例子匹配单词 &lt;code&gt;section&lt;/code&gt; 后跟一个用 &lt;code&gt;{&lt;/code&gt;，&lt;code&gt;}&lt;/code&gt; 括起来的字符串，并将 &lt;code&gt;section&lt;/code&gt; 改为 &lt;code&gt;subsection&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;section{([^}]*)}&apos;)
&amp;gt;&amp;gt;&amp;gt; p.sub(r&apos;subsection{\1}&apos;,&apos;section{First} section{second}&apos;)
&apos;subsection{First} subsection{second}&apos;
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一种语法用于引用由 &lt;code&gt;(?P&amp;lt;name&amp;gt;...)&lt;/code&gt; 语法定义的命名组。&lt;code&gt;\g\&amp;lt;name&amp;gt;&lt;/code&gt; 将使用名为 &lt;code&gt;name&lt;/code&gt; 的组匹配的子字符串，&lt;code&gt;\g\&amp;lt;number&amp;gt;&lt;/code&gt; 使用相应的组号。 因此 &lt;code&gt;\g&amp;lt;2&amp;gt;&lt;/code&gt; 等同于 &lt;code&gt;\2&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p=re.compile(&apos;section{(?P&amp;lt;name&amp;gt;[^}]*)}&apos;)
&amp;gt;&amp;gt;&amp;gt; p.sub(r&apos;subsection{\1}&apos;,&apos;section{First} section{second}&apos;)
&apos;subsection{First} subsection{second}&apos;
&amp;gt;&amp;gt;&amp;gt; p.sub(r&apos;subsection{\g&amp;lt;1&amp;gt;}&apos;,&apos;section{First} section{second}&apos;)
&apos;subsection{First} subsection{second}&apos;
&amp;gt;&amp;gt;&amp;gt; p.sub(r&apos;subsection{\g&amp;lt;name&amp;gt;}&apos;,&apos;section{First} section{second}&apos;)
&apos;subsection{First} subsection{second}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;替换函数&quot;&gt;替换函数&lt;/h4&gt;
&lt;p&gt;replacement还可以是一个函数，它可以为你提供更多控制。如果 &lt;em&gt;replacement&lt;/em&gt; 是一个函数，则为 &lt;em&gt;pattern&lt;/em&gt; 的每次非重叠出现将调用该函数。 在每次调用时，函数都会传递一个匹配的 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#match-objects&quot;&gt;匹配对象&lt;/a&gt; 参数，并可以使用此信息计算所需的替换字符串并将其返回。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&amp;quot;(\d+)&amp;quot;)
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; def replacment_fun(match: re.Match):
...     match_str = match.group()
...     return match_str + &amp;quot;_&amp;quot;
...
&amp;gt;&amp;gt;&amp;gt; result = p.sub(replacment_fun, &amp;quot;11 people eat 22 apples , every people eat 2 apples.&amp;quot;)
&amp;gt;&amp;gt;&amp;gt; print(result)
11_ people eat 22_ apples , every people eat 2_ apples.
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6模块级函数&quot;&gt;6、模块级函数&lt;/h3&gt;
&lt;p&gt;模块级函数让我们不必创建模式对象并调用其方法：&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#module-re&quot;&gt;&lt;code&gt;re&lt;/code&gt;&lt;/a&gt; 模块提供了顶级函数 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.match&quot;&gt;&lt;code&gt;match()&lt;/code&gt;&lt;/a&gt;，&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.search&quot;&gt;&lt;code&gt;search()&lt;/code&gt;&lt;/a&gt;，&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.findall&quot;&gt;&lt;code&gt;findall()&lt;/code&gt;&lt;/a&gt;，&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.sub&quot;&gt;&lt;code&gt;sub()&lt;/code&gt;&lt;/a&gt; 等等。 这些函数采用与相应模式方法相同的参数，并将正则字符串作为第一个参数添加，并仍然返回 &lt;code&gt;None&lt;/code&gt; 或 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#match-objects&quot;&gt;匹配对象&lt;/a&gt; 实例。:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; print(re.match(r&apos;From\s+&apos;, &apos;Fromage amk&apos;))
None
&amp;gt;&amp;gt;&amp;gt; re.match(r&apos;From\s+&apos;, &apos;From amk Thu May 14 19:12:10 1998&apos;)
&amp;lt;re.Match object; span=(0, 5), match=&apos;From &apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质上，这些函数只是为你创建一个模式对象，并在其上调用适当的方法。 它们还将编译对象存储在缓存中，因此使用相同的未来调用将不需要一次又一次地解析该模式。&lt;/p&gt;
&lt;p&gt;你是否应该使用这些模块级函数，还是应该自己获取模式并调用其方法？ 如果你正在循环中访问正则表达式，预编译它将节省一些函数调用。 在循环之外，由于有内部缓存，没有太大区别。&lt;/p&gt;
&lt;h3 id=&quot;7编译标志&quot;&gt;7、编译标志&lt;/h3&gt;
&lt;p&gt;编译标志允许你修改正则表达式的工作方式。 标志在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#module-re&quot;&gt;&lt;code&gt;re&lt;/code&gt;&lt;/a&gt; 模块中有两个名称，长名称如 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.IGNORECASE&quot;&gt;&lt;code&gt;IGNORECASE&lt;/code&gt;&lt;/a&gt; 和一个简短的单字母形式，例如 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.I&quot;&gt;&lt;code&gt;I&lt;/code&gt;&lt;/a&gt;。 （如果你熟悉 Perl 的模式修饰符，则单字母形式使用和其相同的字母；例如， &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.VERBOSE&quot;&gt;&lt;code&gt;re.VERBOSE&lt;/code&gt;&lt;/a&gt; 的缩写形式为 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.X&quot;&gt;&lt;code&gt;re.X&lt;/code&gt;&lt;/a&gt;。）多个标志可以 通过按位或运算来指定它们；例如，&lt;code&gt;re.I | re.M&lt;/code&gt; 设置 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.I&quot;&gt;&lt;code&gt;I&lt;/code&gt;&lt;/a&gt; 和 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.M&quot;&gt;&lt;code&gt;M&lt;/code&gt;&lt;/a&gt; 标志。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;标志&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;含意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.A&quot;&gt;&lt;code&gt;A&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;使几个转义如 &lt;code&gt;\w&lt;/code&gt;、&lt;code&gt;\b&lt;/code&gt;、&lt;code&gt;\s&lt;/code&gt; 和 &lt;code&gt;\d&lt;/code&gt; 匹配仅与具有相应特征属性的 ASCII 字符匹配。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.DOTALL&quot;&gt;&lt;code&gt;DOTALL&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.S&quot;&gt;&lt;code&gt;S&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;使 &lt;code&gt;.&lt;/code&gt; 匹配任何字符，包括换行符。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.IGNORECASE&quot;&gt;&lt;code&gt;IGNORECASE&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.I&quot;&gt;&lt;code&gt;I&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;进行大小写不敏感匹配。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.LOCALE&quot;&gt;&lt;code&gt;LOCALE&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.L&quot;&gt;&lt;code&gt;L&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;进行区域设置感知匹配。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.M&quot;&gt;&lt;code&gt;M&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;多行匹配，影响 &lt;code&gt;^&lt;/code&gt; 和 &lt;code&gt;$&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.VERBOSE&quot;&gt;&lt;code&gt;VERBOSE&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.X&quot;&gt;&lt;code&gt;X&lt;/code&gt;&lt;/a&gt; （为 &apos;扩展&apos;）&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;启用详细的正则，可以更清晰，更容易理解。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在上述表格中的标记中，需要特别关心的标记实际上有四个：&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.DOTALL&quot;&gt;&lt;code&gt;DOTALL&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.IGNORECASE&quot;&gt;&lt;code&gt;IGNORECASE&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.VERBOSE&quot;&gt;&lt;code&gt;VERBOSE&lt;/code&gt;&lt;/a&gt;，其余几个可以暂不考虑。&lt;/p&gt;
&lt;h4 id=&quot;redotall&quot;&gt;re.DOTALL&lt;/h4&gt;
&lt;p&gt;该标志使 &lt;code&gt;&apos;.&apos;&lt;/code&gt; 匹配任何字符，包括换行符；没有这个标志，&lt;code&gt;&apos;.&apos;&lt;/code&gt; 将匹配&lt;em&gt;除了&lt;/em&gt; 换行符外的任何字符。&lt;/p&gt;
&lt;p&gt;举个例子，现在我们有这样一段网页文本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;html_content = &amp;quot;&amp;quot;&amp;quot;&amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;This is a paragraph&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Another paragraph&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&amp;quot;&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何使用正则表达式将div标签中的内容提取出来？&lt;/p&gt;
&lt;p&gt;尝试写个脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

html_content = &amp;quot;&amp;quot;&amp;quot;&amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;This is a paragraph&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Another paragraph&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&amp;quot;&amp;quot;&amp;quot;

# 匹配div标签及其所有内容（包括换行）
div_pattern = r&amp;quot;&amp;lt;div&amp;gt;(.*?)&amp;lt;/div&amp;gt;&amp;quot;
match_div = re.search(div_pattern, html_content)
print(match_div)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果输出是None，也即是说没匹配到。这是因为&lt;code&gt;.*&lt;/code&gt;没有匹配到换行符&lt;code&gt;\n&lt;/code&gt;，此时就可以用到DOTALL标志了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; html_content = &amp;quot;&amp;quot;&amp;quot;&amp;lt;div&amp;gt;
...     &amp;lt;p&amp;gt;This is a paragraph&amp;lt;/p&amp;gt;
...     &amp;lt;p&amp;gt;Another paragraph&amp;lt;/p&amp;gt;
... &amp;lt;/div&amp;gt;&amp;quot;&amp;quot;&amp;quot;
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; # 匹配div标签及其所有内容（包括换行）
&amp;gt;&amp;gt;&amp;gt; div_pattern = r&amp;quot;&amp;lt;div&amp;gt;(.*?)&amp;lt;/div&amp;gt;&amp;quot;
&amp;gt;&amp;gt;&amp;gt; match_div = re.search(div_pattern, html_content, re.DOTALL)
&amp;gt;&amp;gt;&amp;gt; if match_div:
...     print(&amp;quot;\nMatched HTML content:&amp;quot;, match_div.group(1))
...

Matched HTML content:
    &amp;lt;p&amp;gt;This is a paragraph&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Another paragraph&amp;lt;/p&amp;gt;

&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;remultiline&quot;&gt;re.MULTILINE&lt;/h4&gt;
&lt;p&gt;通常 &lt;code&gt;^&lt;/code&gt; 只匹配字符串的开头，而 &lt;code&gt;$&lt;/code&gt; 只匹配字符串的结尾，紧接在字符串末尾的换行符（如果有的话）之前。 当指定了这个标志时，&lt;code&gt;^&lt;/code&gt; 匹配字符串的开头和字符串中每一行的开头，紧跟在每个换行符之后。 类似地，&lt;code&gt;$&lt;/code&gt; 元字符匹配字符串的结尾和每行的结尾（紧接在每个换行符之前）。&lt;/p&gt;
&lt;p&gt;听起来有些抽象，举个例子，现在有个提取错误日志的需求，日志格式如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;log_data = &amp;quot;&amp;quot;&amp;quot;
[INFO] 2023-01-01 10:00:00 Another log entry
[INFO] 2023-01-01 10:00:00 System started
[ERROR] 2023-01-01 10:05:23 Connection failed
[WARNING] 2023-01-01 10:06:10 Low disk space
[ERROR] 2023-01-01 10:00:00 Error details: Connection timeout
[INFO] 2023-01-01 10:10:00 Backup completed
[ERROR] 2023-01-01 10:05:23 Connection failed1
&amp;quot;&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何将ERROR级别的日志提取出来？先看第一种写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;p = re.compile(r&amp;quot;^\[ERROR\].*&amp;quot;)
result = p.findall(log_data)
for item in result:
    print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种写法没有匹配到任何日志，原因就在于log_data是作为整体的字符串来查找的，而限定符&lt;code&gt;^&lt;/code&gt;要求必须以&lt;code&gt;[ERROR]&lt;/code&gt;开头，我们这段字符串是以&lt;code&gt;\n[INFO]&lt;/code&gt;开头的，所以并不会被匹配到，解决方式就是使用&lt;code&gt;re.MULTILINE&lt;/code&gt;标志。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/11/d3f9b0d479a448c783fa18a029811340.png&quot; alt=&quot;image-20250611231910458&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;使用re.MULTILINE之后，每行字符串都能在开头被^匹配，结尾被$匹配。&lt;/p&gt;
&lt;p&gt;好了，现在我们来看看进阶问题，现在日志格式变成了如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;log_data = &amp;quot;&amp;quot;&amp;quot;
[INFO] 2023-01-01 10:00:00 Another log entry
[INFO] 2023-01-01 10:00:00 System started
[ERROR] 2023-01-01 10:05:23 Connection failed
[WARNING] 2023-01-01 10:06:10 Low disk space
[ERROR] 2023-01-01 10:00:00
    Error details: Connection timeout
    Stack trace:
        at com.example.App.main(App.java:10)
[INFO] 2023-01-01 10:10:00 Backup completed
[ERROR] 2023-01-01 10:05:23 Connection failed1
&amp;quot;&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没错，错误日志有换行了，如何将错误日志完整的提取出来？使用之前的正则表达式&lt;code&gt;r&amp;quot;^\[ERROR\].*&amp;quot;&lt;/code&gt;会遗漏部分日志。需要考虑以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;错误日志必须从每行[ERROR]开始匹配，所以需要用到re.MULTILINE标志；但是匹配的时候要跨行匹配，所以需要用到re.DOTALL，两者都要使用，则要使用&lt;code&gt;re.MULTILINE | re.DOTALL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;由于正则表达式的默认贪婪匹配规则，会一次性匹配出最长的字符串，所以要禁用贪婪匹配，方法就是在重复标记后加上问号?，在这里要使用&lt;code&gt;.*?&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.*&lt;/code&gt;要有截止条件，这里要使用&lt;a href=&quot;https://blog.kdyzm.cn/post/309#id-14&quot;&gt;前视断言&lt;/a&gt;，前视断言是一种零宽度断言（关于前视断言在后续章节介绍），这里要同时考虑到ERROR日志在最后一行的情况，所以前视断言的写法为：&lt;code&gt;(?=\[|\Z)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综合考虑以上情况，新的正则表达式写法为：&lt;code&gt;p = re.compile(r&amp;quot;^\[ERROR\].*?(?=\[|\Z)&amp;quot;, re.MULTILINE | re.DOTALL)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;完整代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

log_data = &amp;quot;&amp;quot;&amp;quot;
[INFO] 2023-01-01 10:00:00 Another log entry
[INFO] 2023-01-01 10:00:00 System started
[ERROR] 2023-01-01 10:05:23 Connection failed
[WARNING] 2023-01-01 10:06:10 Low disk space
[ERROR] 2023-01-01 10:00:00
    Error details: Connection timeout
    Stack trace:
        at com.example.App.main(App.java:10)
[INFO] 2023-01-01 10:10:00 Backup completed
[ERROR] 2023-01-01 10:05:23 Connection failed1
&amp;quot;&amp;quot;&amp;quot;

p = re.compile(r&amp;quot;^\[ERROR\].*?(?=\[|\Z)&amp;quot;, re.MULTILINE | re.DOTALL)
result = p.findall(log_data)
for item in result:
    print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;reverbose&quot;&gt;re.VERBOSE&lt;/h4&gt;
&lt;p&gt;此标志允许你编写更易读的正则表达式，方法是为您提供更灵活的格式化方式。 指定此标志后，将忽略正则字符串中的空格，除非空格位于字符类中或前面带有未转义的反斜杠；这使你可以更清楚地组织和缩进正则。 此标志还允许你将注释放在正则中，引擎将忽略该注释；注释标记为 &lt;code&gt;&apos;#&apos;&lt;/code&gt; 既不是在字符类中，也不是在未转义的反斜杠之前。&lt;/p&gt;
&lt;p&gt;例如，这里的正则使用 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.VERBOSE&quot;&gt;&lt;code&gt;re.VERBOSE&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;charref = re.compile(r&amp;quot;&amp;quot;&amp;quot;
 &amp;amp;[#]                # 数字实体引用的开始
 (
     0[0-7]+         # 八进制形式
   | [0-9]+          # 十进制形式
   | x[0-9a-fA-F]+   # 十六进制形式
 )
 ;                   # 末尾分号
&amp;quot;&amp;quot;&amp;quot;, re.VERBOSE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有详细设置，正则将如下所示:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;charref = re.compile(&amp;quot;&amp;amp;#(0[0-7]+&amp;quot;
                     &amp;quot;|[0-9]+&amp;quot;
                     &amp;quot;|x[0-9a-fA-F]+);&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;四正则表达式进阶&quot;&gt;四、正则表达式进阶&lt;/h2&gt;
&lt;p&gt;上面章节介绍了正则表达式的基本使用，下面介绍正则表达式的进阶写法。&lt;/p&gt;
&lt;h3 id=&quot;1零宽度断言&quot;&gt;1、零宽度断言&lt;/h3&gt;
&lt;p&gt;上面已经讨论过&lt;code&gt;.&lt;/code&gt;、&lt;code&gt;*&lt;/code&gt;、&lt;code&gt;?&lt;/code&gt;以及&lt;code&gt;\d&lt;/code&gt;等转义字符，它们都有一个特点：它们本身代表着匹配字符串中的某一段文本。有这样一种字符，它们在匹配字符串中不占用任何字符，只是代表成功或者失败，这种特殊的匹配字符叫做&lt;strong&gt;零宽度断言&lt;/strong&gt;。例如，&lt;code&gt;\b&lt;/code&gt; 是一个断言，指明当前位置位于字边界；这个位置根本不会被 &lt;code&gt;\b&lt;/code&gt; 改变。这意味着&lt;strong&gt;永远不应重复零宽度断言&lt;/strong&gt;，因为如果它们在给定位置匹配一次，它们显然可以无限次匹配。&lt;/p&gt;
&lt;p&gt;零宽度断言包含常见的&lt;code&gt;\^$\A\Z\B\b&lt;/code&gt;以及前视断言。&lt;/p&gt;
&lt;h4 id=&quot;元字符和转义字符中的零宽度断言&quot;&gt;元字符和转义字符中的零宽度断言&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;|&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;或者“or”运算符。 如果 &lt;em&gt;A&lt;/em&gt; 和 &lt;em&gt;B&lt;/em&gt; 是正则表达式，&lt;code&gt;A|B&lt;/code&gt; 将匹配任何与 &lt;em&gt;A&lt;/em&gt; 或 &lt;em&gt;B&lt;/em&gt; 匹配的字符串。 &lt;code&gt;|&lt;/code&gt; 具有非常低的优先级，以便在交替使用多字符字符串时使其合理地工作。 &lt;code&gt;Crow|Servo&lt;/code&gt; 将匹配 &lt;code&gt;&apos;Crow&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;Servo&apos;&lt;/code&gt;，而不是 &lt;code&gt;&apos;Cro&apos;&lt;/code&gt;、&lt;code&gt;&apos;w&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;S&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;ervo&apos;&lt;/code&gt;。要匹配字面 &lt;code&gt;&apos;|&apos;&lt;/code&gt;，请使用 &lt;code&gt;\|&lt;/code&gt;，或将其括在字符类中，如 &lt;code&gt;[|]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;^&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在行的开头匹配。 除非设置了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt; 标志，否则只会在字符串的开头匹配。 在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt; 模式下，这也在字符串中的每个换行符后立即匹配。&lt;/p&gt;
&lt;p&gt;例如，如果你希望仅在行的开头匹配单词 &lt;code&gt;From&lt;/code&gt;，则要使用的正则 &lt;code&gt;^From&lt;/code&gt;。:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; print(re.search(&apos;^From&apos;, &apos;From Here to Eternity&apos;))
&amp;lt;re.Match object; span=(0, 4), match=&apos;From&apos;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; print(re.search(&apos;^From&apos;, &apos;Reciting From Memory&apos;))
None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要匹配字面 &lt;code&gt;&apos;^&apos;&lt;/code&gt;，使用 &lt;code&gt;\^&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;$&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;匹配行的末尾，定义为字符串的结尾，或者后跟换行符的任何位置。:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; print(re.search(&apos;}$&apos;, &apos;{block}&apos;))
&amp;lt;re.Match object; span=(6, 7), match=&apos;}&apos;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; print(re.search(&apos;}$&apos;, &apos;{block} &apos;))
None
&amp;gt;&amp;gt;&amp;gt; print(re.search(&apos;}$&apos;, &apos;{block}\n&apos;))
&amp;lt;re.Match object; span=(6, 7), match=&apos;}&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以匹配字面 &lt;code&gt;&apos;$&apos;&lt;/code&gt;，使用 &lt;code&gt;\$&lt;/code&gt; 或者将其包裹在一个字符类中，例如 &lt;code&gt;[$]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;\A&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;仅匹配字符串的开头。 当不在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt; 模式时，&lt;code&gt;\A&lt;/code&gt; 和 &lt;code&gt;^&lt;/code&gt; 实际上是相同的。 在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt; 模式中，它们是不同的: &lt;code&gt;\A&lt;/code&gt; 仍然只在字符串的开头匹配，但 &lt;code&gt;^&lt;/code&gt; 可以匹配在换行符之后的字符串内的任何位置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;\Z&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只匹配字符串尾。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;\b&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;字边界。 这是一个零宽度断言，仅在单词的开头或结尾处匹配。 单词被定义为一个字母数字字符序列，因此单词的结尾由空格或非字母数字字符表示。&lt;/p&gt;
&lt;p&gt;以下示例仅当它是一个完整的单词时匹配 &lt;code&gt;class&lt;/code&gt;；当它包含在另一个单词中时将不会匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\bclass\b&apos;)
&amp;gt;&amp;gt;&amp;gt; print(p.search(&apos;no class at all&apos;))
&amp;lt;re.Match object; span=(3, 8), match=&apos;class&apos;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; print(p.search(&apos;the declassified algorithm&apos;))
None
&amp;gt;&amp;gt;&amp;gt; print(p.search(&apos;one subclass is&apos;))
None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用这个特殊序列时，你应该记住两个细微之处。 首先，这是 Python 的字符串文字和正则表达式序列之间最严重的冲突。 在 Python 的字符串文字中，&lt;code&gt;\b&lt;/code&gt; 是退格字符，ASCII 值为8。 如果你没有使用原始字符串，那么 Python 会将 &lt;code&gt;\b&lt;/code&gt; 转换为退格，你的正则不会按照你的预期匹配。 以下示例与我们之前的正则看起来相同，但省略了正则字符串前面的 &lt;code&gt;&apos;r&apos;&lt;/code&gt;。:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;\bclass\b&apos;)
&amp;gt;&amp;gt;&amp;gt; print(p.search(&apos;no class at all&apos;))
None
&amp;gt;&amp;gt;&amp;gt; print(p.search(&apos;\b&apos; + &apos;class&apos; + &apos;\b&apos;))
&amp;lt;re.Match object; span=(0, 7), match=&apos;\x08class\x08&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其次，在一个字符类中，这个断言没有用处，&lt;code&gt;\b&lt;/code&gt; 表示退格字符，以便与 Python 的字符串文字兼容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;\B&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;另一个零宽度断言，这与 &lt;code&gt;\b&lt;/code&gt; 相反，仅在当前位置不在字边界时才匹配。&lt;/p&gt;
&lt;h4 id=&quot;前视断言和后视断言&quot;&gt;前视断言和后视断言&lt;/h4&gt;
&lt;p&gt;从“断言”这个词上看，就知道该功能的作用是“判断”，它只有两个值：True或者False。那判断什么呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;(?=…)&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;肯定型前视断言。如果内部的表达式（这里用 &lt;code&gt;...&lt;/code&gt; 来表示）在当前位置可以匹配，则匹配成功，否则匹配失败。 但是，内部表达式尝试匹配之后，正则引擎并不会向前推进；正则表达式的其余部分依然会在断言开始的地方尝试匹配。&lt;/p&gt;
&lt;p&gt;举个例子，在之前&lt;code&gt;re.MULTILINE&lt;/code&gt;章节介绍的正则表达式&lt;code&gt;^\[ERROR\].*?(?=\[|\Z)&lt;/code&gt;，用于提取ERROR级别的完整日志，其中&lt;code&gt;(?=\[|\Z)&lt;/code&gt;就是前视断言，它前面的&lt;code&gt;.*&lt;/code&gt;不能无限匹配到字符串最后，需要有个停止条件，停止条件就是匹配到字符&lt;code&gt;[&lt;/code&gt;（&lt;code&gt;[&lt;/code&gt;字符表示下一条日志的开头）或者匹配到字符串最后也就是&lt;code&gt;\Z&lt;/code&gt;（&lt;code&gt;\Z&lt;/code&gt;能匹配到表示当前匹配的ERROR级别的日志在最后一条）；如果没匹配到，则将会匹配失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;(?!…)&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;否定型前视断言。 与肯定型断言正好相反，如果内部表达式在字符串中的当前位置 &lt;strong&gt;不&lt;/strong&gt; 匹配，则成功。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;(?&amp;lt;=...)&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;肯定型后视断言，它也是一种零宽度断言，表示匹配的内容&lt;strong&gt;必须&lt;/strong&gt;出现在指定模式（断言）之后。&lt;/p&gt;
&lt;p&gt;举个例子，我们有一段文本，&lt;code&gt;text = &amp;quot;Price: $100, Discount: $50, i have 11 mantou&amp;quot;&lt;/code&gt;，我们想提取出来以$开头的数字，如何实现？可以使用&lt;code&gt;re.findall(r&apos;\$\d+&apos;,text)&lt;/code&gt; 提取出来包含$符号的：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/12/546c6d805c05478ca71681f669670cc6.png&quot; alt=&quot;image-20250612131548648&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;但是这样并不符合我们的要求，我们要以$符号开头，但是只要数字。这时候就可以使用肯定型后视断言了，正则表达式可以这样写：&lt;code&gt;re.findall(r&apos;(?&amp;lt;=\$)\d+&apos;, text)&lt;/code&gt;，这表示必须以$符号开头，但是不要匹配$，只要数字。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/12/1093f03ccda846a6b98c65c963d65d95.png&quot; alt=&quot;image-20250612132017134&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;(?&amp;lt;!...)&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;否定型后视断言，表示匹配的内容&lt;strong&gt;不能&lt;/strong&gt;出现在指定的模式之后&lt;/p&gt;
&lt;p&gt;总而言之，前视断言和后视断言在需要匹配特定上下文但不希望这些上下文成为匹配结果一部分时非常有用。&lt;/p&gt;
&lt;h3 id=&quot;2捕获组分组&quot;&gt;2、捕获组（分组）&lt;/h3&gt;
&lt;p&gt;分组是用 &lt;code&gt;&apos;(&apos;&lt;/code&gt;, &lt;code&gt;&apos;)&apos;&lt;/code&gt; 元字符来标记的。 &lt;code&gt;&apos;(&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;)&apos;&lt;/code&gt; 与它们在数学表达式中的含义基本一致：它们会将所包含的表达式合为一组，并且你可以使用限定符例如 &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, 或 &lt;code&gt;{m,n}&lt;/code&gt; 来重复一个分组的内容。 举例来说，&lt;code&gt;(ab)*&lt;/code&gt; 将匹配 &lt;code&gt;ab&lt;/code&gt; 的零次或多次重复。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;(ab)*&apos;)
&amp;gt;&amp;gt;&amp;gt; print(p.match(&apos;ababababab&apos;).span())
(0, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;&apos;(&apos;&lt;/code&gt;，&lt;code&gt;&apos;)&apos;&lt;/code&gt; 表示的组也捕获它们匹配的文本的起始和结束索引；这可以通过将参数传递给 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.group&quot;&gt;&lt;code&gt;group()&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.start&quot;&gt;&lt;code&gt;start()&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.end&quot;&gt;&lt;code&gt;end()&lt;/code&gt;&lt;/a&gt; 以及 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.span&quot;&gt;&lt;code&gt;span()&lt;/code&gt;&lt;/a&gt;。 组从 0 开始编号。组 0 始终存在；它表示整个正则，所以 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#match-objects&quot;&gt;匹配对象&lt;/a&gt; 方法都将组 0 作为默认参数。 稍后我们将看到如何表达不捕获它们匹配的文本范围的组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;(a)b&apos;)
&amp;gt;&amp;gt;&amp;gt; m = p.match(&apos;ab&apos;)
&amp;gt;&amp;gt;&amp;gt; m.group()
&apos;ab&apos;
&amp;gt;&amp;gt;&amp;gt; m.group(0)
&apos;ab&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子组从左到右编号，从 1 向上编号。 组可以嵌套；&lt;strong&gt;要确定编号，只需计算从左到右的左括号字符&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;(a(b)c)d&apos;)
&amp;gt;&amp;gt;&amp;gt; m = p.match(&apos;abcd&apos;)
&amp;gt;&amp;gt;&amp;gt; m.group(0)
&apos;abcd&apos;
&amp;gt;&amp;gt;&amp;gt; m.group(1)
&apos;abc&apos;
&amp;gt;&amp;gt;&amp;gt; m.group(2)
&apos;b&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.group&quot;&gt;&lt;code&gt;group()&lt;/code&gt;&lt;/a&gt; 可以一次传递多个组号，在这种情况下，它将返回一个包含这些组的相应值的元组。:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; m.group(2,1,2)
(&apos;b&apos;, &apos;abc&apos;, &apos;b&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.groups&quot;&gt;&lt;code&gt;groups()&lt;/code&gt;&lt;/a&gt; 方法返回一个元组，其中包含所有子组的字符串，从1到最后一个子组。:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; m.groups()
(&apos;abc&apos;, &apos;b&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模式中的后向引用允许你指定还必须在字符串中的当前位置找到先前捕获组的内容。 例如，如果可以在当前位置找到组 1 的确切内容，则 &lt;code&gt;\1&lt;/code&gt; 将成功，否则将失败。 请记住，Python 的字符串文字也使用反斜杠后跟数字以允许在字符串中包含任意字符，因此正则中引入反向引用时务必使用原始字符串。&lt;/p&gt;
&lt;p&gt;例如，以下正则检测字符串中重复的单词。:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\b(\w+)\s+\1\b&apos;)
&amp;gt;&amp;gt;&amp;gt; p.search(&apos;Paris in the the spring&apos;).group()
&apos;the the&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;分组重复&quot;&gt;分组重复&lt;/h4&gt;
&lt;p&gt;分组如果储存在重复，则在捕获的时候，下一次的捕获会覆盖上一次的捕获。如何理解这句话？还是之前的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(&apos;(ab)*&apos;)
&amp;gt;&amp;gt;&amp;gt; print(p.match(&apos;ababababab&apos;).span())
(0, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;(ab)*&lt;/code&gt;可以完全匹配&lt;code&gt;ababababab&lt;/code&gt;，&lt;code&gt;group()&lt;/code&gt;和&lt;code&gt;group(0)&lt;/code&gt;都是&lt;code&gt;ababababab&lt;/code&gt;，但是group(1)以及groups()都是&lt;code&gt;ab&lt;/code&gt;，实际上(ab)分组匹配了5次，ab则是最后一个分组捕获的结果。&lt;/p&gt;
&lt;p&gt;如果上述案例难以理解，可以再看下面的案例：正则表达式&lt;code&gt;([abc])+&lt;/code&gt;匹配&lt;code&gt;&apos;abc&apos;&lt;/code&gt;字符串。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/12/76cddfbdf63e415a825b4a08785b2e82.png&quot; alt=&quot;image-20250612140259276&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;分别匹配了(a)+、(b)+、(c)+并捕获成功，(b)+覆盖了(a)+的捕获结果，(c)+覆盖了(b)+的捕获结果，所以最后的捕获结果只剩下了(c)+的捕获结果&lt;code&gt;&apos;c&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;分组重复和一般分组不一样，无论重复多少次，最终保留下来的只有最后一次捕获结果。&lt;/p&gt;
&lt;h3 id=&quot;3非捕获组&quot;&gt;3、非捕获组&lt;/h3&gt;
&lt;p&gt;有时我们会想要使用组来表示正则表达式的一部分，但是对检索组的内容不感兴趣。 你可以通过使用非捕获组来显式表达这个事实: &lt;code&gt;(?:...)&lt;/code&gt;，你可以用任何其他正则表达式替换 &lt;code&gt;...&lt;/code&gt;。注意非捕获组的格式是&lt;code&gt;(?:...)&lt;/code&gt;和前视断言&lt;code&gt;(?=...)&lt;/code&gt;不同。&lt;/p&gt;
&lt;p&gt;来看看使用正则表达式&lt;code&gt;r&apos;(\d+)asdf&apos;&lt;/code&gt;匹配&lt;code&gt;&apos;1234asdf&apos;&lt;/code&gt;，&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/12/8c96dd75d63e4539889609543017f5e6.png&quot; alt=&quot;image-20250612141247073&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到捕获组捕获到了1234作为group(1)。如果我将正则表达式改成&lt;code&gt;r&apos;(?:\d+)asdf&apos;&lt;/code&gt; 会怎样？&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/12/c4b7bcc066184f9893cbfe7a9fcdcfdf.png&quot; alt=&quot;image-20250612142052000&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，非捕获组的作用就是&lt;strong&gt;能匹配，但是不捕获&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&quot;4命名分组&quot;&gt;4、命名分组&lt;/h3&gt;
&lt;p&gt;前面说的非捕获组&lt;code&gt;(?:...)&lt;/code&gt;以及前视断言&lt;code&gt;(?=...)&lt;/code&gt;实际上都是Python 支持的 Perl 的扩展，命名组的格式是&lt;code&gt;(?P=&amp;lt;name&amp;gt;...)&lt;/code&gt;，它是Python的特定扩展之一，&lt;em&gt;name&lt;/em&gt; 显然是该组的名称。使用命名组的时候，可以同时使用数字编号以及组名字符串来关联对应的捕获组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;(?P&amp;lt;word&amp;gt;\b\w+\b)&apos;)
&amp;gt;&amp;gt;&amp;gt; m = p.search( &apos;(((( Lots of punctuation )))&apos; )
&amp;gt;&amp;gt;&amp;gt; m.group(&apos;word&apos;)
&apos;Lots&apos;
&amp;gt;&amp;gt;&amp;gt; m.group(1)
&apos;Lots&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意search方法只找第一个匹配，所以只输出了Lots，如果想获取所有匹配，应当使用findall或者finditer方法。&lt;/p&gt;
&lt;h4 id=&quot;命名分组提取为字典&quot;&gt;命名分组提取为字典&lt;/h4&gt;
&lt;p&gt;我们可以通过 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.Match.groupdict&quot;&gt;&lt;code&gt;groupdict()&lt;/code&gt;&lt;/a&gt; 将命名分组提取为一个字典:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; m = re.match(r&apos;(?P&amp;lt;first&amp;gt;\w+)\s+(?P&amp;lt;last&amp;gt;\w+)&apos;, &apos;Jane Doe&apos;)
&amp;gt;&amp;gt;&amp;gt; m.groupdict()
{&apos;first&apos;: &apos;Jane&apos;, &apos;last&apos;: &apos;Doe&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;后向引用-1&quot;&gt;后向引用&lt;/h4&gt;
&lt;p&gt;后向引用是一种新的扩展语法，它的使用格式是：&lt;code&gt;(?P=name)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;举个例子，用于查找重复单词的正则表达式&lt;code&gt;\b(\w+)\s+\1\b&lt;/code&gt; 也可以写为 &lt;code&gt;\b(?P&amp;lt;word&amp;gt;\w+)\s+(?P=word)\b&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;gt;&amp;gt;&amp;gt; p = re.compile(r&apos;\b(?P&amp;lt;word&amp;gt;\w+)\s+(?P=word)\b&apos;)
&amp;gt;&amp;gt;&amp;gt; p.search(&apos;Paris in the the spring&apos;).group()
&apos;the the&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意，在sub替换字符串方法中，命名分组的后向引用要使用&lt;code&gt;\g\&amp;lt;name&amp;gt;&lt;/code&gt;的形式，相关功能科参考替换字符串章节。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
END.
</description>
      <category>python</category>
    </item>
    <item>
      <title>Python正则表达式匹配字符手册</title>
      <link>https://blog.kdyzm.cn/post/309</link>
      <guid>https://blog.kdyzm.cn/post/309</guid>
      <pubDate>Tue, 10 Jun 2025 22:32:25 +0800</pubDate>
      <description>&lt;p&gt;本篇文章是python正则匹配字符手册，用于方便查阅元字符和转义字符相关的细节，不涉及正则表达式如何书写问题。
这部分内容更像是一个手册，用来以后查询使用，想要全部记住有些不容易的。&lt;/p&gt;
&lt;p&gt;大多数字母和符号都会简单地匹配自身。例如，正则表达式 &lt;code&gt;test&lt;/code&gt; 将会精确地匹配到 &lt;code&gt;test&lt;/code&gt; 。但该规则有例外：有些字符是特殊的 &lt;em&gt;元字符（metacharacters）&lt;/em&gt;，并不匹配自身。&lt;/p&gt;
&lt;h3 id=&quot;一元字符&quot;&gt;一、元字符&lt;/h3&gt;
&lt;p&gt;元字符是一种特殊的字符，它并不匹配自身，事实上，它们表示匹配一些非常规的内容，或者通过重复它们或改变它们的含义来影响正则的其他部分。正则表达式的编写几乎就是围绕如何运用元字符来展开的。&lt;/p&gt;
&lt;p&gt;这是元字符的完整列表。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;. ^ $ * + ? { } [ ] \ | ( )
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;id&quot;&gt;.&lt;/h4&gt;
&lt;p&gt;（点号.） 在默认模式下，匹配除换行符以外的任意字符。 如果指定了旗标 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.DOTALL&quot;&gt;&lt;code&gt;DOTALL&lt;/code&gt;&lt;/a&gt;，它将匹配包括换行符在内的任意字符。 &lt;code&gt;(?s:.)&lt;/code&gt; 将匹配任意字符而无视相关旗标。&lt;/p&gt;
&lt;h4 id=&quot;id-1&quot;&gt;^&lt;/h4&gt;
&lt;p&gt;(插入符) 匹配字符串的开头， 并且在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt; 模式下也匹配换行后的首个符号。&lt;/p&gt;
&lt;h4 id=&quot;id-2&quot;&gt;$&lt;/h4&gt;
&lt;p&gt;匹配字符串尾或者在字符串尾的换行符的前一个字符，在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt; 模式下也会匹配换行符之前的文本。 &lt;code&gt;foo&lt;/code&gt; 匹配 &apos;foo&apos; 和 &apos;foobar&apos;，但正则表达式 &lt;code&gt;foo$&lt;/code&gt; 只匹配 &apos;foo&apos;。 更有趣的是，在 &lt;code&gt;&apos;foo1\nfoo2\n&apos;&lt;/code&gt; 中搜索 &lt;code&gt;foo.$&lt;/code&gt;，通常匹配 &apos;foo2&apos;，但在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.MULTILINE&quot;&gt;&lt;code&gt;MULTILINE&lt;/code&gt;&lt;/a&gt; 模式下可以匹配到 &apos;foo1&apos;；在 &lt;code&gt;&apos;foo\n&apos;&lt;/code&gt; 中搜索 &lt;code&gt;$&lt;/code&gt; 会找到两个（空的）匹配：一个在换行符之前，一个在字符串的末尾。&lt;/p&gt;
&lt;h4 id=&quot;id-3&quot;&gt;*&lt;/h4&gt;
&lt;p&gt;对它前面的正则式匹配0到任意次重复， 尽量多的匹配字符串。 &lt;code&gt;ab*&lt;/code&gt; 会匹配 &lt;code&gt;&apos;a&apos;&lt;/code&gt;，&lt;code&gt;&apos;ab&apos;&lt;/code&gt;，或者 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 后面跟随任意个 &lt;code&gt;&apos;b&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&quot;id-4&quot;&gt;+&lt;/h4&gt;
&lt;p&gt;对它前面的正则式匹配1到任意次重复。 &lt;code&gt;ab+&lt;/code&gt; 会匹配 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 后面跟随1个以上到任意个 &lt;code&gt;&apos;b&apos;&lt;/code&gt;，它不会匹配 &lt;code&gt;&apos;a&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&quot;id-5&quot;&gt;?&lt;/h4&gt;
&lt;p&gt;对它前面的正则式匹配0到1次重复。 &lt;code&gt;ab?&lt;/code&gt; 会匹配 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 或者 &lt;code&gt;&apos;ab&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&quot;--&quot;&gt;*?, +?, ??&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;&apos;*&apos;&lt;/code&gt;, &lt;code&gt;&apos;+&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;?&apos;&lt;/code&gt; 数量限定符都是 &lt;em&gt;贪婪的&lt;/em&gt;；它们会匹配尽可能多的文本。 有时这种行为并不被需要；如果 RE &lt;code&gt;&amp;lt;.*&amp;gt;&lt;/code&gt; 针对 &lt;code&gt;&apos;&amp;lt;a&amp;gt; b &amp;lt;c&amp;gt;&apos;&lt;/code&gt; 进行匹配，它将匹配整个字符串，而不只是 &lt;code&gt;&apos;&amp;lt;a&amp;gt;&apos;&lt;/code&gt;。 在数量限定符之后添加 &lt;code&gt;?&lt;/code&gt; 将使其以 &lt;em&gt;非贪婪&lt;/em&gt; 或 &lt;em&gt;最小&lt;/em&gt; 风格来执行匹配；也就是将匹配数量尽可能 &lt;em&gt;少的&lt;/em&gt; 字符。 使用 RE &lt;code&gt;&amp;lt;.*?&amp;gt;&lt;/code&gt; 将只匹配 &lt;code&gt;&apos;&amp;lt;a&amp;gt;&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&quot;---1&quot;&gt;*+, ++, ?+&lt;/h4&gt;
&lt;p&gt;类似于 &lt;code&gt;&apos;*&apos;&lt;/code&gt;, &lt;code&gt;&apos;+&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;?&apos;&lt;/code&gt; 数量限定符，添加了 &lt;code&gt;&apos;+&apos;&lt;/code&gt; 的形式也将匹配尽可能多的次数。 但是，不同于真正的贪婪型数量限定符，这些形式在之后的表达式匹配失败时不允许反向追溯。 这些形式被称为 &lt;em&gt;占有型&lt;/em&gt; 数量限定符。 例如，&lt;code&gt;a*a&lt;/code&gt; 将匹配 &lt;code&gt;&apos;aaaa&apos;&lt;/code&gt; 因为 &lt;code&gt;a*&lt;/code&gt; 将匹配所有的 4 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt;，但是，当遇到最后一个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 时，表达式将执行反向追溯以便最终 &lt;code&gt;a*&lt;/code&gt; 最后变为匹配总计 3 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt;，而第四个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 将由最后一个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 来匹配。 然而，当使用 &lt;code&gt;a*+a&lt;/code&gt; 时如果要匹配 &lt;code&gt;&apos;aaaa&apos;&lt;/code&gt;，&lt;code&gt;a*+&lt;/code&gt; 将匹配所有的 4 个 &lt;code&gt;&apos;a&apos;&lt;/code&gt;，但是在最后一个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 无法找到更多字符来匹配时，表达式将无法被反向追溯并将因此匹配失败。 &lt;code&gt;x*+&lt;/code&gt;, &lt;code&gt;x++&lt;/code&gt; 和 &lt;code&gt;x?+&lt;/code&gt; 分别等价于 &lt;code&gt;(?&amp;gt;x*)&lt;/code&gt;, &lt;code&gt;(?&amp;gt;x+)&lt;/code&gt; 和 &lt;code&gt;(?&amp;gt;x?)&lt;/code&gt;。&lt;em&gt;Added in version 3.11.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;m&quot;&gt;{m}&lt;/h4&gt;
&lt;p&gt;对其之前的正则式指定匹配 &lt;em&gt;m&lt;/em&gt; 个重复；少于 &lt;em&gt;m&lt;/em&gt; 的话就会导致匹配失败。比如， &lt;code&gt;a{6}&lt;/code&gt; 将匹配6个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; , 但是不能是5个。&lt;/p&gt;
&lt;h4 id=&quot;mn&quot;&gt;{m,n}&lt;/h4&gt;
&lt;p&gt;对正则式进行 &lt;em&gt;m&lt;/em&gt; 到 &lt;em&gt;n&lt;/em&gt; 次匹配，在 &lt;em&gt;m&lt;/em&gt; 和 &lt;em&gt;n&lt;/em&gt; 之间取尽量多。 比如，&lt;code&gt;a{3,5}&lt;/code&gt; 将匹配 3 到 5个 &lt;code&gt;&apos;a&apos;&lt;/code&gt;。忽略 &lt;em&gt;m&lt;/em&gt; 意为指定下界为0，忽略 &lt;em&gt;n&lt;/em&gt; 指定上界为无限次。 比如 &lt;code&gt;a{4,}b&lt;/code&gt; 将匹配 &lt;code&gt;&apos;aaaab&apos;&lt;/code&gt; 或者1000个 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 尾随一个 &lt;code&gt;&apos;b&apos;&lt;/code&gt;，但不能匹配 &lt;code&gt;&apos;aaab&apos;&lt;/code&gt;。逗号不能省略，否则无法辨别修饰符应该忽略哪个边界。&lt;/p&gt;
&lt;h4 id=&quot;mn-1&quot;&gt;{m,n}?&lt;/h4&gt;
&lt;p&gt;将导致结果 RE 匹配之前 RE 的 m 至 n 次重复，尝试匹配尽可能 少的 重复次数。 这是之前数量限定符的非贪婪版本。 例如，在 6 个字符的字符串 &apos;aaaaaa&apos; 上，a{3,5} 将匹配 5 个 &apos;a&apos; 字符，而 a{3,5}? 将只匹配 3 个字符。&lt;/p&gt;
&lt;h4 id=&quot;mn-2&quot;&gt;{m,n}+&lt;/h4&gt;
&lt;p&gt;将导致结果 RE 匹配之前 RE 的 m 至 n 次重复，尝试匹配尽可能多的重复而 不会 建立任何反向追溯点。 这是上述数量限定符的占有型版本。 例如，在 6 个字符的字符串 &apos;aaaaaa&apos; 上，a{3,5}+aa 将尝试匹配 5 个 &apos;a&apos; 字符，然后，要求再有 2 个 &apos;a&apos;，这将需要比可用的更多的字符因而会失败，而 a{3,5}aa 的匹配将使 a{3,5} 先捕获 5 个，然后通过反向追溯再匹配 4 个 &apos;a&apos;，然后用模式中最后的 aa 来匹配最后的 2 个 &apos;a&apos;。 x{m,n}+ 就等同于 (?&amp;gt;x{m,n})。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Added in version 3.11.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;id-6&quot;&gt;\&lt;/h4&gt;
&lt;p&gt;转义特殊字符（允许你匹配 &lt;code&gt;&apos;*&apos;&lt;/code&gt;, &lt;code&gt;&apos;?&apos;&lt;/code&gt;, 或者此类其他），或者表示一个特殊序列；特殊序列之后进行讨论。如果你没有使用原始字符串（ &lt;code&gt;r&apos;raw&apos;&lt;/code&gt; ）来表达样式，要牢记Python也使用反斜杠作为转义序列；如果转义序列不被Python的分析器识别，反斜杠和字符才能出现在字符串中。如果Python可以识别这个序列，那么反斜杠就应该重复两次。这将导致理解障碍，所以高度推荐，就算是最简单的表达式，也要使用原始字符串。&lt;/p&gt;
&lt;h4 id=&quot;id-7&quot;&gt;[]&lt;/h4&gt;
&lt;p&gt;用于表示一个字符集合。在一个集合中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字符可以单独列出，比如 &lt;code&gt;[amk]&lt;/code&gt; 匹配 &lt;code&gt;&apos;a&apos;&lt;/code&gt;， &lt;code&gt;&apos;m&apos;&lt;/code&gt;， 或者 &lt;code&gt;&apos;k&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以表示字符范围，通过用 &lt;code&gt;&apos;-&apos;&lt;/code&gt; 将两个字符连起来。比如 &lt;code&gt;[a-z]&lt;/code&gt; 将匹配任何小写ASCII字符， &lt;code&gt;[0-5][0-9]&lt;/code&gt; 将匹配从 &lt;code&gt;00&lt;/code&gt; 到 &lt;code&gt;59&lt;/code&gt; 的两位数字， &lt;code&gt;[0-9A-Fa-f]&lt;/code&gt; 将匹配任何十六进制数位。 如果 &lt;code&gt;-&lt;/code&gt; 进行了转义 （比如 &lt;code&gt;[a\-z]&lt;/code&gt;）或者它的位置在首位或者末尾（如 &lt;code&gt;[-a]&lt;/code&gt; 或 &lt;code&gt;[a-]&lt;/code&gt;），它就只表示普通字符 &lt;code&gt;&apos;-&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除反斜杠外的特殊字符在集中中会失去其特殊含义。 例如，&lt;code&gt;[(+*)]&lt;/code&gt; 将匹配字符字面值 &lt;code&gt;&apos;(&apos;&lt;/code&gt;, &lt;code&gt;&apos;+&apos;&lt;/code&gt;, &lt;code&gt;&apos;*&apos;&lt;/code&gt;, 或 &lt;code&gt;&apos;)&apos;&lt;/code&gt; 中的任何一个。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;反斜杠或者用于转义集合中具有特殊含义的字符如 &lt;code&gt;&apos;-&apos;&lt;/code&gt;, &lt;code&gt;&apos;]&apos;&lt;/code&gt;, &lt;code&gt;&apos;^&apos;&lt;/code&gt; 及 &lt;code&gt;&apos;\\&apos;&lt;/code&gt; 本身或者用于提示代表单个字符的特殊序列如 &lt;code&gt;\xa0&lt;/code&gt; 或 &lt;code&gt;\n&lt;/code&gt; 或者用于字符类如 &lt;code&gt;\w&lt;/code&gt; 或 &lt;code&gt;\S&lt;/code&gt; (定义见下文)。 请注意 &lt;code&gt;\b&lt;/code&gt; 是表示单个 &amp;quot;backspace&amp;quot; 字符，而不是如在集合以外那样表示单词边界，还有数字转义符如 &lt;code&gt;\1&lt;/code&gt; 将总是为八进制形式的转义，而不是分组引用。 不匹配单个字符的特殊转义符如 &lt;code&gt;\A&lt;/code&gt; 和 &lt;code&gt;\Z&lt;/code&gt; 是不被允许的。are not allowed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不在集合范围内的字符可以通过 &lt;em&gt;取反&lt;/em&gt; 来进行匹配。如果集合首字符是 &lt;code&gt;&apos;^&apos;&lt;/code&gt; ，所有 &lt;em&gt;不&lt;/em&gt; 在集合内的字符将会被匹配，比如 &lt;code&gt;[^5]&lt;/code&gt; 将匹配所有字符，除了 &lt;code&gt;&apos;5&apos;&lt;/code&gt;， &lt;code&gt;[^^]&lt;/code&gt; 将匹配所有字符，除了 &lt;code&gt;&apos;^&apos;&lt;/code&gt;. &lt;code&gt;^&lt;/code&gt; 如果不在集合首位，就没有特殊含义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要在集合内匹配一个 &lt;code&gt;&apos;]&apos;&lt;/code&gt; 字面值，可以在它前面加上反斜杠，或是将它放到集合的开头。 例如，&lt;code&gt;[()[\]{}]&lt;/code&gt; 和 &lt;code&gt;[]()[{}]&lt;/code&gt; 都可以匹配右方括号，以及左方括号，花括号和圆括号。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://unicode.org/reports/tr18/&quot;&gt;Unicode Technical Standard #18&lt;/a&gt; 里的嵌套集合和集合操作支持可能在未来添加。这将会改变语法，所以为了帮助这个改变，一个 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/exceptions.html#FutureWarning&quot;&gt;&lt;code&gt;FutureWarning&lt;/code&gt;&lt;/a&gt; 将会在有多义的情况里被 &lt;code&gt;raise&lt;/code&gt;，包含以下几种情况，集合由 &lt;code&gt;&apos;[&apos;&lt;/code&gt; 开始，或者包含下列字符序列 &lt;code&gt;&apos;--&apos;&lt;/code&gt;, &lt;code&gt;&apos;&amp;amp;&amp;amp;&apos;&lt;/code&gt;, &lt;code&gt;&apos;~~&apos;&lt;/code&gt;, 和 &lt;code&gt;&apos;||&apos;&lt;/code&gt;。为了避免警告，需要将它们用反斜杠转义。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.7 版本发生变更:&lt;/em&gt; 如果一个字符串构建的语义在未来会改变的话，一个 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/exceptions.html#FutureWarning&quot;&gt;&lt;code&gt;FutureWarning&lt;/code&gt;&lt;/a&gt; 会 &lt;code&gt;raise&lt;/code&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;id-8&quot;&gt;|&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;A|B&lt;/code&gt;， &lt;em&gt;A&lt;/em&gt; 和 &lt;em&gt;B&lt;/em&gt; 可以是任意正则表达式，创建一个正则表达式，匹配 &lt;em&gt;A&lt;/em&gt; 或者 &lt;em&gt;B&lt;/em&gt;. 任意个正则表达式可以用 &lt;code&gt;&apos;|&apos;&lt;/code&gt; 连接。它也可以在组合（见下列）内使用。扫描目标字符串时， &lt;code&gt;&apos;|&apos;&lt;/code&gt; 分隔开的正则样式从左到右进行匹配。当一个样式完全匹配时，这个分支就被接受。意思就是，一旦 &lt;em&gt;A&lt;/em&gt; 匹配成功， &lt;em&gt;B&lt;/em&gt; 就不再进行匹配，即便它能产生一个更好的匹配。或者说，&lt;code&gt;&apos;|&apos;&lt;/code&gt; 操作符绝不贪婪。 如果要匹配 &lt;code&gt;&apos;|&apos;&lt;/code&gt; 字符，使用 &lt;code&gt;\|&lt;/code&gt;， 或者把它包含在字符集里，比如 &lt;code&gt;[|]&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;id-9&quot;&gt;(...)&lt;/h4&gt;
&lt;p&gt;（组合），匹配括号内的任意正则表达式，并标识出组合的开始和结尾。匹配完成后，组合的内容可以被获取，并可以在之后用 &lt;code&gt;\number&lt;/code&gt; 转义序列进行再次匹配，之后进行详细说明。要匹配字符 &lt;code&gt;&apos;(&apos;&lt;/code&gt; 或者 &lt;code&gt;&apos;)&apos;&lt;/code&gt;, 用 &lt;code&gt;\(&lt;/code&gt; 或 &lt;code&gt;\)&lt;/code&gt;, 或者把它们包含在字符集合里: &lt;code&gt;[(]&lt;/code&gt;, &lt;code&gt;[)]&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;id-10&quot;&gt;(?…)&lt;/h4&gt;
&lt;p&gt;这是个扩展标记法 （一个 &lt;code&gt;&apos;?&apos;&lt;/code&gt; 跟随 &lt;code&gt;&apos;(&apos;&lt;/code&gt; 并无含义）。 &lt;code&gt;&apos;?&apos;&lt;/code&gt; 后面的第一个字符决定了这个构建采用什么样的语法。这种扩展通常并不创建新的组合； &lt;code&gt;(?P&amp;lt;name&amp;gt;...)&lt;/code&gt; 是唯一的例外。 以下是目前支持的扩展。&lt;/p&gt;
&lt;h4 id=&quot;ailmsux&quot;&gt;(?aiLmsux)&lt;/h4&gt;
&lt;p&gt;一个或多个来自 &lt;code&gt;&apos;a&apos;&lt;/code&gt;, &lt;code&gt;&apos;i&apos;&lt;/code&gt;, &lt;code&gt;&apos;L&apos;&lt;/code&gt;, &lt;code&gt;&apos;m&apos;&lt;/code&gt;, &lt;code&gt;&apos;s&apos;&lt;/code&gt;, &lt;code&gt;&apos;u&apos;&lt;/code&gt;, &lt;code&gt;&apos;x&apos;&lt;/code&gt; 集合的字母。） 分组将与空字符串相匹配；这些字母将为整个正则表达式设置相应的旗标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.A&quot;&gt;&lt;code&gt;re.A&lt;/code&gt;&lt;/a&gt; (仅限 ASCII 匹配)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.I&quot;&gt;&lt;code&gt;re.I&lt;/code&gt;&lt;/a&gt; (忽略大小写)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.L&quot;&gt;&lt;code&gt;re.L&lt;/code&gt;&lt;/a&gt; (依赖于语言区域)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.M&quot;&gt;&lt;code&gt;re.M&lt;/code&gt;&lt;/a&gt; (多行)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.S&quot;&gt;&lt;code&gt;re.S&lt;/code&gt;&lt;/a&gt; (点号匹配所有字符)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.U&quot;&gt;&lt;code&gt;re.U&lt;/code&gt;&lt;/a&gt; (Unicode 匹配)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.X&quot;&gt;&lt;code&gt;re.X&lt;/code&gt;&lt;/a&gt; (详细)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（该旗标在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#contents-of-module-re&quot;&gt;模块内容&lt;/a&gt; 中有介绍。） 这适用于当你希望将该旗标包括为正则表达式的一部分，而不是向 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.compile&quot;&gt;&lt;code&gt;re.compile()&lt;/code&gt;&lt;/a&gt; 函数传入 &lt;em&gt;flag&lt;/em&gt; 参数的情况。 旗标应当在表达式字符串的开头使用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.11 版本发生变更:&lt;/em&gt; 此构造只能在表达式的开头使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;id-11&quot;&gt;(?:…)&lt;/h4&gt;
&lt;p&gt;正则括号的非捕获版本。 匹配在括号内的任何正则表达式，但该分组所匹配的子字符串 &lt;em&gt;不能&lt;/em&gt; 在执行匹配后被获取或是之后在模式中被引用。&lt;/p&gt;
&lt;h4 id=&quot;ailmsux-imsx&quot;&gt;(?aiLmsux-imsx:…)&lt;/h4&gt;
&lt;p&gt;（零个或多个来自 &lt;code&gt;&apos;a&apos;&lt;/code&gt;, &lt;code&gt;&apos;i&apos;&lt;/code&gt;, &lt;code&gt;&apos;L&apos;&lt;/code&gt;, &lt;code&gt;&apos;m&apos;&lt;/code&gt;, &lt;code&gt;&apos;s&apos;&lt;/code&gt;, &lt;code&gt;&apos;u&apos;&lt;/code&gt;, &lt;code&gt;&apos;x&apos;&lt;/code&gt; 集合的字母，后面可以带 &lt;code&gt;&apos;-&apos;&lt;/code&gt; 再跟一个或多个来自 &lt;code&gt;&apos;i&apos;&lt;/code&gt;, &lt;code&gt;&apos;m&apos;&lt;/code&gt;, &lt;code&gt;&apos;s&apos;&lt;/code&gt;, &lt;code&gt;&apos;x&apos;&lt;/code&gt; 集合的字母。） 这些字母将为这部分表达式设置或移除相应的旗标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.A&quot;&gt;&lt;code&gt;re.A&lt;/code&gt;&lt;/a&gt; (仅限 ASCII 匹配)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.I&quot;&gt;&lt;code&gt;re.I&lt;/code&gt;&lt;/a&gt; (忽略大小写)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.L&quot;&gt;&lt;code&gt;re.L&lt;/code&gt;&lt;/a&gt; (依赖于语言区域)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.M&quot;&gt;&lt;code&gt;re.M&lt;/code&gt;&lt;/a&gt; (多行)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.S&quot;&gt;&lt;code&gt;re.S&lt;/code&gt;&lt;/a&gt; (点号匹配所有字符)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.U&quot;&gt;&lt;code&gt;re.U&lt;/code&gt;&lt;/a&gt; (Unicode 匹配)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.X&quot;&gt;&lt;code&gt;re.X&lt;/code&gt;&lt;/a&gt; (详细)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（这些旗标在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#contents-of-module-re&quot;&gt;模块内容&lt;/a&gt; 中有介绍。）&lt;/p&gt;
&lt;p&gt;字母 &lt;code&gt;&apos;a&apos;&lt;/code&gt;, &lt;code&gt;&apos;L&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;u&apos;&lt;/code&gt; 在用作内联旗标时是互斥的，所以它们不能相互组合或者带 &lt;code&gt;&apos;-&apos;&lt;/code&gt;。 相反，当它们中的某一个出现于内联的分组时，它将覆盖外层分组中匹配的模式。 在 Unicode 模式中 &lt;code&gt;(?a:...)&lt;/code&gt; 将切换至仅限 ASCII 匹配，而 &lt;code&gt;(?u:...)&lt;/code&gt; 将切换至 Unicode 匹配（默认）。 在字节串模式中 &lt;code&gt;(?L:...)&lt;/code&gt; 将切换为基于语言区域的匹配，而 &lt;code&gt;(?a:...)&lt;/code&gt; 将切换为仅限 ASCII 匹配（默认）。 这种覆盖将只在内联分组范围内生效，而在分组之外将恢复为原始的匹配模式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Added in version 3.6.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.7 版本发生变更:&lt;/em&gt; 符号 &lt;code&gt;&apos;a&apos;&lt;/code&gt;, &lt;code&gt;&apos;L&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;u&apos;&lt;/code&gt; 同样可以用在一个组合内。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;id-12&quot;&gt;(?&amp;gt;...)&lt;/h4&gt;
&lt;p&gt;尝试匹配 &lt;code&gt;...&lt;/code&gt; 就像它是一个单独的正则表达式，如果匹配成功，则继续匹配在它之后的剩余表达式。 如果之后的表达式匹配失败，则栈只能回溯到 &lt;code&gt;(?&amp;gt;...)&lt;/code&gt; &lt;em&gt;之前&lt;/em&gt; 的点，因为一旦退出，这个被称为 &lt;em&gt;原子化分组&lt;/em&gt; 的表达式将会丢弃其自身所有的栈点位。 因此，&lt;code&gt;(?&amp;gt;.*).&lt;/code&gt; 将永远不会匹配任何东西因为首先 &lt;code&gt;.*&lt;/code&gt; 将匹配所有可能的字符，然后，由于没有任何剩余的字符可供匹配，最后的 &lt;code&gt;.&lt;/code&gt; 将匹配失败。 由于原子化分组中没有保存任何栈点位，并且在它之前也没有任何栈点位，因此整个表达式将匹配失败。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Added in version 3.11.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;pname&quot;&gt;(?P&amp;lt;name&amp;gt;…)&lt;/h4&gt;
&lt;p&gt;与常规的圆括号类似，但分组所匹配到了子字符串可通过符号分组名称 &lt;em&gt;name&lt;/em&gt; 来访问。 分组名称必须是有效的 Python 标识符，并且在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/stdtypes.html#bytes&quot;&gt;&lt;code&gt;bytes&lt;/code&gt;&lt;/a&gt; 模式中它们只能包含 ASCII 范围内的字节值。 每个分组名称在一个正则表达式中只能定义一次。 一个符号分组同时也是一个编号分组，就像这个分组没有被命名过一样。&lt;/p&gt;
&lt;p&gt;命名组合可以在三种上下文中引用。如果样式是 &lt;code&gt;(?P&amp;lt;quote&amp;gt;[&apos;&amp;quot;]).*?(?P=quote)&lt;/code&gt; （也就是说，匹配单引号或者双引号括起来的字符串)：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;引用组合 &amp;quot;quote&amp;quot; 的上下文&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;引用方法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;在正则式自身内&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;(?P=quote)&lt;/code&gt; (如示)&lt;code&gt;\1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;处理匹配对象 &lt;em&gt;m&lt;/em&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;m.group(&apos;quote&apos;)``m.end(&apos;quote&apos;)&lt;/code&gt; (等)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;传递到 &lt;code&gt;re.sub()&lt;/code&gt; 里的 &lt;em&gt;repl&lt;/em&gt; 参数中&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;\g&amp;lt;quote&amp;gt;``\g&amp;lt;1&amp;gt;``\1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.12 版本发生变更:&lt;/em&gt; 在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/stdtypes.html#bytes&quot;&gt;&lt;code&gt;bytes&lt;/code&gt;&lt;/a&gt; 模式中，分组 &lt;em&gt;name&lt;/em&gt; 只能包含 ASCII 范围内的字节值 (&lt;code&gt;b&apos;\x00&apos;&lt;/code&gt;-&lt;code&gt;b&apos;\x7f&apos;&lt;/code&gt;)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;pname-1&quot;&gt;(?P=name)&lt;/h4&gt;
&lt;p&gt;反向引用一个命名组合；它匹配前面那个叫 &lt;em&gt;name&lt;/em&gt; 的命名组中匹配到的串同样的字串。&lt;/p&gt;
&lt;h4 id=&quot;id-13&quot;&gt;(?#…)&lt;/h4&gt;
&lt;p&gt;注释；里面的内容会被忽略。&lt;/p&gt;
&lt;h4 id=&quot;id-14&quot;&gt;(?=…)&lt;/h4&gt;
&lt;p&gt;当 &lt;code&gt;…&lt;/code&gt; 匹配时，匹配成功，但不消耗字符串中的任何字符。这个叫做 &lt;em&gt;前视断言&lt;/em&gt; （lookahead assertion）。比如， &lt;code&gt;Isaac (?=Asimov)&lt;/code&gt; 将会匹配 &lt;code&gt;&apos;Isaac &apos;&lt;/code&gt; ，仅当其后紧跟 &lt;code&gt;&apos;Asimov&apos;&lt;/code&gt; 。&lt;/p&gt;
&lt;h4 id=&quot;id-15&quot;&gt;(?!…)&lt;/h4&gt;
&lt;p&gt;当 &lt;code&gt;…&lt;/code&gt; 不匹配时，匹配成功。这个叫 &lt;em&gt;否定型前视断言&lt;/em&gt; （negative lookahead assertion）。例如， &lt;code&gt;Isaac (?!Asimov)&lt;/code&gt; 将会匹配 &lt;code&gt;&apos;Isaac &apos;&lt;/code&gt; ，仅当它后面 &lt;em&gt;不是&lt;/em&gt; &lt;code&gt;&apos;Asimov&apos;&lt;/code&gt; 。&lt;/p&gt;
&lt;h4 id=&quot;id-16&quot;&gt;(?&amp;lt;=…)&lt;/h4&gt;
&lt;p&gt;如果 &lt;code&gt;...&lt;/code&gt; 的匹配内容出现在当前位置的左侧，则匹配。这叫做 &lt;em&gt;肯定型后视断言&lt;/em&gt; （positive lookbehind assertion）。 &lt;code&gt;(?&amp;lt;=abc)def&lt;/code&gt; 将会在 &lt;code&gt;&apos;abcdef&apos;&lt;/code&gt; 中找到一个匹配，因为后视会回退3个字符并检查内部表达式是否匹配。内部表达式（匹配的内容）必须是固定长度的，意思就是 &lt;code&gt;abc&lt;/code&gt; 或 &lt;code&gt;a|b&lt;/code&gt; 是允许的，但是 &lt;code&gt;a*&lt;/code&gt; 和 &lt;code&gt;a{3,4}&lt;/code&gt; 不可以。注意，以肯定型后视断言开头的正则表达式，匹配项一般不会位于搜索字符串的开头。很可能你应该使用 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.search&quot;&gt;&lt;code&gt;search()&lt;/code&gt;&lt;/a&gt; 函数，而不是 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.match&quot;&gt;&lt;code&gt;match()&lt;/code&gt;&lt;/a&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import re
&amp;gt;&amp;gt;&amp;gt; m = re.search(&apos;(?&amp;lt;=abc)def&apos;, &apos;abcdef&apos;)
&amp;gt;&amp;gt;&amp;gt; m.group(0)
&apos;def&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子搜索一个跟随在连字符后的单词：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; m = re.search(r&apos;(?&amp;lt;=-)\w+&apos;, &apos;spam-egg&apos;)
&amp;gt;&amp;gt;&amp;gt; m.group(0)
&apos;egg&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.5 版本发生变更:&lt;/em&gt; 添加定长组合引用的支持。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;id-17&quot;&gt;(?&amp;lt;!…)&lt;/h4&gt;
&lt;p&gt;如果 &lt;code&gt;...&lt;/code&gt; 的匹配内容没有出现在当前位置的左侧，则匹配。这个叫做 &lt;em&gt;否定型后视断言&lt;/em&gt; （negative lookbehind assertion）。类似于肯定型后视断言，内部表达式（匹配的内容）必须是固定长度的。以否定型后视断言开头的正则表达式，匹配项可能位于搜索字符串的开头。&lt;/p&gt;
&lt;p&gt;(?(id/name)yes-pattern|no-pattern)&lt;/p&gt;
&lt;p&gt;如果给定的 &lt;em&gt;id&lt;/em&gt; 或 &lt;em&gt;name&lt;/em&gt; 存在，将会尝试匹配 &lt;code&gt;yes-pattern&lt;/code&gt; ，否则就尝试匹配 &lt;code&gt;no-pattern&lt;/code&gt;，&lt;code&gt;no-pattern&lt;/code&gt; 可选，也可以被忽略。比如， &lt;code&gt;(&amp;lt;)?(\w+@\w+(?:\.\w+)+)(?(1)&amp;gt;|$)&lt;/code&gt; 是一个email样式匹配，将匹配 &lt;code&gt;&apos;&amp;lt;user@host.com&amp;gt;&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;user@host.com&apos;&lt;/code&gt; ，但不会匹配 &lt;code&gt;&apos;&amp;lt;user@host.com&apos;&lt;/code&gt; ，也不会匹配 &lt;code&gt;&apos;user@host.com&amp;gt;&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.12 版本发生变更:&lt;/em&gt; 分组 &lt;em&gt;id&lt;/em&gt; 只能包含 ASCII 数码。 在 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/stdtypes.html#bytes&quot;&gt;&lt;code&gt;bytes&lt;/code&gt;&lt;/a&gt; 模式中，分组 &lt;em&gt;name&lt;/em&gt; 只能包含 ASCII 范围内的字节值 (&lt;code&gt;b&apos;\x00&apos;&lt;/code&gt;-&lt;code&gt;b&apos;\x7f&apos;&lt;/code&gt;)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由 &lt;code&gt;&apos;\&apos;&lt;/code&gt; 和一个字符组成的特殊序列在以下列出。 如果普通字符不是ASCII数位或者ASCII字母，那么正则样式将匹配第二个字符。比如，&lt;code&gt;\$&lt;/code&gt; 匹配字符 &lt;code&gt;&apos;$&apos;&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;二转义字符&quot;&gt;二、转义字符&lt;/h3&gt;
&lt;p&gt;转义字符是通过转义符号\加上字母或者数字的特殊组合形式用以代表特定字符序列的组合字符。&lt;/p&gt;
&lt;h4 id=&quot;number&quot;&gt;\number&lt;/h4&gt;
&lt;p&gt;匹配数字代表的组合。每个括号是一个组合，组合从1开始编号。比如 &lt;code&gt;(.+) \1&lt;/code&gt; 匹配 &lt;code&gt;&apos;the the&apos;&lt;/code&gt; 或者 &lt;code&gt;&apos;55 55&apos;&lt;/code&gt;, 但不会匹配 &lt;code&gt;&apos;thethe&apos;&lt;/code&gt; (注意组合后面的空格)。这个特殊序列只能用于匹配前面99个组合。如果 &lt;em&gt;number&lt;/em&gt; 的第一个数位是0， 或者 &lt;em&gt;number&lt;/em&gt; 是三个八进制数，它将不会被看作是一个组合，而是八进制的数字值。在 &lt;code&gt;&apos;[&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;]&apos;&lt;/code&gt; 字符集合内，任何数字转义都被看作是字符。&lt;/p&gt;
&lt;h4 id=&quot;a&quot;&gt;\A&lt;/h4&gt;
&lt;p&gt;只匹配字符串开始。&lt;/p&gt;
&lt;h4 id=&quot;b&quot;&gt;\b&lt;/h4&gt;
&lt;p&gt;匹配空字符串，但只在单词开始或结尾的位置。 一个单词被定义为一个单词字符的序列。 注意在通常情况下，&lt;code&gt;\b&lt;/code&gt; 被定义为 &lt;code&gt;\w&lt;/code&gt; 和 &lt;code&gt;\W&lt;/code&gt; 字符之间的边界（反之亦然），或是 &lt;code&gt;\w&lt;/code&gt; 和字符串开始或结尾之间的边界。 这意味着 &lt;code&gt;r&apos;\bat\b&apos;&lt;/code&gt; 将匹配 &lt;code&gt;&apos;at&apos;&lt;/code&gt;, &lt;code&gt;&apos;at.&apos;&lt;/code&gt;, &lt;code&gt;&apos;(at)&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;as at ay&apos;&lt;/code&gt; 但不匹配 &lt;code&gt;&apos;attempt&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;atlas&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Unicode (str) 模式中默认的单词类字符是 Unicode 字母数字和下划线，但这可以通过使用 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标来改变。 如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.LOCALE&quot;&gt;&lt;code&gt;LOCALE&lt;/code&gt;&lt;/a&gt; 旗标则单词边界将根据当前语言区域来确定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;备注：在一个字符范围内，&lt;code&gt;\b&lt;/code&gt; 代表退格符，以便与 Python 的字符串字面值保持兼容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;b-1&quot;&gt;\B&lt;/h4&gt;
&lt;p&gt;匹配空字符串，但仅限于它 &lt;em&gt;不在&lt;/em&gt; 单词的开头或结尾的情况。 这意味着 &lt;code&gt;r&apos;at\B&apos;&lt;/code&gt; 将匹配 &lt;code&gt;&apos;athens&apos;&lt;/code&gt;, &lt;code&gt;&apos;atom&apos;&lt;/code&gt;, &lt;code&gt;&apos;attorney&apos;&lt;/code&gt;，但不匹配 &lt;code&gt;&apos;at&apos;&lt;/code&gt;, &lt;code&gt;&apos;at.&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;at!&apos;&lt;/code&gt;。 &lt;code&gt;\B&lt;/code&gt; 与 &lt;code&gt;\b&lt;/code&gt; 正相反，这样 Unicode (str) 模式中的单词类字符是 Unicode 字母数字或下划线，但这可以通过使用 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标来改变。 如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.LOCALE&quot;&gt;&lt;code&gt;LOCALE&lt;/code&gt;&lt;/a&gt; 旗标则单词边界将根据当前语言区域来确定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;备注：请注意 &lt;code&gt;\B&lt;/code&gt; 不会匹配空字符串，这与其他编程语言如 Perl 的 RE 实现不同。 此行为是出于兼容性考虑而保留的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;d&quot;&gt;\d&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于 Unicode (str) 样式：&lt;/p&gt;
&lt;p&gt;匹配任意 Unicode 十进制数码（也就是说，任何属于 Unicode 字符类别 [&lt;a href=&quot;https://www.unicode.org/versions/Unicode15.0.0/ch04.pdf#G134153&quot;&gt;Nd]&lt;/a&gt; 的字符）。 这包括 &lt;code&gt;[0-9]&lt;/code&gt;，还包括许多其他的数码类字符。如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标则匹配 &lt;code&gt;[0-9]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于8位(bytes)样式：&lt;/p&gt;
&lt;p&gt;匹配 ASCII 字符集内的任意十进制数码；这等价于 &lt;code&gt;[0-9]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;d-1&quot;&gt;\D&lt;/h4&gt;
&lt;p&gt;匹配不属于十进制数码的任意字符。 这与 &lt;code&gt;\d&lt;/code&gt; 正相反。&lt;/p&gt;
&lt;p&gt;如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标则匹配 &lt;code&gt;[^0-9]&lt;/code&gt;&lt;/p&gt;
&lt;h4 id=&quot;s&quot;&gt;\s&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于 Unicode (str) 样式：&lt;/p&gt;
&lt;p&gt;匹配 Unicode 空白字符（如 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.isspace&quot;&gt;&lt;code&gt;str.isspace()&lt;/code&gt;&lt;/a&gt; 所定义的）。 这包括 &lt;code&gt;[ \t\n\r\f\v]&lt;/code&gt;，还包括许多其他字符，例如许多语言中由排版规则约定的非中断空白字符。如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标则匹配 &lt;code&gt;[ \t\n\r\f\v]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于8位(bytes)样式：&lt;/p&gt;
&lt;p&gt;匹配ASCII中的空白字符，就是 &lt;code&gt;[ \t\n\r\f\v]&lt;/code&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;s-1&quot;&gt;\S&lt;/h4&gt;
&lt;p&gt;匹配不属于空白字符的任意字符。 这与 &lt;code&gt;\s&lt;/code&gt; 正相反。&lt;/p&gt;
&lt;p&gt;如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标则匹配 &lt;code&gt;[^ \t\n\r\f\v]&lt;/code&gt;&lt;/p&gt;
&lt;h4 id=&quot;w&quot;&gt;\w&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于 Unicode (str) 样式：&lt;/p&gt;
&lt;p&gt;匹配 Unicode 单词类字符；这包括所有 Unicode 字母数字类字符 (由 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.isalnum&quot;&gt;&lt;code&gt;str.isalnum()&lt;/code&gt;&lt;/a&gt; 定义)，以及下划线 (&lt;code&gt;_&lt;/code&gt;)。如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标则匹配 &lt;code&gt;[a-zA-Z0-9_]&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于8位(bytes)样式：&lt;/p&gt;
&lt;p&gt;匹配在 ASCII 字符集中被视为字母数字的字符；这等价于 &lt;code&gt;[a-zA-Z0-9_]&lt;/code&gt;。 如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.LOCALE&quot;&gt;&lt;code&gt;LOCALE&lt;/code&gt;&lt;/a&gt; 旗标，则匹配在当前语言区域中被视为字母数字的字符以及下划线。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;w-1&quot;&gt;\W&lt;/h4&gt;
&lt;p&gt;匹配不属于单词类字符的任意字符。 这与 &lt;code&gt;\w&lt;/code&gt; 正相反。 在默认情况下，将匹配除下划线 (&lt;code&gt;_&lt;/code&gt;) 以外的 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/stdtypes.html#str.isalnum&quot;&gt;&lt;code&gt;str.isalnum()&lt;/code&gt;&lt;/a&gt; 返回 &lt;code&gt;False&lt;/code&gt; 的字符。&lt;/p&gt;
&lt;p&gt;如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.ASCII&quot;&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;/a&gt; 旗标则匹配 &lt;code&gt;[^a-zA-Z0-9_]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果使用了 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html#re.LOCALE&quot;&gt;&lt;code&gt;LOCALE&lt;/code&gt;&lt;/a&gt; 旗标，则匹配在当前语言区域中不属于字母数字且不为下划线的字符。&lt;/p&gt;
&lt;h4 id=&quot;z&quot;&gt;\Z&lt;/h4&gt;
&lt;p&gt;只匹配字符串尾。&lt;/p&gt;
&lt;p&gt;Python 字符串字面值支持的大多数 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/reference/lexical_analysis.html#escape-sequences&quot;&gt;转义序列&lt;/a&gt; 也被正则表达式解析器所接受:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;\a      \b      \f      \n
\N      \r      \t      \u
\U      \v      \x      \\
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（注意 &lt;code&gt;\b&lt;/code&gt; 被用于表示词语的边界，它只在字符集合内表示退格，比如 &lt;code&gt;[\b]&lt;/code&gt; 。）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&apos;\u&apos;&lt;/code&gt;, &lt;code&gt;&apos;\U&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;\N&apos;&lt;/code&gt; 转义序列仅在 Unicode (str) 模式中可被识别。 在字节串模式中它们会导致错误。 未知的 ASCII 字母转义符被保留在未来使用并会被视为错误。&lt;/p&gt;
&lt;p&gt;八进制转义包含为一个有限形式。如果首位数字是 0， 或者有三个八进制数位，那么就认为它是八进制转义。其他的情况，就看作是组引用。对于字符串文本，八进制转义最多有三个数位长。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.3 版本发生变更:&lt;/em&gt; 增加了 &lt;code&gt;&apos;\u&apos;&lt;/code&gt; 和 &lt;code&gt;&apos;\U&apos;&lt;/code&gt; 转义序列。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.6 版本发生变更:&lt;/em&gt; 由 &lt;code&gt;&apos;\&apos;&lt;/code&gt; 和一个ASCII字符组成的未知转义会被看成错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;在 3.8 版本发生变更:&lt;/em&gt; 增加了 &lt;code&gt;&apos;\N{*name*}&apos;&lt;/code&gt; 转义序列。 与在字符串字面值中一样，它扩展了指定的 Unicode 字符 (例如 &lt;code&gt;&apos;\N{EM DASH}&apos;&lt;/code&gt;)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;参考文档：&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/re.html&quot;&gt;《&lt;code&gt;re&lt;/code&gt; --- 正则表达式操作》&lt;/a&gt;&lt;/p&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>Python类型提示简介</title>
      <link>https://blog.kdyzm.cn/post/308</link>
      <guid>https://blog.kdyzm.cn/post/308</guid>
      <pubDate>Thu, 05 Jun 2025 13:40:35 +0800</pubDate>
      <description>&lt;p&gt;Python中的类型提示是一种特殊的语法，这种语法能够显式声明一个变量的类型。通过显式声明变量类型，不仅使得代码可读性变高了，还能够让编辑器为我们编码提供更多的支持。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/f3bd01b4e523426ab0ad7aa779d5da17.gif&quot; alt=&quot;动画43_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;一类型声明&quot;&gt;一、类型声明&lt;/h2&gt;
&lt;p&gt;类型声明主要作用在函数的参数以及返回值上。&lt;/p&gt;
&lt;h3 id=&quot;1简单类型&quot;&gt;1、简单类型&lt;/h3&gt;
&lt;p&gt;简单类型包含&lt;code&gt;str&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;bool&lt;/code&gt;、&lt;code&gt;bytes&lt;/code&gt; 五种类型，这些类型都是python标准类型。以下是一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_items(item_a: str, item_b: int, item_c: float, item_d: bool, item_e: bytes):
    return item_a, item_b, item_c, item_d, item_d, item_e
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2泛型类型&quot;&gt;2、泛型类型&lt;/h3&gt;
&lt;p&gt;Python中的&lt;code&gt;dict&lt;/code&gt;、&lt;code&gt;list&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;以及&lt;code&gt;tuple&lt;/code&gt;类型都是能够包含其它类型值的数据结构，可以直接使用在方法参数上对参数进行标记：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test(list1: list):
    print(list1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这种方式不能有效标记出来list列表中每一项的类型（3.9+版本已经解决这个问题），可以使用&lt;code&gt;typing&lt;/code&gt;模块解决这个问题。&lt;/p&gt;
&lt;h4 id=&quot;list&quot;&gt;List&lt;/h4&gt;
&lt;p&gt;接下来让我们定义一个str类型的list列表&lt;/p&gt;
&lt;p&gt;在3.9+版本，可以直接使用&lt;code&gt;list[type]&lt;/code&gt;，这表示列表中每个元素都是type类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def process_items(items: list[str]):
    for item in items:
        print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是更通用的是使用typing模块，这种在3.8+版本均通用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;tuple&quot;&gt;Tuple&lt;/h4&gt;
&lt;p&gt;Tuple是typing模块的元组类型类， Tuple[int, int, str]表示元组有三个元素，分别是int、int、str类型。&lt;/p&gt;
&lt;p&gt;在3.9+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def process_items(items_t: tuple[int, int, str]):
    return items_t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在3.8+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Tuple


def process_items(items_t: Tuple[int, int, str]):
    return items_t
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;set&quot;&gt;Set&lt;/h4&gt;
&lt;p&gt;Set是typing模块的集合类型类，Set[int]表示集合中的每个元素都是int类型。&lt;/p&gt;
&lt;p&gt;3.9+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def process_items(items_s: set[bytes]):
    return  items_s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.8+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Set


def process_items(items_s: Set[bytes]):
    return items_s
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;dict&quot;&gt;Dict&lt;/h4&gt;
&lt;p&gt;Dict是typing模块的字典类型类，Dict[int,str]表示字典中的key都是int类型，value都是str类型。&lt;/p&gt;
&lt;p&gt;3.9+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def process_items(prices: dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.8+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Dict


def process_items(prices: Dict[str, float]):
    for item_name, item_price in prices.items():
        print(item_name)
        print(item_price)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;union&quot;&gt;Union&lt;/h4&gt;
&lt;p&gt;一个变量可能有几种类型，比如int、str，可以使用typing 模块的Union类表示该变量是某几个类型的其中一个类型。比如Union[int,str]表示可能是int类型，也可能是str类型，但是只可能是其中一种。在3.10版本以前只能使用Union类来表示这种行为，3.10版本引入了新语法，&lt;code&gt;str|int&lt;/code&gt;即可表示Union[str,int]，两者是等价的。&lt;/p&gt;
&lt;p&gt;3.6+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Union


def process_item(item: Union[int, str]):
    print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.10+版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def process_item(item: int | str):
    print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;possibly-none&quot;&gt;Possibly None&lt;/h4&gt;
&lt;p&gt;在 Python 中，&lt;code&gt;None&lt;/code&gt; 是一个&lt;strong&gt;特殊的单例对象&lt;/strong&gt;，它是 &lt;code&gt;NoneType&lt;/code&gt; 类型的唯一实例，用于表示“空”或“无值”。我们定义一个变量：name，它可能是str类型，也可能是None类型。在3.6+版本，可以使用typing模块的Optional来表达这种语义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Optional


def say_hi(name: Optional[str] = None):
    if name is not None:
        print(f&amp;quot;Hey {name}!&amp;quot;)
    else:
        print(&amp;quot;Hello World&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Optional[Something]&lt;/code&gt;实际上等价于&lt;code&gt;Union[Something, None]&lt;/code&gt;，它是一种简化写法，在3.10+版本，可以更进一步写成&lt;code&gt;Something | None&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;3.6+版本Union写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Union


def say_hi(name: Union[str, None] = None):
    if name is not None:
        print(f&amp;quot;Hey {name}!&amp;quot;)
    else:
        print(&amp;quot;Hello World&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.10+新语法更简洁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def say_hi(name: str | None = None):
    if name is not None:
        print(f&amp;quot;Hey {name}!&amp;quot;)
    else:
        print(&amp;quot;Hello World&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;疑问1：None是NoneType类型，Union[str, None] = None的写法不对吧，应该写成Union[str, NoneType] = None才对。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答：在 Python 的类型注解中，&lt;code&gt;None&lt;/code&gt; 和 &lt;code&gt;NoneType&lt;/code&gt; 实际上是等价的，&lt;code&gt;None&lt;/code&gt; 可以直接代表 &lt;code&gt;NoneType&lt;/code&gt;，因此 &lt;code&gt;Union[str, None]&lt;/code&gt; 和 &lt;code&gt;Union[str, NoneType]&lt;/code&gt; 语义上是完全相同的。另外NoneType需要额外导入&lt;code&gt;from types import NoneType&lt;/code&gt;，所以使用&lt;code&gt;Union[str, None]&lt;/code&gt;写法更加简洁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;疑问2：&lt;code&gt;name:Optional[str]=None&lt;/code&gt; 写法是否可以简化写成&lt;code&gt;name:str = None&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答：不可以，Optional[str]=None实际上就是Union[str,None]=None，两个None的意思不一样，不可省略。&lt;code&gt;Union[str, None]&lt;/code&gt; 或 &lt;code&gt;Optional[str]&lt;/code&gt;明确表示 &lt;code&gt;name&lt;/code&gt; 可以是字符串或 &lt;code&gt;None&lt;/code&gt;，语义表达更明确；而=None则是为它赋值了默认值为None，这样可以在不传name参数的时候为name赋予默认值None。实际上None是NoneType类型，为str类型的参数赋值None可能会出现类型不匹配的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;疑问3：Optional[str]和Union[str,None]，应该使用哪一种写法？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我认为应当使用Union[str,None]这种写法，这种写法虽然更繁琐一些，但是它具有更高的可读性；Optional[str]会给人一种错觉，它是可有可无的，而实际上并非这样，它必须有值而且是str或者None的一种。&lt;/p&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Optional


def say_hi(name: Optional[str]):
    print(f&amp;quot;Hey {name}!&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;name参数是Optional(可选的)，但是实际上它并非可选的（Optional），如果直接调用say_hi() 则会报错，因为name参数未传递。&lt;/p&gt;
&lt;h2 id=&quot;二自定义类类型&quot;&gt;二、自定义类类型&lt;/h2&gt;
&lt;p&gt;这个比较简单了，我们自定义一个类Person：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Person:
    def __init__(self, name: str):
        self.name = name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在某个方法传递Person类的实例one_person，则可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_person_name(one_person: Person):
    return one_person.name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在编辑器中能得友好的提示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/06/05/c1ed1c6b72df495586fb576653ed63e5.png&quot; alt=&quot;image06&quot; style=&quot;zoom: 80%;&quot; /&gt;
&lt;h2 id=&quot;三pydantic-models&quot;&gt;三、Pydantic models&lt;/h2&gt;
&lt;p&gt;Pydantic 是一个基于 Python 类型注解的数据验证和解析库，广泛应用于 API 开发、配置管理等领域。使用前需要先安装依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install pydantic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看一看Pydantic的基本使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import datetime
from typing import Union

from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str = &amp;quot;John Doe&amp;quot;
    signup_ts: Union[datetime, None] = None
    friends: list[int] = []


external_data = {
    &amp;quot;id&amp;quot;: &amp;quot;123&amp;quot;,
    &amp;quot;signup_ts&amp;quot;: &amp;quot;2017-06-01 12:22&amp;quot;,
    &amp;quot;friends&amp;quot;: [1, &amp;quot;2&amp;quot;, b&amp;quot;3&amp;quot;],
}
user = User(**external_data)
print(user)
# &amp;gt; User id=123 name=&apos;John Doe&apos; signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# &amp;gt; 123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用时要继承BaseModel类，每个字段可以都使用类型提示加以说明，这样在创建实例的时候Pydantic会将输入自动转换为合适的类型值。比如输入的id是字符串类型的123，由于User类定义的id是int类型，所以会被自动转换为int类型。&lt;/p&gt;
&lt;p&gt;更多Pydantic的使用可以参考官方文档：&lt;a href=&quot;https://docs.pydantic.dev/2.3/usage/models/&quot;&gt;https://docs.pydantic.dev/2.3/usage/models/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;四使用元数据注解的类型提示&quot;&gt;四、使用元数据注解的类型提示&lt;/h2&gt;
&lt;p&gt;在Python中可以使用&lt;code&gt;Annotated&lt;/code&gt;提供类型提示功能的同时提供元数据信息。&lt;/p&gt;
&lt;p&gt;在3.8+版本，Annotated需要引入&lt;code&gt;typing_extensions &lt;/code&gt;模块才能使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing_extensions import Annotated


def say_hello(name: Annotated[str, &amp;quot;this is just metadata&amp;quot;]) -&amp;gt; str:
    return f&amp;quot;Hello {name}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在3.9+版本，Annotated成为了标准库的一部分，所以可以通过typing模块直接导入。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Annotated


def say_hello(name: Annotated[str, &amp;quot;this is just metadata&amp;quot;]) -&amp;gt; str:
    return f&amp;quot;Hello {name}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Annotated的第一个参数是类型，剩余其它参数都是metadata数据。对于Python来说，它不会对Annotated做任何事情，剩余的元数据信息则在不同的框架中有不同的作用，比较典型的比如在langchain框架中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
from typing import List

from langchain_core.tools import tool
from typing_extensions import Annotated


@tool
def tool(
        a: Annotated[int, &amp;quot;scale factor&amp;quot;],
        b: Annotated[List[int], &amp;quot;list of ints over which to take maximum&amp;quot;],
) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply a by the maximum of b.&amp;quot;&amp;quot;&amp;quot;
    return a * max(b)


if __name__ == &apos;__main__&apos;:
    print(f&amp;quot;Name: {tool.name}&amp;quot;)
    print(f&amp;quot;Description: {tool.description}&amp;quot;)
    print(f&amp;quot;args schema: {json.dumps(tool.args, indent=4)}&amp;quot;)
    print(f&amp;quot;returns directly?: {tool.return_direct}&amp;quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码示例中定义了一个tool，a和b的元数据信息会被langchain框架解析用于说明两个参数的作用并提交给大模型，以让大模型理解两个参数该如何传递。&lt;/p&gt;
&lt;h2 id=&quot;五参考文档&quot;&gt;五、参考文档&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://fastapi.tiangolo.com/python-types/&quot;&gt;https://fastapi.tiangolo.com/python-types/&lt;/a&gt;&lt;/p&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>Python脚本：合并mp4和m4a文件</title>
      <link>https://blog.kdyzm.cn/post/307</link>
      <guid>https://blog.kdyzm.cn/post/307</guid>
      <pubDate>Fri, 23 May 2025 11:28:16 +0800</pubDate>
      <description>&lt;p&gt;这个需求产生于B站视频下载下来之后是分开的两个文件：&lt;strong&gt;没有声音的mp4视频文件&lt;/strong&gt;和&lt;strong&gt;m4a音频文件&lt;/strong&gt;。如何下载B站视频，可以参考 &lt;a href=&quot;https://github.com/the1812/Bilibili-Evolved&quot;&gt;Bilibili-Evolved&lt;/a&gt; 的使用教程。&lt;/p&gt;
&lt;h2 id=&quot;一视频下载&quot;&gt;一、视频下载&lt;/h2&gt;
&lt;p&gt;Bilibili-Evolved脚本生效以后，就可以下载B站视频了：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/5399bb746fda493ab350527089e912db.png&quot; alt=&quot;image-20250523095031800&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;默认下载格式是音画分离的，点击开始会看到两个下载按钮&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/22426643c76b4c93adbc99f8225b7154.png&quot; alt=&quot;image-20250523095136484&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;点击后浏览器会直接下载&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/a38b7610595b45c4967d6e5ee846c7c4.png&quot; alt=&quot;image-20250523103047671&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;二音视频合并&quot;&gt;二、音视频合并&lt;/h2&gt;
&lt;p&gt;音视频合并需要使用到工具&lt;code&gt;ffmpeg&lt;/code&gt;，使用该命令行工具只需要一行命令就可以合并音视频文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i &amp;quot;file1&amp;quot; -i &amp;quot;file2&amp;quot; -vcodec copy -acodec copy &amp;quot;target_file&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令很简单，但是每次合并都要拼写这个命令还是很麻烦的，所以这里写了个python脚本来实现&lt;/p&gt;
&lt;h3 id=&quot;1python脚本实现音视频合并&quot;&gt;1、python脚本实现音视频合并&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os
import argparse
from pathlib import Path

parser = argparse.ArgumentParser(
    prog=&apos;merge_mp4_m4a&apos;,
    description=&apos;合并mp4和m4a文件脚本&apos;,
    epilog=&apos;Copyright(r), 2025&apos;
)
parser.add_argument(&apos;input&apos;, help=&apos;m4a文件路径&apos;)
# 解析参数
args = parser.parse_args()


def do_merge_file(mp4_file: Path, m4a_file: Path, target_file: Path):
    command = f&amp;quot;&amp;quot;&amp;quot;
    ffmpeg -i &amp;quot;{mp4_file.absolute()}&amp;quot; -i &amp;quot;{m4a_file.absolute()}&amp;quot; -vcodec copy -acodec copy &amp;quot;{target_file.absolute()}&amp;quot;
    &amp;quot;&amp;quot;&amp;quot;
    os.system(command)


def do_merge_mp4_m4a_files():
    input_m4a_path = args.input
    file = Path(input_m4a_path)
    name = file.name.split(&amp;quot;.&amp;quot;)[0]
    mp4_file = file.parent.joinpath(Path(name + &amp;quot;.mp4&amp;quot;))
    m4a_file = Path(input_m4a_path)
    target_file = file.parent.joinpath(Path(name + &amp;quot;_.mp4&amp;quot;))
    do_merge_file(mp4_file, m4a_file, target_file)
    m4a_file.unlink()
    mp4_file.unlink()
    print(f&amp;quot;output file ={target_file.absolute()}&amp;quot;)


if __name__ == &apos;__main__&apos;:
    do_merge_mp4_m4a_files()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本很简单，它做了如下事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从命令行接受m4a文件路径参数&lt;/li&gt;
&lt;li&gt;根据文件名推导出mp4文件名称&lt;/li&gt;
&lt;li&gt;合并m4a和mp4文件，合并后的文件以&lt;code&gt;文件名_.mp4&lt;/code&gt;命名。&lt;/li&gt;
&lt;li&gt;删除两个源文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在我们将脚本保存为script.py文件，将其复制到和源文件的同级目录，执行命令&lt;code&gt;python script.py &amp;quot;【无损音质】一生所爱 - 卢冠廷.m4a&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/0ab2b63211f945b4aa1bb8a400df4add.gif&quot; alt=&quot;动画40_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;OK，现在已经实现了音视频合并，但是使用起来感觉还不是很方便，将该功能加到右键菜单怎么样？只需要对着m4a文件右键，执行命令即可实现对两个文件的合并：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/b6fd998512d04007a6a69b6c0fd193c6.gif&quot; alt=&quot;动画41_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这就要使用到修改注册表相关的功能了。&lt;/p&gt;
&lt;h3 id=&quot;2右键m4a文件执行python脚本合并文件&quot;&gt;2、右键m4a文件执行python脚本合并文件&lt;/h3&gt;
&lt;p&gt;首先，将python脚本保存到&lt;code&gt;D:\system_script\m4a&lt;/code&gt;，防止误删除python脚本导致脚本不可用。&lt;/p&gt;
&lt;p&gt;接下来修改注册表，实现对着m4a文件右键后显示“合并同名的MP4文件”菜单，点击后执行script脚本。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Windows+R&lt;/code&gt;组合键，输入&lt;code&gt;regedit&lt;/code&gt;，进入注册表管理页，打开路径：&lt;code&gt;计算机\HKEY_CLASSES_ROOT\SystemFileAssociations\.m4a&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/5f71bd5c2f1541e8a0bc309c127b0b38.png&quot; alt=&quot;image-20250523111859820&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;如果.m4a目录下没有shell，则新建项shell。&lt;/p&gt;
&lt;p&gt;在shell文件夹下新建项：MergeMp4File，选中后右侧修改默认值为“合并同名的MP4文件”，然后新建字符串值icon，值为python的路径，我的是&lt;code&gt;D:\ProgramFiles\anaconda3\python.exe&lt;/code&gt;，完成这一步骤后的注册表：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/d21d4bdc1a354fa1aa4cc5f46dfcde31.png&quot; alt=&quot;image-20250523112244352&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;完成上面的步骤之后，对着m4a文件右键就能看到“合并同名的MP4文件”选项了，图标是python程序的样子。&lt;/p&gt;
&lt;p&gt;接下来配置真正执行命令的动作。&lt;/p&gt;
&lt;p&gt;在MergeMp4File下新增项&lt;code&gt;command&lt;/code&gt;，修改默认值为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;D:\ProgramFiles\anaconda3\python.exe D:\system_script\m4a\script.py &amp;quot;%1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;做完这一步后的注册表：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/ae2e854cfc46452591685051d04580ec.png&quot; alt=&quot;image-20250523112554663&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;三运行效果&quot;&gt;三、运行效果&lt;/h2&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/23/b6fd998512d04007a6a69b6c0fd193c6.gif&quot; alt=&quot;动画41_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这样合并两个文件就方便多了。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>Win10映射网络驱动器导致的文件资源管理器无响应问题解决方案</title>
      <link>https://blog.kdyzm.cn/post/306</link>
      <guid>https://blog.kdyzm.cn/post/306</guid>
      <pubDate>Thu, 22 May 2025 16:54:04 +0800</pubDate>
      <description>&lt;p&gt;已经有很长时间了，打开我的电脑或者&lt;code&gt;Windows+E&lt;/code&gt;快捷键打开文件资源管理器之后系统长时间无响应：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/22/6dfc1639a471475b86e5fdc42aeaae4a.gif&quot; alt=&quot;Win10映射网络驱动器连接失败导致文件资源管理器无响应&quot; style=&quot;zoom:30%;&quot; /&gt;
&lt;p&gt;排查了好久终于发现了原来是因为我挂载了很多网络硬盘：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/22/8f57371680f848818ae404e91415df23.png&quot; alt=&quot;image-20250522164440132&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这些网络硬盘都是家庭内网IP挂载的，正常在家使用一点问题没有，但是笔记本拿到公司以后，换了公司网络，这些网络硬盘就都歇菜了。。歇菜就歇菜吧，Windows似乎有什么机制，不连接上不罢休，连带着其它正常的本地分区都显示不出来了，然后就无限loading进度条。。&lt;/p&gt;
&lt;p&gt;问题来了，如何解决呢？难道每次去公司前把这些网络硬盘卸载？太麻烦了。这个问题折磨了我好久，终于在今天突然发现了解决方案：把“&lt;strong&gt;网络位置&lt;/strong&gt;”给折叠起来，没错，就这么简单：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/05/22/dd7ab9e6b0044e4383705ce2b0fe798f.png&quot; alt=&quot;image-20250522165000877&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这个折叠动作是会被持久化记忆的，下次打开资源管理器还是折叠状态，而且只要是折叠状态，Windows就不会尝试连接网络硬盘了。对此，我只想说一句 MMP 表达下心情。。。。。。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>windows</category>
      <category>win10</category>
    </item>
    <item>
      <title>详解Python中的高级特性：切片</title>
      <link>https://blog.kdyzm.cn/post/305</link>
      <guid>https://blog.kdyzm.cn/post/305</guid>
      <pubDate>Thu, 15 May 2025 16:06:16 +0800</pubDate>
      <description>&lt;p&gt;Python 中的切片（Slicing）操作是一种高效且灵活的数据访问方式，主要用于序列类型（如列表、字符串、元组等）。它允许通过指定起始、结束和步长来截取序列的子集。其基本语法如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;sequence[start:stop:step]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;start&lt;/strong&gt;：起始索引（包含），默认为 &lt;code&gt;0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stop&lt;/strong&gt;：结束索引（不包含），默认为序列长度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;step&lt;/strong&gt;：步长（可选），默认为 &lt;code&gt;1&lt;/code&gt;，值表示步长；正负表示方向&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;一特性&quot;&gt;一、特性&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;(1) 索引规则&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正向索引&lt;/strong&gt;：从 &lt;code&gt;0&lt;/code&gt; 开始，左到右。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负向索引&lt;/strong&gt;：从 &lt;code&gt;-1&lt;/code&gt; 开始（倒数第一个），右到左。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;(2) 默认值&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;省略 &lt;code&gt;start&lt;/code&gt;：从序列开头开始（&lt;code&gt;0&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;省略 &lt;code&gt;stop&lt;/code&gt;：截取到序列末尾（包括最后一个元素）。&lt;/li&gt;
&lt;li&gt;省略 &lt;code&gt;step&lt;/code&gt;：默认为 &lt;code&gt;1&lt;/code&gt;（连续截取）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;(3) 步长（step）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正步长&lt;/strong&gt;：从左到右截取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负步长&lt;/strong&gt;：从右到左截取（可实现逆序）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;二常见操作示例&quot;&gt;&lt;strong&gt;二、常见操作示例&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;1-基本截取&quot;&gt;&lt;strong&gt;(1) 基本截取&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;s = [0, 1, 2, 3, 4, 5]
print(s[1:4])    # [1, 2, 3]（包含1，不包含4）
print(s[:3])     # [0, 1, 2]（从开头到索引2）
print(s[3:])     # [3, 4, 5]（从索引3到末尾）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-负索引&quot;&gt;&lt;strong&gt;(2) 负索引&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(s[-3:])     # [3, 4, 5]（倒数第3个到末尾）
print(s[:-2])     # [0, 1, 2, 3]（开头到倒数第3个）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-步长应用&quot;&gt;&lt;strong&gt;(3) 步长应用&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(s[::2])     # [0, 2, 4]（每隔1个取1个）
print(s[::-1])    # [5, 4, 3, 2, 1, 0]（逆序）
print(s[1:5:2])   # [1, 3]（从1到4，步长2）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-字符串切片&quot;&gt;&lt;strong&gt;(4) 字符串切片&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;text = &amp;quot;Python&amp;quot;
print(text[1:4])  # &amp;quot;yth&amp;quot;
print(text[::-1]) # &amp;quot;nohtyP&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;三注意事项&quot;&gt;&lt;strong&gt;三、注意事项&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;越界安全&lt;/strong&gt;：切片索引超出范围时自动截断，不会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;s = [1, 2, 3]
print(s[0:10])  # [1, 2, 3]（自动限制到有效范围）
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;浅拷贝&lt;/strong&gt;：切片返回的是新对象，但对可变序列的修改可能影响原数据（如嵌套列表）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不可变序列&lt;/strong&gt;：字符串、元组的切片会生成新对象，原对象不可变。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;四-高级技巧&quot;&gt;&lt;strong&gt;四、 高级技巧&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;1-赋值与修改&quot;&gt;&lt;strong&gt;(1) 赋值与修改&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;s = [0, 1, 2, 3]
s[1:3] = [10, 20]  # 替换子序列 → [0, 10, 20, 3]
s[::2] = [100, 200] # 按步长赋值 → [100, 10, 200, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-删除元素&quot;&gt;&lt;strong&gt;(2) 删除元素&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;s = [0, 1, 2, 3]
s[1:3] = []  # 删除索引1-2 → [0, 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;五总结&quot;&gt;&lt;strong&gt;五、总结&lt;/strong&gt;&lt;/h2&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;操作&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;基本切片&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;s[1:4]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;截取索引1到3的子序列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;逆序&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;s[::-1]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;反转序列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;步长截取&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;s[::2]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;每隔1个元素取1个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;负索引&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;s[-3:]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;截取最后3个元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;赋值与修改&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;code&gt;s[1:3]=[10,20]&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;替换指定范围的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注：以上内容由LLM生成&lt;/p&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>远程Windows开机：WoL（Wake-on-Lan）客户端Python实现</title>
      <link>https://blog.kdyzm.cn/post/304</link>
      <guid>https://blog.kdyzm.cn/post/304</guid>
      <pubDate>Mon, 21 Apr 2025 14:25:31 +0800</pubDate>
      <description>&lt;p&gt;远程Windows开机实现之前已经使用Java实现过了（详情见《&lt;a href=&quot;https://blog.kdyzm.cn/post/255&quot;&gt;远程Windows开机：WoL（Wake-on-Lan）客户端Java实现&lt;/a&gt;》），现在使用Python重新实现，当然，需要重新复习下网络唤醒的原理：&lt;/p&gt;
&lt;p&gt;借助WoL技术（Wake-on Lan）可以将电脑远程开机，关于Wol，可以参考维基百科：&lt;a href=&quot;https://zh.wikipedia.org/wiki/網路喚醒&quot;&gt;《网络唤醒》&lt;/a&gt; 。&lt;strong&gt;它的基本原理就是支持Wol技术的网卡在电脑关机之后，网卡还有微弱的供电，当它发现网络广播的内容中有特定的“魔法数据包”，并且经过解析发现它所指的地址是自身所处的电脑时，网卡就会通知机内的&lt;a href=&quot;https://zh.wikipedia.org/wiki/主機板&quot;&gt;主板&lt;/a&gt;、&lt;a href=&quot;https://zh.wikipedia.org/wiki/電源供應器&quot;&gt;电源供应器&lt;/a&gt;，开始进行开机（或唤醒）的程序。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;具体来说，只要发送特定格式的UDP广播数据包到子网中，就能将计算机唤醒，数据包格式：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;FF FF FF FF FF FF FF&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;MAC 地址 × 16&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;4-6字节的密码（可空）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;完整Python代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import socket
import os

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 启用广播
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

data = &amp;quot;FFFFFFFFFFFF&amp;quot; + &amp;quot;58112208850B&amp;quot; * 16 + &amp;quot;000000000000&amp;quot;

s.sendto(bytes.fromhex(data), (&apos;192.168.3.255&apos;, 9))

s.close()
os.system(&amp;quot;pause&amp;quot;)

if __name__ == &apos;__main__&apos;:
    pass

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到Python版本的代码非常简洁，而且作为脚本语言不需要编译打包，直接复制到桌面上就可以随时用了，Python是天下第一好的编程语言！（对不起Java，我叛变了）&lt;/p&gt;
</description>
      <category>windows</category>
      <category>win10</category>
      <category>python</category>
    </item>
    <item>
      <title>大模型开发之langchain0.3（五）：手动干预方法调用</title>
      <link>https://blog.kdyzm.cn/post/303</link>
      <guid>https://blog.kdyzm.cn/post/303</guid>
      <pubDate>Thu, 17 Apr 2025 11:23:05 +0800</pubDate>
      <description>&lt;p&gt;本篇文章讨论下如何添加“人机交互（human-in-the-loop）”动作到方法调用的流程中，翻译成大白话，就是怎样手动干预方法调用。&lt;/p&gt;
&lt;p&gt;在之前的文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/296&quot;&gt;大模型开发之langchain0.3（三）：方法调用&lt;/a&gt;》中，已经说明了大模型会根据提示词信息自己决定调用什么方法，现在不直接调用方法了，在调用方法前，必须先得到我的同意才行。如何实现该功能呢？&lt;/p&gt;
&lt;h2 id=&quot;一方法调用中人机交互原理&quot;&gt;一、方法调用中人机交互原理&lt;/h2&gt;
&lt;p&gt;我们先回顾下方法调用的原理：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/01/c84055e4-6894-4bfb-9722-43d31a9f7009.png&quot; alt=&quot;Function Calling Diagram Steps&quot; style=&quot;zoom: 15%;&quot; /&gt;
&lt;p&gt;上图中，在❷处大模型告诉用户应该调用什么方法；用户在❸处执行方法调用。我们在❷和❸之间加上审核的逻辑，就可以干预方法执行了。&lt;/p&gt;
&lt;h2 id=&quot;二cli人机交互实现&quot;&gt;二、CLI人机交互实现&lt;/h2&gt;
&lt;p&gt;代码实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
from typing import List, Dict

from langchain.chat_models import init_chat_model
from langchain_core.messages import AIMessage
from langchain_core.tools import tool


class NotApproved(Exception):
    &amp;quot;&amp;quot;&amp;quot;Custom exception.&amp;quot;&amp;quot;&amp;quot;


def human_approval(msg: AIMessage) -&amp;gt; AIMessage:
    &amp;quot;&amp;quot;&amp;quot;Responsible for passing through its input or raising an exception.

    Args:
        msg: output from the chat model

    Returns:
        msg: original output from the msg
    &amp;quot;&amp;quot;&amp;quot;
    tool_strs = &amp;quot;\n\n&amp;quot;.join(
        json.dumps(tool_call, indent=4) for tool_call in msg.tool_calls
    )
    input_msg = (
        f&amp;quot;Do you approve of the following tool invocations\n\n{tool_strs}\n\n&amp;quot;
        &amp;quot;Anything except &apos;Y&apos;/&apos;Yes&apos; (case-insensitive) will be treated as a no.\n &amp;gt;&amp;gt;&amp;gt;&amp;quot;
    )
    resp = input(input_msg)
    if resp.lower() not in (&amp;quot;yes&amp;quot;, &amp;quot;y&amp;quot;):
        raise NotApproved(f&amp;quot;Tool invocations not approved:\n\n{tool_strs}&amp;quot;)
    return msg


def call_tools(msg: AIMessage) -&amp;gt; List[Dict]:
    &amp;quot;&amp;quot;&amp;quot;Simple sequential tool calling helper.&amp;quot;&amp;quot;&amp;quot;
    tool_map = {tool.name: tool for tool in tools}
    tool_calls = msg.tool_calls.copy()
    for tool_call in tool_calls:
        tool_call[&amp;quot;output&amp;quot;] = tool_map[tool_call[&amp;quot;name&amp;quot;]].invoke(tool_call[&amp;quot;args&amp;quot;])
    return tool_calls


@tool
def count_emails(last_n_days: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Dummy function to count number of e-mails. Returns 2 * last_n_days.&amp;quot;&amp;quot;&amp;quot;
    return last_n_days * 2


@tool
def send_email(message: str, recipient: str) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;Dummy function for sending an e-mail.&amp;quot;&amp;quot;&amp;quot;
    return f&amp;quot;Successfully sent email to {recipient}.&amp;quot;


if __name__ == &apos;__main__&apos;:
    llm = init_chat_model(&amp;quot;gpt-4o-mini&amp;quot;, model_provider=&amp;quot;openai&amp;quot;)
    tools = [count_emails, send_email]
    llm_with_tools = llm.bind_tools(tools)
    chain = llm_with_tools | human_approval | call_tools
    try:
        result = chain.invoke(&amp;quot;how many emails did i get in the last 5 days?&amp;quot;)
        print(json.dumps(result, indent=4))
    except NotApproved as e:
        print()
        print(e)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码是官方给出的控制台版本代码案例，通过构建chain调用链，在中间加上human_approval逻辑实现人工干预方法调用；如果输入Y/Yes之外的字符，将会抛出异常中断调用链的后续执行：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/17/72f3dd0c36d34ca4a3cbf1c9fc4819d3.gif&quot; alt=&quot;动画31_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;如果输入Y，则会输出带有output字段的tool_call信息：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/17/1b2b224ed72649f78d102535bf10aacb.gif&quot; alt=&quot;动画32_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;三langchain人机交互的局限性&quot;&gt;三、langchain人机交互的局限性&lt;/h2&gt;
&lt;p&gt;本篇文章案例来自于官方网站：&lt;a href=&quot;https://python.langchain.com/docs/how_to/tools_human/&quot;&gt;https://python.langchain.com/docs/how_to/tools_human/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;官方网站在开头就明确说了：强烈建议使用langgraph实现人机交互功能，详情参考：&lt;a href=&quot;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&quot;&gt;https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;从以上代码中已经可以看得出直接基于langchain的方法调用实现人机交互还是挺麻烦的，方法调用要手动调用，手动处理结果等，使用langgraph几乎能全自动解决所有问题。&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>langchain</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Python脚本：Typora+minio实现markdown图片自动上传</title>
      <link>https://blog.kdyzm.cn/post/302</link>
      <guid>https://blog.kdyzm.cn/post/302</guid>
      <pubDate>Wed, 16 Apr 2025 18:00:40 +0800</pubDate>
      <description>&lt;p&gt;在之前的一篇文章中，已经实现了java版本的Typora图片自动上传功能：&lt;a href=&quot;https://blog.kdyzm.cn/post/42&quot;&gt;markdown编辑器typora本地图片上传到自己的服务器&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;现在使用Python+minio重新实现该功能，比Java版本精简了太多代码了&lt;/p&gt;
&lt;p&gt;需要的依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install minio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import argparse
import mimetypes
import uuid
from datetime import date
from urllib.request import urlopen

from minio import Minio

parser = argparse.ArgumentParser(
    prog=&apos;typora_upload_image&apos;,
    description=&apos;typora上传图片脚本&apos;,
    epilog=&apos;Copyright(r), 2025&apos;
)
parser.add_argument(&apos;urls&apos;, nargs=&apos;+&apos;, help=&apos;一个或多个URL地址&apos;)

client = Minio(
    &amp;quot;127.0.0.1:9000&amp;quot;,
    access_key=&amp;quot;{your access key}&amp;quot;,
    secret_key=&amp;quot;{your secret key}&amp;quot;,
    secure=False
)


def get_file_path(path):
    now = date.today()
    return &amp;quot;public/&amp;quot; \
           + str(now.year) + &amp;quot;/&amp;quot; \
           + (str(now.month) if now.month &amp;gt; 10 else (&amp;quot;0&amp;quot; + str(now.month))) + &amp;quot;/&amp;quot; \
           + (str(now.day) if now.day &amp;gt; 10 else (&amp;quot;0&amp;quot; + str(now.day))) + &amp;quot;/&amp;quot; \
           + str(uuid.uuid4()).replace(&amp;quot;-&amp;quot;, &amp;quot;&amp;quot;) + &amp;quot;.&amp;quot; \
           + path.split(&amp;quot;.&amp;quot;)[-1]


def upload_to_minio(path: str) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;
    上传图片到minio
    :param path: 源url，可能是本地图片，也可能是网络图片
    :return: minio的图片地址
    &amp;quot;&amp;quot;&amp;quot;
    file_path = get_file_path(path)
    mimetype, encoding = mimetypes.guess_type(path)
    if path.startswith(&amp;quot;http&amp;quot;):
        bynary_io = urlopen(path)
        client.put_object(
            bucket_name=&amp;quot;blog&amp;quot;,
            object_name=file_path,
            data=bynary_io,
            content_type=mimetype,
            length=-1,
            part_size=10 * 1024 * 1024,
        )
    else:
        client.fput_object(
            bucket_name=&amp;quot;blog&amp;quot;,
            object_name=file_path,
            file_path=path,
            content_type=mimetype
        )
    return file_path


if __name__ == &apos;__main__&apos;:
    urls = parser.parse_args().urls
    for url in urls:
        uploaded_url = upload_to_minio(url)
        print(&amp;quot;https://127.0.0.1:9000/blog/&amp;quot; + uploaded_url)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用者需修改以下代码：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;minio地址和端口号&lt;/li&gt;
&lt;li&gt;access_key和secret_key&lt;/li&gt;
&lt;li&gt;公开的特定地址前缀，比如&lt;code&gt;/public&lt;/code&gt;前缀，这需要在minio中设置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将&lt;code&gt;python D:\script.py&lt;/code&gt; 命令填写到&lt;code&gt;文件-&amp;gt;偏好设置-&amp;gt;图像-&amp;gt;命令&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/16/1c51e333c6ac474db50da3f670b4b340.png&quot; alt=&quot;image-20250416175624159&quot; /&gt;&lt;/p&gt;
&lt;p&gt;运行效果：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/16/a931b3b2a96a439e948f87ee689f4e96.gif&quot; alt=&quot;动画30_resize&quot; style=&quot;zoom:40%;&quot; /&gt;
</description>
      <category>typora</category>
      <category>python</category>
    </item>
    <item>
      <title>Python基础：装饰器</title>
      <link>https://blog.kdyzm.cn/post/301</link>
      <guid>https://blog.kdyzm.cn/post/301</guid>
      <pubDate>Wed, 16 Apr 2025 13:38:02 +0800</pubDate>
      <description>&lt;p&gt;装饰器是Python中最强大且优雅的特性之一，它允许你在不修改函数或类源代码的情况下，动态地扩展它们的功能。装饰器本质上是一个&lt;strong&gt;接受函数作为参数&lt;/strong&gt;并&lt;strong&gt;返回新函数&lt;/strong&gt;的高阶函数。对应到java，其形式和注解切面非常相似。&lt;/p&gt;
&lt;h2 id=&quot;一装饰器的基本使用&quot;&gt;一、装饰器的基本使用&lt;/h2&gt;
&lt;p&gt;下面用一个例子解释装饰器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def log(fun):
    def wrapper(*args, **kwargs):
        print(&amp;quot;方法调用前&amp;quot;)
        result = fun(*args, **kwargs)
        print(&amp;quot;方法调用后&amp;quot;)
        return result

    return wrapper


@log
def hello():
    print(&amp;quot;Hello,World&amp;quot;)


if __name__ == &apos;__main__&apos;:
    hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;方法调用前
Hello,World
方法调用后
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码执行了hello方法，hello方法的@log注解实际上相当于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;hello = log(hello)
hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;log方法返回的方法实际上已经变成了wrapper方法，而wrapper方法接收的参数是&lt;code&gt;*args, **kwargs&lt;/code&gt;，所以它可以接受任意参数。&lt;/p&gt;
&lt;p&gt;来看下它的等价代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def log(fun):
    def wrapper(*args, **kwargs):
        print(&amp;quot;方法调用前&amp;quot;)
        result = fun(*args, **kwargs)
        print(&amp;quot;方法调用后&amp;quot;)
        return result

    return wrapper


def hello():
    print(&amp;quot;Hello,World&amp;quot;)


if __name__ == &apos;__main__&apos;:
    hello = log(hello)
    hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果和装饰器的运行结果是一样的。&lt;/p&gt;
&lt;h2 id=&quot;二装饰器传参&quot;&gt;二、装饰器传参&lt;/h2&gt;
&lt;p&gt;如果装饰器方法本身需要一些参数控制装饰器的行为，该如何传参呢？比如以上的log装饰器，我需要传一个参数level，形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@log(level=&amp;quot;INFO&amp;quot;)
def hello():
    print(&amp;quot;Hello,World&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印日志的时候需要将该字符串放到日志前面，如何实现？&lt;/p&gt;
&lt;p&gt;这里需要用到三层嵌套，修改后的代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def log(level):
    def decorator(fun):
        def wrapper(*args, **kwargs):
            print(f&amp;quot;{level}:方法调用前&amp;quot;)
            result = fun(*args, **kwargs)
            print(f&amp;quot;{level}:方法调用后&amp;quot;)
            return result

        return wrapper
    return decorator


@log(level=&amp;quot;INFO&amp;quot;)
def hello():
    print(&amp;quot;Hello,World&amp;quot;)


if __name__ == &apos;__main__&apos;:
    hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;INFO:方法调用前
Hello,World
INFO:方法调用后
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时的&lt;code&gt;hello()&lt;/code&gt;方法调用等价于以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;hello = log(level=&amp;quot;INFO&amp;quot;)(hello)
hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的等价代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def log(level):
    def decorator(fun):
        def wrapper(*args, **kwargs):
            print(f&amp;quot;{level}:方法调用前&amp;quot;)
            result = fun(*args, **kwargs)
            print(f&amp;quot;{level}:方法调用后&amp;quot;)
            return result

        return wrapper

    return decorator


# @log(level=&amp;quot;INFO&amp;quot;)
def hello():
    print(&amp;quot;Hello,World&amp;quot;)


if __name__ == &apos;__main__&apos;:
    hello = log(level=&amp;quot;INFO&amp;quot;)(hello)
    hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;三函数签名变更问题&quot;&gt;三、函数签名变更问题&lt;/h2&gt;
&lt;p&gt;装饰器传参的问题也解决了，接下来看看装饰器引发的函数签名变化问题，运行以下代码，hello方法的&lt;code&gt;__name__&lt;/code&gt;属性还是hello吗？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def log(level):
    def decorator(fun):
        def wrapper(*args, **kwargs):
            print(f&amp;quot;{level}:方法调用前&amp;quot;)
            result = fun(*args, **kwargs)
            print(f&amp;quot;{level}:方法调用后&amp;quot;)
            return result

        return wrapper

    return decorator


@log(level=&amp;quot;INFO&amp;quot;)
def hello():
    print(&amp;quot;Hello,World&amp;quot;)


if __name__ == &apos;__main__&apos;:
    print(hello.__name__)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没错，hello的方法名已经变成了wrapper，这对于某些依赖函数签名的代码执行就会出错。&lt;/p&gt;
&lt;p&gt;首先想到的解决方案就是复制hello方法的&lt;code&gt;__name__&lt;/code&gt;到wrapper方法。。实际上使用内置的&lt;code&gt;functools&lt;/code&gt;模块可以解决这个问题，只需要在wrapper方法上使用&lt;code&gt;@functools.wraps(log)&lt;/code&gt;标记一下即可，来看下它的使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import functools


def log(level):
    def decorator(fun):
        @functools.wraps(log)
        def wrapper(*args, **kwargs):
            print(f&amp;quot;{level}:方法调用前&amp;quot;)
            result = fun(*args, **kwargs)
            print(f&amp;quot;{level}:方法调用后&amp;quot;)
            return result

        return wrapper

    return decorator


@log(level=&amp;quot;INFO&amp;quot;)
def hello():
    print(&amp;quot;Hello,World&amp;quot;)


if __name__ == &apos;__main__&apos;:
    print(hello.__name__)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;log
&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;参考文档：&lt;a href=&quot;https://liaoxuefeng.com/books/python/functional/decorator/index.html&quot;&gt;https://liaoxuefeng.com/books/python/functional/decorator/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>详解python中的*符号</title>
      <link>https://blog.kdyzm.cn/post/300</link>
      <guid>https://blog.kdyzm.cn/post/300</guid>
      <pubDate>Wed, 09 Apr 2025 23:47:43 +0800</pubDate>
      <description>&lt;p&gt;在Python中，星号（&lt;code&gt;*&lt;/code&gt;）是一个多用途运算符，具有多种使用场景。它在函数定义、解包操作、算术运算、模块导入等多个领域扮演重要角色。&lt;/p&gt;
&lt;h2 id=&quot;1算术运算符&quot;&gt;1、算术运算符&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;单星号 &lt;code&gt;*&lt;/code&gt;&lt;/strong&gt;：用于数值的乘法或幂运算中的相乘。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;result = 2 * 3   # 输出6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;双星号 &lt;code&gt;**&lt;/code&gt;&lt;/strong&gt;：表示幂运算（指数）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;result = 2 ** 3  # 输出8（2的三次方）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，比较特殊的，单 &lt;code&gt;*&lt;/code&gt; 在左侧为字符串的情况下表示“复制”N次的意思，其实这就是乘法运算。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;result = &amp;quot;A&amp;quot;*3  # 输出AAA（3个A）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;2函数参数&quot;&gt;2、函数参数&lt;/h2&gt;
&lt;p&gt;关于函数参数，可以先参考文章：&lt;a href=&quot;https://liaoxuefeng.com/books/python/function/parameter/index.html&quot;&gt;https://liaoxuefeng.com/books/python/function/parameter/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;符号*函数参数这里有两个用处。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;*args：&lt;/code&gt;位置参数收集，允许函数接受任意数量的位置参数，并将其存储为一个&lt;strong&gt;元组&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def my_func(*args):
    print(args)
my_func(1, 2, 3)  # 输出：(1, 2, 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;**kwargs&lt;/code&gt;：关键字参数收集，允许函数接受任意数量的关键字参数（键值对），并存储为一个&lt;strong&gt;字典&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def my_func(**kwargs):
    print(kwargs)
my_func(a=1, b=2)  # 输出：{&apos;a&apos;: 1, &apos;b&apos;: 2}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;3参数解包&quot;&gt;3、参数解包&lt;/h2&gt;
&lt;p&gt;解包操作允许将序列（如列表、元组、字典）展开为独立的参数。&lt;/p&gt;
&lt;h3 id=&quot;函数调用中的解包&quot;&gt;函数调用中的解包&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt;解包可迭代对象（如列表、元组）为位置参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def add(a, b): 
    return a + b
nums = [3, 5]
print(add(*nums))  # 输出8，等价于 add(3, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;**&lt;/code&gt;解包字典为关键字参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;config = {&apos;width&apos;: 800, &apos;height&apos;: 600}
def configure(width, height):
    return f&amp;quot;Width: {width}, Height: {height}&amp;quot;
print(configure(**config))  # 输出: &amp;quot;Width: 800, Height: 600&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;序列解构&quot;&gt;&lt;strong&gt;序列解构&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;*&lt;/code&gt; 捕获剩余元素：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 解构列表
a, *rest = [1, 2, 3, 4]
print(a)    # 1
print(rest)  # [2, 3, 4]
# 解构元组
first, *middle, last = (10, 20, 30, 40)
print(first)   # 10
print(middle)  # [20, 30]
print(last)    # 40
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;4模块导入&quot;&gt;4、&lt;strong&gt;模块导入&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;from module import *&lt;/code&gt; 导入模块中的所有公有符号（除以下划线开头的名称）。&lt;strong&gt;不建议在大型项目中使用&lt;/strong&gt;，因为它可能污染命名空间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from math import *
print(sqrt(16))  # 输出4.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;5字典与集合的解包&quot;&gt;5、&lt;strong&gt;字典与集合的解包&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;合并字典&quot;&gt;合并字典&lt;/h3&gt;
&lt;p&gt;在Python3.5之后，可以使用如下方式合并字典&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;dict1 = {&amp;quot;a&amp;quot;: 1, &amp;quot;b&amp;quot;: 2}
dict2 = {&amp;quot;b&amp;quot;: 3, &amp;quot;c&amp;quot;: 4}
merged = {**dict1, **dict2}  # 输出: {&apos;a&apos;:1, &apos;b&apos;:3, &apos;c&apos;:4}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上是先解包，再合并。&lt;/p&gt;
&lt;p&gt;Python3.9之后，可以使用|符号合并字典&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;merged = dict1 | dict2  # 等同于 {**dict1, **dict2}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;合并集合或列表&quot;&gt;合并集合或列表&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;*&lt;/code&gt; 展开可迭代对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# 合并列表
list1 = [1, 2]
list2 = [3, 4]
combined = [*list1, *list2]  # 输出：[1, 2, 3, 4]
# 合并集合
set1 = {1, 2}
set2 = {3, 4}
combined_set = {*set1, *set2}  # 输出：{1,2,3,4}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;6参数分隔符&quot;&gt;6、参数分隔符&lt;/h2&gt;
&lt;p&gt;在函数参数列表中，单独的 &lt;code&gt;*&lt;/code&gt; 用于分隔位置参数和关键字参数：&lt;/p&gt;
&lt;p&gt;例如以下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def add_human_in_the_loop(
    tool: Callable | BaseTool,
    *,
    interrupt_config: HumanInterruptConfig = None,
) -&amp;gt; BaseTool
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这表示 &lt;code&gt;tool&lt;/code&gt; 可以是位置参数或关键字参数&lt;/li&gt;
&lt;li&gt;但 &lt;code&gt;interrupt_config&lt;/code&gt; 必须是关键字参数（调用时必须写成 &lt;code&gt;interrupt_config=值&lt;/code&gt; 的形式）&lt;/li&gt;
&lt;/ul&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>Python基础：协程</title>
      <link>https://blog.kdyzm.cn/post/299</link>
      <guid>https://blog.kdyzm.cn/post/299</guid>
      <pubDate>Wed, 09 Apr 2025 17:36:28 +0800</pubDate>
      <description>&lt;h2 id=&quot;一生成器&quot;&gt;一、生成器&lt;/h2&gt;
&lt;p&gt;在Python中，生成器（Generator）是一种用于创建迭代器的简洁且高效的工具，能够按需生成值而非一次性加载所有数据到内存。举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;g = (x * x for x in range(10))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码定义了一个生成器g，它和列表生成式的用法非常相似，列表生成式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;L = [x * x for x in range(10)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不同的是，生成器只是保存了一个“算法”，它并不会展开计算最后得到一个列表，生成器中的每一个值都是根据前一个值“推导”出来的，只有用到的时候才会计算。‌&lt;/p&gt;
&lt;h3 id=&quot;1创建生成器&quot;&gt;1、创建生成器&lt;/h3&gt;
&lt;p&gt;第一种方式是上面提到的创建方式：&lt;code&gt;g = (x * x for x in range(10))&lt;/code&gt;，这种方式是&lt;strong&gt;生成器表达式&lt;/strong&gt;，能够快速创建生成器表达式；&lt;/p&gt;
&lt;p&gt;第二种方式是通过创建一个普通函数，然后配合&lt;code&gt;yield&lt;/code&gt;关键字创建，下面的代码创建了一个斐波那契数列生成器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def fibonacci(n):
    index, a, b = 0, 0, 1
    while index &amp;lt; n:
        yield a
        a, b = b, a + b
        index = index + 1
    return &apos;done&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2生成器迭代&quot;&gt;2、生成器迭代&lt;/h3&gt;
&lt;h4 id=&quot;第一种方法next函数&quot;&gt;&lt;strong&gt;第一种方法：&lt;/strong&gt;&lt;code&gt;next&lt;/code&gt;函数。&lt;/h4&gt;
&lt;p&gt;比如对于&lt;code&gt;g = (x * x for x in range(10))&lt;/code&gt; 生成器，使用next函数遍历代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;g = (x * x for x in range(10))

if __name__ == &apos;__main__&apos;:
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g))
    print(next(g)) #第11次调用会报错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第11次调用开始报错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;0
1
4
9
16
25
36
49
64
81
Traceback (most recent call last):
  File &amp;quot;D:\gitee_my\python-basic-study\02.aio\00.generator_demo.py&amp;quot;, line 24, in &amp;lt;module&amp;gt;
    print(next(g))
StopIteration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用next函数迭代，每次调用&lt;code&gt;next(g)&lt;/code&gt;，就计算出&lt;code&gt;g&lt;/code&gt;的下一个元素的值，直到计算到最后一个元素，没有更多的元素时，抛出&lt;code&gt;StopIteration&lt;/code&gt;的错误。这种迭代方式着实过于变态，正确的方法是使用&lt;code&gt;for&lt;/code&gt;循环。&lt;/p&gt;
&lt;h4 id=&quot;第二种方式-for循环&quot;&gt;&lt;strong&gt;第二种方式：&lt;/strong&gt; for循环&lt;/h4&gt;
&lt;p&gt;以斐波那契数列生成器为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def fibonacci(n):
    index, a, b = 0, 0, 1
    while index &amp;lt; n:
        yield a
        a, b = b, a + b
        index = index + 1
    return &apos;done&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遍历该生成器可使用for循环：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;g1 = fibonacci(10)
    for i in g1:
        print(i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用def+yield的方式定义的生成器，每次执行到 &lt;code&gt;yield&lt;/code&gt; 时暂停，返回当前值；下次调用时从暂停的位置继续执行。当函数执行完毕或遇到 &lt;code&gt;return&lt;/code&gt; 语句时，抛出 &lt;code&gt;StopIteration&lt;/code&gt; 异常，结束迭代；但是for循环方式会消化掉该异常，同时也无法获取return返回值，如果想获取return返回值，只能通过next函数的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def next_iter():
    g1 = fibonacci(6)
    print(next(g1))
    print(next(g1))
    print(next(g1))
    print(next(g1))
    print(next(g1))
    print(next(g1))
    try:
        print(next(g1))  # 第7次调用会报错
    except StopIteration as e:
        print(&amp;quot;generator return value:&amp;quot;, e.value)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;0
1
1
2
3
5
generator return value: done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3生成器接收值&quot;&gt;3、生成器接收值&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;yield&lt;/code&gt;关键字不仅能返回值，还能接收值。先定义一个生成器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def value_adjuster():
    current = 0
    while True:
        received = yield current  # 暂停并等待接收值
        print(f&amp;quot;接收到的值：{received}&amp;quot;)
        if received is not None:
            current = received  # 更新内部状态
        else:
            current += 1  # 默认自增
            
gen = value_adjuster() #创建生成器实例
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该生成器每次接收一个值，如果值不为空，就将其赋值给内部维护的current；否则current值自增。下次循环时将该值返回给调用方。&lt;/p&gt;
&lt;p&gt;生成器在调用send方法前需要先&lt;strong&gt;初始化&lt;/strong&gt;，初始化的方法有两种：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法一&lt;/strong&gt;：调用next函数初始化，&lt;code&gt;next(gen)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方法二&lt;/strong&gt;：发送空值，&lt;code&gt;gen.send(None)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;测试运行如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(next(gen))  # 初始化生成器，输出 0
print(gen.send(10))  # 发送 10，生成器返回 10
print(next(gen))  # 继续执行，输出 11
print(gen.send(5))  # 发送 5，生成器返回 5
print(next(gen))  # 继续执行，输出 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;0
接收到的值：10
10
接收到的值：None
11
接收到的值：5
5
接收到的值：None
6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，send方法会向生成器发送一个值，之后接收生成器返回的值；如果没有向生成器发送值，那么yield会接收到None。所以，生成器可以连续的调用send方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(gen.send(10))  # 发送 10，生成器返回 10
print(gen.send(5))  # 发送 5，生成器返回 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;二协程&quot;&gt;二、协程&lt;/h2&gt;
&lt;p&gt;协程（Coroutine）是 Python 中实现‌&lt;strong&gt;异步编程&lt;/strong&gt;‌的核心工具，用于编写高性能的 I/O 密集型代码。其核心思想是通过协作式多任务（非抢占式），在单线程内实现并发执行。&lt;/p&gt;
&lt;p&gt;协程和线程有什么区别呢？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;线程/进程&lt;/strong&gt;‌：由操作系统调度，存在上下文切换开销，适合 CPU 密集型任务。&lt;/p&gt;
&lt;p&gt;‌&lt;strong&gt;协程&lt;/strong&gt;‌：由程序员控制切换点（如 &lt;code&gt;await&lt;/code&gt;），无上下文切换开销，适合 I/O 密集型任务（如网络请求、文件读写）。&lt;/p&gt;
&lt;h3 id=&quot;1生成器和协程&quot;&gt;1、生成器和协程&lt;/h3&gt;
&lt;p&gt;Python 3.4 前，Python对协程的支持是通过生成器实现的。可以说协程是生成器的一种高级应用，利用 &lt;code&gt;yield&lt;/code&gt; 的暂停和参数传递特性实现控制流同步。&lt;/p&gt;
&lt;p&gt;实际上上一章节的“生成器接收值”案例正是协程的一个简答案例。接下来以生产者消费者为例说明生成器协程的使用。&lt;/p&gt;
&lt;h4 id=&quot;单生产者消费者&quot;&gt;单生产者消费者&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def consumer():
    r = &apos;&apos;
    while True:
        n = yield r
        if not n:
            return
        print(&apos;[CONSUMER] Consuming %s...&apos; % n)
        r = &apos;200 OK&apos;


def produce(c):
    c.send(None)
    n = 0
    while n &amp;lt; 5:
        n = n + 1
        print(&apos;[PRODUCER] Producing %s...&apos; % n)
        r = c.send(n)
        print(&apos;[PRODUCER] Consumer return: %s&apos; % r)
    c.close()


c = consumer()
produce(c)

if __name__ == &apos;__main__&apos;:
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个案例比较简单，生产者生产出来发送给消费者，消费者消费完成返回生产者“200 OK”，生产者接收到消息后继续生产；由于只是一个生产者和一个消费者，所以整体过程比较清晰。唯一美中不足的是整个过程串行化，无法看到协程在并行处理中的特点。&lt;/p&gt;
&lt;p&gt;接下来看看协程在多生产者多消费者模型中的表现。&lt;/p&gt;
&lt;h4 id=&quot;多生产者多消费者&quot;&gt;多生产者多消费者&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random
import time
from collections import deque


def producer(name, queue, max_items):
    &amp;quot;&amp;quot;&amp;quot;生产者协程，生成数据并发送给队列&amp;quot;&amp;quot;&amp;quot;
    for i in range(max_items):
        item = f&amp;quot;{name}-{i}&amp;quot;
        print(f&amp;quot;生产者 {name} 生产了 {item}&amp;quot;)
        time.sleep(random.random() * 0.5)  # 模拟生产时间
        queue.append(item)
        yield  # 暂停，让出控制权


def consumer(name, queue):
    &amp;quot;&amp;quot;&amp;quot;消费者协程，从队列中消费数据&amp;quot;&amp;quot;&amp;quot;
    while True:
        while queue:
            item = queue.popleft()
            print(f&amp;quot;消费者 {name} 消费了 {item}&amp;quot;)
            time.sleep(random.random() * 2)  # 模拟消费时间
            yield  # 处理完一个项目后暂停
        yield  # 如果队列为空，也暂停


def scheduler(producers, consumers, max_items):
    &amp;quot;&amp;quot;&amp;quot;调度器，协调生产者和消费者的执行&amp;quot;&amp;quot;&amp;quot;
    producer_done = [False] * len(producers)
    remaining_items = max_items * len(producers)

    # 初始化所有协程
    for p in producers:
        next(p)
    for c in consumers:
        next(c)

    # 主调度循环
    while remaining_items &amp;gt; 0:
        # 随机选择一个生产者
        active_producer_idx = random.randint(0, len(producers) - 1)
        if not producer_done[active_producer_idx]:
            try:
                producers[active_producer_idx].send(None)
            except StopIteration:
                producer_done[active_producer_idx] = True

        # 随机选择一个消费者
        active_consumer_idx = random.randint(0, len(consumers) - 1)
        consumers[active_consumer_idx].send(None)

        # 更新剩余项目计数
        remaining_items = sum(not done for done in producer_done) * max_items


if __name__ == &amp;quot;__main__&amp;quot;:
    # 共享队列
    queue = deque()

    # 配置参数
    num_producers = 3
    num_consumers = 2
    items_per_producer = 5

    # 创建生产者和消费者协程
    producers = [
        producer(f&amp;quot;[PRODUCER]-{i}&amp;quot;, queue, items_per_producer)
        for i in range(num_producers)
    ]

    consumers = [
        consumer(f&amp;quot;[CONSUMER]-{i}&amp;quot;, queue)
        for i in range(num_consumers)
    ]

    # 启动调度器
    scheduler(producers, consumers, items_per_producer)

    print(&amp;quot;所有生产消费任务完成!&amp;quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码实际上是&lt;strong&gt;串行运行&lt;/strong&gt;的，因为Python的生成器协程（非&lt;code&gt;asyncio&lt;/code&gt;）在单个线程中通过显式调度交替执行，而不是真正的并发。&lt;/p&gt;
&lt;p&gt;调度器随机选择要执行的生产者或消费者，当队列为空时，消费者会暂停，当生产者完成所有项目后会停止。&lt;/p&gt;
&lt;h3 id=&quot;2asnycawait-协程&quot;&gt;2、asnyc/await 协程&lt;/h3&gt;
&lt;p&gt;生成器协程不能实现真正的并发，而且运行效率低，&lt;strong&gt;Python 3.5&lt;/strong&gt;（2015年9月）首次支持&lt;code&gt;async/await&lt;/code&gt;语法，但此时协程仍需通过&lt;code&gt;asyncio.coroutine&lt;/code&gt;装饰器声明；&lt;strong&gt;Python 3.7+&lt;/strong&gt;（2018年6月）进一步优化，协程可通过&lt;code&gt;async def&lt;/code&gt;直接声明（无需装饰器），并改进了事件循环（&lt;code&gt;asyncio&lt;/code&gt;库）和性能。&lt;/p&gt;
&lt;p&gt;当前我的python版本是3.11，所以无须担心可以直接使用最新版本的协程语法。&lt;/p&gt;
&lt;h4 id=&quot;基本语法&quot;&gt;基本语法&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;async def&lt;/code&gt; 定义协程函数，内部通过 &lt;code&gt;await&lt;/code&gt; 挂起执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def fetch_data():
    print(&amp;quot;开始获取数据&amp;quot;)
    await asyncio.sleep(1)  # 模拟 I/O 操作
    print(&amp;quot;数据获取完成&amp;quot;)
    return &amp;quot;data&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;运行协程&quot;&gt;运行协程&lt;/h4&gt;
&lt;p&gt;协程必须通过‌&lt;strong&gt;事件循环&lt;/strong&gt;‌运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio


async def fetch_data():
    print(&amp;quot;开始获取数据&amp;quot;)
    await asyncio.sleep(1)  # 模拟 I/O 操作
    print(&amp;quot;数据获取完成&amp;quot;)
    return &amp;quot;data&amp;quot;


async def run():
    result = await fetch_data()
    print(&amp;quot;结果：&amp;quot;, result)


if __name__ == &apos;__main__&apos;:
    asyncio.run(run())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;开始获取数据
（等待一秒）
数据获取完成
结果： data
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;多生产者多消费者-1&quot;&gt;多生产者多消费者&lt;/h4&gt;
&lt;p&gt;现在使用async/await版本的协程重新实现多生产者多消费者&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio
from asyncio import Queue


async def producer(name, queue: Queue, max_items):
    &amp;quot;&amp;quot;&amp;quot;异步生产者&amp;quot;&amp;quot;&amp;quot;
    for i in range(max_items):
        item = f&amp;quot;{name}-{i}&amp;quot;
        print(f&amp;quot;生产者 {name} 生产了 {item}&amp;quot;)
        await queue.put(item)
        await asyncio.sleep(1)  # 模拟异步生产


async def consumer(name, queue: Queue):
    &amp;quot;&amp;quot;&amp;quot;异步消费者&amp;quot;&amp;quot;&amp;quot;
    while True:
        item = await queue.get()
        print(f&amp;quot;消费者 {name} 消费了 {item}&amp;quot;)
        queue.task_done()
        await asyncio.sleep(1)  # 模拟异步消费


async def run():
    queue: Queue = asyncio.Queue(maxsize=3)  # 限制队列大小实现背压
    producers = [
        asyncio.create_task(producer(f&amp;quot;P-{i}&amp;quot;, queue, 5))
        for i in range(3)
    ]
    consumers = [
        asyncio.create_task(consumer(f&amp;quot;C-{i}&amp;quot;, queue))
        for i in range(2)
    ]

    await asyncio.gather(*producers)  # 等待所有生产者完成
    await queue.join()  # 等待队列清空
    for c in consumers:  # 取消消费者任务
        c.cancel()


if __name__ == &apos;__main__&apos;:
    asyncio.run(run())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这段代码比生成器版本的写成要精简的多，而且代码也更清晰。代码中使用了asyncio的很多方法，这之后再聊它们的作用。&lt;/p&gt;
&lt;p&gt;上述代码可以尝试调整队列的大小以及生产者、消费者的数量以观察供大于求、供小于求等各种情况下的表现。&lt;/p&gt;
&lt;h2 id=&quot;三asyncio&quot;&gt;三、asyncio&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;asyncio&lt;/code&gt; 是 Python 用于编写并发代码的标准库，使用 async/await 语法。它特别适合 I/O 密集型和高并发的网络应用。&lt;/p&gt;
&lt;h3 id=&quot;1基本用法&quot;&gt;1、基本用法&lt;/h3&gt;
&lt;p&gt;运行协程（同步方法中运行协程）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def main():
    print(&apos;Hello&apos;)
    await asyncio.sleep(1)
    print(&apos;World&apos;)

# Python 3.7+
asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并发运行任务：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def task(name, seconds):
    print(f&amp;quot;{name} starting&amp;quot;)
    await asyncio.sleep(seconds)
    print(f&amp;quot;{name} finished after {seconds}s&amp;quot;)

async def main():
    # 方式1: gather
    await asyncio.gather(
        task(&amp;quot;A&amp;quot;, 2),
        task(&amp;quot;B&amp;quot;, 1),
        task(&amp;quot;C&amp;quot;, 3)
    )
    
    # 方式2: create_task + await
    t1 = asyncio.create_task(task(&amp;quot;X&amp;quot;, 2))
    t2 = asyncio.create_task(task(&amp;quot;Y&amp;quot;, 1))
    await t1
    await t2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要特别注意的是：await关键字调用必须在async块中。&lt;/p&gt;
&lt;h3 id=&quot;2异步生成器&quot;&gt;2、异步生成器&lt;/h3&gt;
&lt;p&gt;之前说过可以使用生成器实现协程功能，实际上使用async的协程也可以搭配生成器一起实现异步生成器的功能：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def async_gen(n):
    for i in range(n):
        await asyncio.sleep(0.5)
        yield i

async def main():
    async for item in async_gen(3):
        print(item)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3限制并发数&quot;&gt;3、限制并发数&lt;/h3&gt;
&lt;p&gt;使用semaphore实现限制并发数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def worker(semaphore, name):
    async with semaphore:
        print(f&amp;quot;{name} started&amp;quot;)
        await asyncio.sleep(2)
        print(f&amp;quot;{name} finished&amp;quot;)

async def main():
    semaphore = asyncio.Semaphore(3)  # 最多3个并发
    tasks = [
        asyncio.create_task(worker(semaphore, f&amp;quot;Worker-{i}&amp;quot;))
        for i in range(10)
    ]
    await asyncio.gather(*tasks)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;semaphore在Java中是一种共享锁，通过信号量机制实现并发数量控制，在python中也是类似的功能，详情可以查看：&lt;a href=&quot;https://blog.kdyzm.cn/post/280&quot;&gt;详解AQS七：深入理解信号量机制Semaphore&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;4同步阻塞队列queue&quot;&gt;4、同步阻塞队列：Queue&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;queue: Queue = asyncio.Queue(maxsize=3)&lt;/code&gt;可以创建一个长度为3的同步阻塞队列，它和java中的&lt;code&gt;BlokingQueue&lt;/code&gt;有异曲同工之妙。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;put方法&lt;/strong&gt;：将一个元素放入队列，如果队列已满，就阻塞等待队列空闲。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;get方法&lt;/strong&gt;：从队列中获取并移除一个元素，如果队列为空，就一直阻塞等待直到队列中有元素为止。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;task_done&lt;/strong&gt;方法：高速队列之前get方法取出的任务已经执行完成&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;join&lt;/strong&gt;方法：等待队列中的任务全部执行完毕，该方法依赖于task_done方法的执行。&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>python</category>
      <category>多线程编程</category>
      <category>协程</category>
    </item>
    <item>
      <title>大模型开发之langchain0.3（四）：方法调用进阶</title>
      <link>https://blog.kdyzm.cn/post/298</link>
      <guid>https://blog.kdyzm.cn/post/298</guid>
      <pubDate>Tue, 08 Apr 2025 14:53:15 +0800</pubDate>
      <description>&lt;p&gt;本篇文章将讲解langchain内置的第三方集成工具以及自定义工具的三种方式、自定义Input Schema的三种方式。&lt;/p&gt;
&lt;h2 id=&quot;一第三方集成工具&quot;&gt;一、第三方集成工具&lt;/h2&gt;
&lt;p&gt;在上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/296&quot;&gt;大模型开发之langchain0.3（三）：方法调用&lt;/a&gt;》中，已经简单介绍过如何自定义工具以及结合支持function calling的大模型使用，本小节将介绍langchain中已经集成各种的第三方工具，这些工具开箱即用，在很多场景下使用起来很方便，不需要我们重复造轮子了。&lt;/p&gt;
&lt;p&gt;全部的第三方集成工具列表：&lt;a href=&quot;https://python.langchain.com/docs/integrations/tools/#all-tools&quot;&gt;https://python.langchain.com/docs/integrations/tools/#all-tools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面介绍几个常见的经典的工具使用案例。&lt;/p&gt;
&lt;h3 id=&quot;1tavily-search让大模型支持联网功能&quot;&gt;1、Tavily Search：让大模型支持联网功能&lt;/h3&gt;
&lt;p&gt;大模型都是默认离线使用的，这导致它们不能获取最新网络上的数据，Tavily Search 则解决了这一痛点。&lt;/p&gt;
&lt;p&gt;Tavily Search官网地址：&lt;a href=&quot;https://tavily.com/&quot;&gt;https://tavily.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Tavily Search控制台地址：&lt;a href=&quot;https://app.tavily.com/home&quot;&gt;https://app.tavily.com/home&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;先注册，注册成功之后进入控制台：控制台上显示Tavily Search每个月有1000次的免费额度，这个额度对于学习和实验用足够用了。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/03/fda2ec76-c7a8-4548-8b5b-d7f93bf475c6.png&quot; alt=&quot;image-20250403093041198&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;复制下来API Key，之后用的到，接下来看看如何使用Tavily Search工具。&lt;/p&gt;
&lt;p&gt;Tavily Search langchain文档：&lt;a href=&quot;https://python.langchain.com/docs/integrations/tools/tavily_search/&quot;&gt;https://python.langchain.com/docs/integrations/tools/tavily_search/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：安装依赖&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install langchain-tavily
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二步：设置环境变量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将之前复制下来的Key设置到环境变量：&lt;strong&gt;TAVILY_API_KEY&lt;/strong&gt; ，注意，设置环境变量之后需要重启PyCharm，这里的重启指的的是关闭PyCharm，然后再打开，而非File-&amp;gt;InvalidCaches / Restart 功能，使用restart功能环境变量不会更新。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：工具定义&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_tavily import TavilySearch

tool = TavilySearch(
    max_results=5,
    topic=&amp;quot;general&amp;quot;,
    # include_answer=False,
    # include_raw_content=False,
    # include_images=False,
    # include_image_descriptions=False,
    # search_depth=&amp;quot;basic&amp;quot;,
    # time_range=&amp;quot;day&amp;quot;,
    # include_domains=None,
    # exclude_domains=None
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来按照上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/296&quot;&gt;大模型开发之langchain0.3（三）：方法调用&lt;/a&gt;》中最后的代码，将该工具加入工具列表中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Tool 创建
tools = [multiply, tavily_search_tool]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行代码，尝试问大模型：今天北京的天气如何？观察输出结果。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/03/fd977e09-e43a-4578-9813-3db5c532275b.gif&quot; alt=&quot;动画26_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;注意，如果运行报错，修改&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;trimmer = trim_messages(
    max_tokens=300,
    strategy=&amp;quot;last&amp;quot;,
    token_counter=model,
    include_system=True,
    allow_partial=False,
    start_on=&amp;quot;human&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将max_tokens值修改为3000再重试。&lt;/p&gt;
&lt;h3 id=&quot;2wolfram-alpha让大模型拥有数学计算能力&quot;&gt;2、Wolfram Alpha：让大模型拥有数学计算能力&lt;/h3&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/07/31b580ce-6ebe-4fc1-aa78-8de3561d070e.png&quot; alt=&quot;image-20250407131554265&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;大模型本身就有数学计算的能力，但是它经常会“算错”，Wolfram Alpha 则赋能大模型让其具有更精准的数学计算的能力。&lt;/p&gt;
&lt;p&gt;Wolfram Alpha 官网：&lt;a href=&quot;https://www.wolframalpha.com/&quot;&gt;https://www.wolframalpha.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Langchain Wolframe Alpha文档：&lt;a href=&quot;https://python.langchain.com/docs/integrations/tools/wolfram_alpha/&quot;&gt;https://python.langchain.com/docs/integrations/tools/wolfram_alpha/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;注册成功之后在“我的应用程序”中创建AppID，记住AppID，后续用得到。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：环境变量设置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将之前复制的AppID设置到环境变量：&lt;strong&gt;WOLFRAM_ALPHA_APPID&lt;/strong&gt;，注意，设置成功后需要关闭PyCharm然后重新打开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：安装依赖包&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install langchain-community==0.3.21 wolframalpha
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第三步：定义工具方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;wolframaAlpha = WolframAlphaQueryRun(api_wrapper=WolframAlphaAPIWrapper())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来按照上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/296&quot;&gt;大模型开发之langchain0.3（三）：方法调用&lt;/a&gt;》中最后的代码，将该工具加入工具列表中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tools = [tavily_search_tool, math_tool]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行代码：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/07/81247ad8-c1e4-4df5-a2d0-c08f36c26b23.gif&quot; alt=&quot;动画27_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;查看后台工具类打印出来的输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;input math req=3x + 4y = 71, x + y = 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，大模型将问题转化成了二元一次方程组，Wolfram Alpha只需要解方程组就好了。&lt;/p&gt;
&lt;h3 id=&quot;3其它工具&quot;&gt;3、其它工具&lt;/h3&gt;
&lt;p&gt;上述两个例子只做抛砖引玉之用，完整的工具列表可查看文档：&lt;a href=&quot;https://python.langchain.com/docs/integrations/tools/#all-tools&quot;&gt;https://python.langchain.com/docs/integrations/tools/#all-tools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;大多数的第三方工具使用起来都很简单，只需要安装依赖，按照官方教程即可使用。&lt;/p&gt;
&lt;p&gt;比如，如何引入维基百科的搜索功能，和Wolfram Alpha相似，只需要一行代码即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;二自定义工具的三种方式&quot;&gt;二、自定义工具的三种方式&lt;/h2&gt;
&lt;p&gt;官方文档：&lt;a href=&quot;https://python.langchain.com/docs/how_to/custom_tools/&quot;&gt;How to create tools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;创建自定义工具有三种方式：普通方法构建、LangChain Runnable方法构建、继承BaseTool类构建。无论是哪一种构建方式，都需要遵循构建自定义工具的规范：工具方法要由name、description、args_schema、return_direct 四部分组成。&lt;/p&gt;
&lt;h3 id=&quot;1自定义工具的组成&quot;&gt;1、自定义工具的组成&lt;/h3&gt;
&lt;p&gt;一个自定义tool由以下几部分组成：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性&lt;/th&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;str&lt;/td&gt;
&lt;td&gt;当提供给LLM或者agent的时候，需要在工具列表中保持唯一；它最好有比较好的语义含义，这样有利于大模型作出合适的选择&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;description&lt;/td&gt;
&lt;td&gt;str&lt;/td&gt;
&lt;td&gt;这个工具是做什么用的。这个描述信息非常重要，大模型或者agent将会使用该信息作为是否调用该方法的依据。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;args_schema&lt;/td&gt;
&lt;td&gt;pydantic.BaseModel&lt;/td&gt;
&lt;td&gt;可选的参数，但是推荐使用；使用该参数可以额外设置一些属性，比如在few-shot模式下给出examples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;return_direct&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;该参数是agent相关的参数，当为True的时候，agnet执行完方法会立即停止执行并将方法执行的结果返回给用户。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;2方法1function方法构建tool&quot;&gt;2、方法1：function方法构建Tool&lt;/h3&gt;
&lt;p&gt;从普通的方法构建中构建Tool在前面的文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/296&quot;&gt;大模型开发之langchain0.3（三）：方法调用&lt;/a&gt;》已经演示过了，在普通方法上加上装饰器&lt;code&gt;@tool&lt;/code&gt;就可以将普通方法转化为Tool，比如一个简单的加法运算：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_core.tools import tool


@tool
def tool(a: int, b: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply two numbers.&amp;quot;&amp;quot;&amp;quot;
    return a * b


if __name__ == &apos;__main__&apos;:
    print(type(tool))
    print(f&amp;quot;Name: {tool.name}&amp;quot;)
    print(f&amp;quot;Description: {tool.description}&amp;quot;)
    print(f&amp;quot;args schema: {tool.args}&amp;quot;)
    print(f&amp;quot;returns directly?: {tool.return_direct}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;lt;class &apos;langchain_core.tools.structured.StructuredTool&apos;&amp;gt;
Name: tool
Description: Multiply two numbers.
args schema: {&apos;a&apos;: {&apos;title&apos;: &apos;A&apos;, &apos;type&apos;: &apos;integer&apos;}, &apos;b&apos;: {&apos;title&apos;: &apos;B&apos;, &apos;type&apos;: &apos;integer&apos;}}
returns directly?: False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，被@tool装饰器装饰之后的方法，类型从&lt;code&gt;function&lt;/code&gt;变成了&lt;code&gt;StructuredTool&lt;/code&gt;，因此，tool就有了name、description、args、return_direct等属性。&lt;/p&gt;
&lt;p&gt;实际上，如果不想使用@tool装饰器，可以使用StructuredTool直接利用现有方法构建一个工具：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_core.tools import StructuredTool
import asyncio

def multiply(a: int, b: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply two numbers.&amp;quot;&amp;quot;&amp;quot;
    return a * b


async def amultiply(a: int, b: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply two numbers.&amp;quot;&amp;quot;&amp;quot;
    return a * b


calculator = StructuredTool.from_function(func=multiply, coroutine=amultiply)

print(calculator.invoke({&amp;quot;a&amp;quot;: 2, &amp;quot;b&amp;quot;: 3}))
print(asyncio.run(calculator.ainvoke({&amp;quot;a&amp;quot;: 2, &amp;quot;b&amp;quot;: 5})))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，对于原有的代码都不需要改动，就可以创建出一个工具，但是上述代码缺少了参数说明，LLM调用的时候是否会报错？可以通过额外新增一个参数说明的schema来描述这些信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class CalculatorInput(BaseModel):
    a: int = Field(description=&amp;quot;first number&amp;quot;)
    b: int = Field(description=&amp;quot;second number&amp;quot;)


def multiply(a: int, b: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply two numbers.&amp;quot;&amp;quot;&amp;quot;
    return a * b


calculator = StructuredTool.from_function(
    func=multiply,
    name=&amp;quot;Calculator&amp;quot;,
    description=&amp;quot;multiply numbers&amp;quot;,
    args_schema=CalculatorInput,
    return_direct=True,
    # coroutine= ... &amp;lt;- you can specify an async method if desired as well
)

print(calculator.invoke({&amp;quot;a&amp;quot;: 2, &amp;quot;b&amp;quot;: 3}))
print(calculator.name)
print(calculator.description)
print(calculator.args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过定义CalculatorInput类，对输入参数进行了说明，这样大模型就能理解输入参数是什么意思了从而使得方法调用逻辑不会出错。&lt;/p&gt;
&lt;h3 id=&quot;3方法2从runnables接口构建tool&quot;&gt;3、方法2：从Runnables接口构建Tool&lt;/h3&gt;
&lt;p&gt;Runnables详解：&lt;a href=&quot;https://python.langchain.com/docs/concepts/runnables/&quot;&gt;Runnable interface&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;官方文档：&lt;a href=&quot;https://python.langchain.com/docs/how_to/convert_runnable_to_tool/&quot;&gt;How to convert Runnables to Tools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Runnable接口是个比较大的话题，简单来说langchain的各种组件比如 &lt;a href=&quot;https://python.langchain.com/docs/concepts/chat_models/&quot;&gt;language models&lt;/a&gt;, &lt;a href=&quot;https://python.langchain.com/docs/concepts/output_parsers/&quot;&gt;output parsers&lt;/a&gt;, &lt;a href=&quot;https://python.langchain.com/docs/concepts/retrievers/&quot;&gt;retrievers&lt;/a&gt;, &lt;a href=&quot;https://langchain-ai.github.io/langgraph/concepts/low_level/#compiling-your-graph&quot;&gt;compiled LangGraph graphs&lt;/a&gt;  等等都实现了该接口，实现了该接口的组件，可以通过as_tool方法构建Tool：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_core.language_models import GenericFakeChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages(
    [(&amp;quot;human&amp;quot;, &amp;quot;Hello. Please respond in the style of {answer_style}.&amp;quot;)]
)

# Placeholder LLM
llm = GenericFakeChatModel(messages=iter([&amp;quot;hello matey&amp;quot;]))

chain = prompt | llm | StrOutputParser()

as_tool = chain.as_tool(
    name=&amp;quot;Style responder&amp;quot;,
    description=&amp;quot;Description of when to use tool.&amp;quot;
)

if __name__ == &apos;__main__&apos;:
    print(as_tool.args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;{&apos;answer_style&apos;: {&apos;title&apos;: &apos;Answer Style&apos;, &apos;type&apos;: &apos;string&apos;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4方法3继承basetool类构建tool&quot;&gt;4、方法3：继承BaseTool类构建Tool&lt;/h3&gt;
&lt;p&gt;通过继承BaseTool类构建Tool需要些更多的代码，但是这是一种能够最大程度掌控工具创建细节的方法。&lt;/p&gt;
&lt;p&gt;这种方式在上一节：《第三方集成工具》中无论是Tavily Search还是Wolfram Alpha，均是使用此种方式创建的工具。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import asyncio
from typing import Optional

from langchain_core.callbacks import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)
from langchain_core.tools import BaseTool
from langchain_core.tools.base import ArgsSchema
from pydantic import BaseModel, Field


class CalculatorInput(BaseModel):
    a: int = Field(description=&amp;quot;first number&amp;quot;)
    b: int = Field(description=&amp;quot;second number&amp;quot;)


# Note: It&apos;s important that every field has type hints. BaseTool is a
# Pydantic class and not having type hints can lead to unexpected behavior.
class CustomCalculatorTool(BaseTool):
    name: str = &amp;quot;Calculator&amp;quot;
    description: str = &amp;quot;useful for when you need to answer questions about math&amp;quot;
    args_schema: Optional[ArgsSchema] = CalculatorInput
    return_direct: bool = True

    def _run(
            self, a: int, b: int, run_manager: Optional[CallbackManagerForToolRun] = None
    ) -&amp;gt; str:
        &amp;quot;&amp;quot;&amp;quot;Use the tool.&amp;quot;&amp;quot;&amp;quot;
        return a * b

    async def _arun(
            self,
            a: int,
            b: int,
            run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
    ) -&amp;gt; str:
        &amp;quot;&amp;quot;&amp;quot;Use the tool asynchronously.&amp;quot;&amp;quot;&amp;quot;
        # If the calculation is cheap, you can just delegate to the sync implementation
        # as shown below.
        # If the sync calculation is expensive, you should delete the entire _arun method.
        # LangChain will automatically provide a better implementation that will
        # kick off the task in a thread to make sure it doesn&apos;t block other async code.
        return self._run(a, b, run_manager=run_manager.get_sync())


if __name__ == &apos;__main__&apos;:
    multiply = CustomCalculatorTool()
    print(multiply.name)
    print(multiply.description)
    print(multiply.args)
    print(multiply.return_direct)

    print(multiply.invoke({&amp;quot;a&amp;quot;: 2, &amp;quot;b&amp;quot;: 3}))
    print(asyncio.run(multiply.ainvoke({&amp;quot;a&amp;quot;: 2, &amp;quot;b&amp;quot;: 3})))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Calculator
useful for when you need to answer questions about math
{&apos;a&apos;: {&apos;description&apos;: &apos;first number&apos;, &apos;title&apos;: &apos;A&apos;, &apos;type&apos;: &apos;integer&apos;}, &apos;b&apos;: {&apos;description&apos;: &apos;second number&apos;, &apos;title&apos;: &apos;B&apos;, &apos;type&apos;: &apos;integer&apos;}}
True
6
6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;三自定义input-schema的三种方式&quot;&gt;三、自定义Input Schema的三种方式&lt;/h2&gt;
&lt;p&gt;官方文档：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://python.langchain.com/docs/how_to/tool_calling/#defining-tool-schemas&quot;&gt;How to use chat models to call tools&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://python.langchain.com/docs/how_to/custom_tools/#tool-decorator&quot;&gt;How to create tools&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;1方法1python-functions&quot;&gt;1、方法1：Python functions&lt;/h3&gt;
&lt;p&gt;这种方式是通过文档注释的方式声明Input Schema的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json

from langchain_core.tools import tool


@tool(parse_docstring=True)
def tool(a: int, b: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Add two integers.

    Args:
        a: First integer
        b: Second integer
    &amp;quot;&amp;quot;&amp;quot;
    return a + b


if __name__ == &apos;__main__&apos;:
    print(f&amp;quot;Name: {tool.name}&amp;quot;)
    print(f&amp;quot;Description: {tool.description}&amp;quot;)
    print(f&amp;quot;args schema: {json.dumps(tool.args, indent=4)}&amp;quot;)
    print(f&amp;quot;returns directly?: {tool.return_direct}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Name: tool
Description: Add two integers.
args schema: {
    &amp;quot;a&amp;quot;: {
        &amp;quot;description&amp;quot;: &amp;quot;First integer&amp;quot;,
        &amp;quot;title&amp;quot;: &amp;quot;A&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
    },
    &amp;quot;b&amp;quot;: {
        &amp;quot;description&amp;quot;: &amp;quot;Second integer&amp;quot;,
        &amp;quot;title&amp;quot;: &amp;quot;B&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
    }
}
returns directly?: False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键是要使用@tool装饰器，而且要设置parse_docstring属性为true。这种方式的好处是代码简洁，坏处是不够灵活，比如不能明确指定directly的值，工具名字和方法名是绑定的。&lt;/p&gt;
&lt;h3 id=&quot;2方法2pydantic-class&quot;&gt;2、方法2：Pydantic class&lt;/h3&gt;
&lt;p&gt;这种方式通过继承BaseModel实现单独的Schema定义&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pydantic import BaseModel, Field
import json

from langchain_core.tools import tool


class AddSchema(BaseModel):
    &amp;quot;&amp;quot;&amp;quot;Add two integers.&amp;quot;&amp;quot;&amp;quot;

    a: int = Field(..., description=&amp;quot;First integer&amp;quot;)
    b: int = Field(..., description=&amp;quot;Second integer&amp;quot;)


@tool(&amp;quot;add tool&amp;quot;, args_schema=AddSchema, return_direct=True)
def tool(a: int, b: int) -&amp;gt; int:
    return a + b


if __name__ == &apos;__main__&apos;:
    print(f&amp;quot;Name: {tool.name}&amp;quot;)
    print(f&amp;quot;Description: {tool.description}&amp;quot;)
    print(f&amp;quot;args schema: {json.dumps(tool.args, indent=4)}&amp;quot;)
    print(f&amp;quot;returns directly?: {tool.return_direct}&amp;quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Name: add tool
Description: Add two integers.
args schema: {
    &amp;quot;a&amp;quot;: {
        &amp;quot;description&amp;quot;: &amp;quot;First integer&amp;quot;,
        &amp;quot;title&amp;quot;: &amp;quot;A&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
    },
    &amp;quot;b&amp;quot;: {
        &amp;quot;description&amp;quot;: &amp;quot;Second integer&amp;quot;,
        &amp;quot;title&amp;quot;: &amp;quot;B&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
    }
}
returns directly?: True

Process finished with exit code 0

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3方式3typeddict-class&quot;&gt;3、方式3：TypedDict class&lt;/h3&gt;
&lt;p&gt;我个人比较喜欢这种方式，通过@tool装饰器、文档注释和字段注解相配合共同构建Input Schema。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
from typing import List

from langchain_core.tools import tool
from typing_extensions import Annotated


@tool
def tool(
        a: Annotated[int, &amp;quot;scale factor&amp;quot;],
        b: Annotated[List[int], &amp;quot;list of ints over which to take maximum&amp;quot;],
) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply a by the maximum of b.&amp;quot;&amp;quot;&amp;quot;
    return a * max(b)


if __name__ == &apos;__main__&apos;:
    print(f&amp;quot;Name: {tool.name}&amp;quot;)
    print(f&amp;quot;Description: {tool.description}&amp;quot;)
    print(f&amp;quot;args schema: {json.dumps(tool.args, indent=4)}&amp;quot;)
    print(f&amp;quot;returns directly?: {tool.return_direct}&amp;quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Name: tool
Description: Multiply a by the maximum of b.
args schema: {
    &amp;quot;a&amp;quot;: {
        &amp;quot;description&amp;quot;: &amp;quot;scale factor&amp;quot;,
        &amp;quot;title&amp;quot;: &amp;quot;A&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
    },
    &amp;quot;b&amp;quot;: {
        &amp;quot;description&amp;quot;: &amp;quot;list of ints over which to take maximum&amp;quot;,
        &amp;quot;items&amp;quot;: {
            &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
        },
        &amp;quot;title&amp;quot;: &amp;quot;B&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;array&amp;quot;
    }
}
returns directly?: False
&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;
&lt;br/&gt;
END.
</description>
      <category>langchain</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Python脚本：解决博客园Markdown编辑器提取图片失败问题</title>
      <link>https://blog.kdyzm.cn/post/297</link>
      <guid>https://blog.kdyzm.cn/post/297</guid>
      <pubDate>Wed, 02 Apr 2025 17:56:16 +0800</pubDate>
      <description>&lt;p&gt;博客园的博友们不知道有没有用过Markdown编辑器下方的“提取图片”功能，该功能就是解析Markdown编辑器中的所有图片链接，将其重新上传到博客园，生成博客园的图片链接后替换掉Markdown中的图片链接。功能是不错的功能，但是这个功能非常慢，而且最近总是会失败。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/02/c87198b3-72f3-4f72-a95f-5b5916d6b6ac.png&quot; alt=&quot;image-20250402171729406&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;我一开始的解决方案是一个图片一个图片的在编辑器中重新上传（在编辑器中直接上传速度很快），但是这种方法效率太低了。。。为了解决这个问题，我写了一个Python脚本，脚本模拟在编辑器直接上传图片，上传成功后将图片链接替换成接口返回的链接。&lt;/p&gt;
&lt;h2 id=&quot;一python脚本&quot;&gt;一、Python脚本&lt;/h2&gt;
&lt;p&gt;运行脚本前，需要先安装第三方模块：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整Python脚本如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;quot;&amp;quot;&amp;quot;
解析markdown文件中的图片并将图片上传到博客园图床，之后替换掉markdown文件中的图片链接

注意：使用该脚本需要替换掉代码中的cookie信息
&amp;quot;&amp;quot;&amp;quot;

import tkinter as tk
from pathlib import Path
from tkinter import filedialog
import os
from typing import List, Dict
import re
import json
import requests
import uuid
import mimetypes
import shutil

#需要替换cookie值
cookie = &amp;quot;&amp;quot;
user_agent = &amp;quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36&amp;quot;

default_work_dir = &amp;quot;C:\\Users\\kdyzm\\Documents&amp;quot;
default_charset = &amp;quot;UTF-8&amp;quot;


def choose_markdown_files() -&amp;gt; str:
    # 创建Tkinter根窗口并隐藏
    root = tk.Tk()
    root.withdraw()
    # 配置文件对话框参数
    file_path = filedialog.askopenfilename(
        title=&amp;quot;请选择markdown文件&amp;quot;,  # 对话框标题
        initialdir=os.path.expanduser(default_work_dir),  # 初始目录为用户主目录
        filetypes=[  # 文件类型过滤
            (&amp;quot;Markdown文件&amp;quot;, &amp;quot;*.md&amp;quot;)
        ],
        # multiple=True
    )
    # 销毁根窗口
    root.destroy()
    return file_path


def parse_content(content: str) -&amp;gt; List[str]:
    origin_urls: List[str] = re.findall(r&amp;quot;https?://.+\.(?:jpg|png|gif|jpeg)&amp;quot;, content, flags=re.IGNORECASE)
    filter_urls = [url for url in origin_urls if &amp;quot;cnblogs.com&amp;quot; not in url]
    return filter_urls


def download_file_to_temp_dir(image_url: str) -&amp;gt; Path:
    &amp;quot;&amp;quot;&amp;quot;
    下载图片到本地
    :param image_url: 远程url地址
    :return: 下载到本地的绝对路径
    &amp;quot;&amp;quot;&amp;quot;
    parent_dir = Path(&amp;quot;temp&amp;quot;)
    if not parent_dir.exists():
        parent_dir.mkdir()
    random_name = uuid.uuid4()
    suffix = image_url.split(&amp;quot;.&amp;quot;)[-1]
    local_image_path = parent_dir.joinpath(str(random_name).lower() + &amp;quot;.&amp;quot; + suffix)
    print(f&amp;quot;正在下载图片：{image_url}&amp;quot;)
    with local_image_path.open(&amp;quot;wb&amp;quot;) as file:
        response = requests.get(image_url)
        file.write(response.content)
    print(f&amp;quot;图片已下载到 {local_image_path.absolute()}&amp;quot;)
    return local_image_path


def do_upload_file(image_url: str) -&amp;gt; dict:
    headers = {
        &amp;quot;user-agent&amp;quot;: user_agent,
        &amp;quot;cookie&amp;quot;: cookie
    }
    local_file = download_file_to_temp_dir(image_url)
    with open(local_file.absolute(), &amp;quot;rb&amp;quot;) as f:
        mimetype, encoding = mimetypes.guess_type(local_file.name)
        files = {
            &amp;quot;imageFile&amp;quot;: (local_file.name, f, mimetype)
        }
        post_data = {
            &amp;quot;host&amp;quot;: &amp;quot;www.cnblogs.com&amp;quot;,
            &amp;quot;uploadType&amp;quot;: &amp;quot;Paste&amp;quot;
        }
        result = requests.post(
            &amp;quot;https://upload.cnblogs.com/imageuploader/CorsUpload&amp;quot;,
            data=post_data,
            files=files,
            headers=headers
        )
        content = result.content.decode(default_charset)
        return json.loads(content)


def upload_file(files: List[str]) -&amp;gt; Dict[str, str]:
    if not files:
        return dict()
    file_map = {file: do_upload_file(file)[&amp;quot;message&amp;quot;] for file in files}
    return file_map


def replace_all(path: Path, image_map: dict):
    &amp;quot;&amp;quot;&amp;quot;
    解析markdown文件并将所有旧文件链接替换为新文件链接
    :param path: markdown文件路径
    :param image_map: 文件map
    :return:
    &amp;quot;&amp;quot;&amp;quot;
    content = path.read_text(encoding=default_charset)
    for key, value in image_map.items():
        content = content.replace(key, value)
    path.write_text(content, encoding=default_charset)


def clean_temp_file():
    path = Path(&amp;quot;temp&amp;quot;)
    if path.is_file():
        path.unlink()  # 删除文件
    elif path.is_dir():
        # 删除非空目录需要递归删除
        shutil.rmtree(path)
    else:
        print(&amp;quot;路径不存在或无效&amp;quot;)


def open_file(path: Path):
    os.startfile(path.absolute())


def process():
    file_path = choose_markdown_files()
    # 处理用户选择结果
    if not file_path:
        print(&amp;quot;用户取消选择&amp;quot;)
        return
    path = Path(file_path)
    content = path.read_text(encoding=default_charset)
    origin_urls = parse_content(content)
    print(&amp;quot;解析markdown文件获取到如下图片列表:&amp;quot;)
    print(f&amp;quot;{json.dumps(origin_urls, indent=4)}&amp;quot;)
    if not origin_urls:
        print(&amp;quot;未解析到符合条件的图片链接，即将退出程序&amp;quot;)
        return
    image_map = upload_file(origin_urls)
    print(&amp;quot;获取到旧文件和新文件的字典：&amp;quot;)
    print(json.dumps(image_map, indent=4))
    replace_all(path, image_map)
    print(&amp;quot;替换文件完成&amp;quot;)
    print(&amp;quot;开始清理暂存文件&amp;quot;)
    clean_temp_file()
    print(&amp;quot;暂存文件清理完成&amp;quot;)
    print(&amp;quot;即将打开目标文件&amp;quot;)
    open_file(path)


if __name__ == &apos;__main__&apos;:
    input(&amp;quot;使用须知：使用此脚本必须修改脚本cookie字段，若已知晓，回车继续：&amp;quot;)
    process()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行这个脚本前，需要先修改代码中的cookie值：登录博客园后，从浏览器中复制。&lt;/p&gt;
&lt;h2 id=&quot;二运行界面&quot;&gt;二、运行界面&lt;/h2&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/02/369843c9-8ead-4cd8-a057-151ac0ef9b26.gif&quot; alt=&quot;动画25_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;脚本运行时会在脚本所在目录创建一个&lt;code&gt;temp&lt;/code&gt;文件夹，用于暂时存放下载的图片，脚本运行结束会自动删除该文件夹。&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>大模型开发之langchain0.3（三）：方法调用</title>
      <link>https://blog.kdyzm.cn/post/296</link>
      <guid>https://blog.kdyzm.cn/post/296</guid>
      <pubDate>Tue, 01 Apr 2025 17:58:48 +0800</pubDate>
      <description>&lt;p&gt;我们思考一个问题：大语言模型是否能帮我们做更多的事情，比如帮我们发送邮件。默认情况下让大模型帮我们发送邮件，大模型会这样回复我们：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/01/33648691-c320-48ae-8f64-72578b4f3b89.png&quot; alt=&quot;image-20250401133439600&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到，大模型无法发送邮件，它只会帮我们生成一个邮件模板，然后让我们自己手动发送出去。如何让大模型拥有发送邮件的能力呢？这里就引入来了一个概念：&lt;code&gt;function calling&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;一概念function-calling&quot;&gt;一、概念：Function calling&lt;/h2&gt;
&lt;p&gt;简单来说，Function calling让大语言模型拥有了调用外部接口的能力，使用这种能力，大模型能做一些比如实时获取天气信息、发送邮件等和现实世界交互的事情。&lt;/p&gt;
&lt;h3 id=&quot;1原理&quot;&gt;1、原理&lt;/h3&gt;
&lt;p&gt;在发送信息给大模型的时候，携带着“工具”列表，这些工具列表代表着大模型能使用的工具。当大模型遇到用户提出的问题时，会先思考是否应该调用工具解决问题，如果需要调用工具，和普通消息不同，这种情况下会返回“function_call”类型的消息，请求方根据返回结果调用对应的工具得到工具输出，然后将之前的信息加上工具输出的信息一起发送给大模型，让大模型整合起来综合判断给出结果。&lt;/p&gt;
&lt;p&gt;以获取天气信息为例，官网给出了获取天气的流程图&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/01/c84055e4-6894-4bfb-9722-43d31a9f7009.png&quot; alt=&quot;Function Calling Diagram Steps&quot; style=&quot;zoom: 15%;&quot; /&gt;
&lt;h3 id=&quot;2案例&quot;&gt;2、案例&lt;/h3&gt;
&lt;p&gt;OpenAI官网Function calling文档：&lt;a href=&quot;https://platform.openai.com/docs/guides/function-calling?api-mode=responses&amp;amp;example=get-weather&quot;&gt;https://platform.openai.com/docs/guides/function-calling?api-mode=responses&amp;amp;example=get-weather&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;文档中给了获取天气、发送邮件、搜索本地知识库这三个例子，以获取天气为例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from openai import OpenAI

client = OpenAI()

tools = [{
    &amp;quot;type&amp;quot;: &amp;quot;function&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;get_weather&amp;quot;,
    &amp;quot;description&amp;quot;: &amp;quot;Get current temperature for a given location.&amp;quot;,
    &amp;quot;parameters&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;object&amp;quot;,
        &amp;quot;properties&amp;quot;: {
            &amp;quot;location&amp;quot;: {
                &amp;quot;type&amp;quot;: &amp;quot;string&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;City and country e.g. Bogotá, Colombia&amp;quot;
            }
        },
        &amp;quot;required&amp;quot;: [
            &amp;quot;location&amp;quot;
        ],
        &amp;quot;additionalProperties&amp;quot;: False
    }
}]

response = client.responses.create(
    model=&amp;quot;gpt-4o&amp;quot;,
    input=[{&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;What is the weather like in Paris today?&amp;quot;}],
    tools=tools
)

print(response.output)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;[{
    &amp;quot;type&amp;quot;: &amp;quot;function_call&amp;quot;,
    &amp;quot;id&amp;quot;: &amp;quot;fc_12345xyz&amp;quot;,
    &amp;quot;call_id&amp;quot;: &amp;quot;call_12345xyz&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;get_weather&amp;quot;,
    &amp;quot;arguments&amp;quot;: &amp;quot;{\&amp;quot;location\&amp;quot;:\&amp;quot;Paris, France\&amp;quot;}&amp;quot;
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，使用OpenAI的官方API调用很繁琐，而且定义工具列表需要使用json格式的字符串，非常的不友好，lagnchain则解决了这些麻烦。&lt;/p&gt;
&lt;h2 id=&quot;二langchain中的tool-calling&quot;&gt;二、langchain中的Tool calling&lt;/h2&gt;
&lt;p&gt;langchain中的&lt;code&gt;Function calling&lt;/code&gt;换了个更直接的名字：&lt;code&gt;Tool calling&lt;/code&gt;，翻译过来叫做“工具调用”，实际上底层还是使用的Function calling。&lt;/p&gt;
&lt;p&gt;Tools概念：&lt;a href=&quot;https://python.langchain.com/docs/concepts/tools/&quot;&gt;https://python.langchain.com/docs/concepts/tools/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Tool calling概念：&lt;a href=&quot;https://python.langchain.com/docs/concepts/tool_calling/&quot;&gt;https://python.langchain.com/docs/concepts/tool_calling/&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;1工具定义&quot;&gt;1、工具定义&lt;/h3&gt;
&lt;p&gt;定义工具很简单，使用装饰器&lt;code&gt;@tool&lt;/code&gt;，比如定义两数相乘的工具如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -&amp;gt; int:
   &amp;quot;&amp;quot;&amp;quot;Multiply two numbers.&amp;quot;&amp;quot;&amp;quot;
   return a * b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，定义一个工具方法很简单，普通方法加上装饰器&lt;code&gt;@tool&lt;/code&gt;即可（关于复杂方法后续再讲）。&lt;/p&gt;
&lt;p&gt;工具定义完成，可以使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(
    json.dumps(
        multiply.args_schema.model_json_schema(),
        indent=4,
        ensure_ascii=False,
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印scheme信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;description&amp;quot;: &amp;quot;Multiply two numbers.&amp;quot;,
    &amp;quot;properties&amp;quot;: {
        &amp;quot;a&amp;quot;: {
            &amp;quot;title&amp;quot;: &amp;quot;A&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
        },
        &amp;quot;b&amp;quot;: {
            &amp;quot;title&amp;quot;: &amp;quot;B&amp;quot;,
            &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;
        }
    },
    &amp;quot;required&amp;quot;: [
        &amp;quot;a&amp;quot;,
        &amp;quot;b&amp;quot;
    ],
    &amp;quot;title&amp;quot;: &amp;quot;multiply&amp;quot;,
    &amp;quot;type&amp;quot;: &amp;quot;object&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2工具调用&quot;&gt;2、工具调用&lt;/h3&gt;
&lt;p&gt;上面我们已经定义好了两数相乘的工具：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -&amp;gt; int:
   &amp;quot;&amp;quot;&amp;quot;Multiply two numbers.&amp;quot;&amp;quot;&amp;quot;
   return a * b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来使用携带该工具访问大模型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Tool 创建
tools = [multiply]
# Tool 绑定
model_with_tools = model.bind_tools(tools)
# Tool 调用 
response = model_with_tools.invoke(&amp;quot;2乘以2等于多少？&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出大模型返回的function_tool信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(json.dumps(response.tool_calls, indent=4))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
    {
        &amp;quot;name&amp;quot;: &amp;quot;multiply&amp;quot;,
        &amp;quot;args&amp;quot;: {
            &amp;quot;a&amp;quot;: 2,
            &amp;quot;b&amp;quot;: 3
        },
        &amp;quot;id&amp;quot;: &amp;quot;chatcmpl-tool-83c83e9537ae4820bc3b1123fec3570b&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;tool_call&amp;quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它告诉我们要调用multiply方法，参数是a=2和b=3，如何调用呢？&lt;/p&gt;
&lt;h3 id=&quot;3工具执行&quot;&gt;3、工具执行&lt;/h3&gt;
&lt;p&gt;大模型已经告诉我们要执行的方法以及调用的参数了，接下来如何执行呢？&lt;/p&gt;
&lt;p&gt;第一步：转换tool列表为字典&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tool_dic = {tool.name: tool for tool in tools}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步：依次执行tool_call列表中的方法&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for tool_call in response.tool_calls:
    selected_tool = tool_dic[tool_call[&amp;quot;name&amp;quot;].lower()]
    tool_msg = selected_tool.invoke(tool_call)
    print(type(tool_msg))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以执行目标方法了。注意这里返回的tool_msg信息类型是ToolMessage。&lt;/p&gt;
&lt;p&gt;接下来需要将上下文信息带着最后输出的工具输出的信息一起打包给大模型，让大模型整合结果输出给出最终答案。&lt;/p&gt;
&lt;h3 id=&quot;4整合到大模型&quot;&gt;4、整合到大模型&lt;/h3&gt;
&lt;p&gt;调用完工具之后需要将结果告诉大模型，让大模型综合上下文得到后续答案。如何告诉大模型呢？在上一篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/294&quot;&gt;大模型开发之langchain0.3（二）：构建带有记忆功能的聊天机器人&lt;/a&gt;》中告诉大模型上下文，也即是历史记录的方法就是构造Message列表，上一步工具执行的结果返回类型是ToolMessage，我们将它加入列表即可；最后将message列表一起发送给大模型，让大模型给出答案。&lt;/p&gt;
&lt;p&gt;完整代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool


@tool
def multiply(a: int, b: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply a and b.&amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;multiply 方法被执行&amp;quot;)
    return a * b


model = init_chat_model(&amp;quot;gpt-3.5-turbo&amp;quot;)
# Tool 创建
tools = [multiply]
# Tool 绑定
model_with_tools = model.bind_tools(tools)
# Tool 调用
history = [HumanMessage(&amp;quot;2乘以3等于多少？&amp;quot;)]
ai_message = model_with_tools.invoke(history)
history.append(ai_message)
tool_dic = {tool.name: tool for tool in tools}
for tool_call in ai_message.tool_calls:
    selected_tool = tool_dic[tool_call[&amp;quot;name&amp;quot;].lower()]
    tool_msg = selected_tool.invoke(tool_call)
    history.append(tool_msg)

ai_message = model_with_tools.invoke(history)
print(ai_message.content)
if __name__ == &apos;__main__&apos;:
    pass

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;multiply 方法被执行
2乘以3等于6。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;三整合gradio&quot;&gt;三、整合gradio&lt;/h2&gt;
&lt;p&gt;为了更直观的查看工具调用的情况，将本节内容整合到gradio是个不错的选择，同时需要兼容上篇文章《&lt;a href=&quot;https://blog.kdyzm.cn/post/294&quot;&gt;大模型开发之langchain0.3（二）：构建带有记忆功能的聊天机器人&lt;/a&gt;》中记忆功能、Context Window限制功能，由于使用了工具调用，暂时没想好如何实现工具调用显示和正文部分流式输出的组合。&lt;/p&gt;
&lt;h3 id=&quot;1代码整合&quot;&gt;1、代码整合&lt;/h3&gt;
&lt;p&gt;核心点在于如何显示方法调，可以参考文档：&lt;a href=&quot;https://www.gradio.app/docs/gradio/chatbot#demos&quot;&gt;https://www.gradio.app/docs/gradio/chatbot#demos&lt;/a&gt;  案例中的&lt;code&gt;chatbot_with_tools&lt;/code&gt; 章节。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from gradio import ChatMessage
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, AIMessage, trim_messages
from langchain_core.tools import tool
import gradio as gr


@tool
def multiply(a: int, b: int) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;Multiply a and b.&amp;quot;&amp;quot;&amp;quot;
    print(&amp;quot;multiply 方法被执行&amp;quot;)
    return a * b


model = init_chat_model(&amp;quot;gpt-3.5-turbo&amp;quot;)
# Tool 创建
tools = [multiply]
# Tool 绑定
model_with_tools = model.bind_tools(tools)

trimmer = trim_messages(
    max_tokens=300,
    strategy=&amp;quot;last&amp;quot;,
    token_counter=model,
    include_system=True,
    allow_partial=False,
    start_on=&amp;quot;human&amp;quot;,
)


def response(input_message, gradio_history):
    # Tool 调用
    history = [HumanMessage(i[&amp;quot;content&amp;quot;]) if i[&amp;quot;role&amp;quot;] == &apos;user&apos; else AIMessage(i[&amp;quot;content&amp;quot;]) for i in gradio_history]
    history.append(HumanMessage(input_message))
    local_gradio_history = list()
    ai_message = model_with_tools.invoke(trimmer.invoke(history))

    if ai_message.tool_calls:
        tool_dic = {tool_item.name: tool_item for tool_item in tools}
        for tool_call in ai_message.tool_calls:
            tool_name = tool_call[&amp;quot;name&amp;quot;].lower()
            selected_tool = tool_dic[tool_name]
            tool_msg = selected_tool.invoke(tool_call)
            history.append(tool_msg)
            local_gradio_history.append(
                ChatMessage(
                    role=&amp;quot;assistant&amp;quot;,
                    content=f&amp;quot;tool &apos;{tool_name}&apos; invoke result is {tool_msg}&amp;quot;,
                    metadata={&amp;quot;title&amp;quot;: f&amp;quot;️ Used tool &apos;{tool_name}&apos;&amp;quot;},
                )
            )
            yield local_gradio_history
            ai_message = model_with_tools.invoke(trimmer.invoke(history))

    local_gradio_history.append(
        ChatMessage(
            role=&amp;quot;assistant&amp;quot;,
            content=ai_message.content,
        )
    )
    yield local_gradio_history


demo = gr.ChatInterface(
    fn=response,
    type=&amp;quot;messages&amp;quot;,
    flagging_mode=&amp;quot;manual&amp;quot;,
    flagging_options=[&amp;quot;Like&amp;quot;, &amp;quot;Spam&amp;quot;, &amp;quot;Inappropriate&amp;quot;, &amp;quot;Other&amp;quot;],
    save_history=True,
)

if __name__ == &apos;__main__&apos;:
    demo.launch()

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2运行界面&quot;&gt;2、运行界面&lt;/h3&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/04/01/dde7bd78-6b58-4678-a118-cfeb16d89aca.gif&quot; alt=&quot;动画24_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;四注意事项&quot;&gt;四、注意事项&lt;/h2&gt;
&lt;p&gt;注意，并非所有的大模型都支持function_call，不支持function_call的大模型输出返回的AIMessage的tool_calls字段一直是空的。&lt;/p&gt;
</description>
      <category>langchain</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Python脚本：GIF压缩工具</title>
      <link>https://blog.kdyzm.cn/post/295</link>
      <guid>https://blog.kdyzm.cn/post/295</guid>
      <pubDate>Mon, 31 Mar 2025 17:11:04 +0800</pubDate>
      <description>&lt;p&gt;我经常使用ScreenToGif工具生成GIF动图，有的时候GIF生成的比较大，互联网上有很多在线GIF压缩工具，但是有些不能用，有些速度慢。。。有没有本地工具压缩GIF呢？其实是有的，那就是Gifsicle。&lt;/p&gt;
&lt;h2 id=&quot;一gifsicle&quot;&gt;一、Gifsicle&lt;/h2&gt;
&lt;p&gt;Gifsicle是一个命令行工具，能够创建、编辑GIF动图。&lt;/p&gt;
&lt;p&gt;官网地址：&lt;a href=&quot;https://www.lcdf.org/gifsicle/&quot;&gt;https://www.lcdf.org/gifsicle/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://eternallybored.org/misc/gifsicle/&quot;&gt;https://eternallybored.org/misc/gifsicle/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;完整的使用手册：&lt;a href=&quot;https://www.lcdf.org/gifsicle/man.html&quot;&gt;https://www.lcdf.org/gifsicle/man.html&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;1安装&quot;&gt;1、安装&lt;/h3&gt;
&lt;p&gt;安装比较简单，下载下来之后将命令行工具目录添加到Path就可以了，运行&lt;code&gt;gifsicle --version&lt;/code&gt;命令，出现如下界面就表示安装成功了：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/31/ff9e5440-487a-4bdc-8bb2-a5ab11de282a.png&quot; alt=&quot;image-20250331161647115&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;2使用&quot;&gt;2、使用&lt;/h3&gt;
&lt;p&gt;使用手册上的内容比较多，我们只关心关于能压缩GIF体积的参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;--optimize&lt;/strong&gt;：最重要的优化参数，能有效压缩GIF大小，使用方式为&lt;code&gt;-O{level}&lt;/code&gt;，level为1、2、3其中一种，数字越大，压缩效果越好，但是耗时也更长，一般想要更好的压缩效果，使用&lt;code&gt;-O3&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;--colors&lt;/strong&gt;：减小GIF中使用到的颜色数量到指定数量，通过减小改值，可以有效减小GIF体积。使用方式：&lt;code&gt;--colors={num}&lt;/code&gt;，num必须是2到256之间的值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;--lossy&lt;/strong&gt;：有损度，默认值大小为20，该值越大，生成的GIF体积越小，使用方式：&lt;code&gt;--lossy={num}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这样，如果我们想压缩一张GIF动图，可以使用如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gifsicle -O3 --lossy=100 --colors=64 input.gif -o output.gif
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;二python脚本&quot;&gt;二、python脚本&lt;/h2&gt;
&lt;p&gt;为了更方便的使用gifsicle工具，我写了一个python脚本。&lt;/p&gt;
&lt;p&gt;需要先安装依赖：&lt;code&gt;pip install tkinter rich&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;完整代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sys
import tkinter as tk
from tkinter import filedialog, messagebox
import os
import subprocess
from pathlib import Path
import time
from rich.progress import Progress

&amp;quot;&amp;quot;&amp;quot;
这是一个压缩gif图像的python脚本
&amp;quot;&amp;quot;&amp;quot;

#默认存放GIF动图的目录，需要修改成自己的
default_work_dir = &amp;quot;E:\\动图&amp;quot;
run_mode = 1


def choose_gif_files():
    # 创建Tkinter根窗口并隐藏
    root = tk.Tk()
    root.withdraw()

    # 配置文件对话框参数
    file_path = filedialog.askopenfilename(
        title=&amp;quot;请选择动图文件&amp;quot;,  # 对话框标题
        initialdir=os.path.expanduser(default_work_dir),  # 初始目录为用户主目录
        filetypes=[  # 文件类型过滤
            (&amp;quot;动图文件&amp;quot;, &amp;quot;*.gif&amp;quot;)
        ],
        # multiple=True
    )
    # 销毁根窗口
    root.destroy()
    return file_path


def do_process_file(file):
    path = Path(file)
    parent_path = path.parent
    origin_file_name = path.name
    split_parts = origin_file_name.split(&amp;quot;.&amp;quot;)
    new_file_path = parent_path.joinpath(f&amp;quot;{split_parts[0]}_resize.{split_parts[1]}&amp;quot;)
    lossy = input(&amp;quot;--lossy（默认300，越大压缩效果越好）:&amp;quot;)
    colors = input(&amp;quot;--colors（默认64，越小压缩效果越好，但是图片可能会失真）:&amp;quot;)
    print(f&amp;quot;正在处理文件{file}的转换......&amp;quot;)
    result = subprocess.Popen(
        [
            &amp;quot;gifsicle&amp;quot;,
            &amp;quot;-O3&amp;quot;,
            f&amp;quot;--lossy={300 if not lossy else lossy}&amp;quot;,
            f&amp;quot;--colors={64 if not colors else colors}&amp;quot;,
            &amp;quot;--no-extensions&amp;quot;,
            str(file).replace(&amp;quot;/&amp;quot;, &amp;quot;\\&amp;quot;),
            &amp;quot;-o&amp;quot;,
            new_file_path.absolute()
        ],
        stdout=subprocess.PIPE,
        text=True
    )
    # 逐行读取输出
    while True:
        output = result.stdout.readline()
        if output == &amp;quot;&amp;quot; and result.poll() is not None:
            break
        if output:
            print(output.strip())

    # 获取返回码
    return_code = result.poll()
    if return_code == 0:
        return path, new_file_path
    else:
        return path, None


def open_dir(file_path):
    try:
        subprocess.Popen(f&apos;explorer /select, &amp;quot;{file_path.absolute()}&amp;quot;&apos;, shell=True)
    except Exception as e:
        messagebox.showerror(&amp;quot;错误&amp;quot;, f&amp;quot;无法打开资源管理器：{e}&amp;quot;)


def format_size(bytes_num):
    &amp;quot;&amp;quot;&amp;quot;
    将字节数转换为自适应单位（KB/MB/GB等）
    :param bytes_num: 文件字节数，支持负数
    :return: 带单位的格式化字符串
    &amp;quot;&amp;quot;&amp;quot;
    units = [&amp;quot;B&amp;quot;, &amp;quot;KB&amp;quot;, &amp;quot;MB&amp;quot;, &amp;quot;GB&amp;quot;, &amp;quot;TB&amp;quot;, &amp;quot;PB&amp;quot;, &amp;quot;EB&amp;quot;, &amp;quot;ZB&amp;quot;, &amp;quot;YB&amp;quot;]
    sign = &apos;-&apos; if bytes_num &amp;lt; 0 else &apos;&apos;
    size = abs(bytes_num)
    index = 0

    while size &amp;gt;= 1024 and index &amp;lt; len(units) - 1:
        size /= 1024.0
        index += 1

    return f&amp;quot;{sign}{size:.2f} {units[index]}&amp;quot;


def print_origin_and_new_file_diff_info(origin_file_path: Path, new_file_path: Path):
    origin_file_size = origin_file_path.stat().st_size
    new_file_size = new_file_path.stat().st_size
    print(
        f&amp;quot;源文件大小：{format_size(origin_file_size)}，新文件大小：{format_size(new_file_size)}，&amp;quot;
        f&amp;quot;节约{(((origin_file_size - new_file_size) / origin_file_size) * 100):.2f}%的空间&amp;quot;
    )


def process():
    file_path = choose_gif_files()
    # 处理用户选择结果
    if not file_path:
        print(&amp;quot;用户取消选择&amp;quot;)
        return
    origin_file_path, new_file_path = do_process_file(file_path)
    if new_file_path:
        print_origin_and_new_file_diff_info(origin_file_path, new_file_path)
        open_result_dir_flag = input(f&amp;quot;转换后文件：{new_file_path.absolute()}，是否打开转换后目标文件夹（y/n）?&amp;quot;)
        if open_result_dir_flag == &apos;n&apos; or open_result_dir_flag == &apos;N&apos;:
            return
        else:
            open_dir(new_file_path)
    else:
        print(&amp;quot;转换失败&amp;quot;)


def exit_program():
    with Progress() as progress:
        # 创建一个隐藏进度条的任务（总时间为3秒）
        task = progress.add_task(
            description=&amp;quot;[bold red]即将退出程序... (5)&amp;quot;,  # 初始描述
            total=5,
            visible=True, 
        )

        for remaining in range(5, 0, -1):
            # 更新描述中的倒计时数字
            progress.update(
                task,
                description=f&amp;quot;[bold red]即将退出程序... ({remaining})&amp;quot;,
                advance=1,
                refresh=True
            )
            time.sleep(1)  # 等待1秒
    sys.exit(0)


if __name__ == &apos;__main__&apos;:
    while True:
        process()
        if run_mode == 1:
            exit_program()
            break
        continue_flag = input(&amp;quot;是否继续转换（y/n）?&amp;quot;)
        if continue_flag == &apos;n&apos; or continue_flag == &apos;N&apos;:
            exit_program()
        continue

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/31/fc12f7b4-4ae5-4cbe-891b-3ab16ee1cedf.gif&quot; alt=&quot;动画22_resize&quot; style=&quot;zoom:50%;&quot; /&gt;
</description>
      <category>python</category>
    </item>
    <item>
      <title>大模型开发之langchain0.3（二）：构建带有记忆功能的聊天机器人</title>
      <link>https://blog.kdyzm.cn/post/294</link>
      <guid>https://blog.kdyzm.cn/post/294</guid>
      <pubDate>Sat, 29 Mar 2025 09:34:50 +0800</pubDate>
      <description>&lt;p&gt;在上一篇文章&lt;a href=&quot;https://blog.kdyzm.cn/post/293&quot;&gt;《大模型开发之langchain0.3（一）：入门篇》&lt;/a&gt; 中已经介绍了langchain开发框架的搭建，最后使用langchain实现了HelloWorld的代码案例，本篇文章将从0到1搭建带有记忆功能的聊天机器人。&lt;/p&gt;
&lt;h2 id=&quot;一gradio&quot;&gt;一、gradio&lt;/h2&gt;
&lt;p&gt;我们可以使用gradio“画”出类似于chatgpt官网的聊天界面，gradio的特点就是“快”，不用考虑html怎么写，css样式怎么写，该怎样处理按钮的响应。。这一切都被gradio处理完了，我们只需要使用即可。&lt;/p&gt;
&lt;p&gt;gradio官网：&lt;a href=&quot;https://www.gradio.app/&quot;&gt;https://www.gradio.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;官网首页上列举了几种典型的gradio使用场景，其中一种正是我们想要的chatbot的使用场景：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/cf824abb-b69c-4efe-bf71-7d6d2c3f83a3.png&quot; alt=&quot;image-20250328213833569&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;找到左下方对应的源码链接，复制到我们的项目：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import gradio as gr

# 生成器函数，用于模拟流式输出
def slow_echo(message, history):
    for i in range(len(message)):
        time.sleep(0.05)
        yield &amp;quot;You typed: &amp;quot; + message[: i + 1]

demo = gr.ChatInterface(
    slow_echo,
    type=&amp;quot;messages&amp;quot;,
    flagging_mode=&amp;quot;manual&amp;quot;,
    flagging_options=[&amp;quot;Like&amp;quot;, &amp;quot;Spam&amp;quot;, &amp;quot;Inappropriate&amp;quot;, &amp;quot;Other&amp;quot;],
    save_history=True,
)

if __name__ == &amp;quot;__main__&amp;quot;:
    demo.launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们安装好最新版本的gradio就可以成功运行以上代码了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install gradio==5.23.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;二聊天机器人实现&quot;&gt;二、聊天机器人实现&lt;/h2&gt;
&lt;p&gt;根据上一节内容，将大模型的输出给到gradio即可，完整实现代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import gradio as gr

from langchain.chat_models import init_chat_model

model = init_chat_model(&amp;quot;gpt-4o&amp;quot;, model_provider=&amp;quot;openai&amp;quot;)


def do_response(message, history):
    resp = model.invoke(message)
    return resp.content


demo = gr.ChatInterface(
    do_response,
    type=&amp;quot;messages&amp;quot;,
    flagging_mode=&amp;quot;manual&amp;quot;,
    flagging_options=[&amp;quot;Like&amp;quot;, &amp;quot;Spam&amp;quot;, &amp;quot;Inappropriate&amp;quot;, &amp;quot;Other&amp;quot;],
    save_history=True,
)

if __name__ == &amp;quot;__main__&amp;quot;:
    demo.launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码运行结果如下所示：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/546393ee-297c-4e74-ad6d-a4ac9b6c0a85.gif&quot; alt=&quot;动画16&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;可以看到，响应时间比较长，足足有十秒钟，在这期间看不到中间的过程，只在最后一次性输出了最终内容，对于用户来说很不友好。&lt;/p&gt;
&lt;p&gt;接下来将它改造成流式输出。&lt;/p&gt;
&lt;h3 id=&quot;优化一流式输出&quot;&gt;优化一：流式输出&lt;/h3&gt;
&lt;p&gt;想要改造流式输出，首先得大模型支持流式输出，再者改造gradio，让它支持流式输出显示。&lt;/p&gt;
&lt;p&gt;关于模型的流式输出文档：&lt;a href=&quot;https://python.langchain.com/docs/how_to/streaming_llm/&quot;&gt;https://python.langchain.com/docs/how_to/streaming_llm/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;关于gradio的流式输出显示文档：&lt;a href=&quot;https://www.gradio.app/guides/creating-a-chatbot-fast#streaming-chatbots&quot;&gt;https://www.gradio.app/guides/creating-a-chatbot-fast#streaming-chatbots&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简单来说，gradio的流式输出很简单，将do_response方法改造成生成器函数即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
import gradio as gr

def slow_echo(message, history):
    for i in range(len(message)):
        time.sleep(0.3)
        yield &amp;quot;You typed: &amp;quot; + message[: i+1]

gr.ChatInterface(
    fn=slow_echo, 
    type=&amp;quot;messages&amp;quot;
).launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而stream方法支持流式输出，使用示例如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_openai import OpenAI

llm = OpenAI(model=&amp;quot;gpt-3.5-turbo-instruct&amp;quot;, temperature=0, max_tokens=512)
for chunk in llm.stream(&amp;quot;Write me a 1 verse song about sparkling water.&amp;quot;):
    print(chunk, end=&amp;quot;|&amp;quot;, flush=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两者结合起来，改造后的流式输出代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import gradio as gr

from langchain.chat_models import init_chat_model

model = init_chat_model(&amp;quot;gpt-4o&amp;quot;, model_provider=&amp;quot;openai&amp;quot;)


def response(message, history):
    resp = &amp;quot;&amp;quot;
    for chunk in model.stream(message):
        resp = resp + chunk.content
        yield resp


demo = gr.ChatInterface(
    fn=response,
    type=&amp;quot;messages&amp;quot;,
    flagging_mode=&amp;quot;manual&amp;quot;,
    flagging_options=[&amp;quot;Like&amp;quot;, &amp;quot;Spam&amp;quot;, &amp;quot;Inappropriate&amp;quot;, &amp;quot;Other&amp;quot;],
    save_history=True,
)

if __name__ == &apos;__main__&apos;:
    demo.launch()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/0f35143b-1448-4aa7-b3cb-c4272c6d3909.gif&quot; alt=&quot;动画15&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;这样就实现了流式输出。&lt;/p&gt;
&lt;p&gt;但是这个程序还有问题：它没有记忆功能，如下所示&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/b6688046-a1c0-448b-8ee3-8c2fcdb4aade.gif&quot; alt=&quot;动画18&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;接下来对它继续优化，加上记忆功能&lt;/p&gt;
&lt;h3 id=&quot;优化二上下文记忆功能&quot;&gt;优化二：上下文记忆功能&lt;/h3&gt;
&lt;p&gt;在改造之前，需要先了解几个概念：&lt;a href=&quot;https://python.langchain.com/docs/concepts/chat_history/&quot;&gt;Chat history&lt;/a&gt;、&lt;a href=&quot;https://python.langchain.com/docs/concepts/messages/&quot;&gt;Messages&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;大模型之所以有记忆功能，是因为每次和大模型对话，都会将历史记录一起送给大模型。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/45ef5d4b-9809-4486-a950-a3a84e462025.png&quot; alt=&quot;image-20250328224915055&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;和大模型的交互的过程中，最常见的有三种消息类型：System Message、Human Message、AI Message。&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;消息类型&lt;/th&gt;
&lt;th&gt;释义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;System Message&lt;/td&gt;
&lt;td&gt;就是开始对话之前对大模型的引导信息，比如“你是一个智能助手，回答用户信息请使用中文”，这样让大模型“扮演”某种角色。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human Message&lt;/td&gt;
&lt;td&gt;我们提出的问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Message&lt;/td&gt;
&lt;td&gt;大模型响应的问题。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这三种消息被langchain封装成了不同的类以方便使用：&lt;a href=&quot;https://python.langchain.com/docs/concepts/messages/#systemmessage&quot;&gt;SystemMessage&lt;/a&gt;、&lt;a href=&quot;https://python.langchain.com/docs/concepts/messages/#humanmessage&quot;&gt;HumanMessage&lt;/a&gt;、&lt;a href=&quot;https://python.langchain.com/docs/concepts/messages/#aimessage&quot;&gt;AIMessage&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;那如何将对话历史记录告诉大模型呢？&lt;/p&gt;
&lt;p&gt;答案在于model.stream方法，stream默认我们只传输了一个字符串，也就是用户的提问消息，实际上它的类型是&lt;code&gt;LanguageModelInput&lt;/code&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/040ec467-5e6a-4301-8daf-23272180d7c3.png&quot; alt=&quot;image-20250328225545349&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;code&gt;LanguageModelInput&lt;/code&gt;的定义如下&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/e95ebd3b-6fa8-4a6e-9d93-45584e4db398.png&quot; alt=&quot;image-20250328225640119&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;Union的意思就是“选择其中之一”的意思，也就是说LanguageModelInput可以是PromptValue、字符串，或者Sequence[MessageLikeRepresentation]任意之一，关键点就在于Sequence[MessageLikeRepresentation]，从字面意思上来看它是一个列表类的对象，MessageLikeRepresentation的定义如下，它支持BaseMessage类型&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/8d56e3fa-296f-4435-95ca-45cdbcc1e4db.png&quot; alt=&quot;image-20250328225859669&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;也就是说，可以传递一个BaseMessage类型的List给stream方法，而&lt;a href=&quot;https://python.langchain.com/docs/concepts/messages/#systemmessage&quot;&gt;SystemMessage&lt;/a&gt;、&lt;a href=&quot;https://python.langchain.com/docs/concepts/messages/#humanmessage&quot;&gt;HumanMessage&lt;/a&gt;、&lt;a href=&quot;https://python.langchain.com/docs/concepts/messages/#aimessage&quot;&gt;AIMessage&lt;/a&gt; 均是BaseMessage的子类。。。一切清晰明了了，可以用一行代码实现gradio历史记录到BaseMessage列表的转换：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;h = [HumanMessage(i[&amp;quot;content&amp;quot;]) if i[&amp;quot;role&amp;quot;] == &apos;user&apos; else AIMessage(i[&amp;quot;content&amp;quot;]) for i in history]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优化后的完整代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import gradio as gr
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, AIMessage

model = init_chat_model(&amp;quot;gpt-4o&amp;quot;, model_provider=&amp;quot;openai&amp;quot;)


def response(message, history):
    h = [HumanMessage(i[&amp;quot;content&amp;quot;]) if i[&amp;quot;role&amp;quot;] == &apos;user&apos; else AIMessage(i[&amp;quot;content&amp;quot;]) for i in history]
    h.append(HumanMessage(message))
    resp = &amp;quot;&amp;quot;
    for chunk in model.stream(h):
        resp = resp + chunk.content
        yield resp


demo = gr.ChatInterface(
    fn=response,
    type=&amp;quot;messages&amp;quot;,
    flagging_mode=&amp;quot;manual&amp;quot;,
    flagging_options=[&amp;quot;Like&amp;quot;, &amp;quot;Spam&amp;quot;, &amp;quot;Inappropriate&amp;quot;, &amp;quot;Other&amp;quot;],
    save_history=True,
)

if __name__ == &apos;__main__&apos;:
    demo.launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/51ab5458-84aa-4b28-bd92-231207133e33.gif&quot; alt=&quot;动画19&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;好了，到此，我们用了不到30行代码实现了一个基本的智能聊天机器人，这个程序还有什么问题需要注意的吗？我们思考一下，每次和大模型交互，都要将所有历史记录传递给大模型，这行的通吗？实际上每种大模型都有输入长度的限制，如果不加以限制的话，会很容易超出大模型能够输入字符的上限，接下来改造下这段代码，限制输入的字符数量。&lt;/p&gt;
&lt;h3 id=&quot;优化三限制输入长度&quot;&gt;优化三：限制输入长度&lt;/h3&gt;
&lt;p&gt;关于输入长度过长的优化，实际上是一个比较复杂的问题，可以参考以下官方文档：&lt;/p&gt;
&lt;p&gt;Context Window的概念：&lt;a href=&quot;https://python.langchain.com/docs/concepts/chat_models/#context-window&quot;&gt;https://python.langchain.com/docs/concepts/chat_models/#context-window&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Memory的概念：&lt;a href=&quot;https://langchain-ai.github.io/langgraph/concepts/memory/&quot;&gt;https://langchain-ai.github.io/langgraph/concepts/memory/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;trim_message的详细用法：&lt;a href=&quot;https://python.langchain.com/docs/how_to/trim_messages/&quot;&gt;https://python.langchain.com/docs/how_to/trim_messages/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;trim_message在chatbot中的应用案例：&lt;a href=&quot;https://python.langchain.com/docs/tutorials/chatbot/#managing-conversation-history&quot;&gt;https://python.langchain.com/docs/tutorials/chatbot/#managing-conversation-history&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;总结一下，用户能输入的字符长度实际上是大模型能“记住”的文本长度，如果过长，就会达到&amp;quot;Context Window&amp;quot;的极限，大模型要么删除一部分文本继续处理，要么直接抛出异常，前者会导致处理数据结果不准确，后者则会导致应用程序直接报错。&lt;/p&gt;
&lt;p&gt;可以使用trim_message方法解决该问题，它有各种策略截取过长的输入文本，甚至可以自定义策略。比如以下使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain_core.messages import trim_messages

trimmer = trim_messages(
    max_tokens=300,
    strategy=&amp;quot;last&amp;quot;,
    token_counter=model,
    include_system=True,
    allow_partial=False,
    start_on=&amp;quot;human&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该案例中制定的策略是只允许输入最大300个字符，超出的字符从尾部向前查找删除，而且不允许对消息部分删除（保留问题完整性）。&lt;/p&gt;
&lt;p&gt;完整代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;from langchain.chat_models import init_chat_model
import gradio as gr
from langchain_core.messages import HumanMessage, AIMessage, trim_messages

model = init_chat_model(&amp;quot;gpt-4o&amp;quot;, model_provider=&amp;quot;openai&amp;quot;)

trimmer = trim_messages(
    max_tokens=300,
    strategy=&amp;quot;last&amp;quot;,
    token_counter=model,
    include_system=True,
    allow_partial=False,
    start_on=&amp;quot;human&amp;quot;,
)


def response(message, history):
    h = [HumanMessage(i[&amp;quot;content&amp;quot;]) if i[&amp;quot;role&amp;quot;] == &apos;user&apos; else AIMessage(i[&amp;quot;content&amp;quot;]) for i in history]
    h.append(HumanMessage(message))
    result = trimmer.invoke(h)
    //查看trim结果
    print(result)
    resp = &amp;quot;&amp;quot;
    for chunk in model.stream(result):
        resp = resp + chunk.content
        yield resp


demo = gr.ChatInterface(
    fn=response,
    type=&amp;quot;messages&amp;quot;,
    flagging_mode=&amp;quot;manual&amp;quot;,
    flagging_options=[&amp;quot;Like&amp;quot;, &amp;quot;Spam&amp;quot;, &amp;quot;Inappropriate&amp;quot;, &amp;quot;Other&amp;quot;],
    save_history=True,
)

if __name__ == &apos;__main__&apos;:
    demo.launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/c0b7d9af-8e5c-45c3-b7e0-65d8f8c754cb.gif&quot; alt=&quot;动画22&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;后台打印的被优化的消息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;[HumanMessage(content=&apos;你好，我的名字叫kdyzm&apos;, additional_kwargs={}, response_metadata={})]
[HumanMessage(content=&apos;你好，我的名字叫kdyzm&apos;, additional_kwargs={}, response_metadata={}), AIMessage(content=&apos;你好，kdyzm！很高兴认识你。有什么我可以帮忙的吗？&apos;, additional_kwargs={}, response_metadata={}), HumanMessage(content=&apos;帮我写一段python版本的快速排序算法，并分析&apos;, additional_kwargs={}, response_metadata={})]
[HumanMessage(content=&apos;我是谁？&apos;, additional_kwargs={}, response_metadata={})]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，第三次询问的时候，之前的消息就被删除，不再发送给大模型，这导致大模型忘记了我之前告诉它的我的名字。&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>langchain</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>大模型开发之langchain0.3（一）：入门篇.md</title>
      <link>https://blog.kdyzm.cn/post/293</link>
      <guid>https://blog.kdyzm.cn/post/293</guid>
      <pubDate>Fri, 28 Mar 2025 13:50:03 +0800</pubDate>
      <description>&lt;p&gt;这是一篇langchain0.3框架helloworld级别的入门教程，包含了python版本的选型、包下载以及helloworld代码案例。&lt;/p&gt;
&lt;h2 id=&quot;一langchain简介&quot;&gt;一、langchain简介&lt;/h2&gt;
&lt;p&gt;langchain是一个用于开发由大语言模型（LLM，Large Language Model）驱动的应用程序开发框架，被业界称为大语言模型开发的“瑞士军刀”：langchain有大量的组件（components）以及第三方集成框架，可以快速实现业务功能开发。&lt;/p&gt;
&lt;p&gt;需要注意的是，实际上langchain家族有三种框架：langchain、langsmith、langgraph&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;langchain：langchain的核心就是&amp;quot;chain&amp;quot;，也就是链式调用，还包含了agent、RAG技术等相关的核心API&lt;/li&gt;
&lt;li&gt;langsmith：可以用langsmith监控和评估应用程序，观察应用程序运行的细节。&lt;/li&gt;
&lt;li&gt;langgraph：它是一种编排框架，其核心就是“Agent”，相对于langchain将各种组件编排为一种“流线型”架构，langgraph将各种组件编排为一种图（graph）状架构，能够解决完成更复杂的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/527f82ad-a579-4bda-8f8b-5418bf42852c.png&quot; alt=&quot;image-20250328103355280&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h2 id=&quot;二版本选择&quot;&gt;二、版本选择&lt;/h2&gt;
&lt;h3 id=&quot;1langchain版本选择&quot;&gt;1、langchain版本选择&lt;/h3&gt;
&lt;p&gt;langchain是一个很新的框架，这两年随着大语言模型的兴起而兴起，版本迭代到现在（2025年3月28日），也才迭代到0.3.21版本，所以既然要学习就学习最新的，可以到 &lt;a href=&quot;https://pypi.org/&quot;&gt;https://pypi.org/&lt;/a&gt;  网站查看该包的发布情况&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/f70c0735-21fd-4326-a544-10ff19a88616.png&quot; alt=&quot;image-20250328104444490&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;h3 id=&quot;2python版本选择&quot;&gt;2、python版本选择&lt;/h3&gt;
&lt;p&gt;python版本应当适配选择的langchain版本，点击上图中的0.3.21版本详情页，在左下角可以看到对python版本的要求&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/2d0a3a72-3086-4b14-ab9c-f8ec021ba9f3.png&quot; alt=&quot;image-20250328104707949&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;python版本应当大于等于3.9，我这里选择了3.11.11，为什么选择这个版本呢？因为另外一个组件langgraph，在其文档中说明了使用3.11版本最适合&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://academy.langchain.com/courses/take/intro-to-langgraph/texts/58238105-getting-set-up&quot;&gt;https://academy.langchain.com/courses/take/intro-to-langgraph/texts/58238105-getting-set-up&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/e0f0267d-c4b5-4994-9f0c-e75815420bed.png&quot; alt=&quot;image-20250328105245793&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;最后，要使用conda新建python运行环境，防止学习的过程中把环境搞乱了，conda的安装和使用可以参考：&lt;a href=&quot;https://blog.kdyzm.cn/post/289&quot;&gt;python版本管理神器：conda&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;三helloworld&quot;&gt;三、Hello，World！&lt;/h2&gt;
&lt;p&gt;为了能够通过代码调用OpenAI服务，需要申请OpenAI的秘钥key。当然像是国内开源的大模型基本上也都兼容OpenAI的接口形式，所以也可以申请国内大模型的ApiKey替代，代码中可以显示指定使用的Key，但是每次指定都不方便，还容易泄露key，所以一般是使用环境变量的方式指定key。&lt;/p&gt;
&lt;h3 id=&quot;1环境变量设置&quot;&gt;1、环境变量设置&lt;/h3&gt;
&lt;p&gt;设置环境变量：&lt;/p&gt;
&lt;table class=&quot;table table-bordered&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量名&lt;/th&gt;
&lt;th&gt;变量值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPENAI_API_BASE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;默认的OpenAI的值是&lt;code&gt;https://api.openai.com&lt;/code&gt;，国内的比如硅基流动则是&lt;code&gt;https://api.siliconflow.cn&lt;/code&gt;，其它的服务提供商有可能带有&lt;code&gt;/v1&lt;/code&gt;后缀，如果代码运行报错，可以尝试加上&lt;code&gt;/v1&lt;/code&gt;后缀试试。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;申请的秘钥。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;设置完成环境变量后，可以在命令行中查看是否设置生效：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;echo %OPENAI_API_BASE%&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;或者可以通过&lt;code&gt;set&lt;/code&gt;命令查看所有的环境变量列表。&lt;/p&gt;
&lt;p&gt;需要注意的是，如果修改了环境变量的值，webstorm的命令行有可能有缓存导致报错，这时候需要重启webstorm就可以解决缓存问题，如果不行，就反复重启几次。&lt;/p&gt;
&lt;h3 id=&quot;2安装依赖&quot;&gt;2、安装依赖&lt;/h3&gt;
&lt;p&gt;假设读者已经使用conda创建了新的python开发环境&lt;code&gt;langchain0.3.21&lt;/code&gt;，并且应用到了webstorm，则在webstorm的终端环境输出&lt;code&gt;python --version&lt;/code&gt;，则有如下显示&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/28/95e09a75-fa4a-49cf-885c-6b0a9153af24.png&quot; alt=&quot;image-20250328134026126&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;接下来在终端中运行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install langchain[openai]==0.3.21
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装相关依赖（可能由于网络问题会比较慢，读者自行解决）&lt;/p&gt;
&lt;h3 id=&quot;3helloworld&quot;&gt;3、Hello，World！&lt;/h3&gt;
&lt;p&gt;新建一个Python文件，运行如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import getpass
import os

if not os.environ.get(&amp;quot;OPENAI_API_KEY&amp;quot;):
    os.environ[&amp;quot;OPENAI_API_KEY&amp;quot;] = getpass.getpass(&amp;quot;Enter API key for OpenAI: &amp;quot;)

from langchain.chat_models import init_chat_model

model = init_chat_model(&amp;quot;gpt-4o&amp;quot;, model_provider=&amp;quot;openai&amp;quot;)

resp = model.invoke(&amp;quot;Hello, world!&amp;quot;)
print(type(resp))
print(resp.content)

if __name__ == &apos;__main__&apos;:
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;lt;class &apos;langchain_core.messages.ai.AIMessage&apos;&amp;gt;
Hello! How can I assist you today?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，一个HelloWorld级别的代码示例就运行成功了。&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>langchain</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Ubuntu+Windows双系统彻底卸载Ubuntu系统的方法</title>
      <link>https://blog.kdyzm.cn/post/292</link>
      <guid>https://blog.kdyzm.cn/post/292</guid>
      <pubDate>Sun, 23 Mar 2025 21:46:34 +0800</pubDate>
      <description>&lt;p&gt;&lt;strong&gt;第一步：下载DiskGenius软件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下载地址：&lt;a href=&quot;https://clamowo.lanzoui.com/b05agns3g&quot;&gt;https://clamowo.lanzoui.com/b05agns3g&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：删除Ubuntu系统分区&lt;/strong&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/23/19c2612e-57f9-46ac-8c74-6cf7ad44170b.png&quot; alt=&quot;image-20250323213450755&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;第三步：EFI引导删除Ubuntu选项&lt;/strong&gt;&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/23/1800a26b-4fb3-4059-be46-cf58d565399f.png&quot; alt=&quot;image-20250323213742294&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;最后重启电脑即可。&lt;/p&gt;
</description>
      <category>windows</category>
      <category>linux</category>
      <category>ubuntu</category>
      <category>win10</category>
    </item>
    <item>
      <title>http-server优化默认列表展示</title>
      <link>https://blog.kdyzm.cn/post/291</link>
      <guid>https://blog.kdyzm.cn/post/291</guid>
      <pubDate>Thu, 06 Mar 2025 11:22:46 +0800</pubDate>
      <description>&lt;p&gt;node安装好http-server模块后，通过&lt;code&gt;http-server .&lt;/code&gt;命令浏览器默认展示效果如下：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/06/665c4c66-853e-4fd1-a211-b45ed0ef179c.png&quot; alt=&quot;image-20250306105233951&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;个人觉得挺丑的，忍不了一点，我想改一改其CSS样式，优化下展示效果，但是找了半天网上竟然没有教程。。。。没办法，硬着头皮找了半天代码，终于找到了在哪里修改&lt;/p&gt;
&lt;h2 id=&quot;第一步找到http-server安装目录&quot;&gt;&lt;strong&gt;第一步：找到http-server安装目录&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;我使用的nvm管理多个node版本，使用&lt;code&gt;nvm root&lt;/code&gt;命令可以看到nvm的安装目录，全局安装的node module都在对应的版本下的node文件夹中。&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/06/b018d6d2-4c01-441a-a12e-f48e57bd6c76.png&quot; alt=&quot;image-20250306105626483&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;若是直接安装的node，可以使用命令&lt;code&gt;npm root -g&lt;/code&gt; 查看全局package安装的位置。&lt;/p&gt;
&lt;h2 id=&quot;第二步修改css样式&quot;&gt;&lt;strong&gt;第二步：修改css样式&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;打开&lt;code&gt;node_modules\http-server\lib\core\show-dir&lt;/code&gt;文件夹，修改&lt;code&gt;styles.js&lt;/code&gt;文件，复制如下内容覆盖&lt;code&gt;styles.js&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;use strict&apos;;

const icons = require(&apos;./icons.json&apos;);

const IMG_SIZE = 16;

let css = `i.icon { display: block; height: ${IMG_SIZE}px; width: ${IMG_SIZE}px; }\n`;
css += &apos;table tr { white-space: nowrap; line-height: 30px;color:rgb(68, 68, 68);}\n&apos;;
css += &apos;td.perms {}\n&apos;;
css += &apos;td.file-size { text-align: right; padding-left: 1em; }\n&apos;;
css += &apos;td.display-name { padding-left: 1em; }\n&apos;;
css += &apos;.displayName{text-decoration: none;cursor: pointer;}&apos;;
css += &apos;a{color:rgb(68, 68, 68);}&apos;;
css += &apos;a:hover{color:black;font-weight:bold;}&apos;;

Object.keys(icons).forEach((key) =&amp;gt; {
  css += `i.icon-${key} {\n`;
  css += `  background-image: url(&amp;quot;data:image/png;base64,${icons[key]}&amp;quot;);\n`;
  css += &apos;}\n\n&apos;;
});

exports.icons = icons;
exports.css = css;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;第三步修改默认显示模板&quot;&gt;&lt;strong&gt;第三步：修改默认显示模板&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;打开&lt;code&gt;node_modules\http-server\lib\core\show-dir&lt;/code&gt;文件夹，修改&lt;code&gt;index.js&lt;/code&gt;文件，复制如下内容覆盖&lt;code&gt;index.js&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;use strict&apos;;

const styles = require(&apos;./styles&apos;);
const lastModifiedToString = require(&apos;./last-modified-to-string&apos;);
const permsToString = require(&apos;./perms-to-string&apos;);
const sizeToString = require(&apos;./size-to-string&apos;);
const sortFiles = require(&apos;./sort-files&apos;);
const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);
const he = require(&apos;he&apos;);
const etag = require(&apos;../etag&apos;);
const url = require(&apos;url&apos;);
const status = require(&apos;../status-handlers&apos;);

const supportedIcons = styles.icons;
const css = styles.css;

module.exports = (opts) =&amp;gt; {
  // opts are parsed by opts.js, defaults already applied
  const cache = opts.cache;
  const root = path.resolve(opts.root);
  const baseDir = opts.baseDir;
  const humanReadable = opts.humanReadable;
  const hidePermissions = opts.hidePermissions;
  const handleError = opts.handleError;
  const showDotfiles = opts.showDotfiles;
  const si = opts.si;
  const weakEtags = opts.weakEtags;

  return function middleware(req, res, next) {
    // Figure out the path for the file from the given url
    const parsed = url.parse(req.url);
    const pathname = decodeURIComponent(parsed.pathname);
    const dir = path.normalize(
      path.join(
        root,
        path.relative(
          path.join(&apos;/&apos;, baseDir),
          pathname
        )
      )
    );

    fs.stat(dir, (statErr, stat) =&amp;gt; {
      if (statErr) {
        if (handleError) {
          status[500](res, next, { error: statErr });
        } else {
          next();
        }
        return;
      }

      // files are the listing of dir
      fs.readdir(dir, (readErr, _files) =&amp;gt; {
        let files = _files;

        if (readErr) {
          if (handleError) {
            status[500](res, next, { error: readErr });
          } else {
            next();
          }
          return;
        }

        // Optionally exclude dotfiles from directory listing.
        if (!showDotfiles) {
          files = files.filter(filename =&amp;gt; filename.slice(0, 1) !== &apos;.&apos;);
        }

        res.setHeader(&apos;content-type&apos;, &apos;text/html&apos;);
        res.setHeader(&apos;etag&apos;, etag(stat, weakEtags));
        res.setHeader(&apos;last-modified&apos;, (new Date(stat.mtime)).toUTCString());
        res.setHeader(&apos;cache-control&apos;, cache);
        function render(dirs, renderFiles, lolwuts) {
          // each entry in the array is a [name, stat] tuple

		  var dispayPathname = pathname;
		  if(dispayPathname.length&amp;gt;=3){
			dispayPathname = pathname.substring(1,pathname.length-1)
		  }
          let html = `${[
            &apos;&amp;lt;!doctype html&amp;gt;&apos;,
            &apos;&amp;lt;html&amp;gt;&apos;,
            &apos;  &amp;lt;head&amp;gt;&apos;,
            &apos;    &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;&apos;,
            &apos;    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width&amp;quot;&amp;gt;&apos;,
            `    &amp;lt;title&amp;gt;${he.encode(dispayPathname)}&amp;lt;/title&amp;gt;`,
            `    &amp;lt;style type=&amp;quot;text/css&amp;quot;&amp;gt;${css}&amp;lt;/style&amp;gt;`,
            &apos;  &amp;lt;/head&amp;gt;&apos;,
            &apos;  &amp;lt;body&amp;gt;&apos;,
            `&amp;lt;h1&amp;gt;${he.encode(dispayPathname)}&amp;lt;/h1&amp;gt;`,
          ].join(&apos;\n&apos;)}\n`;

          html += &apos;&amp;lt;table&amp;gt;&apos;;

          const failed = false;
          const writeRow = (file) =&amp;gt; {
            // render a row given a [name, stat] tuple
            const isDir = file[1].isDirectory &amp;amp;&amp;amp; file[1].isDirectory();
            let href = `./${encodeURIComponent(file[0])}`;

            // append trailing slash and query for dir entry
            if (isDir) {
              href += `/${he.encode((parsed.search) ? parsed.search : &apos;&apos;)}`;
            }

            const displayName = he.encode(file[0]) + ((isDir) ? &apos;/&apos; : &apos;&apos;);
            const ext = file[0].split(&apos;.&apos;).pop();
            const classForNonDir = supportedIcons[ext] ? ext : &apos;_page&apos;;
            const iconClass = `icon-${isDir ? &apos;_blank&apos; : classForNonDir}`;
			
            // TODO: use stylessheets?
            html += `${&apos;&amp;lt;tr class=&amp;quot;customTr&amp;quot;&amp;gt;&apos; +
              &apos;&amp;lt;td&amp;gt;&amp;lt;i class=&amp;quot;icon &apos;}${iconClass}&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/td&amp;gt;`;
            if (!hidePermissions) {
              html += `&amp;lt;td class=&amp;quot;perms&amp;quot;&amp;gt;&amp;lt;code&amp;gt;(${permsToString(file[1])})&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;`;
            }
            html +=
              `&amp;lt;td class=&amp;quot;last-modified&amp;quot;&amp;gt;${lastModifiedToString(file[1])}&amp;lt;/td&amp;gt;` +
              `&amp;lt;td class=&amp;quot;file-size&amp;quot;&amp;gt;&amp;lt;code&amp;gt;${sizeToString(file[1], humanReadable, si)}&amp;lt;/code&amp;gt;&amp;lt;/td&amp;gt;`;
			  if(isDir){
				html+=`&amp;lt;td class=&amp;quot;display-name&amp;quot;&amp;gt;&amp;lt;a class=&amp;quot;displayName&amp;quot; href=&amp;quot;${href}&amp;quot;&amp;gt;${displayName}&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;`;  
			  }else{
				  var displayName1  = displayName;
				  if(validationEnd(displayName,&amp;quot;.html&amp;quot;)){
					  displayName1= displayName.substring(0,displayName.length-5);
				  }
				html+=`&amp;lt;td class=&amp;quot;display-name&amp;quot;&amp;gt;&amp;lt;a class=&amp;quot;displayName&amp;quot; href=&amp;quot;${href}&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;${displayName1}&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;`;  
			  }
              
              html +=&apos;&amp;lt;/tr&amp;gt;\n&apos;;
          };

          dirs.sort((a, b) =&amp;gt; a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);
          renderFiles.sort((a, b) =&amp;gt; a.toString().localeCompare(b.toString())*-1).forEach(writeRow);
          lolwuts.sort((a, b) =&amp;gt; a[0].toString().localeCompare(b[0].toString())).forEach(writeRow);

          

          if (!failed) {
            res.writeHead(200, { &apos;Content-Type&apos;: &apos;text/html&apos; });
            res.end(html);
          }
        }

		function validationEnd (str, appoint) {
		   str=str.toLowerCase();  //不区分大小写：全部转为小写后进行判断
				
		   var start = str.length-appoint.length;  //相差长度=字符串长度-特定字符长度
		   var char= str.substr(start,appoint.length);//将相差长度作为开始下标，特定字符长度为截取长度
			
		   if(char== appoint){ //两者相同，则代表验证通过
			  return true;
		   }
		   return false;
		}

        sortFiles(dir, files, (lolwuts, dirs, sortedFiles) =&amp;gt; {
          // It&apos;s possible to get stat errors for all sorts of reasons here.
          // Unfortunately, our two choices are to either bail completely,
          // or just truck along as though everything&apos;s cool. In this case,
          // I decided to just tack them on as &amp;quot;??!?&amp;quot; items along with dirs
          // and files.
          //
          // Whatever.

          // if it makes sense to, add a .. link
          if (path.resolve(dir, &apos;..&apos;).slice(0, root.length) === root) {
            fs.stat(path.join(dir, &apos;..&apos;), (err, s) =&amp;gt; {
              if (err) {
                if (handleError) {
                  status[500](res, next, { error: err });
                } else {
                  next();
                }
                return;
              }
              dirs.unshift([&apos;..&apos;, s]);
              render(dirs, sortedFiles, lolwuts);
            });
          } else {
            render(dirs, sortedFiles, lolwuts);
          }
        });
      });
    });
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;第四步修改日期展示&quot;&gt;第四步：修改日期展示&lt;/h2&gt;
&lt;p&gt;打开&lt;code&gt;node_modules\http-server\lib\core\show-dir&lt;/code&gt;文件夹，修改&lt;code&gt;last-modified-to-string.js&lt;/code&gt;文件，复制如下内容覆盖&lt;code&gt;last-modified-to-string.js&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;use strict&apos;;

Date.prototype.format = function(format)
{
 var o = {
 &amp;quot;M+&amp;quot; : this.getMonth()+1, //month
 &amp;quot;d+&amp;quot; : this.getDate(),    //day
 &amp;quot;h+&amp;quot; : this.getHours(),   //hour
 &amp;quot;m+&amp;quot; : this.getMinutes(), //minute
 &amp;quot;s+&amp;quot; : this.getSeconds(), //second
 &amp;quot;q+&amp;quot; : Math.floor((this.getMonth()+3)/3),  //quarter
 &amp;quot;S&amp;quot; : this.getMilliseconds() //millisecond
 }
 if(/(y+)/.test(format)) format=format.replace(RegExp.$1,
 (this.getFullYear()+&amp;quot;&amp;quot;).substr(4 - RegExp.$1.length));
 for(var k in o)if(new RegExp(&amp;quot;(&amp;quot;+ k +&amp;quot;)&amp;quot;).test(format))
 format = format.replace(RegExp.$1,
 RegExp.$1.length==1 ? o[k] :
 (&amp;quot;00&amp;quot;+ o[k]).substr((&amp;quot;&amp;quot;+ o[k]).length));
 return format;
}

module.exports = function lastModifiedToString(stat) {
  const t = new Date(stat.mtime);
  return t.format(&apos;yyyy-MM-dd hh:mm:ss&apos;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;最后效果展示&quot;&gt;最后：效果展示&lt;/h2&gt;
&lt;p&gt;首页：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/06/751e6158-530c-463d-9360-02ea27051060.png&quot; alt=&quot;image-20250306112018699&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;p&gt;点击进入子目录：&lt;/p&gt;
&lt;img src=&quot;https://blog.kdyzm.cn/blog/public/2025/03/06/a643902e-f77e-4c93-94a6-8eeee38d5813.png&quot; alt=&quot;image-20250306112110418&quot; style=&quot;zoom:50%;&quot; /&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;END.&lt;/p&gt;
</description>
      <category>node</category>
      <category>npm</category>
    </item>
    <item>
      <title>python脚本：批量修改文件名</title>
      <link>https://blog.kdyzm.cn/post/290</link>
      <guid>https://blog.kdyzm.cn/post/290</guid>
      <pubDate>Sun, 02 Mar 2025 19:07:17 +0800</pubDate>
      <description>&lt;p&gt;该脚本用于Windows系统下批量重命名文件，可以将文件名中的某些字符串删除或者替换为指定字符串。&lt;/p&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标文件夹：目标文件夹的绝对路径，将会从该文件夹递归查询目标文件&lt;/li&gt;
&lt;li&gt;目标文件后缀：查询的文件后缀，比如&lt;code&gt;mp4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;待替换字符串：支持正则表达式，注意转义字符，比如：&lt;code&gt;\[.*\]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;替换成的字符串：如果为空将会删除待替换的字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re
from pathlib import Path

&amp;quot;&amp;quot;&amp;quot;
该脚本用于Windows系统下批量重命名文件，可以将文件名中的某些字符串删除或者替换为指定字符串。

输入：

目标文件夹：目标文件夹的绝对路径，将会从该文件夹递归查询目标文件
目标文件后缀：查询的文件后缀，比如mp4
待替换字符串：支持正则表达式，注意转义字符，比如：\\[.*\\]
替换成的字符串：如果为空将会删除待替换的字符串
&amp;quot;&amp;quot;&amp;quot;


def batch_rename_files():
    dir_name = input(&amp;quot;请输入目标文件夹：&amp;quot;)
    suffix = input(&amp;quot;请输入目标文件后缀：&amp;quot;)
    replace_str = input(&amp;quot;请输入待替换字符串（支持正则表达式）：&amp;quot;)
    replaced_str = input(&amp;quot;请输入替换成的字符串：（为空表示删除，选填）：&amp;quot;)
    pattern = re.compile(&amp;quot;^.*(&amp;quot; + replace_str + &amp;quot;).*$&amp;quot;)

    if not Path(dir_name).exists():
        raise RuntimeError(f&amp;quot;目标文件不存在：{dir_name}&amp;quot;)
    if not Path(dir_name).is_dir():
        raise RuntimeError(f&amp;quot;目标不是文件夹类型：{dir_name}&amp;quot;)
    p = Path(dir_name)
    all_files = [i for i in p.glob(f&amp;quot;**/*.{suffix}&amp;quot;) if not i.is_dir()]
    for file in all_files:
        file_name = file.name
        if not pattern.match(file_name):
            continue
        result = re.sub(replace_str, lambda x: replaced_str, file_name)
        final_path = file.parent.joinpath(result)
        if final_path == file:
            print(f&amp;quot;skip rename {file} to {final_path} &amp;quot;)
            continue
        print(f&amp;quot;正在重命名 {file} 为 {final_path} &amp;quot;)
        if final_path.exists():
            final_path.unlink()
        file.rename(final_path)
    print(&amp;quot;结束执行任务&amp;quot;)
    input(&amp;quot;输入任意字符继续......&amp;quot;)


if __name__ == &apos;__main__&apos;:
    while True:
        batch_rename_files()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：重命名后的文件如果存在，会先删除旧文件&lt;/p&gt;
</description>
      <category>python</category>
    </item>
  </channel>
</rss>
