<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">qiNan</title>
    <updated>2026-04-12T05:38:44+08:00</updated>
        <id>http://www.dingqinan.com</id>
        <link rel="alternate" type="text/html" href="http://www.dingqinan.com" />
        <link rel="self" type="application/atom+xml" href="http://www.dingqinan.com/atom.xml" />
    <rights>Copyright © 2026, qiNan</rights>
    <generator uri="https://halo.run/" version="1.5.4">Halo</generator>
            <entry>
                <title><![CDATA[Vuex]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/vuex" />
                <id>tag:http://www.dingqinan.com,2026-04-12:vuex</id>
                <published>2026-04-12T05:38:44+08:00</published>
                <updated>2026-04-12T05:38:44+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="vuex" tabindex="-1">Vuex</h1><p>vuex是一个专为vue.js应用程序开发的状态管理模式+库。它采用集中式存储管理应用的所有组件的状态，并以相应的规则保证状态以一种可预测的方式发生变化</p><p>简单来说，状态管理可以理解成为了更方便的管理组件之间的数据交互，提供了一个集中式的管理方案，任何塑胶都可以按照指定的方式进行读取和改变数据</p><h2 id="%E4%BD%BF%E7%94%A8" tabindex="-1">使用</h2><p>安装vuex：</p><pre><code class="language-shell">npm install --save vuex</code></pre><p>配置vuex文件：</p><pre><code class="language-javascript">import { createStore } from &#39;vuex&#39;export default createStore({    //数据放在这里    state:{        count:0    }})</code></pre><p>在主文件中引入vuex：</p><pre><code class="language-javascript">import store from &#39;./store/index&#39;createApp(App).use(store).mount(&#39;#app&#39;)</code></pre><p>在项目中使用：</p><pre><code class="language-vue">&lt;p&gt;count = {{ $store.state.count }}&lt;/p&gt;</code></pre><h2 id="%E5%BF%AB%E6%8D%B7%E8%AF%BB%E5%8F%96" tabindex="-1">快捷读取</h2><p>使用vuex提供的mapState：</p><pre><code class="language-vue">&lt;script&gt;import { mapState } from &#39;vuex&#39;;export default {  //专门读取vux的数据  computed:{    ...mapState([&quot;count&quot;])  }}&lt;/script&gt;</code></pre><p>页面中使用：</p><pre><code class="language-vue">&lt;p&gt;{{ count }}&lt;/p&gt;</code></pre><h2 id="getter" tabindex="-1">Getter</h2><p>对vuex中的数据进行过滤</p><p>定义getters：</p><pre><code class="language-vue">import { createStore } from &#39;vuex&#39;export default createStore({    //数据放在这里    state:{        count:0    },    getters:{        getCount(state){            return state.count &gt; 0 ? state.count : &quot;数据异常&quot;        }    }})</code></pre><p>使用getters：</p><pre><code class="language-vue">&lt;template&gt;  &lt;p&gt;{{ $store.getters.getCount }}&lt;/p&gt;  &lt;p&gt;{{ getCount }}&lt;/p&gt;&lt;/template&gt;&lt;script&gt;import { mapGetters } from &#39;vuex&#39;;export default {  //专门读取vux的数据  computed:{    ...mapGetters([&quot;getCount&quot;])  }}&lt;/script&gt;</code></pre><h2 id="mutation" tabindex="-1">Mutation</h2><p>更改vuex的store中的状态的唯一方法是提交mutation</p><p>定义Mutation：</p><pre><code class="language-vue">import { createStore } from &#39;vuex&#39;export default createStore({    state:{        count:0    },    mutations:{        //参数num可选，没有使用时不填即可        addCount(state,num){            state.count+=num        }    }})</code></pre><p>使用Mutation：</p><pre><code class="language-vue">&lt;template&gt;  &lt;p&gt;hello = {{ $store.state.count }}&lt;/p&gt;  &lt;button @click=&quot;add&quot;&gt;增加&lt;/button&gt;&lt;/template&gt;&lt;script&gt;import { mapMutations } from &#39;vuex&#39;;export default {  methods:{    ...mapMutations([&quot;addCount&quot;]),    add(){      //基本写法      // this.$store.commit(&quot;addCount&quot;,10)      //便携写法      this.addCount(20)    }  }}&lt;/script&gt;</code></pre><h2 id="action" tabindex="-1">Action</h2><p>类似于mutation，不同在于：</p><ul><li>action提交的是mutation，而不是直接变更状态</li><li>action可以包含任意异步操作</li></ul><p>定义Action：</p><pre><code class="language-vue">import { createStore } from &#39;vuex&#39;export default createStore({    state:{        count:0    },    mutations:{        setCount(state,num){            state.count+=num        }    },    actions:{        asyncAddCount({commit}){            commit(&quot;setCount&quot;,11)        }    }})</code></pre><p>使用Action：</p><pre><code class="language-vue">&lt;template&gt;  &lt;p&gt;hello = {{ $store.state.count }}&lt;/p&gt;  &lt;button @click=&quot;asyncAdd&quot;&gt;异步增加&lt;/button&gt;&lt;/template&gt;&lt;script&gt;import { mapActions } from &#39;vuex&#39;;export default {  methods:{    ...mapActions([&quot;asyncAddCount&quot;]),    asyncAdd(){      // this.$store.dispatch(&quot;asyncAddCount&quot;)      this.asyncAddCount()    }  }}&lt;/script&gt;</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Vue路由]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/vue-lu-you" />
                <id>tag:http://www.dingqinan.com,2026-04-09:vue-lu-you</id>
                <published>2026-04-09T05:50:04+08:00</published>
                <updated>2026-04-09T05:50:04+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="vue%E8%B7%AF%E7%94%B1" tabindex="-1">Vue路由</h1><p>VueRouter是vue官方路由，与vue.js核心深度集成，让vue.js构建单页应用变得轻而易举</p><h2 id="%E4%BD%BF%E7%94%A8" tabindex="-1">使用</h2><p>安装：</p><pre><code class="language-shell">npm install --save vue-router</code></pre><p>定义路由文件：</p><pre><code class="language-javascript">import { createRouter,createWebHashHistory } from &quot;vue-router&quot;;import aView from &quot;../views/a-view.vue&quot;import bView from &quot;../views/b-view.vue&quot;//配置信息中需要页面的相关配置const routes = [    {        path:&quot;/&quot;,//路径        component:aView//跳转页面    },{        path:&quot;/haha&quot;,        component:bView    }]const router = createRouter({        //createWebHashHistory:访问路径有# 。原理a标签的描点链接    //createWebHistory:访问路径没有#,此种方式需要后台配合做重定向，否则会404。原理h5的pushState()    history:createWebHashHistory(),    routes})export default router</code></pre><p>启动路由：</p><pre><code class="language-javascript">import { createApp } from &#39;vue&#39;import App from &#39;./App.vue&#39;import router from &quot;./router/index&quot;//使用use明确路由createApp(App).use(router).mount(&#39;#app&#39;)</code></pre><p>展示路由页面</p><pre><code class="language-vue">&lt;template&gt;  &lt;!-- 路由显示路口 --&gt;   &lt;nav&gt;    &lt;RouterLink to=&quot;/&quot;&gt;首页&lt;/RouterLink&gt;    &lt;RouterLink to=&quot;/haha&quot;&gt;关于&lt;/RouterLink&gt;   &lt;/nav&gt;     &lt;RouterView /&gt;&lt;/template&gt;</code></pre><h2 id="%E4%BC%A0%E9%80%92%E5%8F%82%E6%95%B0" tabindex="-1">传递参数</h2><p>配置路由，并设置key：</p><pre><code class="language-js">const routes = [{        path:&quot;/xixi&quot;,        //异步加载方式        component:()=&gt; import(&quot;../views/c-view.vue&quot;)    },{        path:&quot;/hehe/:name&quot;,//通过/:设置key        component:()=&gt; import(&quot;../views/d-view.vue&quot;)    }]</code></pre><p>to页面：</p><pre><code class="language-vue">&lt;template&gt;    &lt;ul&gt;        &lt;li&gt;&lt;RouterLink to=&quot;/hehe/百度&quot;&gt;百度新闻&lt;/RouterLink&gt;&lt;/li&gt;        &lt;li&gt;&lt;RouterLink to=&quot;/hehe/网易&quot;&gt;网易新闻&lt;/RouterLink&gt;&lt;/li&gt;        &lt;li&gt;&lt;RouterLink to=&quot;/hehe/头条&quot;&gt;头条新闻&lt;/RouterLink&gt;&lt;/li&gt;    &lt;/ul&gt;&lt;/template&gt;</code></pre><p>from页面：</p><pre><code class="language-vue">&lt;template&gt;    &lt;h3&gt;新闻详情&lt;/h3&gt;    &lt;p&gt;{{ $route.params.name }}&lt;/p&gt;&lt;/template&gt;</code></pre><h2 id="%E5%B5%8C%E5%A5%97%E8%B7%AF%E7%94%B1" tabindex="-1">嵌套路由</h2><p>配置二级路由：</p><pre><code class="language-javascript">const routes = [{        path:&quot;/user&quot;,        component:()=&gt; import(&quot;../views/e-view.vue&quot;),        children:[            {                //二级导航的路径不要加/                path:&quot;info&quot;,                component:()=&gt; import(&quot;../views/f-view.vue&quot;)            }        ]    }]</code></pre><p>在一级vue中添加：</p><pre><code class="language-vue">&lt;template&gt;    &lt;h3&gt;用户列表&lt;/h3&gt;    &lt;RouterLink to=&quot;/user/info&quot;&gt;用户详情&lt;/RouterLink&gt;    &lt;RouterView /&gt;&lt;/template&gt;</code></pre><blockquote><p>如果需要设置一级路由的默认二级展示，可以使用<code>redirect:完整二级路径</code>的方式设置</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Vue整合swiperjs]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/vue-zheng-he-swiperjs" />
                <id>tag:http://www.dingqinan.com,2026-04-02:vue-zheng-he-swiperjs</id>
                <published>2026-04-02T05:09:47+08:00</published>
                <updated>2026-04-02T05:09:47+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="vue%E6%95%B4%E5%90%88swiperjs" tabindex="-1">Vue整合swiperjs</h1><p>swiper是一个第三方轮播图组件</p><blockquote><p>官网地址：<a href="https://swiperjs.com/vue#swiper-props" target="_blank">https://swiperjs.com/vue#swiper-props</a></p></blockquote><h2 id="%E5%AE%89%E8%A3%85" tabindex="-1">安装</h2><pre><code class="language-shell">npm i swiper</code></pre><p>安装后package.json文件的dependencies会显示swiper的版本信息</p><h2 id="%E5%AF%BC%E5%85%A5" tabindex="-1">导入</h2><pre><code class="language-javascript">&lt;script&gt;import { Swiper, SwiperSlide } from &#39;swiper/vue&#39;;import &#39;swiper/css&#39;;export default {    components:{        Swiper,        SwiperSlide    }}&lt;/script&gt;</code></pre><h2 id="%E4%BD%BF%E7%94%A8" tabindex="-1">使用</h2><pre><code class="language-html">    &lt;Swiper&gt;        &lt;SwiperSlide&gt;            &lt;img src=&quot;../assets/a.png&quot;&gt;        &lt;/SwiperSlide&gt;        &lt;SwiperSlide&gt;            &lt;img src=&quot;../assets/b.png&quot;&gt;        &lt;/SwiperSlide&gt;        &lt;SwiperSlide&gt;            &lt;img src=&quot;../assets/c.png&quot;&gt;        &lt;/SwiperSlide&gt;    &lt;/Swiper&gt;</code></pre><h2 id="%E6%B7%BB%E5%8A%A0%E5%88%86%E9%A1%B5" tabindex="-1">添加分页</h2><p>额外引入：</p><pre><code class="language-javascript">import { Pagination } from &#39;swiper/modules&#39;;import &#39;swiper/css/pagination&#39;;</code></pre><p>在data中声明：</p><pre><code class="language-javascript">data(){        return{            modules:[Pagination]        }    }</code></pre><p>swiper标签中添加：</p><pre><code class="language-html">&lt;Swiper :modules=&quot;modules&quot; :pagination=&quot;{ clickable: true }&quot;&gt;    &lt;/Swiper&gt;</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Vue整合Axios]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/vue-zheng-he-axios" />
                <id>tag:http://www.dingqinan.com,2026-04-02:vue-zheng-he-axios</id>
                <published>2026-04-02T05:08:54+08:00</published>
                <updated>2026-04-02T05:08:54+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="vue%E6%95%B4%E5%90%88axios" tabindex="-1">Vue整合Axios</h1><h2 id="%E5%AE%89%E8%A3%85" tabindex="-1">安装</h2><pre><code class="language-shell">npm install --save axiosnpm install --save querystring</code></pre><h2 id="%E5%B7%A5%E5%85%B7%E7%B1%BB" tabindex="-1">工具类</h2><p>封装request.js文件方便后续使用</p><pre><code class="language-javascript">//引入import axios from &quot;axios&quot;;import querystring from &quot;querystring&quot;//处理响应函数const errorHandle = (status,info) =&gt;{    switch(status){        case 400:            console.log(&quot;语意错误&quot;)            break        case 500:            console.log(&quot;服务器意外&quot;)            break        default:            console.log(info)    }}//创建实例const instance = axios.create({    //公共配置    timeout:5000})//拦截器instance.interceptors.request.use(    config=&gt;{        if(config.method === &quot;post&quot;){            config.data = querystring.stringify(config.data)        }        return config    },    error=&gt;{        return Promise.reject(error)    })//获取数据前instance.interceptors.response.use(    response =&gt; {        return response.status === 200 ? Promise.resolve(response) : Promise.reject(response)    },    error=&gt;{        const {response} = error        //错误处理        errorHandle(response.status,response.info)    })export default instance</code></pre><h2 id="%E4%BD%BF%E7%94%A8" tabindex="-1">使用</h2><pre><code class="language-javascript">import axios from &quot;../utils/request&quot;const api = {    getUrl(){        return axios.get(&quot;请求路径&quot;)    }}export default api</code></pre><p>页面渲染后调用：</p><pre><code class="language-javascript">&lt;script&gt;import api from &quot;../api/index&quot;export default {    mounted(){        console.log(&quot;渲染后&quot;)        api.getUrl().then(res =&gt;{            console.log(res.data)        })    }}&lt;/script&gt;</code></pre><h2 id="%E8%A7%A3%E5%86%B3%E8%B7%A8%E5%9F%9F" tabindex="-1">解决跨域</h2><p>在vue.config.js文件中添加配置：</p><pre><code class="language-javascript">const { defineConfig } = require(&#39;@vue/cli-service&#39;)module.exports = defineConfig({  transpileDependencies: true,  //解决跨域 proxy方式  devServer:{    proxy:{      &quot;/api&quot;:{        target:&quot;//产生跨越的地址（域名）&quot;,        changeOrigin:true      }    }  }})</code></pre><blockquote><p>注意：修改此配置后需要重启服务</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Redis-执行流程]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/redis--zhi-xing-liu-cheng" />
                <id>tag:http://www.dingqinan.com,2025-08-30:redis--zhi-xing-liu-cheng</id>
                <published>2025-08-30T14:35:16+08:00</published>
                <updated>2025-08-30T14:35:16+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="redis-%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B" tabindex="-1">Redis-执行流程</h1><ol><li><strong>建立连接</strong><ol><li>客户端通过tcp协议和redis服务器建立连接</li><li>redis的io多路复用机制监听到新的连接事件</li><li>主事件循环会分配一个<code>连接应答处理器</code>来处理这个连接</li><li>处理器为这个连接创建一个client结构体，用于存储所有状态</li></ol></li><li><strong>读取命令</strong><ol><li>客户端发送命令（resp格式）</li><li>io多路复用监听到客户端的套接字可读（数据到达内核缓冲区）</li><li>事件循环触发命令请求处理器</li><li>处理器调用函数，将数据从内核套接字缓冲区读取到该客户端专属的输入缓冲区中</li></ol></li><li><strong>解析命令</strong><ol><li>redis尝试从缓冲区中按照resp的格式解析数据（以<code>\r\n</code>作为分隔符）</li><li>解析出来的参数被保存在client结构体的argv（参数数组）和argc（参数个数）字段中</li></ol></li><li><strong>查找命令</strong><ol><li>取第一个参数<code>argv[0]</code>作为命令名</li><li>在命令表中查找该命令名（命令表是一个字典，在服务器启动时完成初始化，值是一个redisCommand结构体）</li><li>redisCommand结构体包含了命令的所有元信息，其中的<code>proc</code>函数指针，指向了实际实现函数</li></ol></li><li><strong>执行前检查</strong><ol><li>在调用函数前，redis会进行一系列检查。如果任何一项失败，都直接返回错误。如：参数个数、认证、内存、集群、数据类型（部分）的检查</li></ol></li><li><strong>调用实现函数</strong><ol><li>根据之前找到的函数，和argv中的参数，调用函数</li><li>更新服务器的统计信息，如key命中次数，内存使用量等</li></ol></li><li><strong>执行后操作</strong><ol><li>命令执行完毕后，还会执行一些后续操作，如：慢查询日志、aof持久化、主从复制等</li></ol></li><li><strong>回复客户端</strong><ol><li>函数将执行结果写入客户端的输出缓冲区</li><li>事件循环会监听到客户端的套接字变得可写</li><li>事件循环调用<code>命令回复处理器</code></li><li>处理器将输出缓冲区的内容通过网络发送回客户端</li><li>客户端收到回复并解析</li></ol></li><li><strong>持久化（可选）</strong><ul><li>AOF：根据appendfsync策略，由后台线程将aof缓冲区的内容刷到磁盘</li><li>RDB：如果满足了配置的保存条件，主进程会fork子进程来创建rdb快照，不影响主进程</li></ul></li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[ElasticsSearch-Segment的组成]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/elasticssearch-segment-de-zu-cheng" />
                <id>tag:http://www.dingqinan.com,2025-08-26:elasticssearch-segment-de-zu-cheng</id>
                <published>2025-08-26T15:50:32+08:00</published>
                <updated>2025-08-26T15:50:32+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="elasticssearch-segment%E7%9A%84%E7%BB%84%E6%88%90" tabindex="-1">ElasticsSearch-Segment的组成</h1><p>在Elasticsearch中，<strong>Segment</strong>（段）是Lucene的核心数据结构，它是索引不可变的、基础的数据块</p><p>每个Segment本质上是一个独立的、微型的倒排索引，它由多个文件组成。这些文件共享一个通用的前缀（即Segment名），并使用不同的扩展名来标识其包含的数据类型</p><h2 id="%E7%BB%84%E6%88%90" tabindex="-1">组成</h2><ol><li><strong><code>.si</code> (Segment Info) - 段信息文件</strong><ul><li><strong>作用</strong>：这是Segment的“元数据文件”。它描述了该Segment的基本信息，例如：<ul><li>该Segment包含了多少文档。</li><li>使用的Lucene版本。</li><li>有关该Segment的诊断信息。</li><li>指向其他组件的“桥接”信息。</li></ul></li></ul></li><li><strong><code>.fnm</code> (Fields) - 字段信息文件</strong><ul><li><strong>作用</strong>：存储此Segment中所有<strong>字段（Field）的元信息</strong>。</li><li><strong>内容</strong>：对于每个字段，记录其：<ul><li><strong>名称</strong>（Name）</li><li><strong>索引选项</strong>（Index Options）：是否被索引（<code>INDEXED</code>）、是否存储词向量（<code>DOCS_AND_FREQS_AND_POSITIONS</code>）等。</li><li><strong>属性</strong>：是否存储（<code>STORED</code>）、是否进行分词（<code>TOKENIZED</code>）等。</li><li>使用的编解码器（Codec）信息。</li></ul></li></ul></li><li><strong><code>.fdx</code> (Field Index) 和 <code>.fdt</code> (Field Data) - 存储字段文件</strong><ul><li><strong>作用</strong>：如果你在映射中设置了 <code>&quot;store&quot;: true</code>，文档的原始JSON内容会被存储到这里，以便于通过 <code>stored_fields</code> API检索。</li><li><strong>分工</strong>：<ul><li><strong><code>.fdt</code></strong> 文件是真正存储压缩后的文档数据的地方。</li><li><strong><code>.fdx</code></strong> 文件是一个索引文件，它像一个指针簿，可以快速定位到某个文档在 <code>.fdt</code> 文件中的具体位置，避免扫描整个 <code>.fdt</code> 文件。</li></ul></li></ul></li><li><strong><code>.tim</code> (Term Dictionary) - 词项字典文件</strong><ul><li><strong>作用</strong>：这是<strong>倒排索引的核心</strong>。它包含了该Segment内所有字段的所有<strong>词项（Term）</strong> 的排序列表。</li><li><strong>内容</strong>：类似于一本字典的“所有词条列表”，例如：<code>[&quot;apple&quot;, &quot;banana&quot;, &quot;elasticsearch&quot;, ...]</code>。</li></ul></li><li><strong><code>.tip</code> (Term Index) - 词项索引文件</strong><ul><li><strong>作用</strong>：为了快速在 <code>.tim</code> 文件中找到某个词项，<code>.tip</code> 文件存储了 <code>.tim</code> 文件的<strong>索引</strong>。</li><li><strong>原理</strong>：它存储了词项字典的“前缀索引”，类似于字典的“部首检字表”或“拼音索引”。通过 <code>.tip</code> 可以快速跳转到 <code>.tim</code> 的大致位置，然后再进行精细查找，极大地减少了磁盘寻址次数。</li></ul></li><li><strong><code>.doc</code> (Postings List) - 倒排表文件</strong><ul><li><strong>作用</strong>：存储每个词项对应的<strong>倒排列表（Postings List）</strong>。</li><li><strong>内容</strong>：对于词项 <code>&quot;apple&quot;</code>，它的倒排列表会包含所有包含 <code>&quot;apple&quot;</code> 的文档ID（DocID），以及在该文档中出现的<strong>词频（Term Frequency）</strong>。</li></ul></li><li><strong><code>.pos</code> (Positions) - 位置信息文件</strong><ul><li><strong>作用</strong>：存储词项在文档中出现的位置（Position）信息。</li><li><strong>用途</strong>：用于支持<strong>短语查询（<code>&quot;quick brown fox&quot;</code>）</strong> 和 <strong>邻近度查询（proximity queries）</strong>。如果没有这个文件，ES只能判断一个词是否在文档中出现，而无法判断多个词之间的位置关系。</li></ul></li><li><strong><code>.pay</code> (Payloads) - 载荷文件</strong><ul><li><strong>作用</strong>：存储可选的、与每个位置相关联的额外信息（Payload），例如自定义的权重。</li><li><strong>用途</strong>：这是一个高级功能，使用相对较少。例如，可以在索引时给某个词项附加一个权重值，在查询时使用这个权重来计算相关性分数。</li></ul></li><li><strong><code>.nvd</code>, <code>.nvm</code> (Norms) - 长度规范文件</strong><ul><li><strong>作用</strong>：存储<strong>规范化因子（Norms）</strong> 数据，用于在查询时对较短的字段进行奖励（Boost），实现“字段长度归一化”。</li><li><strong>原理</strong>：一个包含10个词的文档命中查询，比一个包含1000个词的文档命中查询，相关性应该更高。Norms数据就用于这种计算。</li></ul></li><li><strong><code>.dvd</code>, <code>.dvm</code> (DocValues) - 列式存储文件</strong><ul><li><strong>作用</strong>：这是为排序、聚合和脚本访问字段值而设计的<strong>列式存储</strong>结构。它与倒排索引（行式存储）互为补充。</li><li><strong>原理</strong>：<code>.dvd</code> 文件存储所有文档某个字段的实际值（例如所有文档的 <code>price</code>），而 <code>.dvm</code> 文件存储元数据，帮助定位 <code>.dvd</code> 文件中的值。</li><li><strong>重要性</strong>：这是聚合操作（如 <code>terms</code>, <code>avg</code>, <code>histogram</code>）性能高的根本原因。</li></ul></li><li><strong><code>.liv</code> (Live Documents) - 存活文档文件</strong><ul><li><strong>作用</strong>：标记哪些文档是<strong>被删除的</strong>。由于Segment是不可变的，删除文档并不是直接从文件中抹去数据，而是通过一个特殊的“黑名单”文件来标记该文档已被逻辑删除。</li><li><strong>后续处理</strong>：这些被标记删除的文档会在后续的Segment合并（Merge）过程中被物理清除。</li></ul></li></ol><h2 id="%E6%80%BB%E7%BB%93" tabindex="-1">总结</h2><p>可以将一个Segment想象成一本完整的、不可更改的书：</p><table><thead><tr><th style="text-align:left">Segment 文件</th><th style="text-align:left">书的比喻</th><th style="text-align:left">主要用途</th></tr></thead><tbody><tr><td style="text-align:left"><strong><code>.si</code></strong></td><td style="text-align:left">书的版权页、前言、目录总览</td><td style="text-align:left">段的元信息</td></tr><tr><td style="text-align:left"><strong><code>.fnm</code></strong></td><td style="text-align:left">书中所有章节的标题列表</td><td style="text-align:left">字段的元信息</td></tr><tr><td style="text-align:left"><strong>(<code>.tip</code>+<code>.tim</code>)</strong></td><td style="text-align:left">书的<strong>索引部分</strong>（例如，按拼音排序的索引表）</td><td style="text-align:left"><strong>快速查找词项</strong>（倒排索引的“索引层”）</td></tr><tr><td style="text-align:left"><strong><code>.doc</code></strong></td><td style="text-align:left">索引表后面指向的<strong>页码列表</strong></td><td style="text-align:left"><strong>查找包含词项的文档</strong>（倒排表）</td></tr><tr><td style="text-align:left"><strong>(<code>.fdx</code>+<code>.fdt</code>)</strong></td><td style="text-align:left">书的<strong>正文内容</strong>本身</td><td style="text-align:left"><strong>存储原始文档</strong></td></tr><tr><td style="text-align:left"><strong>(<code>.dvm</code>+<code>.dvd</code>)</strong></td><td style="text-align:left">书后按类别整理的<strong>表格数据</strong>（如所有地名列表）</td><td style="text-align:left"><strong>排序、聚合</strong>（列式存储）</td></tr><tr><td style="text-align:left"><strong><code>.liv</code></strong></td><td style="text-align:left">一份勘误表，指明哪些页码的内容作废</td><td style="text-align:left"><strong>标记删除的文档</strong></td></tr><tr><td style="text-align:left"><strong><code>.pos</code></strong></td><td style="text-align:left">记录某个关键词在页码中出现的具体行数</td><td style="text-align:left"><strong>短语查询</strong></td></tr></tbody></table><p>这些文件共同协作，使得Elasticsearch能够高效地进行全文搜索、精确值处理、聚合分析等各种操作。<strong>Segment合并（Merge）</strong> 过程本质上就是将这些小“书”合并成大“书”，并在这个过程中物理清除被删除的文档（<code>.liv</code> 文件中的记录），最终优化索引结构和查询性能。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringAI-RAG]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/springai-rag" />
                <id>tag:http://www.dingqinan.com,2025-06-21:springai-rag</id>
                <published>2025-06-21T03:15:27+08:00</published>
                <updated>2025-06-21T03:15:27+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="springai-rag" tabindex="-1">SpringAI-RAG</h1><h2 id="%E4%BE%9D%E8%B5%96" tabindex="-1">依赖</h2><pre><code class="language-xml">    &lt;dependencies&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;            &lt;artifactId&gt;spring-ai-starter-model-openai&lt;/artifactId&gt;        &lt;/dependency&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;            &lt;artifactId&gt;spring-ai-advisors-vector-store&lt;/artifactId&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;    &lt;dependencyManagement&gt;        &lt;dependencies&gt;            &lt;dependency&gt;                &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;                &lt;artifactId&gt;spring-ai-bom&lt;/artifactId&gt;                &lt;version&gt;1.0.0&lt;/version&gt;                &lt;type&gt;pom&lt;/type&gt;                &lt;scope&gt;import&lt;/scope&gt;            &lt;/dependency&gt;        &lt;/dependencies&gt;    &lt;/dependencyManagement&gt;</code></pre><h2 id="%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" tabindex="-1">配置文件</h2><pre><code class="language-yaml">spring:  ai:    openai:      api-key: ${ALI_API_KEY}      base-url: https://dashscope.aliyuncs.com/compatible-mode      chat:        options:          model: qwen-plus      embedding:        api-key: ${ALI_API_KEY}        base-url: https://dashscope.aliyuncs.com/compatible-mode        options:          model: text-embedding-v4</code></pre><h2 id="%E5%90%91%E9%87%8F%E6%95%B0%E6%8D%AE%E5%BA%93" tabindex="-1">向量数据库</h2><pre><code class="language-java">    @Bean    public VectorStore vectorStore(@Qualifier(&quot;openAiEmbeddingModel&quot;) EmbeddingModel embeddingModel) {        //使用内存        return SimpleVectorStore.builder(embeddingModel).build();    }</code></pre><h2 id="%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E6%8D%AE" tabindex="-1">初始化数据</h2><pre><code class="language-java">    @Bean    public CommandLineRunner init(VectorStore vectorStore,                                  @Value(&quot;classpath:rag/data.txt&quot;)Resource resource) {        return args -&gt; {            //加载数据            List&lt;Document&gt; documentList = new TextReader(resource).read();            //拆分            TextSplitter splitter = new TextSplitter() {                @Override                protected List&lt;String&gt; splitText(String text) {                    //按照行拆分                    return List.of(text.split(&quot;\\n&quot;));                }            };            List&lt;Document&gt; transform = splitter.transform(documentList);            //存入向量数据库            vectorStore.write(transform);        };    }</code></pre><h2 id="%E6%A3%80%E7%B4%A2" tabindex="-1">检索</h2><pre><code class="language-java">@RestController@RequestMapping(&quot;rag&quot;)public class RAGController {    @Autowired    private ChatClient chatClient;    @Autowired    private VectorStore vectorStore;    @GetMapping(&quot;{message}&quot;)    public String chat(@PathVariable(&quot;message&quot;) String message){        return chatClient.prompt()                .user(message)                .advisors(QuestionAnswerAdvisor.builder(vectorStore)                                .searchRequest(SearchRequest.builder()                                        .query(message)                                        .topK(1)                                        .build())                        .build())                .call()                .content();    }}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringAI实现数据库增删改查]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/springai-shi-xian-shu-ju-ku-zeng-shan-gai-cha" />
                <id>tag:http://www.dingqinan.com,2025-06-20:springai-shi-xian-shu-ju-ku-zeng-shan-gai-cha</id>
                <published>2025-06-20T01:43:37+08:00</published>
                <updated>2025-06-20T01:44:04+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="springai%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A2%9E%E5%88%A0%E6%94%B9%E6%9F%A5" tabindex="-1">SpringAI实现数据库增删改查</h1><h2 id="%E6%95%B0%E6%8D%AE%E5%BA%93%E8%A1%A8%E7%BB%93%E6%9E%84" tabindex="-1">数据库表结构</h2><pre><code class="language-mysql">    CREATE TABLE &#96;t_user&#96; (        &#96;id&#96; int NOT NULL AUTO_INCREMENT,        &#96;username&#96; varchar(50) DEFAULT NULL,        &#96;password&#96; varchar(50) DEFAULT NULL,        &#96;age&#96; int DEFAULT NULL,        &#96;create_time&#96; datetime DEFAULT NULL,        &#96;is_deleted&#96; int DEFAULT NULL,        PRIMARY KEY (&#96;id&#96;)    )</code></pre><h2 id="%E6%8F%90%E7%A4%BA%E8%AF%8D" tabindex="-1">提示词</h2><p><strong>查询</strong></p><pre><code class="language-">根据用户需求和提供数据库表结构，生成查询sql语法要求使用mysql语法，只能生成查询的sql只返回生成的sql，不要返回其他只能查询逻辑删除字段，值为0的数据用户需求：{question}数据库表结构：{ddl}当前时间：{date}</code></pre><p><strong>新增</strong></p><pre><code class="language-">根据用户需求和提供数据库表结构，生成插入sql语法要求使用mysql语法，只能生成插入的sql只返回生成的sql，不要返回其他尽量补全字段，如果有必填字段未获取到则只返回 1新增的数据逻辑删除字段为0用户需求：{question}数据库表结构：{ddl}当前时间：{date}</code></pre><p><strong>修改</strong></p><pre><code class="language-">根据用户需求和提供数据库表结构，生成修改sql语法要求使用mysql语法，只能生成修改的sql只返回生成的sql，不要返回其他修改的sql必须有条件，否则只返回 1用户需求：{question}数据库表结构：{ddl}当前时间：{date}</code></pre><p><strong>删除</strong></p><pre><code class="language-">根据用户需求和提供数据库表结构，生成逻辑删除sql，即将逻辑删除字段设置为1语法要求使用mysql语法，只能生成逻辑删除的sql只返回生成的sql，不要返回其他删除的sql必须有条件，否则只返回 1用户需求：{question}数据库表结构：{ddl}当前时间：{date}</code></pre><p><strong>类型判断</strong></p><pre><code class="language-">根据用户的输入判断对应的操作类型类型只有一种，匹配最接近的类型只能是：SELECT,UPDATE,INSERT,DELETE只返回类型，不要返回其他东西用户输入：{question}</code></pre><h2 id="controller" tabindex="-1">controller</h2><pre><code class="language-java">@RestController@RequestMapping(&quot;sql&quot;)@Slf4jpublic class SqlController {    @Autowired    private ChatClient chatClient;    @Autowired    private JdbcTemplate jdbcTemplate;        @Value(&quot;classpath:sql/schema.sql&quot;)    private Resource ddl;    @Value(&quot;classpath:st/type.st&quot;)    private Resource typeTemp;    @Value(&quot;classpath:st/query.st&quot;)    private Resource queryTemp;    @Value(&quot;classpath:st/save.st&quot;)    private Resource saveTemp;    @Value(&quot;classpath:st/update.st&quot;)    private Resource updateTemp;    @Value(&quot;classpath:st/delete.st&quot;)    private Resource deleteTemp;    @SneakyThrows    @GetMapping(&quot;{message}&quot;)    public Object sql(@PathVariable String message){        //判断操作类型        String type = chatClient.prompt()                .user(us -&gt; us.text(typeTemp).param(&quot;question&quot;, message))                .call()                .content();        log.info(&quot;sql type:{}&quot;,type);        //根据类型返回对应模板        Resource temp = switch (type){            case &quot;SELECT&quot; -&gt; queryTemp;            case &quot;INSERT&quot; -&gt; saveTemp;            case &quot;UPDATE&quot; -&gt; updateTemp;            case &quot;DELETE&quot; -&gt; deleteTemp;            default -&gt; null;        };        //数据库表结构        String schema = ddl.getContentAsString(Charset.defaultCharset());        //生成sql        String sql = chatClient.prompt()                .user(us -&gt; us.text(temp)//使用模板                        .param(&quot;question&quot;, message)//设置参数                        .param(&quot;ddl&quot;, schema)                        .param(&quot;date&quot;, LocalDateTime.now().toString()))                .call()                .content();        log.info(&quot;sql:{}&quot;,sql);        //执行sql        if (&quot;SELECT&quot;.equals(type)){            //查询数据库            return jdbcTemplate.queryForList(sql);        }else {            if (&quot;1&quot;.equals(sql)) {                return &quot;操作异常&quot;;            }            //写数据库            jdbcTemplate.execute(sql);            return &quot;ok&quot;;        }    }}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringAI-1.0.0使用]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/springai-100-shi-yong" />
                <id>tag:http://www.dingqinan.com,2025-06-19:springai-100-shi-yong</id>
                <published>2025-06-19T01:36:35+08:00</published>
                <updated>2025-06-25T00:30:47+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="springai-1.0.0%E4%BD%BF%E7%94%A8" tabindex="-1">SpringAI-1.0.0使用</h1><h2 id="%E4%BE%9D%E8%B5%96" tabindex="-1">依赖</h2><pre><code class="language-xml">    &lt;dependencyManagement&gt;        &lt;dependencies&gt;            &lt;dependency&gt;                &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;                &lt;artifactId&gt;spring-ai-bom&lt;/artifactId&gt;                &lt;version&gt;1.0.0&lt;/version&gt;                &lt;type&gt;pom&lt;/type&gt;                &lt;scope&gt;import&lt;/scope&gt;            &lt;/dependency&gt;        &lt;/dependencies&gt;    &lt;/dependencyManagement&gt;    &lt;dependencies&gt;        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;            &lt;artifactId&gt;spring-ai-starter-model-openai&lt;/artifactId&gt;        &lt;/dependency&gt;    &lt;/dependencies&gt;</code></pre><h2 id="%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" tabindex="-1">配置文件</h2><pre><code class="language-yaml">spring:  ai:    openai:      api-key: ${ALI_API_KEY}      base-url: https://dashscope.aliyuncs.com/compatible-mode      chat:        options:          model: qwen-plus</code></pre><blockquote><p>注意：需要去掉官方提供的base-url后的v1</p></blockquote><h2 id="%E9%85%8D%E7%BD%AEchatclient" tabindex="-1">配置ChatClient</h2><pre><code class="language-java">    @Bean    public ChatClient chatClient(ChatClient.Builder builder) {        return builder.build();    }</code></pre><h2 id="%E7%AE%80%E5%8D%95%E5%AF%B9%E8%AF%9D" tabindex="-1">简单对话</h2><pre><code class="language-java">    @Autowired    private ChatClient chatClient;    @GetMapping(&quot;{message}&quot;)    public String chat(@PathVariable String message) {        return chatClient.prompt()                .user(message)                .call()                .content();    }</code></pre><h2 id="%E6%B5%81%E5%BC%8F%E8%BE%93%E5%87%BA" tabindex="-1">流式输出</h2><pre><code class="language-java">    @GetMapping(value = &quot;stream/{message}&quot;,produces = &quot;text/stream;charset=utf-8&quot;)    public Flux&lt;String&gt; stream(@PathVariable String message) {        return chatClient.prompt()                .user(message)                .stream()                .content();    }</code></pre><h2 id="%E8%A7%92%E8%89%B2%E9%A2%84%E8%AE%BE" tabindex="-1">角色预设</h2><pre><code class="language-java">    @Bean    public ChatClient chatClient(ChatClient.Builder builder) {        return builder                .defaultSystem(&quot;你是一个法律专家&quot;)                .build();    }</code></pre><h2 id="%E6%89%93%E5%8D%B0%E6%97%A5%E5%BF%97" tabindex="-1">打印日志</h2><p>添加日志Advisor</p><pre><code class="language-java">    @Bean    public ChatClient chatClient(ChatClient.Builder builder) {        return builder                .defaultAdvisors(new SimpleLoggerAdvisor())                .build();    }</code></pre><p>设置日志等级</p><pre><code class="language-yaml">logging:  level:    org.springframework.ai.chat.client.advisor: debug</code></pre><h2 id="%E5%AF%B9%E8%AF%9D%E8%AE%B0%E5%BF%86" tabindex="-1">对话记忆</h2><pre><code class="language-java">    @Autowired    private ChatMemory chatMemory; //新版本可直接使用，默认基于内存    @Bean    public ChatClient chatClient(ChatClient.Builder builder) {        return builder                .defaultAdvisors(MessageChatMemoryAdvisor                        .builder(chatMemory).build())                .build();    }</code></pre><h2 id="%E5%AF%B9%E8%AF%9D%E9%9A%94%E7%A6%BB" tabindex="-1">对话隔离</h2><pre><code class="language-java">    @GetMapping(&quot;quarantine/{id}/{message}&quot;)    public String quarantine(@PathVariable Integer id,                             @PathVariable String message) {        return chatClient.prompt()                .user(message)                .advisors(ds -&gt; ds.param(ChatMemory.CONVERSATION_ID,id))//根据id进行隔离                .call()                .content();    }</code></pre><h2 id="%E6%9F%A5%E7%9C%8B%E5%8E%86%E5%8F%B2" tabindex="-1">查看历史</h2><pre><code class="language-java">    @Autowired    private ChatMemory chatMemory;@GetMapping(&quot;history/{id}&quot;)    public List&lt;String&gt;  history(@PathVariable Integer id) {        List&lt;Message&gt; messages = chatMemory.get(id.toString());        return messages.stream()                .filter(message -&gt; MessageType.USER.equals(message.getMessageType()))//筛选类型                .map(Message::getText)                .toList();    }</code></pre><h2 id="%E4%BD%BF%E7%94%A8%E5%B7%A5%E5%85%B7" tabindex="-1">使用工具</h2><p>定义工具</p><pre><code class="language-java">public class MyTools {    @Tool(description = &quot;获取城市的天气&quot;)    String getWeather(@ToolParam(description = &quot;城市&quot;) String city){        if (&quot;杭州&quot;.equals(city)){            return &quot;暴雨&quot;;        }        return &quot;晴天&quot;;    }}</code></pre><p>配置工具</p><pre><code class="language-java">    @Bean    public ChatClient chatClient(ChatClient.Builder builder) {        return builder                .defaultTools(new MyTools())                .build();    }</code></pre><h2 id="%E4%BD%BF%E7%94%A8ollama" tabindex="-1">使用Ollama</h2><p>依赖</p><pre><code class="language-xml">        &lt;dependency&gt;            &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;            &lt;artifactId&gt;spring-ai-starter-model-ollama&lt;/artifactId&gt;        &lt;/dependency&gt;</code></pre><p>配置</p><pre><code class="language-yaml">spring:  ai:    ollama:      chat:        model: deepseek-r1:1.5b      base-url: http://localhost:11434</code></pre><p>使用</p><pre><code class="language-java">@RestController@RequestMapping(&quot;ollama&quot;)public class OllamaController {    private ChatClient chatClient;    public OllamaController(@Qualifier(&quot;ollamaChatModel&quot;) ChatModel chatModel) {        chatClient = ChatClient.builder(chatModel)                .build();    }    @GetMapping(&quot;chat/{message}&quot;)    public String chat(@PathVariable(&quot;message&quot;) String message) {        return chatClient.prompt()                .user(message)                .call()                .content();    }}</code></pre><blockquote><p>注意：如果项目中同时引入多个模型，需要使用ChatModel并使用@Qualifier进行区分，ChatClient.Builder方式会报错</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringBoot3整合Resilience4j]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/springboot3-zheng-he-resilience4j" />
                <id>tag:http://www.dingqinan.com,2025-06-06:springboot3-zheng-he-resilience4j</id>
                <published>2025-06-06T04:07:13+08:00</published>
                <updated>2025-06-06T04:07:13+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="springboot3%E6%95%B4%E5%90%88resilience4j" tabindex="-1">SpringBoot3整合Resilience4j</h1><h2 id="%E4%BE%9D%E8%B5%96" tabindex="-1">依赖</h2><pre><code class="language-groovy">implementation(&quot;io.github.resilience4j:resilience4j-spring-boot3:2.2.0&quot;)implementation(&quot;org.springframework.boot:spring-boot-starter-aop&quot;)</code></pre><h2 id="%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" tabindex="-1">配置文件</h2><pre><code class="language-yaml">resilience4j:  timelimiter: #超时    instances: #实例名      timeoutService:        timeoutDuration: 2s # 超时时间为2秒  circuitbreaker: #熔断    instances:      circuitbreakerService:        slidingWindowSize: 10 # 滑动窗口大小为10        failureRateThreshold: 50 # 失败率阈值为50%        waitDurationInOpenState: 10s # 打开状态等待时间为10秒  ratelimiter: #限流 限制请求速率    instances:      ratelimiterService:        limitForPeriod: 1 # 每个周期允许的最大请求数为1        limitRefreshPeriod: 10s # 速率限制刷新周期为10秒        timeoutDuration: 500ms # 等待许可的超时时间为500毫秒  retry: #重试    instances:      retryService:        maxAttempts: 5 # 最大重试次数为5次        waitDuration: 1000ms # 重试间隔为1000毫秒（1秒）        enableExponentialBackoff: true # 启用指数回退        exponentialBackoffMultiplier: 1.5 # 指数回退倍数为1.5        retryExceptions:          - java.lang.RuntimeException # 需要重试的异常类型  bulkhead: #批隔离 限制并发数量    instances:      bulkheadService:        maxConcurrentCalls: 3 # 批隔离允许的最大并发调用数为5        maxWaitDuration: 1s # 等待许可的最大时间为1秒</code></pre><h2 id="%E4%BD%BF%E7%94%A8" tabindex="-1">使用</h2><pre><code class="language-java">@RestControllerpublic class MyController {    //重试    @Retry(name = &quot;retryService&quot;,fallbackMethod = &quot;fallBack&quot;)    @GetMapping(&quot;test01/{msg}&quot;)    public String test01(@PathVariable String msg){        System.out.println(&quot;ok&quot;);        if(&quot;1&quot;.equals(msg)){            throw new RuntimeException(&quot;服务调用失败&quot;);        }        return &quot;ok&quot;;    }    //熔断    @CircuitBreaker(name = &quot;circuitbreakerService&quot;,fallbackMethod = &quot;fallBack&quot;)    @GetMapping(&quot;test02/{msg}&quot;)    public String test02(@PathVariable String msg){        System.out.println(&quot;ok&quot;);        if(&quot;1&quot;.equals(msg)){            throw new RuntimeException(&quot;服务调用失败&quot;);        }        return &quot;ok&quot;;    }    //限流    @RateLimiter(name = &quot;ratelimiterService&quot;,fallbackMethod = &quot;fallBack&quot;)    @GetMapping(&quot;test03&quot;)    public String test03(){        System.out.println(&quot;ok&quot;);        return &quot;ok&quot;;    }    //超时,对返回类型有要求    @TimeLimiter(name = &quot;timeoutService&quot;,fallbackMethod = &quot;fallBack2&quot;)    @GetMapping(&quot;test04/{i}&quot;)    public CompletableFuture&lt;String&gt; test04(@PathVariable int i){        return CompletableFuture.supplyAsync(()-&gt;{            System.out.println(&quot;ok&quot;);            try {                Thread.sleep(1000*i);            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }            return &quot;ok&quot;;        });    }    //批隔离    @Bulkhead(name = &quot;bulkheadService&quot;,fallbackMethod = &quot;fallBack&quot;)    @SneakyThrows    @GetMapping(&quot;test05&quot;)    public String test05(){        System.out.println(&quot;ok&quot;);        Thread.sleep(5000);        return &quot;ok&quot;;    }    public String fallBack(Throwable throwable){        System.out.println(&quot;fallBck：&quot;+throwable.getMessage());        return &quot;error&quot;;    }    //超时使用    public CompletableFuture&lt;String&gt; fallBack2(int i,Throwable throwable){        return CompletableFuture.supplyAsync(()-&gt;{            System.out.println(&quot;i:&quot;+i);            System.out.println(&quot;fallBck：&quot;+throwable.getMessage());            return &quot;error&quot;;        });    }}</code></pre><h2 id="%E7%9B%91%E5%90%AC%E5%99%A8" tabindex="-1">监听器</h2><pre><code class="language-java">@Component@Slf4j@AllArgsConstructorpublic class ResilienceListener {    private final RetryRegistry retryRegistry;    private final TimeLimiterRegistry timeLimiterRegistry;    private final CircuitBreakerRegistry circuitBreakerRegistry;    private final RateLimiterRegistry rateLimiterRegistry;    @PostConstruct    public void postConstruct() {        //  注册 Retry 事件监听器        Retry retry = retryRegistry.retry(&quot;retryService&quot;);        retry.getEventPublisher()                .onEvent(event -&gt; log.info(&quot;Retry event: {}&quot;, event));        // 注册 CircuitBreaker 事件监听器        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker(&quot;circuitbreakerService&quot;);        circuitBreaker.getEventPublisher()                .onEvent(event -&gt; log.info(&quot;CircuitBreaker Event: {}&quot;, event));        // 注册 RateLimiter 事件监听器        RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter(&quot;ratelimiterService&quot;);        rateLimiter.getEventPublisher()                .onEvent(event -&gt; log.info(&quot;RateLimiter Event: {}&quot;, event));        // 注册 TimeLimiter 事件监听器        TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter(&quot;timeoutService&quot;);        timeLimiter.getEventPublisher()                .onEvent(event -&gt; log.info(&quot;TimeLimiter Event: {}&quot;, event));    }}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[RSocket]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/rsocket" />
                <id>tag:http://www.dingqinan.com,2025-06-06:rsocket</id>
                <published>2025-06-06T01:39:38+08:00</published>
                <updated>2025-06-06T01:39:38+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="rsocket" tabindex="-1">RSocket</h1><p><strong>工作模式</strong></p><table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">数据方向</th><th style="text-align:left">背压支持</th><th style="text-align:left">典型场景</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Request-Response</strong></td><td style="text-align:left">1请求 → 1响应</td><td style="text-align:left">✔️</td><td style="text-align:left">替代 REST API</td></tr><tr><td style="text-align:left"><strong>Fire-and-Forget</strong></td><td style="text-align:left">1请求 → 无响应</td><td style="text-align:left">❌</td><td style="text-align:left">非关键性操作（如日志）</td></tr><tr><td style="text-align:left"><strong>Request-Stream</strong></td><td style="text-align:left">1请求 → N响应流</td><td style="text-align:left">✔️</td><td style="text-align:left">实时数据推送（如股票行情）</td></tr><tr><td style="text-align:left"><strong>Channel</strong></td><td style="text-align:left">M请求流 ↔ N响应流</td><td style="text-align:left">✔️</td><td style="text-align:left">全双工交互（如聊天、游戏）</td></tr></tbody></table><p><strong>优势</strong></p><ol><li><strong>多模式适配</strong>：覆盖从简单查询到实时流的所有场景。</li><li><strong>背压原生支持</strong>：避免消费者被压垮（基于 Reactive Streams）。</li><li><strong>协议无关性</strong>：可运行在 TCP、WebSocket、HTTP/2 等传输层上。</li><li><strong>高性能</strong>：二进制编码、多路复用减少连接开销。</li></ol><h2 id="%E5%8E%9F%E7%94%9F%E5%BC%80%E5%8F%91" tabindex="-1">原生开发</h2><h3 id="%E5%AF%BC%E5%85%A5%E4%BE%9D%E8%B5%96" tabindex="-1">导入依赖</h3><pre><code class="language-groovy">implementation &#39;org.springframework.boot:spring-boot-starter-rsocket&#39;</code></pre><h3 id="%E5%A4%84%E7%90%86%E5%99%A8" tabindex="-1">处理器</h3><pre><code class="language-java">@Slf4jpublic class MessageRSocketHandler implements RSocket {    /**     * 无响应，常用于如日志等     * @param payload Request payload. 附加信息     * @return     */    @Override    public Mono&lt;Void&gt; fireAndForget(Payload payload) {        String message = payload.getDataUtf8();        log.info(&quot;fireAndForget message={}&quot;, message);        return Mono.empty();//返回以供空消息    }    /**     * 传统模式     * @param payload Request payload.     * @return     */    @Override    public Mono&lt;Payload&gt; requestResponse(Payload payload) {        String message = payload.getDataUtf8();        log.info(&quot;requestResponse message={}&quot;, message);        return Mono.just(DefaultPayload.create(&quot;[echo]&quot;+message));    }    /**     * 返回流数据     * @param payload Request payload.     * @return     */    @Override    public Flux&lt;Payload&gt; requestStream(Payload payload) {        String message = payload.getDataUtf8();        log.info(&quot;requestStream message={}&quot;, message);        return Flux                .fromStream(message.chars()                        .mapToObj(c-&gt;Character.toUpperCase((char) c)))                .map(Object::toString)                .map(DefaultPayload::create);    }    /**     * 双向流     * @param payloads Stream of request payloads.     * @return     */    @Override    public Flux&lt;Payload&gt; requestChannel(Publisher&lt;Payload&gt; payloads) {        return Flux.from(payloads)                .map(Payload::getDataUtf8)                .map(msg-&gt;{                    log.info(&quot;requestStream message={}&quot;,msg);                    return msg;                })                .map(DefaultPayload::create);    }}</code></pre><h3 id="%E8%BF%9E%E6%8E%A5%E5%99%A8" tabindex="-1">连接器</h3><pre><code class="language-java">public class MessageRSocketAcceptor implements SocketAcceptor {    @Override    public Mono&lt;RSocket&gt; accept(ConnectionSetupPayload setup, RSocket sendingSocket) {        return Mono.just(new MessageRSocketHandler());//配置处理类    }}</code></pre><h3 id="%E9%85%8D%E7%BD%AE%E6%9C%8D%E5%8A%A1" tabindex="-1">配置服务</h3><pre><code class="language-java">public class MessageServer {    //用于释放任务    private static Disposable disposable;    public static void start(){        RSocketServer rSocketServer = RSocketServer.create();        rSocketServer.acceptor(new MessageRSocketAcceptor());        //采用零拷贝        rSocketServer.payloadDecoder(PayloadDecoder.ZERO_COPY);        disposable = rSocketServer                .bind(TcpServerTransport.create(6565))//使用6565端口                .subscribe();    }    public static void stop(){        //释放        disposable.dispose();    }}</code></pre><h3 id="%E6%B5%8B%E8%AF%95" tabindex="-1">测试</h3><pre><code class="language-java">@TestMethodOrder(MethodOrderer.OrderAnnotation.class)public class MessagesTests {    private static RSocket rsocket;    @BeforeAll    public static void setUpClient(){        //服务启动        MessageServer.start();        //客户端进行链接        rsocket = RSocketConnector                .connectWith(TcpClientTransport.create(6565))                .block();    }    //测试    @Test    void testFireAndForget() {        getRequestPayload()                .flatMap(payload -&gt; rsocket.fireAndForget(payload))                .blockLast(Duration.ofMinutes(1));    }    @Test    void testRequestAndResponse() {        getRequestPayload()                .flatMap(payload -&gt; rsocket.requestResponse(payload))                .doOnNext(resp -&gt; System.out.println(&quot;接收响应：&quot;+resp.getDataUtf8()))                .blockLast(Duration.ofMinutes(1));    }    @Test    void testRequestStream(){        getRequestPayload()                .flatMap(payload -&gt; rsocket.requestStream(payload))                .doOnNext(resp -&gt; System.out.println(&quot;接收响应：&quot;+resp.getDataUtf8()))                .blockLast(Duration.ofMinutes(1));    }    @Test    void testRequestChannel(){        rsocket.requestChannel(getRequestPayload())                        .doOnNext(resp -&gt; System.out.println(&quot;接收响应：&quot;+resp.getDataUtf8()))                                .blockLast(Duration.ofMinutes(1));    }    //请求数据    private static Flux&lt;Payload&gt; getRequestPayload() {        return Flux.just(&quot;java&quot;,&quot;spring&quot;,&quot;rsocket&quot;)                .delayElements(Duration.ofSeconds(1))                .map(DefaultPayload::create);    }    @AfterAll    public static void stopServer(){        MessageServer.stop();    }}</code></pre><h2 id="%E6%95%B4%E5%90%88springboot" tabindex="-1">整合SpringBoot</h2><p>定义一个数据传输的实体类</p><pre><code class="language-java">@Data@AllArgsConstructor@NoArgsConstructorpublic class Message {    private String title;    private String content;}</code></pre><h3 id="%E6%9C%8D%E5%8A%A1%E7%AB%AF" tabindex="-1">服务端</h3><h4 id="%E4%B8%9A%E5%8A%A1%E7%B1%BB" tabindex="-1">业务类</h4><pre><code class="language-java">@Servicepublic class MessageService {    public List&lt;Message&gt; list(){        return List.of(                new Message(&quot;java&quot;,&quot;java hhh&quot;),                new Message(&quot;spring&quot;,&quot;spring xxxx&quot;),                new Message(&quot;rsocket&quot;,&quot;rsocket aaa&quot;)        );    }    public Message get(String title){        return new Message(title,title+&quot;xixi&quot;);    }    public Message echo(Message message){        message.setTitle(&quot;[echo]&quot;+message.getTitle());        message.setContent(&quot;[echo]&quot;+message.getContent());        return message;    }}</code></pre><h4 id="controller" tabindex="-1">controller</h4><pre><code class="language-java">@Controller@Slf4jpublic class MessageController {    @Autowired    private MessageService messageService;    @MessageMapping(&quot;message.echo&quot;)    public Mono&lt;Message&gt; echoMessage(Mono&lt;Message&gt; message) {        return message                .doOnNext(m-&gt;messageService.echo(m))// 响应处理                .doOnNext(msg -&gt; log.info(&quot;接收消息：{}&quot;,msg));    }    @MessageMapping(&quot;message.delete&quot;)    public void deleteMessage(Mono&lt;String&gt; title) {        title.doOnNext(msg-&gt;log.info(&quot;消息删除：{}&quot;,msg)).subscribe();    }    @MessageMapping(&quot;message.list&quot;)    public Flux&lt;Message&gt; listMessages() {        return Flux.fromStream(messageService.list().stream());    }    @MessageMapping(&quot;message.get&quot;)    public Flux&lt;Message&gt; getMessages(Flux&lt;String&gt; title) {        return title.doOnNext(t-&gt;log.info(&quot;消息查询：{}&quot;,t))                .map(String::toUpperCase)                .map(messageService::get)                .delayElements(Duration.ofSeconds(1));    }}</code></pre><h4 id="%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" tabindex="-1">配置文件</h4><pre><code class="language-yaml">spring:  rsocket:    server:      port: 6869 #rsocket端口</code></pre><h3 id="%E5%AE%A2%E6%88%B7%E7%AB%AF" tabindex="-1">客户端</h3><h4 id="%E9%85%8D%E7%BD%AE%E7%B1%BB" tabindex="-1">配置类</h4><pre><code class="language-java">@Configurationpublic class RSocketConfig {    //注册    @Bean    public RSocketStrategies rSocketStrategies(){        return RSocketStrategies.builder()                //编码器                .encoders(encoders -&gt; encoders.add(new Jackson2CborEncoder()))                //解码器                .decoders(decoders -&gt; decoders.add(new Jackson2CborDecoder()))                .build();    }    @Bean    public Mono&lt;RSocketRequester&gt; rSocketRequester(RSocketRequester.Builder builder){        return Mono.just(builder.rsocketConnector(connector -&gt;                connector.reconnect(Retry.fixedDelay(2, Duration.ofSeconds(2))))//失败处理                .dataMimeType(MediaType.APPLICATION_CBOR)//数据的传输类型                .transport(TcpClientTransport.create(6869)));//设置链接端口    }}</code></pre><h4 id="%E6%B5%8B%E8%AF%95-1" tabindex="-1">测试</h4><pre><code class="language-java">@SpringBootTestpublic class SocketTests {    @Autowired    private Mono&lt;RSocketRequester&gt; requester;    @Test    public void testEcho(){        requester.map(r-&gt;r.route(&quot;message.echo&quot;)//地址                .data(new Message(&quot;java&quot;,&quot;java haha&quot;)))//请求数据                .flatMap(r-&gt;r.retrieveMono(Message.class))                .doOnNext(System.out::println).block();    }    @Test    public void testDelete(){        requester.map(r-&gt;r.route(&quot;message.delete&quot;)                        .data(&quot;rsocket...&quot;))                .flatMap(RSocketRequester.RetrieveSpec::send)                .block();    }    @Test    public void testList(){        requester.map(r-&gt;r.route(&quot;message.list&quot;))                .flatMapMany(r-&gt;r.retrieveFlux(Message.class))                .doOnNext(System.out::println).blockLast();    }    @Test    public void testGet(){        Flux&lt;String&gt; title = Flux.just(&quot;java&quot;,&quot;spring&quot;,&quot;rsocket&quot;);        requester.map(r-&gt;r.route(&quot;message.get&quot;)                        .data(title))                .flatMapMany(r-&gt;r.retrieveFlux(Message.class))                .doOnNext(System.out::println).blockLast();    }}</code></pre><h3 id="%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0" tabindex="-1">文件上传</h3><p><strong>配置类</strong></p><pre><code class="language-java">@Configurationpublic class RSocketConfig {    //策略    @Bean    public RSocketStrategies rSocketStrategies(){        return RSocketStrategies.builder()                //编码器                .encoders(e-&gt;e.add(new Jackson2CborEncoder()))                //解码器                .decoders(e-&gt;e.add(new Jackson2CborDecoder()))                //元数据注册                .metadataExtractorRegistry(registry -&gt; {                    registry.metadataToExtract(                            MimeType.valueOf(&quot;message/x.upload.file.name&quot;),                            String.class,                            &quot;file.name&quot;                    );                    registry.metadataToExtract(                            MimeType.valueOf(&quot;message/x.upload.file.extension&quot;),                            String.class,                            &quot;file.ext&quot;                    );                })                .build();    }}</code></pre><p><strong>controller</strong></p><pre><code class="language-java">@Controller@Slf4jpublic class UploadController {    @Value(&quot;upload&quot;)    private Path path;    @SneakyThrows    @MessageMapping(&quot;message.upload&quot;)    public Flux&lt;String&gt; upload(            @Headers Map&lt;String,Object&gt; headers, //元数据            @Payload Flux&lt;DataBuffer&gt; payload            ){        log.info(&quot;文件上传 path:{}&quot;,path);        //获取文件名称        var fileName = headers.get(&quot;file.name&quot;);        //获取文件后缀        var fileExt = headers.get(&quot;file.ext&quot;);        var filePath = Paths.get(fileName+&quot;.&quot;+fileExt);        log.info(&quot;filePath:{}&quot;,filePath);        //异步文件通道        AsynchronousFileChannel channel = AsynchronousFileChannel.open(                path.resolve(filePath),//解析文件路径                StandardOpenOption.CREATE,//文件创建                StandardOpenOption.WRITE//文件写入        );        return Flux.concat(DataBufferUtils.write(payload,channel)                .map(s-&gt;&quot;处理中&quot;), Mono.just(&quot;处理成功&quot;))                .doOnComplete(()-&gt; {                    try {                        //完成后关闭通道                        channel.close();                    } catch (IOException e) {                        throw new RuntimeException(e);                    }                }).onErrorReturn(&quot;失败了&quot;);//失败处理    }}</code></pre><p><strong>客户端测试</strong></p><pre><code class="language-java">@SpringBootTestpublic class SocketTests {    @Autowired    private Mono&lt;RSocketRequester&gt; requester;    @Value(&quot;classpath:t1.png&quot;)    private Resource resource;    @Test    public void testUpload(){        Flux&lt;DataBuffer&gt; flux = DataBufferUtils.read(                resource,                new DefaultDataBufferFactory(),                1024        ).doOnNext(s-&gt; System.out.println(&quot;文件上传&quot;+s));        Flux&lt;String&gt; uploadFlux = requester                .map(r-&gt;r.route(&quot;message.upload&quot;)                        .metadata(metadataSpec -&gt; {                            System.out.println(&quot;上传文件名&quot;);                            //设置文件名称                            metadataSpec.metadata(&quot;t1&quot;, MimeType.valueOf(&quot;message/x.upload.file.name&quot;));                            //设置文件后缀                            metadataSpec.metadata(&quot;png&quot;,MimeType.valueOf(&quot;message/x.upload.file.extension&quot;));                        }).data(flux))//文件上传数据                .flatMapMany(r-&gt;r.retrieveFlux(String.class))                .doOnNext(s -&gt; System.out.println(&quot;上传进度&quot;+s));        uploadFlux.blockLast();//阻塞等待    }}</code></pre><h2 id="%E4%BD%BF%E7%94%A8websocket" tabindex="-1">使用WebSocket</h2><pre><code class="language-yaml">spring:  rsocket:    server:      port: 6969 #监听端口      transport: websocket #处理协议      mapping-path: /ws #映射路径</code></pre><p>rsocket支持tcp和websocket两种协议，默认tcp</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[JdbcClient使用]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/jdbcclient-shi-yong" />
                <id>tag:http://www.dingqinan.com,2025-06-03:jdbcclient-shi-yong</id>
                <published>2025-06-03T02:51:01+08:00</published>
                <updated>2025-06-03T02:51:01+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="jdbcclient%E4%BD%BF%E7%94%A8" tabindex="-1">JdbcClient使用</h1><p>SpringBoot 3.2引入了新的 JdbcClient 用于数据库操作，JdbcClient对JdbcTemplate进行了封装，采用了 fluent API 的风格，可以进行链式调用。</p><p>对于不适合使用复杂的ORM框架，或者需要编写复杂的SQL的场景，可以使用JdbcClient自己编写SQL来操作数据库。不过JdbcClient不支持数据的批量操作和存储过程调用，对于这种情况就需要使用JdbcTemplate。</p><h2 id="%E4%BE%9D%E8%B5%96" tabindex="-1">依赖</h2><pre><code class="language-groovy">    implementation &#39;org.springframework.boot:spring-boot-starter-data-jdbc&#39;    runtimeOnly &#39;mysql:mysql-connector-java:8.0.33&#39;</code></pre><h2 id="%E9%85%8D%E7%BD%AE" tabindex="-1">配置</h2><pre><code class="language-yaml">spring:  datasource:    url: jdbc:mysql://localhost:3306/demo    username: root    password: 123456    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre><h2 id="%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8" tabindex="-1">简单使用</h2><pre><code class="language-java">    @Autowired    private JdbcClient jdbcClient;</code></pre><h3 id="%E6%9F%A5%E8%AF%A2" tabindex="-1">查询</h3><p>精准查询</p><pre><code class="language-java">        User user = jdbcClient.sql(&quot;select * from t_user where id = ?&quot;)                .param(1)                .query(User.class)                .single();</code></pre><p>批量查询</p><pre><code class="language-java">        List&lt;User&gt; list = jdbcClient.sql(&quot;select * from t_user&quot;)                .query(User.class)                .list();</code></pre><p>如果需要字段映射</p><pre><code class="language-java">        List&lt;User&gt; list = jdbcClient.sql(&quot;select username name,password pwd from t_user&quot;)                .query((rs, rowNum) -&gt;                        new User()                                .username(rs.getString(&quot;name&quot;))                                .password(rs.getString(&quot;pwd&quot;)))                .list();</code></pre><p>查询数量</p><pre><code class="language-java">        Integer count = jdbcClient.sql(&quot;select count(*) from t_user&quot;)                .query(Integer.class)                .single();</code></pre><h3 id="%E6%96%B0%E5%A2%9E" tabindex="-1">新增</h3><p>多参数时需要保证传入参数的顺序，也可以使用<code>:xxx</code>来命名</p><pre><code class="language-java">        int update = jdbcClient.sql(&quot;insert into t_user (username,password) values(:username,:password)&quot;)                .param(&quot;username&quot;, &quot;wangwu&quot;)                .param(&quot;password&quot;, &quot;123456&quot;)                .update();</code></pre><p>也可以使用map进行封装</p><pre><code class="language-java">        Map&lt;String,Object&gt;  map = new HashMap&lt;&gt;();        map.put(&quot;username&quot;,&quot;tianliu&quot;);        map.put(&quot;password&quot;,&quot;abcde&quot;);        int update = jdbcClient.sql(&quot;insert into t_user (username,password) values(:username,:password)&quot;)                .params(map)                .update();</code></pre><p>也可以直接传入pojo对象</p><pre><code class="language-java">        User user = new User().username(&quot;sunqi&quot;).password(&quot;qwe&quot;);        int update = jdbcClient.sql(&quot;insert into t_user (username,password) values(:username,:password)&quot;)                .paramSource(user)                .update();</code></pre><h3 id="%E4%BF%AE%E6%94%B9" tabindex="-1">修改</h3><pre><code class="language-java">        int update = jdbcClient.sql(&quot;update t_user set username=:username where id=:id&quot;)                .param(&quot;id&quot;, 1)                .param(&quot;username&quot;, &quot;zhangsan123&quot;)                .update();</code></pre><h3 id="%E5%88%A0%E9%99%A4" tabindex="-1">删除</h3><pre><code class="language-java">        int update = jdbcClient.sql(&quot;delete from t_user where id=:id&quot;)                .param(&quot;id&quot;, 6)                .update();</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Gradle-依赖配置]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/gradle--yi-lai-pei-zhi" />
                <id>tag:http://www.dingqinan.com,2025-06-03:gradle--yi-lai-pei-zhi</id>
                <published>2025-06-03T01:29:35+08:00</published>
                <updated>2025-06-03T01:29:35+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="gradle-%E4%BE%9D%E8%B5%96%E9%85%8D%E7%BD%AE" tabindex="-1">Gradle-依赖配置</h1><p>gradle中在dependencies模块中可以用多种标签来声明依赖范围</p><table><thead><tr><th>标签</th><th>说明</th></tr></thead><tbody><tr><td>implementation</td><td>默认的依赖方式，编译和运行时都可用，但不会暴露给其他模块</td></tr><tr><td>compileOnly</td><td>仅在编译时可用，不会打包到最终产物（如 WAR/JAR）</td></tr><tr><td>runtimeOnly</td><td>仅在运行时可用，编译时不可用</td></tr><tr><td>api</td><td>类似 <code>implementation</code>，但会传递暴露给依赖该模块的其他模块（常用于库开发）</td></tr><tr><td>testImplementation</td><td>仅用于测试代码（如 JUnit、TestNG）</td></tr><tr><td>testCompileOnly</td><td>仅在测试编译时可用</td></tr><tr><td>testRuntimeOnly</td><td>仅在测试运行时可用</td></tr><tr><td>annotationProcessor</td><td>用于注解处理器（如 Lombok、Dagger）</td></tr></tbody></table><blockquote><p>maven对应依赖范围可以查看：</p><p><a href="http://www.dingqinan.com/archives/maven-%E4%BE%9D%E8%B5%96%E8%8C%83%E5%9B%B4" target="_blank">maven-依赖范围 | qiNan</a></p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Netty笔记]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/netty-bi-ji" />
                <id>tag:http://www.dingqinan.com,2025-05-28:netty-bi-ji</id>
                <published>2025-05-28T03:54:47+08:00</published>
                <updated>2025-05-28T03:54:47+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="netty%E7%AC%94%E8%AE%B0" tabindex="-1">Netty笔记</h1><h2 id="%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B" tabindex="-1">线程模型</h2><p>Netty 采用多线程 Reactor 模型，核心线程分为两类：</p><ol><li><strong>Boss 线程组 (EventLoopGroup)</strong><ul><li>负责接受客户端连接</li><li>将新连接注册到 Worker 线程组</li><li>通常只需要1-2个线程</li></ul></li><li><strong>Worker 线程组 (EventLoopGroup)</strong><ul><li>处理已建立连接的I/O操作</li><li>执行用户的 ChannelHandler</li><li>线程数通常为 CPU 核心数×2</li></ul></li></ol><pre><code class="language-java">EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();</code></pre><h2 id="%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6" tabindex="-1">核心组件</h2><p><strong>Channel</strong></p><ul><li>网络连接的抽象，代表一个打开的连接</li><li>不同类型的实现：NIO、OIO、Epoll 等</li></ul><p><strong>EventLoop</strong></p><ul><li>事件循环，处理 Channel 的I/O事件</li><li>一个 EventLoop 可服务多个 Channel</li><li>每个 Channel 的生命周期内只绑定一个 EventLoop</li></ul><p><strong>ChannelPipeline</strong></p><ul><li>处理链，包含一系列 ChannelHandler</li><li>入站(Inbound)和出站(Outbound)处理分开</li></ul><p><strong>ChannelHandler</strong></p><ul><li>实际业务逻辑处理器</li><li>常用实现：编解码器、业务处理器等</li></ul><p><strong>ChannelFuture</strong></p><ul><li>异步操作的结果通知机制</li><li>可以添加监听器处理操作完成事件</li></ul><h2 id="%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B" tabindex="-1">工作流程</h2><p>典型 TCP 服务器工作流程：</p><ol><li>初始化两个 EventLoopGroup</li><li>创建 ServerBootstrap 并配置参数</li><li>绑定端口，启动服务</li><li>Boss 线程接受连接，分配给 Worker 线程</li><li>Worker 线程处理连接的读写事件</li><li>事件在 ChannelPipeline 中流转处理</li></ol><pre><code class="language-java">ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer&lt;SocketChannel&gt;() {     @Override     public void initChannel(SocketChannel ch) {         ch.pipeline().addLast(new MyHandler());     } });ChannelFuture f = b.bind(port).sync();</code></pre><h2 id="%E5%85%B3%E9%94%AE%E8%AE%BE%E8%AE%A1" tabindex="-1">关键设计</h2><h3 id="%E9%9B%B6%E6%8B%B7%E8%B4%9D" tabindex="-1">零拷贝</h3><p>Netty 通过以下方式实现高效数据传输：</p><p><strong>ByteBuf</strong></p><ul><li>自定义的字节容器，支持堆内存和直接内存</li><li>引用计数管理，减少内存拷贝</li><li>池化技术减少内存分配开销</li></ul><p><strong>CompositeByteBuf</strong></p><ul><li>合并多个 ByteBuf，逻辑上视为一个整体</li><li>避免合并时的内存拷贝</li></ul><p><strong>FileRegion</strong></p><ul><li>文件传输时使用零拷贝技术</li><li>直接通过 DMA 将文件内容发送到网络</li></ul><h3 id="%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86" tabindex="-1">内存管理</h3><p><strong>内存池化</strong></p><ul><li>预先分配大块内存，减少频繁分配/释放</li><li>通过 Arena 分配策略提高并发性能</li></ul><p><strong>引用计数</strong></p><ul><li>基于 ReferenceCounted 接口</li><li>当引用计数为0时自动释放资源</li></ul><p><strong>内存泄漏检测</strong></p><ul><li>通过采样方式检测未释放的 ByteBuf</li><li>开发阶段可开启严格检测模式</li></ul><h3 id="%E9%AB%98%E6%80%A7%E8%83%BD%E8%AE%BE%E8%AE%A1" tabindex="-1">高性能设计</h3><p><strong>无锁化设计</strong></p><ul><li>每个 Channel 绑定固定 EventLoop</li><li>单线程处理 Channel 的所有事件</li><li>避免多线程竞争</li></ul><p><strong>高效序列化</strong></p><ul><li>支持 Protobuf、Thrift 等高效编解码</li><li>提供 LengthFieldBasedFrameDecoder 解决粘包问题</li></ul><p><strong>灵活的线程模型</strong></p><ul><li>支持单线程、多线程模型</li><li>可配置业务线程池处理耗时操作</li></ul><h2 id="%E6%A0%B8%E5%BF%83%E6%9C%BA%E5%88%B6" tabindex="-1">核心机制</h2><h3 id="%E4%BA%8B%E4%BB%B6%E4%BC%A0%E6%92%AD%E6%9C%BA%E5%88%B6" tabindex="-1">事件传播机制</h3><p>事件在 Pipeline 中的流动方向：</p><ul><li><strong>Inbound 事件</strong>：从网络到应用方向<ul><li>如 channelRead、channelActive 等</li><li>依次调用 InboundHandler</li></ul></li><li><strong>Outbound 事件</strong>：从应用到网络方向<ul><li>如 write、connect 等</li><li>逆序调用 OutboundHandler</li></ul></li></ul><h3 id="%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F" tabindex="-1">责任链模式</h3><p>ChannelPipeline 采用责任链模式：</p><ol><li>每个 Handler 处理特定功能</li><li>可以动态添加/移除 Handler</li><li>事件依次传递，可中途终止</li></ol><pre><code class="language-java">pipeline.addLast(&quot;decoder&quot;, new MyDecoder());pipeline.addLast(&quot;encoder&quot;, new MyEncoder());pipeline.addLast(&quot;handler&quot;, new BusinessHandler());</code></pre><h3 id="%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B" tabindex="-1">异步编程模型</h3><p>基于 Future-Listener 机制：</p><ol><li>I/O 操作返回 ChannelFuture</li><li>可以同步等待或异步监听</li><li>通过回调处理操作结果</li></ol><pre><code class="language-java">ChannelFuture future = channel.write(msg);future.addListener(f -&gt; {    if (f.isSuccess()) {        // 操作成功    } else {        // 操作失败    }});</code></pre><h2 id="%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96" tabindex="-1">性能优化</h2><ol><li><strong>合理配置线程数</strong><ul><li>BossGroup 通常1个线程足够</li><li>WorkerGroup 建议2-16个线程</li></ul></li><li><strong>避免阻塞EventLoop</strong><ul><li>耗时操作应使用业务线程池</li><li>不要在执行器中执行阻塞操作</li></ul></li><li><strong>合理使用内存</strong><ul><li>优先使用池化的直接内存</li><li>及时释放引用计数对象</li></ul></li><li><strong>优化Handler设计</strong><ul><li>共享无状态的Handler</li><li>避免在Handler中创建大量临时对象</li></ul></li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[SpringSecurity笔记]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/springsecurity-bi-ji" />
                <id>tag:http://www.dingqinan.com,2025-05-28:springsecurity-bi-ji</id>
                <published>2025-05-28T03:54:10+08:00</published>
                <updated>2025-05-28T03:54:10+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="springsecurity%E7%AC%94%E8%AE%B0" tabindex="-1">SpringSecurity笔记</h1><p>Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架，是保护基于 Spring 的应用程序的事实标准</p><h2 id="%E8%BF%87%E6%BB%A4%E5%99%A8%E9%93%BE-(filter-chain)" tabindex="-1">过滤器链 (Filter Chain)</h2><p>Spring Security 本质上是一个 Servlet 过滤器链，在 web 请求到达控制器之前进行安全处理：</p><ul><li><strong>DelegatingFilterProxy</strong>：作为入口点，将请求委托给 Spring 管理的 <code>FilterChainProxy</code></li><li><strong>FilterChainProxy</strong>：包含多个安全过滤器，按顺序处理请求</li><li><strong>SecurityFilterChain</strong>：决定哪些过滤器应用于当前请求</li></ul><h2 id="%E5%85%B3%E9%94%AE%E8%BF%87%E6%BB%A4%E5%99%A8" tabindex="-1">关键过滤器</h2><ol><li><strong>SecurityContextPersistenceFilter</strong>：在请求间存储安全上下文</li><li><strong>UsernamePasswordAuthenticationFilter</strong>：处理表单登录</li><li><strong>BasicAuthenticationFilter</strong>：处理 HTTP 基本认证</li><li><strong>RememberMeAuthenticationFilter</strong>：处理&quot;记住我&quot;功能</li><li><strong>AnonymousAuthenticationFilter</strong>：为未认证用户分配匿名身份</li><li><strong>SessionManagementFilter</strong>：处理会话相关功能</li><li><strong>ExceptionTranslationFilter</strong>：处理安全异常</li><li><strong>FilterSecurityInterceptor</strong>：进行访问控制决策</li></ol><h2 id="%E8%AE%A4%E8%AF%81%E6%B5%81%E7%A8%8B" tabindex="-1">认证流程</h2><ol><li>用户提交凭证(用户名/密码等)</li><li>认证管理器(AuthenticationManager)委托给 AuthenticationProvider</li><li>Provider 使用 UserDetailsService 加载用户信息</li><li>密码编码器(PasswordEncoder)验证密码</li><li>认证成功后构建 Authentication 对象</li><li>安全上下文(SecurityContextHolder)存储认证信息</li></ol><pre><code class="language-java">Authentication authentication = authenticationManager.authenticate(    new UsernamePasswordAuthenticationToken(username, password));SecurityContextHolder.getContext().setAuthentication(authentication);</code></pre><h2 id="%E6%8E%88%E6%9D%83%E6%B5%81%E7%A8%8B" tabindex="-1">授权流程</h2><ol><li>访问受保护资源时触发授权检查</li><li>AccessDecisionManager 基于投票机制做决策</li><li>配置的投票器(Voter)根据安全规则投票</li><li>基于投票结果允许或拒绝访问</li></ol><h2 id="%E5%AE%89%E5%85%A8%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BC%A0%E6%92%AD" tabindex="-1">安全上下文传播</h2><ul><li><strong>SecurityContextHolder</strong>：存储当前安全上下文</li><li><strong>ThreadLocal</strong> 策略：默认将上下文绑定到当前线程</li><li><strong>SecurityContextRepository</strong>：跨请求持久化上下文(通常使用 HTTP Session)</li></ul><h2 id="%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6" tabindex="-1">核心组件</h2><ol><li><strong>Authentication</strong>：表示认证主体及其凭证</li><li><strong>UserDetails</strong>：用户核心信息接口</li><li><strong>UserDetailsService</strong>：加载用户特定数据</li><li><strong>GrantedAuthority</strong>：授予主体的权限</li><li><strong>AuthenticationManager</strong>：认证的主要接口</li><li><strong>ProviderManager</strong>：AuthenticationManager 的标准实现</li><li><strong>AccessDecisionManager</strong>：授权决策的核心接口</li></ol><h2 id="%E8%A7%84%E5%88%99%E9%85%8D%E7%BD%AE" tabindex="-1">规则配置</h2><p>Spring Security 的配置主要通过两个关键类：</p><ol><li><strong>WebSecurityConfigurerAdapter</strong> (旧版)或 <strong>SecurityFilterChain</strong> (新版)</li><li><strong>HttpSecurity</strong>：配置具体的 HTTP 安全规则</li></ol><pre><code class="language-java">@Configuration@EnableWebSecuritypublic class SecurityConfig {        @Bean    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {        http            .authorizeHttpRequests(auth -&gt; auth                .requestMatchers(&quot;/public/**&quot;).permitAll()                .anyRequest().authenticated()            )            .formLogin(form -&gt; form                .loginPage(&quot;/login&quot;)                .permitAll()            );        return http.build();    }}</code></pre><h2 id="%E5%AE%89%E5%85%A8%E6%9C%BA%E5%88%B6" tabindex="-1">安全机制</h2><ol><li><strong>CSRF 防护</strong>：默认启用，防止跨站请求伪造</li><li><strong>CORS 支持</strong>：跨域资源共享配置</li><li><strong>Session 管理</strong>：会话固定保护、并发控制等</li><li><strong>HTTP 安全头</strong>：自动添加安全相关 HTTP 头</li><li><strong>方法级安全</strong>：通过注解实现方法级别的访问控制</li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[etcd笔记]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/etcd-bi-ji" />
                <id>tag:http://www.dingqinan.com,2025-05-25:etcd-bi-ji</id>
                <published>2025-05-25T05:46:36+08:00</published>
                <updated>2025-05-25T05:46:56+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="etcd%E7%AC%94%E8%AE%B0" tabindex="-1">etcd笔记</h1><h2 id="%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4" tabindex="-1">常用命令</h2><table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td>put key value</td><td>设置或更新某个键的值</td></tr><tr><td>get key</td><td>获取指定键的值</td></tr><tr><td>get key --hex</td><td>以16进制格式返回</td></tr><tr><td>get key1 key2</td><td>获取区间的值，开半区间：左闭右开</td></tr><tr><td>get --prefix key</td><td>获取指定前缀的值</td></tr><tr><td>get --prefix key --limit n</td><td>限制返回的熟练为n</td></tr><tr><td>get --from-key key</td><td>按key的字典顺序获取，读取字典顺序大于等于key的值</td></tr><tr><td>get key -w=json</td><td>以json格式返回key的详细信息</td></tr><tr><td>get key --rev=n</td><td>查看key的第n个版本的值</td></tr><tr><td>del key</td><td>删除一个key</td></tr><tr><td>del key1 key2</td><td>删除一个范围的key</td></tr><tr><td>dev key --prev-kv</td><td>返回被删除的键值</td></tr><tr><td>watch key</td><td>监听一个key，可以连续监听</td></tr></tbody></table><h2 id="lease(%E7%A7%9F%E7%BA%A6)" tabindex="-1">lease(租约)</h2><p>类似redis的TTL，etcd中的键值对可以绑定到租约上，实现存活周期控制。<br />应用客户端可以为etcd里的键授予租约，一旦租约到期，租约就会过期并且所有附带的键都会被删除</p><pre><code class="language-shell">#授予租约 TTL为30setcdctl lease grant 30#键租约赋予keyetcdctl put --lease=694d96fdc4880b07 k1 v1#撤销租约etcdctl lease revoke 694d96fdc4880b07#刷新租期etcdctl lease keep-alive 694d96fdc4880b07#查询租约信息etcdctl lease timtolive 694d96fdc4880b07#查看租约绑定的keyetcdctl lease timtolive --keys 694d96fdc4880b07</code></pre><h2 id="%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86" tabindex="-1">权限管理</h2><table><thead><tr><th>命令</th><th>说明</th></tr></thead><tbody><tr><td>auth status</td><td>查看当前权限状态</td></tr><tr><td>auth enable</td><td>开启权限，需要root用户</td></tr><tr><td>user add u1</td><td>创建用户</td></tr><tr><td>user del u1</td><td>删除用户</td></tr><tr><td>user passwd u1</td><td>修改密码</td></tr><tr><td>user list</td><td>查看用户列表</td></tr><tr><td>user get u1</td><td>查看指定用户绑定角色</td></tr><tr><td>role add r1</td><td>添加角色</td></tr><tr><td>role grant-permission r1 readwrite key</td><td>赋予角色对一个目录的读写权限。权限包括read、write、readwrite</td></tr><tr><td>role revoke-permission r1 key</td><td>回收角色</td></tr><tr><td>role del r1</td><td>删除角色</td></tr><tr><td>role list</td><td>角色列表</td></tr><tr><td>user grant-role u1 r1</td><td>赋予用户角色</td></tr></tbody></table><blockquote><p>可以通过命令后添加<code>--user=xxx</code>的方式指定使用的用户，使用<code>--password=xxx</code>指定密码</p></blockquote><h2 id="readindex" tabindex="-1">ReadIndex</h2><p>ReadIndex机制就是实现线性读功能的重要机制，当server收到一个线性读请求时，</p><p>如果自己是leader节点，则会立即返回，如果自己是follower节点的话，则会向server节点发送一个readIndex请求来获取最新的已提交日志索引（committed index）；</p><p>sever收到readIndex请求，还要向其他follower节点发送心跳来防止脑裂异常场景，一半以上节点确认leader节点身份后，leader才会把committed index返回给follower请求节点；</p><p>follower节点收到committed index后会和自己的applied index比较，如果applied index值大于committed index时，才表示自己的状态机数据是最新的，这时才会去通知读请求去状态机读取数据。</p><blockquote><p>串行读和线性读</p><p>虽然etcd能保证一致性，但是保证强一致性是需要消耗性能的，会牺牲部分吞吐量。因此当出现数据一致性问题时，这时就有串行读与线性读的区别了。数据一致性问题又是由于etcd只有leader节点能处理写请求写数据导致。</p><p>（1）当收到写请求时，leader节点先将请求内容持久化到WAL日志中，并且广播给所有follower节点；</p><p>（2）如果leader节点有收到一半以上节点的持久化成功消息，那么该请求对应的日志会被标识为已提交；</p><p>（3）各个节点的server会异步从raft模块获取已提交的日志条目，应用到状态机（boltdb）。</p><h3 id="%E4%B8%B2%E8%A1%8C%E8%AF%BB" tabindex="-1">串行读</h3><p>直接读取对应server状态机（boltdb）的数据，不会经过Raft协议与集群交互，具有低延迟、高吞吐量的特点，但是可能存在读取结果不一致的情况。</p><h3 id="%E7%BA%BF%E6%80%A7%E8%AF%BB" tabindex="-1">线性读</h3><p>etcd默认的是线性读，在读取数据时会经过raft协议与集群交互保证数据一致，所以在延迟和吞吐量方面会比串行读有所降低。但是能保证数据的一致性。</p><p>线性读保证数据一致性的原理，离不开ReadIndex机制。</p></blockquote><h2 id="mvcc" tabindex="-1">MVCC</h2><p>ETCD的键值存储以及版本信息涉及到一个B树treeIndex和一个B+树boltdb。</p><p>treeIndex的作用是作为辅助内存索引，加速对键的范围查询。treeIndex里面会存储key和对应的版本号。</p><p>boltdb里面保存了key的值以及历史版本信息。</p><p>读取数据时，先从treeIndex中获取key的版本号，再以版本号作为boltdb的key，从boltdb中得到具体的value的信息。</p><p>实际读取数据时，还涉及到一个buffer缓冲区，在读取数据时，并非所有请求都要经过boltdb。在访问boltdb前，ETCD会先从buffer中查找是否有key对应的值。如果有就可以直接返回而不用经过boltdb。ETCD通过buffer可以实现一部分的性能提高和数据一致性问题解决。因为buffer是将数据暂存在内存中，可以减少boltdb处理中对磁盘的读写操作；另外buffer会暂未提交的数据，此时可能boltdb里面没有，但是在buffer里面可以提前拿到。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[langgraph断点]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/langgraph-duan-dian" />
                <id>tag:http://www.dingqinan.com,2025-05-18:langgraph-duan-dian</id>
                <published>2025-05-18T05:51:55+08:00</published>
                <updated>2025-05-18T05:51:55+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="langgraph%E6%96%AD%E7%82%B9" tabindex="-1">langgraph断点</h1><p>断点是langgraph提供的在流程中供用户暂停修改的机制</p><h2 id="%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8" tabindex="-1">基本使用</h2><p><strong>大模型</strong></p><pre><code class="language-python">from langchain_openai import ChatOpenAIimport osllm = ChatOpenAI(    api_key=os.environ.get(&quot;ALI_API_KEY&quot;),    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,    model=&quot;qwen-max&quot;)</code></pre><p><strong>工具</strong></p><pre><code class="language-python">from langchain_core.tools import tool@tooldef multiply(a:int,b:int)-&gt;int:    &quot;&quot;&quot;计算a乘b&quot;&quot;&quot;    return a*b@tooldef add(a:int,b:int)-&gt;int:    &quot;&quot;&quot;计算a加b&quot;&quot;&quot;    return a+b@tooldef divide(a:int,b:int)-&gt;float:    &quot;&quot;&quot;计算a除b&quot;&quot;&quot;    return a/b#绑定tools = [add,multiply,divide]# parallel_tool_calls关闭工具并行llm_with_tools = llm.bind_tools(tools,parallel_tool_calls=False)</code></pre><p><strong>节点和状态</strong></p><pre><code class="language-python">from typing_extensions import TypedDictfrom typing import Annotatedfrom langgraph.graph.message import add_messagesfrom langchain_core.messages import AnyMessage, HumanMessage, SystemMessage# 提示词sys_msg = SystemMessage(&quot;你是一个乐于助人的助手，负责对一组输入进行算数运算&quot;)#状态class MessageState(TypedDict):    # 每次更新进行追加    messages : Annotated[list[AnyMessage],add_messages]# 节点def assistant(state : MessageState):    return {&quot;messages&quot;:[llm_with_tools.invoke([sys_msg]+state[&quot;messages&quot;])]}</code></pre><p><strong>定义图</strong></p><pre><code class="language-python">from langgraph.graph import StateGraph,STARTfrom langgraph.prebuilt import ToolNode,tools_conditionfrom langgraph.checkpoint.memory import MemorySaverbuilder = StateGraph(MessageState)# 定义节点builder.add_node(&quot;assistant&quot;, assistant)builder.add_node(&quot;tools&quot;,ToolNode(tools))#定义边builder.add_edge(START,&quot;assistant&quot;)builder.add_conditional_edges(&quot;assistant&quot;,tools_condition)# 循环工具到llm的边builder.add_edge(&quot;tools&quot;,&quot;assistant&quot;)memory = MemorySaver()#在tools节点之前进行打断graph = builder.compile(interrupt_before=[&quot;tools&quot;],checkpointer=memory)</code></pre><p><strong>测试</strong></p><pre><code class="language-python">thread = {&quot;configurable&quot;:{&quot;thread_id&quot;:&quot;1&quot;}}# 流式输出for event in graph.stream({&quot;messages&quot;:HumanMessage(&quot;3加4是多少&quot;)},thread,stream_mode=&quot;values&quot;):    event[&quot;messages&quot;][-1].pretty_print()#查看图中打断的方法state = graph.get_state(thread)print(state.next)#获取用户反馈user_approval = input(&quot;是否调用工具？（是/否）：&quot;)if user_approval.lower() == &quot;是&quot;:    #继续执行,通过传None实现    for event in graph.stream(None,thread,stream_mode=&quot;values&quot;):        event[&quot;messages&quot;][-1].pretty_print()else:    print(&quot;用户操作已取消&quot;)</code></pre><h2 id="%E6%96%AD%E7%82%B9%E6%97%B6%E4%BF%AE%E6%94%B9%E7%8A%B6%E6%80%81" tabindex="-1">断点时修改状态</h2><p>首先修改打断的时机，在调用大模型前进行断点</p><pre><code class="language-python">#在assistant节点之前进行打断graph = builder.compile(interrupt_before=[&quot;assistant&quot;],checkpointer=memory)</code></pre><p><strong>测试</strong></p><pre><code class="language-python">thread = {&quot;configurable&quot;:{&quot;thread_id&quot;:&quot;1&quot;}}# 流式输出for event in graph.stream({&quot;messages&quot;:&quot;3加4是多少&quot;},thread,stream_mode=&quot;values&quot;):    event[&quot;messages&quot;][-1].pretty_print()#修改状态graph.update_state(    thread,    {&quot;messages&quot;:[HumanMessage(&quot;改成3乘4是多少&quot;)]})# 继续执行,通过传None实现for event in graph.stream(None, thread, stream_mode=&quot;values&quot;):    event[&quot;messages&quot;][-1].pretty_print()</code></pre><h3 id="%E7%94%A8%E5%A4%96%E9%83%A8%E6%96%B9%E5%BC%8F%E4%BF%AE%E6%94%B9" tabindex="-1">用外部方式修改</h3><p>创建一个空节点专门用来修改状态</p><p><strong>空节点</strong></p><pre><code class="language-python">def human_feedback(state : MessagesState):    pass</code></pre><p><strong>定义图</strong></p><p>加入这个空节点，并添加断点</p><pre><code class="language-python">builder = StateGraph(MessagesState)# 定义节点builder.add_node(&quot;assistant&quot;, assistant)builder.add_node(&quot;tools&quot;,ToolNode(tools))builder.add_node(&quot;human_feedback&quot;,human_feedback)#定义边builder.add_edge(START,&quot;human_feedback&quot;)builder.add_edge(&quot;human_feedback&quot;,&quot;assistant&quot;)builder.add_conditional_edges(&quot;assistant&quot;,tools_condition)builder.add_edge(&quot;tools&quot;,&quot;assistant&quot;)memory = MemorySaver()#在human_feedback节点之前进行打断graph = builder.compile(interrupt_before=[&quot;human_feedback&quot;],checkpointer=memory)</code></pre><p><strong>测试</strong></p><pre><code class="language-python">thread = {&quot;configurable&quot;:{&quot;thread_id&quot;:&quot;1&quot;}}# 流式输出for event in graph.stream({&quot;messages&quot;:&quot;3加4是多少&quot;},thread,stream_mode=&quot;values&quot;):    event[&quot;messages&quot;][-1].pretty_print()#用户输出user_approval = input(&quot;输入你的修改&quot;)#修改状态graph.update_state(    thread,    {&quot;messages&quot;:user_approval},    as_node=&quot;human_feedback&quot;)# 继续执行,通过传None实现for event in graph.stream(None, thread, stream_mode=&quot;values&quot;):    event[&quot;messages&quot;][-1].pretty_print()</code></pre><h2 id="%E5%8A%A8%E6%80%81%E6%96%AD%E7%82%B9" tabindex="-1">动态断点</h2><p>通过抛出NodeInterrupt异常可用实现动态断点的效果</p><p><strong>定义状态和节点</strong></p><pre><code class="language-python">from typing_extensions import TypedDictfrom langgraph.errors import NodeInterrupt#状态class State(TypedDict):    input:str#节点def step_1(state:State):    print(&quot;step_1&quot;)    return statedef step_2(state:State):    if len(state[&quot;input&quot;]) &gt; 5:        raise NodeInterrupt(f&quot;收到的长度超过5个字符&quot;)    print(&quot;step_2&quot;)    return statedef step_3(state:State):    print(&quot;step_3&quot;)    return state</code></pre><p><strong>构建图</strong></p><pre><code class="language-python">builder = StateGraph(State)builder.add_node(&quot;step_1&quot;,step_1)builder.add_node(&quot;step_2&quot;,step_2)builder.add_node(&quot;step_3&quot;,step_3)builder.add_edge(START,&quot;step_1&quot;)builder.add_edge(&quot;step_1&quot;,&quot;step_2&quot;)builder.add_edge(&quot;step_2&quot;,&quot;step_3&quot;)builder.add_edge(&quot;step_3&quot;,END)memory = MemorySaver()graph = builder.compile(checkpointer=memory)</code></pre><p><strong>测试</strong></p><pre><code class="language-python">thread = {&quot;configurable&quot;:{&quot;thread_id&quot;:&quot;1&quot;}}for event in graph.stream({&quot;input&quot;:&quot;你好123123&quot;},thread,stream_mode=&quot;values&quot;):    print(event)#查看下一步计划state = graph.get_state(thread)print(state.next)#查看中断状态print(state.tasks)#更新状态graph.update_state(    thread,    {&quot;input&quot;:&quot;你好&quot;})#重新运行for event in graph.stream(None,thread,stream_mode=&quot;values&quot;):    print(event)</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[langgraph自定义输入输出状态]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/langgraph-zi-ding-yi-shu-ru-shu-chu-zhuang-tai" />
                <id>tag:http://www.dingqinan.com,2025-05-18:langgraph-zi-ding-yi-shu-ru-shu-chu-zhuang-tai</id>
                <published>2025-05-18T05:36:37+08:00</published>
                <updated>2025-05-18T05:36:37+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="langgraph%E8%87%AA%E5%AE%9A%E4%B9%89%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA%E7%8A%B6%E6%80%81" tabindex="-1">langgraph自定义输入输出状态</h1><p><strong>定义状态</strong></p><pre><code class="language-python">class InputState(TypedDict):    question : strclass OutputState(TypedDict):    answer : strclass OverallState(TypedDict):    question : str    answer : str    notes : str</code></pre><p><strong>节点</strong></p><pre><code class="language-python">def thinking_node(state : InputState):    return {&quot;answer&quot;:&quot;你好&quot;,&quot;notes&quot;:&quot;他是张三&quot;}def answer_node(state : OverallState) -&gt; OutputState:    return {&quot;answer&quot;:&quot;再见&quot;}</code></pre><p><strong>定义图</strong></p><pre><code class="language-python">#通过input和output参数设置进入流程和结束流程的状态类型graph = StateGraph(OverallState,input=InputState,output=OutputState)graph.add_node(&quot;thinking_node&quot;,thinking_node)graph.add_node(&quot;answer_node&quot;,answer_node)graph.add_edge(START,&quot;thinking_node&quot;)graph.add_edge(&quot;thinking_node&quot;,&quot;answer_node&quot;)graph.add_edge(&quot;answer_node&quot;,END)g = graph.compile()</code></pre><p><strong>测试</strong></p><pre><code class="language-python">s = g.invoke({&quot;question&quot;:&quot;hello&quot;})print(s)</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[langgraph使用]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/langgraph-shi-yong" />
                <id>tag:http://www.dingqinan.com,2025-05-18:langgraph-shi-yong</id>
                <published>2025-05-18T05:30:11+08:00</published>
                <updated>2025-05-18T05:30:11+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="langgraph%E4%BD%BF%E7%94%A8" tabindex="-1">langgraph使用</h1><h2 id="%E7%AE%80%E5%8D%95%E4%B8%8A%E6%89%8B" tabindex="-1">简单上手</h2><p><strong>状态</strong></p><pre><code class="language-python">from typing_extensions import TypedDictclass State(TypedDict):    graph_state: str</code></pre><p><strong>节点</strong></p><pre><code class="language-python">def node_1(state: State):    print(&quot;node 1&quot;)    return {&quot;graph_state&quot;: state[&quot;graph_state&quot;]+&quot;我很&quot;}def node_2(state: State):    print(&quot;node 2&quot;)    return {&quot;graph_state&quot;: state[&quot;graph_state&quot;]+&quot;高兴！&quot;}def node_3(state: State):    print(&quot;node 3&quot;)    return {&quot;graph_state&quot;: state[&quot;graph_state&quot;]+&quot;悲伤！&quot;}</code></pre><p><strong>条件边</strong></p><pre><code class="language-python">from typing import Literalimport randomdef decide_mood(state: State) -&gt; Literal[&quot;node_2&quot;,&quot;node_3&quot;]:    # 通常使用状态来决定下一个要访问的节点    #user_input = state[&quot;graph_state&quot;]    #这里随机访问    if random.random() &lt; 0.5:        return &quot;node_2&quot;    return &quot;node_3&quot;</code></pre><p><strong>定义图</strong></p><pre><code class="language-python">from langgraph.graph import StateGraph,START,ENDbuilder = StateGraph(State)#设置节点builder.add_node(node_1)builder.add_node(node_2)builder.add_node(node_3)#添加边builder.add_edge(START,&quot;node_1&quot;)#条件边builder.add_conditional_edges(&quot;node_1&quot;,decide_mood)builder.add_edge(&quot;node_2&quot;,END)builder.add_edge(&quot;node_3&quot;,END)graph = builder.compile()</code></pre><p><strong>测试</strong></p><pre><code class="language-python">s = graph.invoke({&quot;graph_state&quot;:&quot;你好&quot;})print(s)</code></pre><h2 id="%E6%95%B4%E5%90%88%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%92%8Ctools" tabindex="-1">整合大模型和Tools</h2><p><strong>大模型</strong></p><pre><code class="language-python">from langchain_openai import ChatOpenAIimport osllm = ChatOpenAI(    api_key=os.environ.get(&quot;ALI_API_KEY&quot;),    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,    model=&quot;qwen-max&quot;)</code></pre><p><strong>定义工具并绑定</strong></p><pre><code class="language-python">from langchain_core.tools import tool@tooldef multiply(a:int,b:int)-&gt;int:    &quot;&quot;&quot;计算a乘b&quot;&quot;&quot;    return a*b#绑定llm_with_tools = llm.bind_tools([multiply])</code></pre><p><strong>状态</strong></p><pre><code class="language-python">from typing_extensions import TypedDictfrom typing import Annotatedfrom langgraph.graph.message import add_messagesfrom langchain_core.messages import AnyMessage, HumanMessageclass MessageState(TypedDict):    # 每次更新进行追加    messages : Annotated[list[AnyMessage],add_messages]</code></pre><p><strong>节点</strong></p><pre><code class="language-python">def tool_calling_llm(state : MessageState):    return {&quot;messages&quot;:[llm_with_tools.invoke(state[&quot;messages&quot;])]}</code></pre><p><strong>构建图</strong></p><pre><code class="language-python">builder = StateGraph(MessageState)builder.add_node(&quot;tool_calling_llm&quot;, tool_calling_llm)builder.add_edge(START,&quot;tool_calling_llm&quot;)builder.add_edge(&quot;tool_calling_llm&quot;,END)graph = builder.compile()</code></pre><p><strong>测试</strong></p><pre><code class="language-python">messages = graph.invoke({&quot;messages&quot;:HumanMessage(&quot;2*3等于多少&quot;)})for m in messages[&quot;messages&quot;]:    m.pretty_print()</code></pre><blockquote><p>此时是无法自动调用tools的</p></blockquote><h3 id="%E8%87%AA%E5%8A%A8%E8%B0%83%E7%94%A8tools" tabindex="-1">自动调用tools</h3><p>构建图时将</p><pre><code class="language-python">from langgraph.prebuilt import ToolNode,tools_conditionbuilder = StateGraph(MessageState)builder.add_node(&quot;tool_calling_llm&quot;, tool_calling_llm)#将工具封装成节点builder.add_node(&quot;tools&quot;,ToolNode([multiply]))builder.add_edge(START,&quot;tool_calling_llm&quot;)#如果llm的结果是工具调用则路由到工具，否则路由的ENDbuilder.add_conditional_edges(&quot;tool_calling_llm&quot;,tools_condition)builder.add_edge(&quot;tools&quot;,END)graph = builder.compile()</code></pre><h2 id="react%E6%9E%B6%E6%9E%84" tabindex="-1">ReAct架构</h2><p><strong>定义工具并绑定</strong></p><pre><code class="language-python">@tooldef multiply(a:int,b:int)-&gt;int:    &quot;&quot;&quot;计算a乘b&quot;&quot;&quot;    return a*b@tooldef add(a:int,b:int)-&gt;int:    &quot;&quot;&quot;计算a加b&quot;&quot;&quot;    return a+b@tooldef divide(a:int,b:int)-&gt;float:    &quot;&quot;&quot;计算a除b&quot;&quot;&quot;    return a/b#绑定tools = [add,multiply,divide]# parallel_tool_calls关闭工具并行llm_with_tools = llm.bind_tools(tools,parallel_tool_calls=False)</code></pre><p><strong>定义状态和节点</strong></p><pre><code class="language-python"># 提示词sys_msg = SystemMessage(&quot;你是一个乐于助人的助手，负责对一组输入进行算数运算&quot;)#状态class MessageState(TypedDict):    # 每次更新进行追加    messages : Annotated[list[AnyMessage],add_messages]# 节点def assistant(state : MessageState):    return {&quot;messages&quot;:[llm_with_tools.invoke([sys_msg]+state[&quot;messages&quot;])]}</code></pre><p><strong>定义图</strong></p><pre><code class="language-python">builder = StateGraph(MessageState)# 定义节点builder.add_node(&quot;assistant&quot;, assistant)builder.add_node(&quot;tools&quot;,ToolNode(tools))#定义边builder.add_edge(START,&quot;assistant&quot;)builder.add_conditional_edges(&quot;assistant&quot;,tools_condition)# 循环工具到llm的边builder.add_edge(&quot;tools&quot;,&quot;assistant&quot;)graph = builder.compile()</code></pre><p><strong>测试</strong></p><pre><code class="language-python">messages = graph.invoke({&quot;messages&quot;:HumanMessage(&quot;3加4再乘2最后除以3是多少&quot;)})for m in messages[&quot;messages&quot;]:    m.pretty_print()</code></pre><h2 id="%E5%AE%9E%E7%8E%B0%E8%AE%B0%E5%BF%86" tabindex="-1">实现记忆</h2><p>在构建图时，设置checkpointer检查点以实现记忆功能</p><pre><code class="language-python">from langgraph.checkpoint.memory import MemorySavermemory = MemorySaver()graph = builder.compile(checkpointer=memory)</code></pre><p><strong>测试</strong></p><pre><code class="language-python">#定义一个线程id 这里将记忆点写死为1config = {&quot;configurable&quot;:{&quot;thread_id&quot;:&quot;1&quot;}}#调用messages1 = graph.invoke({&quot;messages&quot;:HumanMessage(&quot;3加4是多少&quot;)},config)for m in messages1[&quot;messages&quot;]:    m.pretty_print()messages2 = graph.invoke({&quot;messages&quot;:HumanMessage(&quot;再乘2是多少&quot;)},config)for m in messages2[&quot;messages&quot;]:    m.pretty_print()</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[langserve使用]]></title>
                <link rel="alternate" type="text/html" href="http://www.dingqinan.com/archives/langserve-shi-yong" />
                <id>tag:http://www.dingqinan.com,2025-05-16:langserve-shi-yong</id>
                <published>2025-05-16T01:32:33+08:00</published>
                <updated>2025-05-16T01:32:33+08:00</updated>
                <author>
                    <name>起男</name>
                    <uri>http://www.dingqinan.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="langserve%E4%BD%BF%E7%94%A8" tabindex="-1">langserve使用</h1><h2 id="%E6%9E%84%E5%BB%BAchain" tabindex="-1">构建chain</h2><pre><code class="language-python">import osfrom langchain_core.output_parsers import StrOutputParserfrom langchain_core.prompts import ChatPromptTemplatefrom langchain_openai import ChatOpenAI# 大模型llm = ChatOpenAI(    api_key=os.environ.get(&quot;ALI_API_KEY&quot;),    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,    model=&quot;qwen-max&quot;)# 提示词模板prompt = ChatPromptTemplate.from_messages([    (&quot;system&quot;,&quot;你是一个园区的客服&quot;),    (&quot;user&quot;,&quot;{text}&quot;)])#结果解析器parser = StrOutputParser()# lcelchain = prompt| llm | parser</code></pre><h2 id="langserve%E6%9C%8D%E5%8A%A1%E7%AB%AF" tabindex="-1">langserve服务端</h2><pre><code class="language-python">from fastapi import FastAPIfrom langserve import add_routes# 构建服务端对象app = FastAPI(title=&quot;大模型园区助手&quot;, version=&quot;1.0&quot;, description=&quot;基于langchain构建的大模型助手&quot;)# 添加路由信息add_routes(    app,    chain,    path=&quot;/langchain_demo&quot;)#启动服务if __name__ == &quot;__main__&quot;:    import uvicorn    uvicorn.run(app, host=&quot;0.0.0.0&quot;, port=8000)</code></pre><p>服务启动后，可用通过浏览器<code>http://localhost:8000/langchain_demo/playground/</code>访问</p><h2 id="langserve%E5%AE%A2%E6%88%B7%E7%AB%AF" tabindex="-1">langserve客户端</h2><pre><code class="language-python">from langserve import RemoteRunnableclient = RemoteRunnable(&quot;http://127.0.0.1:8000/langchain_demo&quot;)s = client.invoke({&quot;text&quot;:&quot;你是谁&quot;})print(s)</code></pre>]]>
                </content>
            </entry>
</feed>
