[{"content":"200. 岛屿数量 岛屿数量\n思路 :\n使用 标记数组 标记每一个走过的路地 ，并且计数。\n如果没有走过路地，那么从当前路地节点走一遍bfs 跑完所有路地节点 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #define x first #define y second typedef pair\u0026lt;int,int\u0026gt; PII ; const int N = 310 ; bool st[N][N]; int cnt ; PII q[N*2]; int n,m; void init() { memset(st,0,sizeof(st)); cnt = 0 ; n = 0 ; m = 0 ; } int wx[] = {1,-1,0,0} ; int wy[] = {0,0,1,-1}; void bfs(int sx,int sy,vector\u0026lt;vector\u0026lt;char\u0026gt;\u0026gt;\u0026amp; g) { int hh = 0 , tt = 0 ; q[0] = {sx,sy}; st[sx][sy] = 1 ; while(hh \u0026lt;= tt) { PII t = q[hh ++ ]; for(int i = 0 ; i \u0026lt; 4 ; i ++ ) { int dx = t.x + wx[i]; int dy = t.y + wy[i]; if(dx \u0026lt; 0 || dx \u0026gt;= n || dy \u0026lt; 0 || dy \u0026gt;= m ) continue; if(g[dx][dy] == \u0026#39;0\u0026#39; || st[dx][dy]) continue ; q[++tt] = {dx,dy}; st[dx][dy] = 1; } } } class Solution { public: int numIslands(vector\u0026lt;vector\u0026lt;char\u0026gt;\u0026gt;\u0026amp; grid) { init() ; n = grid.size() ; m = grid[0].size() ; for(int i = 0 ; i \u0026lt; n ; i ++ ) { for(int j = 0 ; j \u0026lt; m ; j ++ ) { if(grid[i][j] == \u0026#39;1\u0026#39; \u0026amp;\u0026amp; !st[i][j]) { bfs(i,j,grid); cnt ++ ; } } } return cnt ; } }; 994.腐烂的橘子 腐烂的橘子\n思路 :\n和岛屿数量差不多 。无非是让 烂橘子 进行bfs\n计算每个烂橘子到新鲜橘子的最短距离。 然后求一个最大值即可\n因为 n,m = 10 . 所以直接这样子暴力了\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #define x first #define y second typedef pair\u0026lt;int,int\u0026gt; PII ; const int N = 20 ; const int INF = 0x3f3f3f3f ; int dist[N][N]; PII q[N*N]; int n,m; bool st[20][20]; void init() { memset(dist,INF,sizeof(dist)); n = 0 ; m = 0 ; } int wx[] = {1,-1,0,0} ; int wy[] = {0,0,1,-1}; void bfs(int sx,int sy,vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; g) { memset(st,0,sizeof(st)); int hh = 0 , tt = 0 ; q[0] = {sx,sy}; dist[sx][sy] = 0 ; st[sx][sy] = 1; while(hh \u0026lt;= tt) { PII t = q[hh ++ ]; for(int i = 0 ; i \u0026lt; 4 ; i ++ ) { int dx = t.x + wx[i]; int dy = t.y + wy[i]; if(dx \u0026lt; 0 || dx \u0026gt;= n || dy \u0026lt; 0 || dy \u0026gt;= m ) continue; if(g[dx][dy] == 0 || g[dx][dy] == 2) continue ; if(st[dx][dy]) continue; st[dx][dy] = 1; dist[dx][dy] = min(dist[dx][dy], dist[t.x][t.y] + 1); q[++tt] = {dx,dy}; } } } class Solution { public: int orangesRotting(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; grid) { init(); n = grid.size(); m = grid[0].size(); for(int i = 0 ; i \u0026lt; n ; i ++ ) for(int j = 0 ; j \u0026lt; m ; j ++ ) { if(grid[i][j] == 2) { bfs(i,j,grid); } } int ans = 0; for(int i = 0 ; i \u0026lt; n ; i ++ ) for(int j = 0 ; j \u0026lt; m ; j ++ ) { if(grid[i][j] == 1 ) { if (dist[i][j] == INF)return -1; else ans = max(ans , dist[i][j]); } } return ans ; } }; 当然也可以使用 多源 bfs 进行优化\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class Solution { public: int orangesRotting(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; grid) { int n = grid.size(), m = grid[0].size(); int wx[4] = {1, -1, 0, 0}; int wy[4] = {0, 0, 1, -1}; queue\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; q; int fresh = 0; // 所有烂橘子同时入队，作为多个源点 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; m; j++) { if (grid[i][j] == 2) { q.push({i, j}); } else if (grid[i][j] == 1) { fresh++; } } } if (fresh == 0) return 0; int minutes = 0; while (!q.empty()) { int sz = q.size(); bool infected = false; // 同一分钟内，当前层全部扩散完 for (int i = 0; i \u0026lt; sz; i++) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k \u0026lt; 4; k++) { int dx = x + wx[k]; int dy = y + wy[k]; if (dx \u0026lt; 0 || dx \u0026gt;= n || dy \u0026lt; 0 || dy \u0026gt;= m) continue; if (grid[dx][dy] != 1) continue; grid[dx][dy] = 2; // 标记已腐烂，避免重复入队 q.push({dx, dy}); fresh--; infected = true; } } if (infected) minutes++; } return fresh == 0 ? minutes : -1; } }; 207. 课程表 课程表\n思路 :\n主要考察的是拓扑排序 拓扑排序我们的方法是\n查找所有入度为0的点进去队列，然后减少其关联的点\n然后再重新入所有入度为0的点\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 const int N = 4e3 + 10 ; int h[N] , e[N], ne[N], idx; int d[N] ; int num ; void init(){ memset(h,-1,sizeof(h)); memset(e,0,sizeof(e)); memset(ne,0,sizeof(ne)); memset(d,0,sizeof(d)); idx = 0 ; num = 0 ; } // a -\u0026gt; b void add(int a,int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx ++ ; } bool topsort() { int cnt = 0 ; queue\u0026lt;int\u0026gt; q; for(int i = 0; i \u0026lt; num ; i ++ ) { if(!d[i]) q.push(i); } while(!q.empty()) { int t = q.front() ; q.pop () ; cnt ++ ; for(int i = h[t] ; i!= -1 ; i = ne[i]) { int j = e[i] ; d[j] -- ; if(!d[j]) q.push(j); } } return cnt == num; } class Solution { public: bool canFinish(int numCourses, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; prerequisites) { init(); num = numCourses ; for(auto x : prerequisites) { int a = x[0]; int b = x[1]; add(a,b); d[b] ++ ; } return topsort(); } }; 208. 实现 Trie (前缀树) 实现 Trie (前缀树)\nTire 的实现思路\n定义 son[p][u] ; 表示第 p 层节点是否有值 u\n然后再进行顺序即可\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Trie { static const int N = 4e4 + 10 ; int son[N][26]; int idx ; bool isEnd[N]; public: Trie() { memset(son,0,sizeof(son)); memset(isEnd,0,sizeof(isEnd)); idx = 0 ; } void insert(string word) { int p = 0 ; for(auto x : word) { int u = x-\u0026#39;a\u0026#39;; if(!son[p][u]) son[p][u] = ++idx; p = son[p][u]; } isEnd[p] = 1; } bool search(string word) { int p = 0 ; for(auto x : word) { int u = x-\u0026#39;a\u0026#39;; if(son[p][u]) p = son[p][u]; else return false; } return isEnd[p]; } bool startsWith(string prefix) { int p = 0 ; for(auto x : prefix) { int u = x-\u0026#39;a\u0026#39;; if(son[p][u]) p = son[p][u]; else return false; } return p ; } }; /** * Your Trie object will be instantiated and called as such: * Trie* obj = new Trie(); * obj-\u0026gt;insert(word); * bool param_2 = obj-\u0026gt;search(word); * bool param_3 = obj-\u0026gt;startsWith(prefix); */ ","date":"2026-08-11T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/leetcode-map-0x01/","title":"[LeetCode][hot100] 图论"},{"content":"前言 由于不少面试最近都增加了 Ai Coding 的题目 特别先来熟悉一下\n看了一眼 LeetCode 和 牛客 发现只有牛客有 所以单独开辟一个专栏看一下\nHelloWorld 做这道题 可以看到 右边有一个 AI Agent。 可以沟通 第一题很简单直接给出了做法 。\n具体做法预计就是\n把自己的思路写到 一个文档里面 。\n然后@Agent 进行执行\n感觉和日常开发一样。 另外 我们最终文件是可以编辑的 。\n不过这里竟然没有满分，只有97分\n我们发现过程分占了很多 70 分去了 。结果分只有30分 。看来 AI coding 更注重的不是算出结果\n这个过程分没有给出合理的解释 不过看现有的维度有\n对话的轮次\n用时\n接受 AI 建议\n拒绝 AI 建议\n运行的次数\nToken 输入\nToken 输出\n因此可以总结出第一个 高分思路\n目的 :\nxxxx 思路 :\n根据 xxxx 通过 xxxx 实现 xxxx 要求 :\n优化文字回答内容,要求清晰简单 下面我们继续看下一题吧\n卡牌翻翻乐 这道题 一开始编写了一个 soultion.md . 之前开发的习惯, 喜欢使用 md 存放思路 然后@给Agent。因为之前输入的prompts不能太长。使用文件传入 Cursor会优化\n简单给了 要求和示例 以及 题目给的描述 就直接给AI了 这里增加了一轮回答 因为我 md 内容中是 logic.js , AI访问不到 ,让我重复提交了一次 commit 应该是 js/logic.js 第一次抽奖没抽出来，功能性只有70分。看了一下 发现我实现的效果 和 预期效果差点意思。所以又使用 upgrade 根据产品的角度额外调整了一下 soultion.md :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 目的 : 完善 @js/logic.js 的逻辑。 使得 @index.html 能够实现 翻牌数字匹配的游戏 思路 : function flipCard(board, index) board 是牌桌数组。 每个位置为 null，或一个 [value, faceUp] 二元数组。 value 是整数，faceUp 是布尔值。 null 表示该位置当前没有牌。 index 使用从 0 开始的数组下标。 调用时，index 一定指向一个 [value, false]；牌桌中至多有一个 [value, true]。 返回处理后的完整牌桌，数组长度和元素格式不变。可以修改传入数组，也可以返回新数组。 要求 : 1. 保留文件末尾的导出方式： if (typeof module !== \u0026#34;undefined\u0026#34;) module.exports = { flipCard }; if (typeof window !== \u0026#34;undefined\u0026#34;) window.flipCard = flipCard; 2. 优化文字回答的内容,要求简单清晰 示例： flipCard( [[1, false], [2, false], [1, false], [2, false]], 0 ); // 返回： // [[1, true], [2, false], [1, false], [2, false]] 保留文件末尾的导出方式： if (typeof module !== \u0026#34;undefined\u0026#34;) module.exports = { flipCard }; if (typeof window !== \u0026#34;undefined\u0026#34;) window.flipCard = flipCard; upgrade.md :\n1 2 3 4 5 6 7 8 9 优化现有的 @js/logic.js 逻辑, 保证 @index.html 中的逻辑符合如下内容 : 目的 : 1. 我们如果点开两个不一样的牌, 需要清空两个牌的展示。不需要保留最后一次牌的内容 相关资料可以参考 : @solution.md 第一次提交后的分数很低 。 这个评价也很扯，认为我直接引用文件 没有思考\n然后第二次 我尝试把文件的内容塞到 prompts里面进行求解。 并且一次命中 。得分也只有77分\n虽为单轮委托且缺乏显式校验与迭代过程 我不确定这个显示校验是什么，写一个ut ？ 迭代过程可能就是缺少了多轮对话 毕竟我们是一次命中的 。\n总结 做题不和开发一样 。 尽量使用 prompts的形式进行 避免使用 markdown @file 的形式\n对于不再同一个层级的包或者是其他文件可能会有权限问题，需要额外 @\n需要增加一些产品类型的描述。 细化每个功能的细节\n数珠游龙 目的 : 实现 @js/logic.js 中的内容 。 在 @index.html 实现一个 数珠游龙 的游戏 。\nfunction insertNumber(chain, index, value) chain 是一个由整数组成的一维数组，也可能为空。 index 是插入位置，范围为 0 到 chain.length；0 表示最前方，chain.length 表示最后方。 value 是本次插入的整数。 返回本次操作结束后的完整一维整数数组。可以修改传入数组，也可以返回新数组。\n功能需求 :\n用户操作会发送一个小球进入到本次队列 对于相同数字 例如 \u0026lt;2,2\u0026gt;,\u0026lt;3,3\u0026gt; 需要进行消除。 如果是不同的数字 直接插入到当前位置例如 \u0026lt;2,3,4\u0026gt; 在\u0026lt;3,4\u0026gt;中间插入了一个数5那么队列变成 \u0026lt;2,3,5,4\u0026gt; 思路 :\n根据 xxxx 通过 xxxx 实现 xxxx 要求 :\n优化文字回答内容,要求清晰简单\n保留文件末尾的导出方式： if (typeof module !== \u0026ldquo;undefined\u0026rdquo;) module.exports = { insertNumber }; if (typeof window !== \u0026ldquo;undefined\u0026rdquo;) window.insertNumber = insertNumber;\n示例： insertNumber([2, 4, 1], 2, 3); // 返回 [2, 4, 3, 1]\n优化 @js/logic.js 保证 @index.html 满足:\n消除逻辑不对，对于初始队列 \u0026lt;4,2,2,4,1,3,3,2\u0026gt; 我在末尾插入一个 2 前面的数字就都消失了 可以参考 @demo.ncdemo 的实现 优化 @js/logic.js 保证 @index.html 满足:\n消除逻辑不正确 , 我们需要保证对于 \u0026lt;2,2\u0026gt; 相同的形式是被消除的 。 对于\u0026lt;2,2,2\u0026gt;也应该全部消除，而不是只保留\u0026lt;2\u0026gt; ","date":"2026-08-11T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/nowcoder-aicoding-0x01/","title":"AiCoding "},{"content":"最终实现 能够自动创建对应的请假审批 整体流程如下 注册 ApproveHandle 这里由上层 Agent 判断调度\nApproveHandle 支持 ApproveAdd 和 ApproveFind 分别对应创建审批和查找审批\n细节 ApproveAdd 这里的逻辑很简单\n根据 Agent 传递的 input 解析出对应的 参数\n根据参数build对应的 Service实现\n然后透传 用户原始信息去 Create\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // Call 解析审批类型与原文，并创建审批 func (a *ApprovalAdd) Call(ctx context.Context, input string) (string, error) { // ... out, err := a.outputparser.Parse(input) if err != nil { return \u0026#34;\u0026#34;, err } data := out.(map[string]any) var approvalType float64 if t, ok := data[\u0026#34;type\u0026#34;]; ok { switch v := t.(type) { case float64: approvalType = v case int: approvalType = float64(v) } } userInput := input if v, ok := data[\u0026#34;input\u0026#34;].(string); ok \u0026amp;\u0026amp; v != \u0026#34;\u0026#34; { userInput = v } // 保留用户原文，同时补充工时上下文供二级抽取使用 enriched := fmt.Sprintf(\u0026#34;%s\\n%s\u0026#34;, userInput, workHoursHint) ap, err := approval.NewApproval(a.svc, model.ApprovalType(approvalType)) if err != nil { return \u0026#34;\u0026#34;, err } id, err := ap.Create(ctx, enriched) if err != nil { return \u0026#34;\u0026#34;, err } return Success + \u0026#34;\\n created approval id : \u0026#34; + id, nil } 我们可以看到一个 Create实现，这里由额外进行了一次LLM操作，根据透传的 input 进行额外的解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (m *GoOut) Create(ctx context.Context, input string) (string, error) { out, err := chains.Predict(ctx, m.c, map[string]any{ langchain.Input: input, }, chains.WithCallback(m.svc.Callbacks)) if err != nil { return \u0026#34;\u0026#34;, xerr.WithMessage(err, \u0026#34;chains.Predict : \u0026#34;+input) } v, err := m.outPutParser.Parse(out) if err != nil { return \u0026#34;\u0026#34;, xerr.WithMessage(err, \u0026#34;m.outPutParser.Parse\u0026#34;) } var data domain.GoOut if err := decodeJSON(v, \u0026amp;data); err != nil { return \u0026#34;\u0026#34;, xerr.WithMessage(err, \u0026#34;decode goOut\u0026#34;) } return createApproval(ctx, m.svc, domain.Approval{ Type: int(model.GoOutApproval), GoOut: \u0026amp;data, }) } ApproveFind Approve Find 同样 内容更加简单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 func (a *ApprovalFind) Call(ctx context.Context, input string) (string, error) { if a.Callback != nil { a.Callback.HandleText(ctx, \u0026#34;approval find start input : \u0026#34;+input) } out, err := a.outputparser.Parse(input) if err != nil { return \u0026#34;\u0026#34;, err } data := out.(map[string]any) if data == nil { data = make(map[string]any) } // List API 使用当前登录用户；type 为「我提交/我审核」 listType := float64(model.ApprovalSubmit) if t, ok := data[\u0026#34;type\u0026#34;].(float64); ok \u0026amp;\u0026amp; t \u0026gt; 0 { listType = t } data[\u0026#34;type\u0026#34;] = int(listType) if data[\u0026#34;count\u0026#34;] == nil { data[\u0026#34;count\u0026#34;] = 10 } if data[\u0026#34;page\u0026#34;] == nil { data[\u0026#34;page\u0026#34;] = 1 } res, err := curl.GetRequest(token.GetTokenStr(ctx), a.svc.Config.Host+\u0026#34;/v1/approval/list\u0026#34;, data) if err != nil { return \u0026#34;\u0026#34;, err } if a.Callback != nil { a.Callback.HandleText(ctx, \u0026#34;approval find end data : \u0026#34;+string(res)) } return ResParser(res, domain.ApprovalFind, nil) } ","date":"2026-08-10T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-helper-rest-0x03/","title":"AutoApprove"},{"content":"最终实现 用户可以通过 @机器人, 实现总结自己所发的内容 整体流程如下 前端发起调用 传入 Time Range\n后端获取对应的群聊消息 。整理聚合给 LLM\nLLM 处理之后进行输出\n细节 创建的时候 制定一下 templatePrompt ，然后自己开辟一条chain 1 2 3 4 5 6 7 8 9 10 11 12 func NewChatLogHandle(svc *svc.ServiceContext) *ChatLogHandle { return \u0026amp;ChatLogHandle{ svc: svc, chains: chains.NewLLMChain(svc.LLMs, prompts.NewPromptTemplate( _defaultChatLogPrompts, []string{\u0026#34;input\u0026#34;}, )), } } func (c *ChatLogHandle) Chains() chains.Chain { return chains.NewTransform(c.transform, nil, nil) } 去数据库获取对应的MSG 然后整合给LLM 进行输出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func (c *ChatLogHandle) transform(ctx context.Context, inputs map[string]any, opts ...chains.ChainCallOption) (map[string]any, error) { //... msgs, err := c.chatLog(ctx, cid, startTime, endTime) if err != nil { return nil, err } //... res, err := chains.Call(ctx, c.chains, map[string]any{ \u0026#34;input\u0026#34;: msgs, }, opts...) if err != nil { return nil, err } text, ok := res[langchain.OutPut].(string) if !ok { return nil, chains.ErrInvalidOutputValues } //... if err != nil { return nil, err } return map[string]any{ langchain.OutPut: string(b), }, nil } ","date":"2026-08-10T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-helper-rest-0x03/","title":"SummaryChat"},{"content":"最终实现 我们能够通过前端上传 pdf 进行知识库的更新\n我们可以通过 自然语言询问 文章里面的内容 。 并且更新对应的内容\n同样我们可以重新上传文章进行更新\n细节 大体框架 我们提供了一个 KnowLedgeHandle 其中 . 介绍 我们这个 Handle 是做什么内容的 1 2 3 4 5 Name : knowledge Desc : This is the company\u0026#39;s knowledge base. Use for: summarizing uploaded documents, retrieving knowledge content, answering questions about employee manuals, attendance, leave, approval process, and other office policies. Also use for updating/ingesting the knowledge base from uploaded files, and for revising/correcting knowledge facts (e.g. 更改工作时间). Prefer this destination whenever the user mentions 知识库 / 文档 / 手册 / 总结 / 更改手册内容 这个Handle 对应3个 Tool\nKnowledgeQA 用于进行 RAG 回答问题\nKnowledgePatch 部分更新 vector 中的内容\nKnowledgeUpdate 更新全量的文章\nknowledgeQa 我们看一下 knowledgeQa 这里做的内容\n获取 Store 也就是获取我们的 Redis CLi\n后去其中的 Content 内容 。并且存入到 qa 里面 。 这里使用了 RAG 相似度匹配\n然后去询问 AI\n整个流程大致这样，相似度匹配也是框架帮我们做的事\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func (k *KnowledgeRetrievalQA) Call(ctx context.Context, input string) (string, error) { var err error if k.qa == nil { k.store, err = getKnowledgeStore(ctx, k.svc) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;连接知识库向量存储失败(请确认 Redis Stack 已启动): %v\u0026#34;, err) } // topK 取多块，目录/全文结构类问题才有足够上下文 k.qa = chains.NewRetrievalQAFromLLM(k.svc.LLMs, vectorstores.ToRetriever(k.store, 5)) } res, err := chains.Predict(ctx, k.qa, map[string]any{ \u0026#34;query\u0026#34;: input, }) if err != nil { return \u0026#34;\u0026#34;, err } return `请基于检索资料回答用户。写作要求： 1. 直接输出当前有效规定，语气像员工手册正文，条理清晰。 2. 若资料里同一主题有多条说法，只保留最新有效内容合并进答案；不要对比新旧，不要出现「修订」「原规定」「冲突」「优先于」「最新版本」等词。 3. 用户问某类政策时，尽量给出完整条目，而不是只讲变更点。 检索资料： ` + res, nil } KnowledgeUPdate KnowledgeUPdate 就好像是一个 Python 脚本，可以看作想是一个skills\n不过这里还是有几点需要注意的\nParam . 我们可以看到 Update 在操作之前解析了参数 path,name,time . 这几个参数从哪里来？\n我们上传完文件之后会传入到 放入到 memory 中\n1 2 3 4 5 6 7 8 9 func (l *chat) File(ctx context.Context, files []*domain.FileResp) (err error) { // ... err = l.memory.SaveContext(ctx, map[string]any{ // 将文件信息保存到记忆机制中 langchain.OutPut: string(b), // langchain.OutPut: `[{\u0026#34;Path\u0026#34;:\u0026#34;upload/xxx.pdf\u0026#34;,\u0026#34;Name\u0026#34;:\u0026#34;...\u0026#34;,\u0026#34;Time\u0026#34;:\u0026#34;...\u0026#34;}]` }, map[string]any{ langchain.OutPut: \u0026#34;uploaded files\u0026#34;, // AI 的回复内容 }) // ... } 然后框架会自动 把 memory 挂载到 history 中 。 从而模型就能够看到这部分memory\n1 2 newValues := memory.LoadMemoryVariables(...) // → {\u0026#34;history\u0026#34;: \u0026#34;Human: [{Path,Name,Time}]\\nAI: uploaded files\\n...\u0026#34;} 合并进 fullValues 通过 PDFprocess 解析pdf文件 然后通过 redis 重新更新 Vector 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 func NewKnowledgeUpdate(svc *svc.ServiceContext) *KnowledgeUpdate { return \u0026amp;KnowledgeUpdate{ //. .. outPutParser: outputparserx.NewStructured([]outputparserx.ResponseSchema{ { Name: \u0026#34;path\u0026#34;, Description: \u0026#34;the path to file\u0026#34;, }, { Name: \u0026#34;name\u0026#34;, Description: \u0026#34;the name to file\u0026#34;, }, { Name: \u0026#34;time\u0026#34;, Description: \u0026#34;file update time\u0026#34;, }, }), } } func (k *KnowledgeUpdate) Call(ctx context.Context, input string) (string, error) { // ... var data any data, err := k.outPutParser.Parse(input) // ... file := data.(map[string]any) filePath := resolveKnowledgeFilePath(fmt.Sprintf(\u0026#34;%v\u0026#34;, file[\u0026#34;path\u0026#34;])) if _, err := os.Stat(filePath); err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;文件不存在: %s\u0026#34;, filePath) } pdfProcessor := NewPDFProcessor() chunkedDocuments, err := pdfProcessor.LoadAndSplitPDF(ctx, filePath, 800, 100) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;PDF处理失败: %v\u0026#34;, err) } if len(chunkedDocuments) == 0 { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;PDF处理失败: 未生成任何文档块\u0026#34;) } // ... if _, err = k.store.AddDocuments(ctx, chunkedDocuments); err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;写入知识库失败: %v\u0026#34;, err) } return fmt.Sprintf(\u0026#34;%s：文件 %s 已入库，共 %d 个文档块（已覆盖同内容旧数据）。若用户还要求总结或问答文档内容，请继续调用 knowledge_retrieval_qa；若仅要求更新知识库，再给出 Final Answer。\u0026#34;, knowledgeUpdateSuccessPrefix, filepath.Base(filePath), len(chunkedDocuments)), nil } 遗留问题 memory 的实现\n多个Handle共用memory\n方案 ： chatid = uid + busienss_type 根据业务进行区分 ","date":"2026-08-09T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-helper-knowledge-index-0x02/","title":"Rag-Knowledge"},{"content":"最终实现 在聊天窗口让 AI 创建一个待办。给出具体信息之后 会创建对应的待办信息 整体流程如下 前端 调用 /call 接口传入自然语言信息\n后端 拼接已经注册的 路由Handle + input 去查找 然后返回对应的 key\n1 2 3 4 5 6 给你一份路由清单，请根据用户输入选出一个最合适的， 如果都不合适就返回Default: intput : req -front - xxxx 并且根据 xxx 格式返回 查找到对应的 Handle 然后去访问对应 Handle 支持的 Tool\n使用 langchain.OneShotAgent 再次询问AI 应该访问哪一个 Tool\n调用Tool 发起请求 。返回响应\n细节 第一次 LLM 拼接 prompts 我们在 NewRouter 的时候 就把所有已经实现的 Handler 写进去 . 这里会根据 handle 进行整合 opt\n1 2 3 4 5 6 7 func NewRouter(llm llms.Model, handler []Handler, opts ...Option) *Router { opt := executorDefaultOptions(handler) for _, o := range opts { o(\u0026amp;opt) } // ... } Option 会返回一个 prompt . 类型 PromptTemplate\n我们可以看到 这里会遍历没用过 hander 然后拼接他们的 Name() 和 Descritipn()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type Option func(options *Options) // executorDefaultOptions 创建默认的路由器配置选项 func executorDefaultOptions(handler []Handler) Options { return Options{ prompt: createPrompt(handler), memory: memory.NewSimple(), } } func createPrompt(handler []Handler) prompts.PromptTemplate { return prompts.PromptTemplate{ Template: MULTI_PROMPT_ROUTER_TEMPLATE, InputVariables: []string{_input}, TemplateFormat: prompts.TemplateFormatGoTemplate, PartialVariables: map[string]any{ _destinations: handlerDestinations(handler), _formatting: _outputparser.GetFormatInstructions(), }, } } 这里可以看到 handle 的定义如下 , 和 Tool 一样自带说明书\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func NewTodoHandle(svc *svc.ServiceContext) *TodoHandle { //... } // Name 返回处理器名称，用于路由识别 func (t *TodoHandle) Name() string { return \u0026#34;todo\u0026#34; } // Description 返回处理器描述，帮助 AI 理解何时使用此处理器 func (t *TodoHandle) Description() string { return \u0026#34;suitable for todo processing, such as todo creation, query, modification, deletion, etc\u0026#34; } // Chains 返回处理链，使用转换链包装代理输出 func (t *TodoHandle) Chains() chains.Chain { return chains.NewTransform(t.transform, nil, nil) } 寻找Handle进行处理 因为我们在提示词中增加了,格式化返回 . 我们可以从 会使用 outputparser 解析对应的 output\n我们通过 chains.Call(ctx, r.handlers[next].Chains(), inputs) 调用对应的 Tool\n1 2 3 \u0026lt;\u0026lt; FORMATTING \u0026gt;\u0026gt; Return a markdown code snippet with a JSON object formatted to look like: {{.formatting}} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func (r *Router) Call(ctx context.Context, inputs map[string]any, options ...chains.ChainCallOption) (map[string]any, error) { // ... out, err := r.outputparser.Parse(text.(string)) if err != nil { return nil, err } if r.callbacks != nil { r.callbacks.HandleChainEnd(ctx, map[string]any{ \u0026#34;out\u0026#34;: out, }) } data := out.(map[string]string) next, ok := data[_destinations] if !ok || next == Empty || r.handlers[next] == nil { if r.emptyHandle != nil { return chains.Call(ctx, r.emptyHandle.Chains(), inputs) } return nil, ErrNotHandles } return chains.Call(ctx, r.handlers[next].Chains(), inputs) } Agents Find tool 我们可以看到 TodoHandle 支持了 todoAdd和 TodoFind 这两个Tool\n对于每一个 Tool 都会有格式化输出配置 同样也有 Name +Desc 。Tool 的基本定义 一个说明书\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 func NewTodoHandle(svc *svc.ServiceContext) *TodoHandle { return \u0026amp;TodoHandle{ svc: svc, // 创建包含待办工具的 AI 代理 agentChain: agents.NewExecutor(agents.NewOneShotAgent(svc.LLMs, []tools.Tool{ toolx.NewTodoAdd(svc), // 待办创建工具 toolx.NewTodoFind(svc), // 待办查询工具 })), } } func NewTodoAdd(svc *svc.ServiceContext) *TodoAdd { return \u0026amp;TodoAdd{ svc: svc, callback: svc.Callbacks, // 配置结构化输出解析器，定义待办事项的字段格式 outputparser: outputparserx.NewStructured([]outputparserx.ResponseSchema{ { Name: \u0026#34;title\u0026#34;, Description: \u0026#34;todo title\u0026#34;, }, { Name: \u0026#34;deadlineAt\u0026#34;, Description: \u0026#34;calculate the final deadline based on the time information entered by the user and combined with today\u0026#39;s time. a Unix time\u0026#34;, Type: \u0026#34;int64\u0026#34;, }, { Name: \u0026#34;desc\u0026#34;, Description: \u0026#34;todo description\u0026#34;, }, { Name: \u0026#34;executeIds\u0026#34;, Description: \u0026#34;list of participating users in the backlog. the data type is a set of string ids. none is empty\u0026#34;, Type: \u0026#34;[]string\u0026#34;, }, }), } } // Name 返回工具名称，用于 AI 代理识别 func (t *TodoAdd) Name() string { return \u0026#34;todo_add\u0026#34; } // Description 返回工具描述和使用说明，包含输出格式指令 func (t *TodoAdd) Description() string { template := ` a todo add interface. use when you need to create a todo. keep Chinese output. ` + t.outputparser.GetFormatInstructions() return template } 我们看 NewOneShotAgent ，这里也是单独创建了一个 LLM Chain 。并且使用自己的 getMrklPrompt 传入了 tools 用于寻找 tool .方式和我们实现 Router 一样\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func NewOneShotAgent(llm llms.Model, tools []tools.Tool, opts ...Option) *OneShotZeroAgent { options := mrklDefaultOptions() for _, opt := range opts { opt(\u0026amp;options) } return \u0026amp;OneShotZeroAgent{ Chain: chains.NewLLMChain( llm, options.getMrklPrompt(tools), chains.WithCallback(options.callbacksHandler), ), Tools: tools, OutputKey: options.outputKey, CallbacksHandler: options.callbacksHandler, } } ","date":"2026-08-09T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-helper-rest-0x02/","title":"RouterChain"},{"content":"鉴权 \u0026amp; 白名单 使用 group 加上 Jwt Parse 实现 Check\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func (h *User) InitRegister(engine *gin.Engine) { g0 := engine.Group(\u0026#34;v1/user\u0026#34;) g0.POST(\u0026#34;/login\u0026#34;, h.Login) g1 := engine.Group(\u0026#34;v1/user\u0026#34;, h.svcCtx.Jwt.Handler) g1.GET(\u0026#34;/:id\u0026#34;, h.Info) g1.POST(\u0026#34;\u0026#34;, h.Create) g1.PUT(\u0026#34;\u0026#34;, h.Edit) g1.DELETE(\u0026#34;/:id\u0026#34;, h.Delete) g1.GET(\u0026#34;/list\u0026#34;, h.List) g1.POST(\u0026#34;/password\u0026#34;, h.UpdatePassword) } func (m *Jwt) Handler(ctx *gin.Context) { r, err := m.tokenParser.ParseWithContext(ctx.Request) if err != nil { httpx.FailWithErr(ctx, err) ctx.Abort() return } ctx.Request = r ctx.Next() } 同样的 我们在 Websocket 那边也是使用 JWT 进行鉴权\n1 2 3 4 5 6 7 8 9 10 11 12 func (s *Ws) auth(r *http.Request) (uid string, tokenStr string, err error) { // 优先从请求头中获取WebSocket认证Token（保持原有功能） tok := r.Header.Get(\u0026#34;websocket\u0026#34;) //... // 解析JWT Token，获取用户身份信息 claims, tokenStr, err := s.tokenParser.ParseToken(tok) if err != nil { return \u0026#34;\u0026#34;, \u0026#34;\u0026#34;, err } //... } 用户模块 支持 Login、Info、Create、Edit、Delete、List、UpdatePassword\n这一部分基本上都是 CRUD 并且没有做并发控制。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // User 用户业务逻辑接口 type User interface { // Login 用户登录验证 Login(ctx context.Context, req *domain.LoginReq) (resp *domain.LoginResp, err error) // Info 根据ID获取用户信息 Info(ctx context.Context, req *domain.IdPathReq) (resp *domain.User, err error) // Create 创建新用户 Create(ctx context.Context, req *domain.User) (err error) // Edit 更新用户信息 Edit(ctx context.Context, req *domain.User) (err error) // Delete 删除指定用户 Delete(ctx context.Context, req *domain.IdPathReq) (err error) // List 分页查询用户列表 List(ctx context.Context, req *domain.UserListReq) (resp *domain.UserListResp, err error) // UpdatePassword 更新用户密码 UpdatePassword(ctx context.Context, req *domain.UpdatePasswordReq) (err error) } 表结构设计 1 2 3 4 5 6 7 8 9 10 11 type User struct { ID primitive.ObjectID `bson:\u0026#34;_id,omitempty\u0026#34; json:\u0026#34;id,omitempty\u0026#34;` // TODO: Fill your own fields Name string `bson:\u0026#34;name\u0026#34; json:\u0026#34;name\u0026#34;` Password string `bson:\u0026#34;Password\u0026#34; json:\u0026#34;Password\u0026#34;` Status int `bson:\u0026#34;status\u0026#34; json:\u0026#34;status\u0026#34;` IsAdmin bool `bson:\u0026#34;isAdmin\u0026#34; json:\u0026#34;isAdmin\u0026#34;` // 是否为管理员 UpdateAt int64 `bson:\u0026#34;updateAt\u0026#34; json:\u0026#34;updateAt\u0026#34;` CreateAt int64 `bson:\u0026#34;createAt\u0026#34; json:\u0026#34;createAt\u0026#34;` } Department Soa 方法 会根据所有的部门构建一个部门树进行展示\n全量查出所有部门 按 ParentPath 分组 否则按照父路径放进 groupDep 1 2 3 公司(A) ParentPath=\u0026#34;\u0026#34; └─ 技术部(B) ParentPath=\u0026#34;:A\u0026#34; └─ 后端组(C) ParentPath=\u0026#34;:A:B\u0026#34; 其他的 设计基本上都是 CRUD\n1 2 3 4 5 6 7 8 9 10 11 type Department interface { Soa(ctx context.Context) (resp *domain.DepartmentSoaResp, err error) Info(ctx context.Context, req *domain.IdPathReq) (resp *domain.Department, err error) Create(ctx context.Context, req *domain.Department) (err error) Edit(ctx context.Context, req *domain.Department) (err error) Delete(ctx context.Context, req *domain.IdPathReq) (err error) SetDepartmentUsers(ctx context.Context, req *domain.SetDepartmentUser) (err error) AddDepartmentUser(ctx context.Context, req *domain.AddDepartmentUser) (err error) RemoveDepartmentUser(ctx context.Context, req *domain.RemoveDepartmentUser) (err error) DepartmentUserInfo(ctx context.Context, req *domain.IdPathReq) (resp *domain.Department, err error) } 表结构设计 设计一个 部门数据表 一个部门用户表\n1 2 3 4 5 6 7 8 9 10 11 12 type Department struct { ID primitive.ObjectID `bson:\u0026#34;_id,omitempty\u0026#34; json:\u0026#34;id,omitempty\u0026#34;` // 部门ID Name string `bson:\u0026#34;name\u0026#34; json:\u0026#34;name\u0026#34;` // 部门名称 ParentId string `bson:\u0026#34;parentId,omitempty\u0026#34; json:\u0026#34;parentId,omitempty\u0026#34;` // 父部门ID ParentPath string `bson:\u0026#34;parentPath,omitempty\u0026#34; json:\u0026#34;parentPath,omitempty\u0026#34;` // 父部门路径 Level int `bson:\u0026#34;level\u0026#34; json:\u0026#34;level\u0026#34;` // 部门层级 LeaderId string `bson:\u0026#34;leaderId,omitempty\u0026#34; json:\u0026#34;leaderId,omitempty\u0026#34;` // 部门负责人ID Leader string `bson:\u0026#34;leader,omitempty\u0026#34; json:\u0026#34;leader,omitempty\u0026#34;` // 部门负责人姓名 Count int64 `bson:\u0026#34;count\u0026#34; json:\u0026#34;count\u0026#34;` // 部门人数 UpdateAt int64 `bson:\u0026#34;updateAt,omitempty\u0026#34; json:\u0026#34;updateAt,omitempty\u0026#34;` // 更新时间 CreateAt int64 `bson:\u0026#34;createAt,omitempty\u0026#34; json:\u0026#34;createAt,omitempty\u0026#34;` // 创建时间 } 1 2 3 4 5 6 7 8 9 type DepartmentUser struct { ID primitive.ObjectID `bson:\u0026#34;_id,omitempty\u0026#34; json:\u0026#34;id,omitempty\u0026#34;` // 关联ID DepId string `bson:\u0026#34;depId,omitempty\u0026#34;` // 部门ID UserId string `bson:\u0026#34;userId,omitempty\u0026#34;` // 用户ID UpdateAt int64 `bson:\u0026#34;updateAt,omitempty\u0026#34; json:\u0026#34;updateAt,omitempty\u0026#34;` // 更新时间 CreateAt int64 `bson:\u0026#34;createAt,omitempty\u0026#34; json:\u0026#34;createAt,omitempty\u0026#34;` // 创建时间 } Todo 基本上也都是CRUD接口\n1 2 3 4 5 6 7 8 9 type Todo interface { Info(ctx context.Context, req *domain.IdPathReq) (resp *domain.TodoInfoResp, err error) // 获取待办事项详情 Create(ctx context.Context, req *domain.Todo) (resp *domain.IdResp, err error) // 创建待办事项 Edit(ctx context.Context, req *domain.Todo) (err error) // 编辑待办事项 Delete(ctx context.Context, req *domain.IdPathReq) (err error) // 删除待办事项 Finish(ctx context.Context, req *domain.FinishedTodoReq) (err error) // 完成待办事项 CreateRecord(ctx context.Context, req *domain.TodoRecord) (err error) // 创建待办操作记录 List(ctx context.Context, req *domain.TodoListReq) (resp *domain.TodoListResp, err error) // 获取待办事项列表 } 表结构设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 type TodoRecord struct { TodoId string `json:\u0026#34;todoId,omitempty\u0026#34;` UserId string `json:\u0026#34;userId,omitempty\u0026#34;` UserName string `json:\u0026#34;userName,omitempty\u0026#34;` Content string `json:\u0026#34;content,omitempty\u0026#34;` Image string `json:\u0026#34;image,omitempty\u0026#34;` CreateAt int64 `json:\u0026#34;createAt,omitempty\u0026#34;` } type Todo struct { ID string `json:\u0026#34;id,omitempty\u0026#34;` CreatorId string `json:\u0026#34;creatorId,omitempty\u0026#34;` CreatorName string `json:\u0026#34;creatorName,omitempty\u0026#34;` Title string `json:\u0026#34;title,omitempty\u0026#34;` DeadlineAt int64 `json:\u0026#34;deadlineAt,omitempty\u0026#34;` Desc string `json:\u0026#34;desc,omitempty\u0026#34;` Status int `json:\u0026#34;status,omitempty\u0026#34;` Records []*TodoRecord `json:\u0026#34;records,omitempty\u0026#34;` ExecuteIds []string `json:\u0026#34;executeIds,omitempty\u0026#34;` // 待办执行人 TodoStatus int `json:\u0026#34;todoStatus,omitempty\u0026#34;` } type UserTodo struct { ID string `json:\u0026#34;id,omitempty\u0026#34;` UserId string `json:\u0026#34;userId,omitempty\u0026#34;` UserName string `json:\u0026#34;userName,omitempty\u0026#34;` TodoId string `json:\u0026#34;todoId,omitempty\u0026#34;` TodoStatus int `json:\u0026#34;todoStatus,omitempty\u0026#34;` // 待办事项的状态 } type TodoInfoResp struct { ID string `json:\u0026#34;id,omitempty\u0026#34;` CreatorId string `json:\u0026#34;creatorId,omitempty\u0026#34;` CreatorName string `json:\u0026#34;creatorName,omitempty\u0026#34;` Title string `json:\u0026#34;title,omitempty\u0026#34;` DeadlineAt int64 `json:\u0026#34;deadlineAt,omitempty\u0026#34;` Desc string `json:\u0026#34;desc,omitempty\u0026#34;` Records []*TodoRecord `json:\u0026#34;records,omitempty\u0026#34;` ExecuteIds []*UserTodo `json:\u0026#34;executeIds,omitempty\u0026#34;` Status int `json:\u0026#34;status,omitempty\u0026#34;` TodoStatus int `json:\u0026#34;todoStatus,omitempty\u0026#34;` } 审批 基本上也是CRUD\n1 2 3 4 5 6 7 8 func (h *Approval) InitRegister(engine *gin.Engine) { // 创建审批路由组，添加JWT中间件进行身份验证 g := engine.Group(\u0026#34;v1/approval\u0026#34;, h.svcCtx.Jwt.Handler) g.GET(\u0026#34;/:id\u0026#34;, h.Info) // 获取审批详情 g.POST(\u0026#34;\u0026#34;, h.Create) // 创建审批申请 g.PUT(\u0026#34;/dispose\u0026#34;, h.Dispose) // 处理审批（通过/拒绝/撤销） g.GET(\u0026#34;/list\u0026#34;, h.List) // 获取审批列表 } 表结构设计 一张超级无敌巨宽的表\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 Approval struct { ID primitive.ObjectID `bson:\u0026#34;_id,omitempty\u0026#34; json:\u0026#34;id,omitempty\u0026#34;` // 数据库ID UserId string `bson:\u0026#34;userId,omitempty\u0026#34; json:\u0026#34;userId,omitempty\u0026#34;` // 申请人用户ID No string `bson:\u0026#34;no,omitempty\u0026#34; json:\u0026#34;no,omitempty\u0026#34;` // 审批编号 Type ApprovalType `bson:\u0026#34;type,omitempty\u0026#34; json:\u0026#34;type,omitempty\u0026#34;` // 审批类型 Status ApprovalStatus `bson:\u0026#34;status,omitempty\u0026#34; json:\u0026#34;status,omitempty\u0026#34;` // 审批状态 Title string `bson:\u0026#34;title,omitempty\u0026#34; json:\u0026#34;title,omitempty\u0026#34;` // 审批标题 Abstract string `bson:\u0026#34;abstract,omitempty\u0026#34; json:\u0026#34;abstract,omitempty\u0026#34;` // 审批摘要 Reason string `bson:\u0026#34;reason,omitempty\u0026#34; json:\u0026#34;reason,omitempty\u0026#34;` // 申请理由 ApprovalId string `bson:\u0026#34;approvalId,omitempty\u0026#34;` // 当前审批人ID ApprovalIdx int `bson:\u0026#34;approvalIdx,omitempty\u0026#34;` // 当前审批人索引 Approvers []*Approver `bson:\u0026#34;approvers,omitempty\u0026#34;` // 审批人列表 CopyPersons []*Approver `bson:\u0026#34;copyPersons,omitempty\u0026#34;` // 抄送人列表 Participation []string `bson:\u0026#34;participation,omitempty\u0026#34;` // 参与人员ID列表 FinishAt int64 `bson:\u0026#34;finishAt,omitempty\u0026#34; json:\u0026#34;finishAt,omitempty\u0026#34;` // 完成时间戳 FinishDay int64 `bson:\u0026#34;finishDay,omitempty\u0026#34; json:\u0026#34;finishDay,omitempty\u0026#34;` // 完成日期 FinishMonth int64 `bson:\u0026#34;finishMonth,omitempty\u0026#34; json:\u0026#34;finishMonth,omitempty\u0026#34;` // 完成月份 FinishYeas int64 `bson:\u0026#34;finishYeas,omitempty\u0026#34; json:\u0026#34;finishYeas,omitempty\u0026#34;` // 完成年份 MakeCard *MakeCard `bson:\u0026#34;makeCard,omitempty\u0026#34; json:\u0026#34;makeCard,omitempty\u0026#34;` // 补卡申请详情 Leave *Leave `bson:\u0026#34;leave,omitempty\u0026#34; json:\u0026#34;leave,omitempty\u0026#34;` // 请假申请详情 GoOut *GoOut `bson:\u0026#34;goOut,omitempty\u0026#34; json:\u0026#34;goOut,omitempty\u0026#34;` // 外出申请详情 UpdateAt int64 `bson:\u0026#34;updateAt,omitempty\u0026#34; json:\u0026#34;updateAt,omitempty\u0026#34;` // 更新时间戳 CreateAt int64 `bson:\u0026#34;createAt,omitempty\u0026#34; json:\u0026#34;createAt,omitempty\u0026#34;` // 创建时间戳 } // Approver 审批人数据模型 Approver struct { UserId string `bson:\u0026#34;userId,omitempty\u0026#34;` // 用户ID UserName string `bson:\u0026#34;userName,omitempty\u0026#34;` // 用户姓名 Status ApprovalStatus `bson:\u0026#34;status,omitempty\u0026#34;` // 审批状态 Reason string `bson:\u0026#34;reason,omitempty\u0026#34;` // 审批理由 } // MakeCard 补卡 MakeCard struct { Date int64 `bson:\u0026#34;date,omitempty\u0026#34;` //补卡时间 Reason string `bson:\u0026#34;reason,omitempty\u0026#34;` //补卡理由 Day int64 `bson:\u0026#34;day,omitempty\u0026#34;` //补卡日期(20221011) CheckType WorkCheckType `bson:\u0026#34;workCheckType,omitempty\u0026#34;` //补卡类型 } // Leave 请假 Leave struct { Type LeaveType `bson:\u0026#34;type,omitempty\u0026#34;` //请假类型 StartTime int64 `bson:\u0026#34;startTime,omitempty\u0026#34;` //开始时间 EndTime int64 `bson:\u0026#34;endTime,omitempty\u0026#34;` //结束时间 Reason string `bson:\u0026#34;reason,omitempty\u0026#34;` //请假原由 TimeType TimeFormatType `bson:\u0026#34;timeType,omitempty\u0026#34;` //请假类型 1=小时 2=天 } // GoOut 外出 GoOut struct { StartTime int64 `bson:\u0026#34;startTime,omitempty\u0026#34;` //开始时间 EndTime int64 `bson:\u0026#34;endTime,omitempty\u0026#34;` //结束时间 Duration float32 `bson:\u0026#34;duration,omitempty\u0026#34;` //时长(小时) Reason string `bson:\u0026#34;reason,omitempty\u0026#34;` //外出原由 } Chat AiHelper WebSocket 聊天技术总结 结论：聊天能力集中在后端独立 WS 服务（gorilla/websocket），与 Gin REST 共用 JWT；front/ 为空，无正式客户端。仓库内无架构图，设计意图主要体现在代码注释与 Chat_API_测试指南.md。\n1. 整体架构 进程内双服务并行启动（共享同一 svc.ServiceContext）：\n服务 框架 默认地址 职责 REST API Gin 0.0.0.0:8888 登录、用户管理等 WebSocket net/http + gorilla 0.0.0.0:9000，路径 /ws 实时私聊/群聊 1 2 3 4 5 6 7 8 9 10 客户端 │ ├─ POST /v1/user/login ──► Gin API ──► JWT(AccessToken) │ └─ WS /ws (Header: websocket: \u0026lt;JWT\u0026gt;) │ ▼ handler/ws.Ws ──► logic.Chat ──► MongoDB chat_log │ └─ 内存 uidToConn / ConnToUid 推送 入口：/Users/gzm/gopath/src/AiHelper/backend/main.go\napi.NewHandle(svcContext).Run() ws.NewWs(svcContext).Run() 依赖：github.com/gorilla/websocket v1.5.3（无 nhooyr、无 gin-websocket）\n2. 连接生命周期 核心文件：/Users/gzm/gopath/src/AiHelper/backend/internal/handler/ws/ws.go\n阶段 函数 行为 启动 Ws.Run http.HandleFunc(\u0026quot;/ws\u0026quot;, ServerWs) + ListenAndServe 握手前鉴权 Ws.auth 读 Header websocket，JWT 解析取 uid 升级 ServerWs → Upgrader.Upgrade 响应头回写 websocket: \u0026lt;token\u0026gt; 登记连接 addConn 写入双向 map；同 uid 旧连接先 Close（单点在线） 读循环 handleConn（goroutine） ReadMessage → JSON → 按 chatType 分发 发送 send / sendByUids TextMessage + JSON 断开 closeConn 读失败或处理失败时清理 map 并关闭 要点：\nCheckOrigin 恒为 true（注释写明生产应收紧） 鉴权失败只打日志并 return，不写 HTTP 错误体 JSON 解析失败或业务错误会 return 结束循环，但不一定走 closeConn（潜在连接泄漏） 无 ping/pong、心跳、重连协议 3. 消息协议 领域模型：domain.Message（/Users/gzm/gopath/src/AiHelper/backend/internal/domain/ws.go）\n1 2 3 4 5 6 7 8 { \u0026#34;conversationId\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;recvId\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;sendId\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;chatType\u0026#34;: 1, \u0026#34;content\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;contentType\u0026#34;: 1 } 字段 说明 conversationId 群聊强制 \u0026quot;all\u0026quot;；私聊空则服务端生成 recvId 私聊必填；群聊可空 sendId 服务端强制覆盖为 JWT uid，防伪造 chatType 见下表（以代码为准） content / contentType 内容；contentType 注释为 1 文字 / 2 图片 / 3 表情等，未入库 model.ChatType（chatlogtypes.go）：\n值 常量 含义 1 GroupChatType 群聊 2 SingleChatType 私聊 分发（handleConn）：\nSingleChatType → privateChat → logic.Chat.PrivateChat → sendByUids(recvId) GroupChatType → groupChat → logic.Chat.GroupChat → sendByUids()（全员广播） 注意：backend/doc/Chat_API_测试指南.md 把私聊写成 chatType:1、群聊写成 2，与代码相反；以 domain/model 注释为准。\n无独立事件类型字段（无 event/type/ack）；协议即「一条 JSON = 一条聊天消息」。\n4. Auth / Session 与 REST 关系 登录（REST）\nPOST /v1/user/login → User.Login → logic.user.Login token.GetJwtToken(secret, iat, expire, user.ID.Hex()) Claims：exp/iat + 自定义键 aihelper（常量 token.Identify）= uid 响应：LoginResp.AccessToken（JSON 字段名 token） REST 鉴权\nHeader：Authorization: Bearer \u0026lt;JWT\u0026gt; 中间件：middleware.Jwt.Handler → token.Parse.ParseWithContext WS 鉴权\nHeader：websocket: \u0026lt;JWT\u0026gt;（非 Bearer） Ws.auth → 同一 token.Parse + 同一 Jwt.Secret 连接期把 uid/token 放入 context（Ws.context），供业务/日志使用 会话模型：\n无独立 chat room / WS session 表 在线态 = 进程内 uidToConn / ConnToUid 会话 ID： 群聊：\u0026quot;all\u0026quot;（全局大厅） 私聊：GenerateUniqueID(sendId, recvId) = 排序后拼接 → SHA256 → Base64Raw 取前 22 位（顺序无关、稳定） 持久化：ChatLogModel.Insert → MongoDB chat_log；无按会话拉历史的 REST/WS API（模型仅有 Insert/FindOne/Update/Delete）。\n5. 关键组件与数据流 关键文件 路径 角色 backend/main.go API + WS 双 goroutine 启动 backend/internal/handler/ws/ws.go 连接管理、鉴权、读写循环 backend/internal/handler/ws/conversation.go privateChat / groupChat 推送 backend/internal/domain/ws.go Message 协议结构 backend/internal/logic/chat.go 落库、GenerateUniqueID backend/internal/model/chatlogtypes.go ChatLog、ChatType backend/internal/model/chatlogmodel.go MongoDAO backend/pkg/token/{token,ctxtoken}.go JWT 签发/解析 backend/internal/middleware/jwt.go REST JWT backend/internal/logic/user.go 登录发 Token backend/internal/config/config.go + etc/*/api.yaml Ws.Addr、Jwt.* backend/doc/Chat_API_测试指南.md 手工联调文档（部分与代码不一致） backend/internal/logic/chat_test.go 仅测 GenerateUniqueID 客户端 front/ 目录为空 文档用 wscat -c ws://localhost:9000/ws -H \u0026quot;websocket:{token}\u0026quot; 数据流（私聊） A 发 JSON（recvId=B, chatType=2） handleConn 设 SendId=A PrivateChat → 必要时生成 conversationId → Insert sendByUids(ctx, msg, B) → 仅 B 在线则推送 发送者 A 收不到自己消息的 echo（代码注释已说明） 数据流（群聊） chatType=1 → ConversationId=\u0026quot;all\u0026quot; → 落库 sendByUids 无 uid → 广播所有在线用户（含发送者） 文档写「除发送者外」与代码不符；也不是真实群组房间，而是全站在线广播 6. 重要设计决策 API/WS 端口分离：Gin 与裸 net/http 各占端口，共享 Mongo/JWT 配置 共享 ServiceContext：避免双实例初始化竞态（main.go 注释） Header 传 Token：自定义 websocket 头，规避部分 WS 客户端难带 Authorization 的问题 服务端信任边界：sendId 只认 JWT，客户端不可伪造 单连接/用户：新连踢旧连 轻量会话：无私聊房间实体；群聊固定 \u0026quot;all\u0026quot; 先存后推：落库成功再推送；接收方离线则只落库、不推 内存连接表：单机、无 Redis；水平扩展需改造 协议极简：无 ACK、回执、已读、错误帧、心跳 安全简化：全放行 Origin；鉴权失败无标准 HTTP 状态 7. 配置与联调 本地配置示例（backend/etc/local/api.yaml）：\nAPI：0.0.0.0:8888 WS：0.0.0.0:9000 JWT：Secret + Expire 推荐联调顺序（与文档一致、chatType 按代码）：\nPOST /v1/user/login 取 token 与 id wscat 带 websocket 头连 /ws 私聊：chatType: 2 + recvId 群聊：chatType: 1 8. 缺口与风险（实现现状） 无前端 WS 客户端 无聊天历史查询 API contentType 未持久化 文档 chatType、群聊是否回显发送者与代码不一致 处理错误路径可能未清理连接 群聊非真实群组，而是全局广播 无多实例在线路由能力 ","date":"2026-08-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-helper-rest--0x01/","title":"AI helper 常规开发"},{"content":"RAG 本篇的目的是为了 将文件向量化后 存到数据库中 这里面的步骤有\n读取文件\n切分文件\n索引 （embedding 和 存醋）\n流程编排 解释一下 下面的这个流程\nStart 程序启动\nFile loader 读取文件 转换成 schema.Document (文档对象列表)\nMarkdown Transfomer 转换成 Markdown 格式\nMilvus Index 向量索引\n使用 oepnai 进行 Embedding 把文本变成向量 然后 把 [原文 + 向量 + 元数据] 写入 Milvus Collections 结束\n生成完后，会在目录看到这些组件\n最终实现 我们的一个文本 最后会被分片插入到向量数据库中 代码解析 首先调用 knowledge_index_pipeline.BuildKnowledgeIndexing 创建了一个 runner\n并且使用 r.Invoke 传入了文件的地址\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func main() { ctx := context.Background() r, err := knowledge_index_pipeline.BuildKnowledgeIndexing(ctx) if err != nil { panic(err) } err = filepath.WalkDir(\u0026#34;./docs\u0026#34;, func(path string, d fs.DirEntry, err error) error { if err != nil { return fmt.Errorf(\u0026#34;walk dir failed: %w\u0026#34;, err) } if d.IsDir() { return nil } if !strings.HasSuffix(path, \u0026#34;.md\u0026#34;) { fmt.Printf(\u0026#34;[skip] not a markdown file: %s\\n\u0026#34;, path) return nil } fmt.Printf(\u0026#34;[start] indexing file: %s\\n\u0026#34;, path) ids, err := r.Invoke(ctx, document.Source{URI: path}) if err != nil { return fmt.Errorf(\u0026#34;invoke index graph failed: %w\u0026#34;, err) } fmt.Printf(\u0026#34;[done] indexing file: %s, len of parts: %d，%s\\n\u0026#34;, path, len(ids), ids) return nil }) if err != nil { panic(err) } } Runnable 详细拆解一下 BuildKnowledgeIndexing\n我们可以看到 这个方法返回了一个 compose.Runnable[document.Source, []string] 接口 使用 doucument 返回 []string\nInvoke 在注释里面说明 。 是输出的意思\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func BuildKnowledgeIndexing(ctx context.Context) (r compose.Runnable[document.Source, []string], err error) { // ... } // Runnable is the interface for an executable object. Graph, Chain can be compiled into Runnable. // runnable is the core conception of eino, we do downgrade compatibility for four data flow patterns, // and can automatically connect components that only implement one or more methods. // eg, if a component only implements Stream() method, you can still call Invoke() to convert stream output to invoke output. type Runnable[I, O any] interface { Invoke(ctx context.Context, input I, opts ...Option) (output O, err error) Stream(ctx context.Context, input I, opts ...Option) (output *schema.StreamReader[O], err error) Collect(ctx context.Context, input *schema.StreamReader[I], opts ...Option) (output O, err error) Transform(ctx context.Context, input *schema.StreamReader[I], opts ...Option) (output *schema.StreamReader[O], err error) } BuildKnowledgeIndexing 详细看一下其他流程\n这里有很多 AddEdge 。 我们看里面的参数 start, fileLoader, markdown... 可以看出来 就是我们在 Eino 里面绘制的点\n也就是这里组装了一下整个流程 然后返回一个执行器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func BuildKnowledgeIndexing(ctx context.Context) (r compose.Runnable[document.Source, []string], err error) { const ( FileLoader = \u0026#34;FileLoader\u0026#34; MarkdownSplitter = \u0026#34;MarkdownSplitter\u0026#34; Indexer = \u0026#34;Indexer\u0026#34; ) g := compose.NewGraph[document.Source, []string]() fileLoaderKeyOfLoader, err := newLoader(ctx) if err != nil { return nil, err } _ = g.AddLoaderNode(FileLoader, fileLoaderKeyOfLoader) markdownSplitterKeyOfDocumentTransformer, err := newDocumentTransformer(ctx) if err != nil { return nil, err } _ = g.AddDocumentTransformerNode(MarkdownSplitter, markdownSplitterKeyOfDocumentTransformer) indexerKeyOfIndexer, err := newIndexer(ctx) if err != nil { return nil, err } _ = g.AddIndexerNode(Indexer, indexerKeyOfIndexer) _ = g.AddEdge(compose.START, FileLoader) _ = g.AddEdge(Indexer, compose.END) _ = g.AddEdge(FileLoader, MarkdownSplitter) _ = g.AddEdge(MarkdownSplitter, Indexer) r, err = g.Compile(ctx, compose.WithGraphName(\u0026#34;KnowledgeIndexing\u0026#34;), compose.WithNodeTriggerMode(compose.AnyPredecessor)) if err != nil { return nil, err } return r, err } Loader 我们看下面的这个代码 可以看 返回值是一个 document.Loader 的接口\n这个接口需要实现 load 方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func newLoader(ctx context.Context) (ldr document.Loader, err error) { // UseNameAsID 必开：Redis Indexer 的默认映射要求 doc.ID 非空。 config := \u0026amp;file.FileLoaderConfig{ UseNameAsID: true, } ldr, err = file.NewFileLoader(ctx, config) if err != nil { return nil, err } return ldr, nil } type Loader interface { Load(ctx context.Context, src Source, opts ...LoaderOption) ([]*schema.Document, error) } File Load 详细看一下官方的 file loader 是怎么实现的\nOpenFile 打开文件\n读取文件\n构建元数据 ExtraMeta\n构造返回值 然后返回\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 func (f *FileLoader) Load(ctx context.Context, src document.Source, opts ...document.LoaderOption) (docs []*schema.Document, err error) { // ... file, err := openFile(src.URI) //... name := filepath.Base(src.URI) ext := filepath.Ext(src.URI) meta := map[string]any{ MetaKeyExtension: ext, MetaKeyFileName: name, MetaKeySource: src.URI, } //... o := document.GetLoaderCommonOptions(\u0026amp;document.LoaderOptions{}, opts...) docs, err = f.Parser.Parse(ctx, file, append([]parser.Option{parser.WithURI(src.URI), parser.WithExtraMeta(meta)}, o.ParserOptions...)...) if err != nil { return nil, fmt.Errorf(\u0026#34;file parse err of [%s]: %w\u0026#34;, src.URI, err) } // ... return docs, nil } func (dp TextParser) Parse(ctx context.Context, reader io.Reader, opts ...Option) ([]*schema.Document, error) { data, err := io.ReadAll(reader) if err != nil { return nil, err } opt := GetCommonOptions(\u0026amp;Options{}, opts...) meta := make(map[string]any) meta[MetaKeySource] = opt.URI for k, v := range opt.ExtraMeta { meta[k] = v } doc := \u0026amp;schema.Document{ Content: string(data), MetaData: meta, } return []*schema.Document{doc}, nil } Transformer newDocumentTransformer 返回一个 Transformer 的接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func newDocumentTransformer(ctx context.Context) (tfr document.Transformer, err error) { // 按 Markdown 标题级别切分；Headers 为必填，key 只能是 \u0026#39;#\u0026#39; 组成。 config := \u0026amp;markdown.HeaderConfig{ Headers: map[string]string{ \u0026#34;#\u0026#34;: \u0026#34;h1\u0026#34;, \u0026#34;##\u0026#34;: \u0026#34;h2\u0026#34;, \u0026#34;###\u0026#34;: \u0026#34;h3\u0026#34;, }, TrimHeaders: false, // 每个 chunk 需要唯一 ID，否则 Redis 会用同一个 key 互相覆盖。 IDGenerator: func(_ context.Context, originalID string, splitIndex int) string { return fmt.Sprintf(\u0026#34;%s_%d\u0026#34;, originalID, splitIndex) }, } tfr, err = markdown.NewHeaderSplitter(ctx, config) if err != nil { return nil, err } return tfr, nil } type Transformer interface { Transform(ctx context.Context, src []*schema.Document, opts ...TransformerOption) ([]*schema.Document, error) } HeaderSplitter 可以看 这里先进行了 Split\n然后每个切片复制一个 uuid\n并且给每个分片增加元数据信息\n返回\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func (h *headerSplitter) Transform(ctx context.Context, docs []*schema.Document, opts ...document.TransformerOption) ([]*schema.Document, error) { var ret []*schema.Document for _, doc := range docs { result := h.splitText(ctx, doc.Content) for i := range result { nDoc := \u0026amp;schema.Document{ ID: h.idGenerator(ctx, doc.ID, i), Content: result[i].chunk, MetaData: deepCopyAnyMap(doc.MetaData), } if nDoc.MetaData == nil { nDoc.MetaData = make(map[string]any, len(result[i].meta)) } for k, v := range result[i].meta { nDoc.MetaData[k] = v } ret = append(ret, nDoc) } } return ret, nil } Index 构建 Indexer 返回一个 Indexer 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func newIndexer(ctx context.Context) (idr indexer.Indexer, err error) { embeddingIns, err := newEmbedding(ctx) if err != nil { return nil, err } // 写入 Attu 中看到的 default/biz；向量维度需与 embedding 模型一致（vision=2048）。 config := \u0026amp;milvus2.IndexerConfig{ ClientConfig: \u0026amp;milvusclient.ClientConfig{ Address: \u0026#34;localhost:19530\u0026#34;, }, Collection: \u0026#34;biz\u0026#34;, Vector: \u0026amp;milvus2.VectorConfig{ Dimension: 2048, MetricType: milvus2.COSINE, VectorField: \u0026#34;vector\u0026#34;, }, Embedding: embeddingIns, } idr, err = milvus2.NewIndexer(ctx, config) if err != nil { return nil, err } return idr, nil } type Indexer interface { // Store stores the documents. Store(ctx context.Context, docs []*schema.Document, opts ...Option) (ids []string, err error) // invoke } Milvus Indexer 首先是 对分片 进行向量 获取向量数组\n构造符合 milvus 表记录的结构体 。 id、content、vector、metadata\n构造完成记录后 , 插入到数据库 。最后返回所有 id\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 func (i *Indexer) Store(ctx context.Context, docs []*schema.Document, opts ...indexer.Option) (ids []string, err error) { // ... vectors, err := i.embedDocuments(ctx, co.Embedding, docs) if err != nil { return nil, err } upsertResult, err := i.upsertDocuments(ctx, docs, vectors, io.Partition) if err != nil { return nil, err } // .. return upsertResult, nil } func (i *Indexer) embedDocuments(ctx context.Context, emb embedding.Embedder, docs []*schema.Document) ([][]float64, error) { if emb == nil { return nil, nil // Return nil vectors if no embedder } texts := make([]string, 0, len(docs)) for _, doc := range docs { texts = append(texts, doc.Content) } vectors, err := emb.EmbedStrings(i.makeEmbeddingCtx(ctx, emb), texts) if err != nil { return nil, fmt.Errorf(\u0026#34;[Indexer.Store] failed to embed documents: %w\u0026#34;, err) } if len(vectors) != len(docs) { return nil, fmt.Errorf(\u0026#34;[Indexer.Store] embedding result length mismatch: need %d, got %d\u0026#34;, len(docs), len(vectors)) } return vectors, nil } func (i *Indexer) upsertDocuments(ctx context.Context, docs []*schema.Document, vectors [][]float64, partition string) ([]string, error) { columns, err := i.config.DocumentConverter(ctx, docs, vectors) if err != nil { return nil, fmt.Errorf(\u0026#34;[Indexer.Store] failed to convert documents: %w\u0026#34;, err) } insertOpt := milvusclient.NewColumnBasedInsertOption(i.config.Collection) if partition != \u0026#34;\u0026#34; { insertOpt = insertOpt.WithPartition(partition) } for _, col := range columns { insertOpt = insertOpt.WithColumns(col) } result, err := i.client.Upsert(ctx, insertOpt) if err != nil { return nil, fmt.Errorf(\u0026#34;[Indexer.Store] failed to upsert documents: %w\u0026#34;, err) } ids := make([]string, 0, result.IDs.Len()) for idx := 0; idx \u0026lt; result.IDs.Len(); idx++ { idStr, err := idValueAsString(result.IDs, idx) if err != nil { return nil, fmt.Errorf(\u0026#34;[Indexer.Store] failed to get id: %w\u0026#34;, err) } ids = append(ids, idStr) } return ids, nil } 总结 总到来看，主要就是我们上面编排的流程\n文件加载\n文件分块\n文件索引 (分片 - 向量化)\n","date":"2026-08-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/callagent-rag-0x01/","title":"Rag 实战"},{"content":"RAG 本篇的目的是为了 使用 LLM 进行 RAG 的召回 也就是我们的下面的流程\n在我们做完 文件索引和向量化存储 之后\n我们需要进行 提问 召回向量数据库中国呢的信息\n将 input和 召回的 docs 一起发给大模型\n结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main() { ctx := context.Background() r, err := retriever.NewMilvusRetriever(ctx) if err != nil { panic(err) } query := \u0026#34;服务下线是什么原因\u0026#34; docs, err := r.Retrieve(ctx, query) if err != nil { panic(err) } fmt.Println(\u0026#34;Q：\u0026#34;, query) for _, doc := range docs { fmt.Println(\u0026#34;A：\u0026#34;, doc.Content) } fmt.Println(\u0026#34;Done\u0026#34;, len(docs)) } 拆解实现 NewMilvusRetriever 我们创建 milvus 的 Retriver\n要求指定向量字段 Vector。并且需要返回的字段有 id、content、metadata\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func NewMilvusRetriever(ctx context.Context) (rtr retriever.Retriever, err error) { eb, err := embedder.DoubaoEmbedding(ctx) if err != nil { return nil, err } r, err := milvus2.NewRetriever(ctx, \u0026amp;milvus2.RetrieverConfig{ ClientConfig: \u0026amp;milvusclient.ClientConfig{ Address: milvusAddress, }, Collection: milvusCollectionName, VectorField: \u0026#34;vector\u0026#34;, OutputFields: []string{ \u0026#34;id\u0026#34;, \u0026#34;content\u0026#34;, \u0026#34;metadata\u0026#34;, }, TopK: 1, SearchMode: search_mode.NewApproximate(milvus2.COSINE), Embedding: eb, }) if err != nil { return nil, err } return r, nil } Retrive 这里的 Retrieve 支持多种方法 ，不过大体思路都是\n对于输入的问题进行 向量化 EmbedQuery\n然后调用相似度查询接口 client.Search\n构造返回结构体并且返回\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 type Retriever interface { Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error) } func (r *Retriever) Retrieve(ctx context.Context, query string, opts ...retriever.Option) (docs []*schema.Document, err error) { // ... docs, err = r.config.SearchMode.Retrieve(ctx, r.client, r.config, query, opts...) // ... return docs, nil } func (a *Approximate) Retrieve(ctx context.Context, client *milvusclient.Client, conf *milvus2.RetrieverConfig, query string, opts ...retriever.Option) ([]*schema.Document, error) { if conf.Embedding == nil { return nil, fmt.Errorf(\u0026#34;embedding is required for approximate search\u0026#34;) } queryVector, err := EmbedQuery(ctx, conf.Embedding, query) if err != nil { return nil, err } searchOpt, err := a.BuildSearchOption(ctx, conf, queryVector, opts...) if err != nil { return nil, fmt.Errorf(\u0026#34;failed to build search option: %w\u0026#34;, err) } result, err := client.Search(ctx, searchOpt) if err != nil { return nil, fmt.Errorf(\u0026#34;failed to search: %w\u0026#34;, err) } if len(result) == 0 { return []*schema.Document{}, nil } return conf.DocumentConverter(ctx, result[0]) } 这几个 Mode :\n这里其实叠了两层概念，容易混在一起。\nMode 作用 Approximate ANN 近似最近邻（你们现在用的） Range 按半径/阈值过滤 Hybrid 稠密 + 稀疏/BM25 混合 Sparse 稀疏向量检索 Iterator 分批迭代拉取 Scalar 标量查询 ","date":"2026-08-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/callagent-rag-0x02/","title":"Rag 召回"},{"content":"简历 preview 简历上 : 实现了一个完整的用户关系功能\n需求分析 :\n大部分平台为了提供 社交互动、个性化体验，内容传播\n这一部分功能被称为用户关系\n积极用法 ： 关注\n消极用法 : 屏蔽或者拉黑，从系统设计的角度来说没有本质的区别\n设计难点 高并发 : 每打开一篇文章，都要判定你是否关注了这个创作者\n大数据 : 假设每个用户平均会关注100个人，那么关注数据的行数就是用户数的100倍\n功能 \u0026amp; 表结构设计 关注功能设计 :\n关注, 取消关注\n获取某个人的关注列表, 获取某个人关注另外一个人的详细信息\n获取某人的粉丝列表, 获取默认的关注人数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 service FollowService { // 增删 rpc Follow (FollowRequest) returns (FollowResponse); rpc CancelFollow(CancelFollowRequest) returns (CancelFollowResponse); // 改，例如说你准备支持备注、标签类的，那么就会有对应的修改功能 // 获得某个人的关注列表 rpc GetFollowee (GetFolloweeRequest) returns (GetFolloweeResponse); // 获得某个人关注另外一个人的详细信息 rpc FollowInfo (FollowInfoRequest) returns (FollowInfoResponse); // 获取某人的粉丝列表 rpc GetFollower (GetFollowerRequest)returns(GetFollowerResponse ); // 获取默认的关注人数 rpc GetFollowStatic(GetFollowStaticRequest)returns(GetFollowStaticResponse); } 表结构设计 :\n索引设计\n创建联合唯一索引 follwer_followee 这个索引用于查看所有关注的人\n并且反向创建一个普通索引 followee_follwer 这个索用于查看谁关注了我\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type FollowRelation struct { ID int64 `gorm:\u0026#34;primaryKey,autoIncrement,column:id\u0026#34;` Follower int64 `gorm:\u0026#34;type:int(11);not null;uniqueIndex:follower_followee,priority:1;index:followee_follower,priority:2\u0026#34;` Followee int64 `gorm:\u0026#34;type:int(11);not null;uniqueIndex:follower_followee,priority:2;index:followee_follower,priority:1\u0026#34;` Status uint8 // 这里你可以根据自己的业务来增加字段，比如说 // 关系类型，可以搞些什么普通关注，特殊关注 // Type int64 `gorm:\u0026#34;column:type;type:int(11);comment:关注类型 0-普通关注\u0026#34;` // 备注 // Note string `gorm:\u0026#34;column:remark;type:varchar(255);\u0026#34;` // 创建时间 Ctime int64 Utime int64 } 功能实现 关注某人 DB 创建的时候 维护 Upsert 语意 进行创建关注关系\n1 2 3 4 5 6 7 8 9 10 11 12 func (g *GORMFollowRelationDAO) CreateFollowRelation(ctx context.Context, f FollowRelation) error { now := time.Now().UnixMilli() f.Utime = now f.Ctime = now f.Status = FollowRelationStatusActive return g.db.WithContext(ctx).Clauses(clause.OnConflict{ DoUpdates: clause.Assignments(map[string]any{ \u0026#34;utime\u0026#34;: now, \u0026#34;status\u0026#34;: FollowRelationStatusActive, }), }).Create(\u0026amp;f).Error } 使用 TxPipeline 实现双写两个 Map 。 关注者和粉丝 单独维护两个 HMap\n1 2 3 4 5 6 7 8 9 func (r *RedisFollowCache) updateStaticsInfo(ctx context.Context, follower, followee int64, delta int64) error { tx := r.client.TxPipeline() // 增加 follower 的关注多少人的数量 tx.HIncrBy(ctx, r.staticsKey(follower), fieldFolloweeCnt, delta) // 增加 followee 被多少人关注的数量 tx.HIncrBy(ctx, r.staticsKey(followee), fieldFollowerCnt, delta) _, err := tx.Exec(ctx) return err } 取消关注 DB 层面只需要更新一下 status 即可\n1 2 3 4 5 6 7 8 9 func (g *GORMFollowRelationDAO) UpdateStatus(ctx context.Context, followee int64, follower int64, status uint8) error { now := time.Now().UnixMilli() return g.db.WithContext(ctx). Where(\u0026#34;follower = ? AND followee = ?\u0026#34;, follower, followee). Updates(map[string]any{ \u0026#34;status\u0026#34;: status, \u0026#34;utime\u0026#34;: now, }).Error } Cache 层面同样使用, HIncryBy 进行-1\n1 2 3 func (r *RedisFollowCache) CancelFollow(ctx context.Context, follower, followee int64) error { return r.updateStaticsInfo(ctx, follower, followee, -1) } 查询 \u0026amp; 缓存 获取关注列表 这个功能不需要缓存\n没有人会频繁去看这个功能 缓存效率低\n分页接口不好缓存\n1 2 3 4 5 6 7 8 9 func (g *GORMFollowRelationDAO) FollowerRelationList(ctx context.Context, followee, offset, limit int64) ([]FollowRelation, error) { var res []FollowRelation err := g.db.WithContext(ctx). Where(\u0026#34;followee = ? AND status = ?\u0026#34;, followee, FollowRelationStatusActive). Offset(int(offset)).Limit(int(limit)). Find(\u0026amp;res).Error return res, err } 关注数量和粉丝数量 借助 Redis + 兜底 Count 功能来计算关注数量和粉丝数量 通过 HGetAll 直接获取 Uid 的粉丝数量和关注数量 然后返回\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (r *RedisFollowCache) StaticsInfo(ctx context.Context, uid int64) (domain.FollowStatics, error) { data, err := r.client.HGetAll(ctx, r.staticsKey(uid)).Result() if err != nil { return domain.FollowStatics{}, err } // 也认为没有数据 if len(data) == 0 { return domain.FollowStatics{}, ErrKeyNotExist } // 理论上来说，这里不可能有 error followerCnt, _ := strconv.ParseInt(data[fieldFollowerCnt], 10, 64) followeeCnt, _ := strconv.ParseInt(data[fieldFolloweeCnt], 10, 64) return domain.FollowStatics{ Followees: followeeCnt, Followers: followerCnt, }, nil } 慢查询就是需要去确认DB, DB查询结束之后再回写缓存\n1 2 3 4 5 6 7 8 func (g *GORMFollowRelationDAO) CntFollower(ctx context.Context, uid int64) (int64, error) { var res int64 err := g.db.WithContext(ctx). Select(\u0026#34;count(follower)\u0026#34;). Where(\u0026#34;followee = ? AND status = ?\u0026#34;, uid, FollowRelationStatusActive).Count(\u0026amp;res).Error return res, err } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func (d *CachedRelationRepository) GetFollowStatics(ctx context.Context, uid int64) (domain.FollowStatics, error) { // 快路径 res, err := d.cache.StaticsInfo(ctx, uid) if err == nil { return res, err } // 慢路径 res.Followers, err = d.dao.CntFollower(ctx, uid) if err != nil { return res, err } res.Followees, err = d.dao.CntFollowee(ctx, uid) if err != nil { return res, err } err = d.cache.SetStaticsInfo(ctx, uid, res) if err != nil { // 这里记录日志 d.l.Error(\u0026#34;缓存关注统计信息失败\u0026#34;, logger.Error(err.Error()), logger.Int64(\u0026#34;uid\u0026#34;, uid)) } return res, nil } 难点尝试解决 我们之前提到的 用户关系的量级 和 用户本身还要高一个数量级 。单表肯定是不可以的 因此需要考虑\n分库分表 换用别的存储中间件 我们这里可以采用 TableStore 进行存储。\n部分代码如下，无非是把分库分表的操作丢给了第三方应用\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (t *TableStoreFollowRelationDao) UpdateStatus(ctx context.Context, followee int64, follower int64, status uint8) error { cond := tablestore.NewCompositeColumnCondition(tablestore.LO_AND) cond.AddFilter(tablestore.NewSingleColumnCondition(\u0026#34;follower\u0026#34;, tablestore.CT_EQUAL, follower)) cond.AddFilter(tablestore.NewSingleColumnCondition(\u0026#34;followee\u0026#34;, tablestore.CT_EQUAL, followee)) req := new(tablestore.UpdateRowChange) req.TableName = FollowRelationTableName req.SetCondition(tablestore.RowExistenceExpectation_EXPECT_EXIST) req.SetColumnCondition(cond) req.PutColumn(\u0026#34;status\u0026#34;, int64(status)) _, err := t.client.UpdateRow(\u0026amp;tablestore.UpdateRowRequest{ UpdateRowChange: req, }) return err } 面试 总结类似于 维护计数的方案\n如何优化 COUNT 查询 ？\nRedis 直接维护结果 避免 COUNT 利用索引 Redis 的 Pipeline 是什么 ？ Transaction 又是什么 ？ 用来解决什么问题 ？\n","date":"2026-08-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/contract-0x01/","title":"关注 detail"},{"content":"简历 preview 简历上 : 实现了一个完整的用户关系功能 支持 关注、取消关注、获取关注列表\n流程设计 需求分析 :\n要求支持 用户的关注，取消关注，获取关注列表，获取关注数量的功能 模块分析 :\n完全独立的一个业务，单独拆分成一个模块 关注模块 支持的功能 关注某人\n取消关注\n查看用户粉丝列表\n查看用户关注列表\n查看用户关注数量\n表结构设计 表结构维护 Follower 和 Followee 以及 status 进行维护关系之间的展示\n并且创建 唯一索引 \u0026lt;follower_followee\u0026gt; 和 普通索引 \u0026lt;followee_follower\u0026gt; 来优化 查询用户的关注列表和粉丝列表\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type FollowRelation struct { ID int64 `gorm:\u0026#34;primaryKey,autoIncrement,column:id\u0026#34;` Follower int64 `gorm:\u0026#34;type:int(11);not null;uniqueIndex:follower_followee,priority:1;index:followee_follower,priority:2\u0026#34;` Followee int64 `gorm:\u0026#34;type:int(11);not null;uniqueIndex:follower_followee,priority:2;index:followee_follower,priority:1\u0026#34;` Status uint8 // 这里你可以根据自己的业务来增加字段，比如说 // 关系类型，可以搞些什么普通关注，特殊关注 // Type int64 `gorm:\u0026#34;column:type;type:int(11);comment:关注类型 0-普通关注\u0026#34;` // 备注 // Note string `gorm:\u0026#34;column:remark;type:varchar(255);\u0026#34;` // 创建时间 Ctime int64 Utime int64 } 缓存设计 使用 Hash 结构维护一个 follow 和 followee 的关注和被关注数量\n使用 TxPiepline 进行一致性控制\n在关注和取消关注的时候 进行同步的 +1 和 -1 操作\n其他细节 拉黑，屏蔽 的功能不额外创建表进行存放 。 考虑这个操作是一个低频操作，因此使用 status 直接存放到 关系表里面\n对于 关系表 ， 一个用户关注100个人，那么行数就会增加100倍。 使用 阿里的 tablestore 进行类似分库分表的优化\n获取用户关注数量和粉丝数量的时候 。 优先去获取 Redis 的数量 然后再通过 Mysql Count 进行统计\n获取关注列表不进行缓存 。\n考虑是一个低频操作 缓存命中率低 并且是一个分页查询 不好缓存 难点 \u0026amp; 亮点 问题 :\n维护关注数量 , 我们可以采用单独创一张表进行实现。 但是为关注服务直接创一张表太重了 方案 :\n因此使用 Redis 进行直接统计 计数 在关注和取消关注的时候分别操作\n在获取关注数量的时候 使用 Cache-Aside快慢路径 ， 优先获取 Redis 的数据 然后再根据 Count 进行计数 。 因为 我们关注表的索引设计，保证了能够命中索引\n问题 :\n创建了唯一索引后 \u0026lt;followee_follower\u0026gt; 。这个只能解决查询粉丝数量，但是对于关注数量 并不能很好的命中索引 方案 :\n反向创建一个普通索引，不需要额外创建唯一索引 因为原有的索引就能控制 。 亮点 :\n使用 tablestore 进行维护大数据量级的 关系信息 。 避免自身系统的复杂性\n使用 TxPiePline 进行 控制 同时写入 粉丝和关注 的计数 。\n获取关注和粉丝列表是一个分页接口 考虑 用户使用频率低 缓存命中率差。不进行额外缓存\n取消关注 通过 表设计的 status 进行处理 更新 status 就可以完成取消关注，软删除 。 并且在关注功能的时候 ， 使用 Upsert 保证语意\n缺点 对于关系维护的大数据量级处理 只是引入了三方的 tablestore 进行处理比较简单\n如果 Redis 崩溃，那么会有大部分 Count操作进入到 我们的Mysql 中 并没有很好的容错机制\n总分：68 / 100 对「2 年经验、讲关注关系模块」的面试准备来说：骨架够用，能过一轮筛选，但扛不住有经验的面试官追问。 更像「能把做过的事串起来」，还没到「能把设计讲透、把坑讲清楚」。\n优点 需求边界清晰：关注 / 取消关注 / 列表 / 数量，模块独立，简历表述和正文能对上。 表设计基本正确：Follower + Followee + 唯一索引防重复关注，反向普通索引服务粉丝查询，这是面试里常见的加分点。 有取舍意识：列表不缓存（低频 + 分页）、拉黑用 status 软状态、取消关注用 status + Upsert，说明不是只会「全量加 Redis」。 有自我批评：缺点里提到 Redis 挂了打爆 MySQL、tablestore 引入偏简单，态度加分。 缺点（面试里容易被打穿） 一致性讲错/讲浅了\nTxPipeline 只保证 Redis 内部多命令原子，不保证 MySQL ↔ Redis 一致。面试官一问「DB 成功、Redis 失败怎么办？」现在的稿子接不住。\n索引表述前后矛盾\n前文唯一索引是 follower_followee，难点里写成「唯一索引 \u0026lt;followee_follower\u0026gt;」——细节错了会显得没真正想清楚。\n计数方案深度不够\nHash + 同步 ±1 + Cache-Aside，缺：key 设计、并发丢更新、缓存击穿/雪崩、冷启动回源、与 DB 对账。\n亮点偏「名词」\ntablestore、Pipeline 写得很空，2 年经验候选人若只背名词，追问「为什么不用分表 / 为什么不用计数表」会立刻露馅。\n业务边界漏洞\n未关注却要拉黑、自己关注自己、重复关注并发、软删后重新关注的语义、分页游标 vs offset，基本没准备。\n几乎没有 Golang 工程视角\n接口分层、Repo、事务边界、幂等、错误码、超时降级——对「Golang 岗」这是硬伤。\n文档完成度差\nxxxx、TxPiepline 拼写、序号重复（两个「2.」），会给面试官「准备潦草」的印象。\n建议（按优先级） 优先级 做什么 P0 把一致性故事补全：写路径（先 DB 再 Redis / Outbox / 对账）、读路径降级、Redis 不可用时的限流与熔断 P0 画一张「关注 / 取消关注」时序图，标清事务、Upsert、status、计数更新顺序 P1 准备 3 个追问标准答：并发重复关注、未关注拉黑、百万粉丝列表分页 P1 把 tablestore 改成「可选扩展」：先讲单机/单库上限与分片思路，再提引入外部存储的原因与代价 P2 补一层 Go 实现：FollowService 接口、幂等、GORM 事务片段，能对着代码讲 2 分钟 P2 修正文案与术语：Pipeline 拼写、索引名统一、标题 description 写完整 分项参考（面试官视角） 维度 分数 说明 需求与模块拆分 78 清楚，但偏浅 存储与索引 72 方向对，表述有错 缓存与一致性 55 最大短板 难点/亮点说服力 60 有点，深度不够 自我认知与表达 70 有缺点意识，文档粗糙 对 2 年岗的匹配度 68 能讲项目，难打深水区 一句话：当成「项目介绍提纲」大约 70+；当成「能过中高级追问的面试稿」现在大概 60 出头。把 DB–缓存一致性 和 3～5 个边界追问 补扎实，整体可以冲到 80+。\n","date":"2026-08-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/contract-0x02/","title":"关注 one"},{"content":"3. 无重复字符的最长子串 无重复字符的最长子串\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include \u0026lt;bits/stdc++.h\u0026gt; const int N = 1e5 + 10 ; int mp[N]; class Solution { public: int lengthOfLongestSubstring(string s) { memset(mp,0,sizeof(mp)); int ans = 0 ; int n = s.size(); int j = 0 ; for(int i = 0 ; i \u0026lt; n ; i ++ ) { mp[s[i]] ++ ; while(mp[s[i]] \u0026gt; 1 \u0026amp;\u0026amp; j \u0026lt; i) { mp[s[j ++ ]] -- ; } ans =max(ans , i - j + 1); } return ans ; } }; 心路历程 说实话挑着滑动窗口做的，但是这道题是一个双指针的题 。 没什么难度 直接就做了 。 另外 leetcode 这种需要 人眼 debug 的模式 确实很刺激 。\n一开始写的是 mp[j] \u0026gt; 1 该成了 mp[s[j]] 后面 自测还是不对 又改成了 mp[s[i]] 人眼 debug 加缺少深度思考 确实很容易写偏\n438. 找到字符串中所有字母异位词 找到字符串中所有字母异位词\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 const int N = 3e4 + 10 ; int tt , hh ; int cnt[27],cnts[27]; int q[N]; void init() { tt = - 1; hh = 0 ; memset(cnt,0,sizeof(cnt)); memset(cnts,0,sizeof(cnts)); } bool check() { for(int i = 0 ; i \u0026lt; 26 ; i ++ ) { if(cnt[i] != cnts[i]) { return false; } } return true ; } class Solution { public: vector\u0026lt;int\u0026gt; findAnagrams(string s, string p) { init(); int n = s.size(); int k = p.size(); vector\u0026lt;int\u0026gt; ans ; for(int i = 0 ; i \u0026lt; k ; i ++ ) { cnt[p[i]- \u0026#39;a\u0026#39;] ++ ; } for(int i = 0 ; i \u0026lt; n ; i ++ ) { q[++tt] = s[i]; cnts[s[i] - \u0026#39;a\u0026#39;]++; // 2. 超过 k 则左端出 if (tt - hh + 1 \u0026gt; k) { cnts[q[hh++] - \u0026#39;a\u0026#39;]--; } // 3. 长度刚好 k 再判断 if (tt - hh + 1 == k \u0026amp;\u0026amp; check()) { ans.push_back(i - k + 1); } } return ans ; } }; // 固定窗口 len(p) // 寻找 cnt[p...] = cnt[ans , ans + len(p)] 这里看着是 O(n) 的 // 找的是异位词 如果是同位词 感觉就像是 KMP里面的next 数组了 // 需要有一个算法能够快速 check cnt , 外面的循环肯定是 On 的 // 我们只 check 26个字母。 那么就是 On * 26 只需要通过滑动窗口固定一下窗口即可，当成为队列的时候 check 一下 心路历程 这道题上来就压力拉满了 还以为要我写 kmp next 数组的求法 但是想了一下我们可以暴力的使用 cnt 进行遍历\n不过一开始写滑动窗口的代码很傻逼\n我一开始是这样子处理的,如果队列 把当前字符插入到队列里面 。 如果队列满了那么就判断 。\n我这里的判读逻辑导致我遗漏了 当前满节点的这个值 。\n后来优化了一下代码成上面的样子 之后就完成了\n1 2 3 4 5 6 7 8 9 10 11 for(int i = 0 ; i \u0026lt; n ; i ++ ) { if(tt \u0026lt; hh || (tt - hh + 1 \u0026lt; k )) { q[++tt] = s[i]; // 空的话入队列 或者 窗口比较小 cnts[s[i] - \u0026#39;a\u0026#39;] ++ ; }else { // 队列已经满了 if(check()) { ans.push_back( i - k + 1); } cnts[q[hh ++ ] - \u0026#39;a\u0026#39;] -- ; // 队头弹出队列 } } 实际可以改成\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for(int i = 0 ; i \u0026lt; n ; i ++ ) { if(tt \u0026lt; hh || (tt - hh + 1 \u0026lt; k )) { q[++tt] = s[i]; // 空的话入队列 或者 窗口比较小 cnts[s[i] - \u0026#39;a\u0026#39;] ++ ; }else { // 队列已经满了 if(check()) { ans.push_back( i - k); } cnts[s[i] - \u0026#39;a\u0026#39;] ++ ; q[++tt] = s[i]; cnts[q[hh ++ ] - \u0026#39;a\u0026#39;] -- ; // 队头弹出队列 } } if(check()) { ans.push_back( n - k); } ","date":"2026-08-07T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/leetcode-slide-window-0x01/","title":"[LeetCode][hot100] 滑动窗口"},{"content":"简历 preview 简历上 : 实现 点赞文章Feed\n前置 什么是 Feed 流 : 用于将实时生成的数据 传递给应用程序\n特点 ：\n实时性: 数据实时推送,确保用户获取最新消息\n个性化: 根据用户兴趣和行为跳转展示内容\n多样性: 支持各种类型的内容, 如文本、图片、视频等\n解决了什么问题 如果我要查询, 我关注的人最近发了什么文章. 我们很容易想到一个实现是直接查询数据库\n1 2 3 4 5 -- 我关注的人最近发的文章 SELECT * FROM articles WHERE author_id IN (我关注的人) ORDER BY ctime DESC LIMIT 20 但是现有的问题是 :\n用户不只是要看文章的更新。 还想要看 点赞、关注、评论 。\n每次打开首页都需要 查关注列表 → 查文章 → 查点赞 → 查关注事件 → 自己合并排序。 并且关注的人又很多 。 例如一个人关注了 1000个人 。 那么每次打开首页都需要扫 1000 人的数据\n大 V 发送一条内容。 一个大V 如果有100w的粉丝， 那么100w的粉丝每次去查询的时候都会发起 100w个select , 。\n因此 Feed 流做的事是 :\n在事件发生(发文、点赞) 先记录下来, 整理成 每人一条时间线 。 用户打开首页，仅读这条时间线,不再现场拼凑 没有 Feed 有 Feed 打开首页时 实时去查：我关注了谁 → 每人最近文章 → 再拼点赞、关注等 → 自己排序合并 直接读「我的收件箱/时间线」 复杂度 读路径很重，关注 1000 人就很惨 读路径轻，复杂逻辑提前做完 返回内容 临时拼出来的结果 预先沉淀好的事件流 同理 :\nFeed、热榜定时算、缓存预热，都属于同一类思路：\n把读路径上又重又复杂的活，提前算好（或换个地方算），业务方直接读结果。\n差别主要在「提前到什么时候、给谁算」：\n热榜定时算 缓存预热 Feed（推模型） 何时算 定时批处理 访问前/启动时 事件发生时（有人发文） 给谁 通常一份全局榜 热点 key 每个用户一份时间线 读的时候 直接取榜 直接取缓存 直接取「我的 inbox」 需求分析 从已有功能来说, Feed 流的数据来源可以包含 :\n用户关注的人发表了新的作品 用户发表的作品被人点赞、收藏、评论了 用户发表的评论被人回复了 有人关注了该用户 系统通知 目前的平台在组织 Feed 流的时候大体上有两种方式\n第一种方式是把前面列举的内容 都合并在一起\n只把关注者发表新作品做成 Feed 流 其余做成系统通知\n第一种例如 :\n1 2 3 4 5 张三 发布了《Go 并发》 李四 赞了你的文章 王五 关注了你 赵六 发布了《Redis 笔记》 钱七 评论了你：写得真好 第二种例如 :\nFeed 流\n1 2 张三 发布了《Go 并发》 赵六 发布了《Redis 笔记》 系统通知\n1 2 3 李四 赞了你的文章 王五 关注了你 钱七 评论了你 第一种方式实现起来更复杂，并且可以删减代码来转化为第二种 。 因此我们考虑使用第一种方式\nFeed 流式设计模式 拉模型 这应该是我们最先能想到的一个办法 就是我们直接去手动的获取相关数据。 然后聚合成 Feed 流\n这种方式的缺陷 :\n用户每发起一个查询请求，最终都会扩散为 N个数据库查询，数据库压力很大\n分页问题难以处理 : 我们正常使用 Feed 流查询的时候 都是分页返回 。 例如每次返回 20条, 而这 20条是按照时间戳来排序的。 我们没办法知道这20条会在哪些数据库上\n推模型 推模型是以 Feed 模块为核心, 不同业务方将数据推送过来 。\n读扩散 我们从 拉模型中 将查询 DB 的操作 改为 查询 发件箱 的操作\n对于每个业务维护自己的发件箱。 用于通知操作\nFeed 服务 每次需要\n查询 A 关注了哪些人 再查询 A 关注的人里面, 发件箱里面有什么数据 聚合排序、取出所需的数据 写扩散 从 推模型 中引入 收件箱 的概念, 每个业务都有自己的收件箱\n对于一个用户爱说，如果用户 A 关注了 用户B.\n那么 B 再发表一篇新文章的时候 就会把数据写入到 A\n对于 A 需要查询自己数据的时候 只需要查询自己的 收件箱即可\n写扩散的缺陷 :\n极大的放大的流量 , 如果 B 有 100w 粉丝 。他发一篇文章的时候 就需要同步给 100w 个粉丝 一瞬间就产生了 100w 条记录\n读扩散和写扩散的综合应用 我们知道\n读扩散的缺点 :\n每次读取都需要读取 每个业务相关的数据 分页不好处理 写扩散的缺点 :\n对于一个 大V来说， 100w的粉丝，一次推送就需要推送 100w条数据 因此我们可以考虑一个基本思路\n如果一个人的粉丝并不多, 那么就直接使用写扩散模型\n如果一个人的粉丝很多, 那么就直接使用读扩散模型\n并且这里还可以扩展\n判断粉丝是不是活跃用户，在写入数据的时候 针对活跃用户进行写扩散 综合使用的优缺点 :\n综合使用只能说是一个还不错的方案，是我们在读扩散和写扩散之间做了权衡之后不得不考虑的方案。\n• 从写流程来说，它依旧有写扩散的问题，但是我们能够限制住写扩散的数据并不多。\n• 从读流程来说，它依旧有聚合排序的问题。如果发件箱已经分库分表的话，那么也会存在跨库跨表查询的问 题。\n只是说，这两个缺陷都比单独使用读扩散或者单独使用写扩散要轻微，而不是完全没有。\n这个综合方案来说，可以优先保障活跃用户的使用体验，而对于非活跃用户来说，它的查询性能就要稍微差一 些。\n流程设计 写入流程 因为 Feed 里面不同类型的数据，字段会不一样 。 怎么存业务方同步过来的数据\n并且我们进一步考虑\n我们是否需要存储一些冗余数据 。 例如 用户的昵称，文章的标题\n冗余数据的好处就是从 Feed 里面拿到最完整的数据，而不需要进一步回查业务方\n存储扩展数据 对于存储 不同事件 Feed 独有的消息有两种方案\n一个大的 Json 字段存储这些所有的扩展数据 1 2 3 4 5 6 7 8 9 type FeedEvent struct { ID int64 Uid int64 Type string // 决定 JSON 怎么解读 Ctime time.Time Ext ExtendFields // map[string]string，个性化字段 } type ExtendFields map[string]string 使用扩展表 1 2 3 4 5 6 7 8 9 10 type FeedEvent struct{ Id int64 Type string } type ArticleEvent struct{ Id int64 Fid int64 Article int64 } 表结构设计 FeedPushEvent 推模型，写扩散 。 也就是收件箱\nFeedPullEvent 拉模型，读扩散 。 也就是发件箱\n最关键的是 我们这个 Content 字段 。 我们设计存储不同事件的个性化字段，是一个 JSON 字段\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type FeedPushEvent struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34;` // 收件人 UID int64 `gorm:\u0026#34;index\u0026#34;` // Type 用来标记是什么类型的事件 // 这边决定了 Content 怎么解读 Type string // 大的 json 串 Content string Ctime int64 `gorm:\u0026#34;index\u0026#34;` // 这个表理论上来说，是没有 Update 操作的 Utime int64 } type FeedPullEvent struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34;` // 发件人 UID int64 `gorm:\u0026#34;index\u0026#34;` // Type 用来标记是什么类型的事件 // 这边决定了 Content 怎么解读 Type string // 大的 json 串 Content string Ctime int64 `gorm:\u0026#34;index\u0026#34;` // 这个表理论上来说，是没有 Update 操作的 Utime int64 } 扩展字段处理流程 在 Feed 的时候 我们并不关心 个性化数据是什么 。 业务方可以随意传递\n然后我们再把对应的数据返回回去 。 由 BFF 自己去解决对应的数据查找\n服务定义 定义一个 共用的 feedService 和 一个业务自己实现的 feedHandler\n对于 FeedService 单纯只处理推模型\n而 feedHandler 可以根据业务自定义是否要采用 拉模型\n1 2 3 4 5 6 7 8 9 10 11 type FeedService interface { CreateFeedEvent(ctx context.Context, feed domain.FeedEvent) error GetFeedEventList(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error) } // Handler 具体业务处理逻辑 // 按照 type 来分。因为 type 是天然标记了哪个业务 type Handler interface { CreateFeedEvent(ctx context.Context, ext domain.ExtendFields) error FindFeedEvents(ctx context.Context, uid, timestamp, limit int64) ([]domain.FeedEvent, error) } 发表文章 对于发表文章来说，我们需要考虑我们最上面的问题\n对于一个 大V 来说, 如果我们发表一篇文章 。 应该是 让用户自己来读 大V的发件箱 ，也就是拉模型 。而不是大V 发送100条消息\n所以这里需要额外处理一下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 func (a *ArticleEventHandler) CreateFeedEvent(ctx context.Context, ext domain.ExtendFields) error { followee, err := ext.Get(\u0026#34;uid\u0026#34;).AsInt64() if err != nil { return err } // 要灵活判定是拉模型（读扩散）还是推模型（写扩散） static, err := a.followClient.GetFollowStatic(ctx, \u0026amp;followv1.GetFollowStaticRequest{ Followee: followee, }) if err != nil { return err } // 粉丝数超过阈值了，然后读扩散，不然写扩散 if static.FollowStatic.Followers \u0026gt; threshold { return a.repo.CreatePullEvent(ctx, domain.FeedEvent{ Type: ArticleEventName, Uid: followee, Ext: ext, }) } else { // 写扩散 followers, err := a.followClient.GetFollower(ctx, \u0026amp;followv1.GetFollowerRequest{Followee: followee}) if err != nil { return err } // 在这里，判定写扩散还是读扩散 // 要综合考虑什么活跃用户，是不是铁粉， // 在这里判定 events := slice.Map(followers.FollowRelations, func(idx int, src *followv1.FollowRelation) domain.FeedEvent { return domain.FeedEvent{ Uid: src.Follower, Type: ArticleEventName, Ext: ext, } }) return a.repo.CreatePushEvents(ctx, events) } } 部分问题 Q : 为什么同步数据的时候是异步接口 ？\n在设计 Feed 流的时候，我们一般会说 Feed 对实时性的要求很高。那么就会有一个问题，如果要是实时性要求很 高的话，为什么在这里我还是用了异步接口？\nA :\n虽然明面上我们对 Feed 的实时性要求高，但是这种要求其实是秒级，甚至十秒级。也就是说，如果 A 发 了一篇文章之后，B 能够在十秒内知道，那么我们认为这个实时性是可以接受的。\n查询流程 我们在整个 Feed 系统设计里面 只有两个东西\n发件箱\n收件箱\n当用户查询 Feed 的时候, 要求我们聚合这两部分数据 。我们有两种方法\n在 Service 层统一查询\n查询的时候转交 handler 来处理 ， Handler 内部可以处理一些和具体业务有关的事情\n面试 什么是 Feed 流 ？ 系统设计方面有什么难点？\n大数据高并发 在 Feed 里面，什么是拉模型 / 读扩散 ？ 有什么优缺点\n在 Feed 里面 什么是推模型/写扩散 ？ 有什么优缺点\n在 Feed 里面应该用 推模型还是拉模型 ？\n为什么大部分平台都是使用推和拉模型\n","date":"2026-08-07T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/feed-0x01/","title":"feed detail"},{"content":"简历 preview 简历上 : 实现 关注,点赞, 收藏, 发布文章 Feed 流的实现\n流程设计 需求分析 :\n对于用户关注的博主, 希望能够在博主发布文章的第一时间进行通知\n对于点赞 , 关注,收藏 操作希望能够第一时间进行通知\n模块分析 :\n提供一个 Feed 服务 供给业务方 读取和写入\nFeed 模块 支持的功能 通用的 写入 收件箱\n业务定制化的 写入收件箱 并且根据 业务定制 考虑优先写入到发件箱\n通用的 查询 用户 Feed 流\n业务定制化的查询 用户 Feed 流\n表结构设计 对于 收件箱 和 发件箱 维护一个 大JSON列进行处理\n创建 uid和 ctime 的索引\n1 2 3 4 5 6 7 8 9 10 11 12 13 type FeedPushEvent struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34;` // 收件人 UID int64 `gorm:\u0026#34;index\u0026#34;` // Type 用来标记是什么类型的事件 // 这边决定了 Content 怎么解读 Type string // 大的 json 串 Content string Ctime int64 `gorm:\u0026#34;index\u0026#34;` // 这个表理论上来说，是没有 Update 操作的 Utime int64 } 缓存设计 无 。因为需要保证实时性 并不考虑设计缓存\n但是引入 kafka 进行削峰 。 因为在业务上能够容忍 1s -10s 的响应误差\n其他细节 在 文章服务 写入收件箱的时候考虑定制化服务\n对于大V 关注的人很多, 写扩散来说会产生很多内容。 因此根据 用户粉丝数量来判断写入 发件箱还是收件箱\n同样的 查询用户 Feed 流的时候 同样在文章服务做定制化服务\n因为大V 可能发送了文章之后又设置了隐藏，因此会额外根据 status 进行一层过滤\n难点 \u0026amp; 亮点 难点 :\n问题 ：\n对于文章服务, 如果是一个 大V 采用写扩散会一时间产生很多数据 ，但是普通用户无所谓 。 但是对于一个 普通用户 使用读扩散 会导致请求多次数据库\n方案 :\n设计 文章服务 混合使用 推拉模型 ，而点赞/关注服务只进行写扩散 难点 :\n问题 :\n读路径 可能存在业务方定制 和 非业务方定制 , 并且还需要考虑数据聚合 方案 :\n考虑优先读取业务定制的内容,通过每个handle 自己的实现进行读取。 然后再通过通用逻辑进行读取 。 另外对于 第三方业务方 如果降级的情况 会考虑 不进行读取 。 全局limit查询之后 再聚合内容进行排序\n亮点 :\n使用 Kafka 进行异步的处理 Feed 流\n设计了一个通用的和业务定制化的 Feed 流 。 关联之前开发的 点赞、收藏、文章服务\n缺点 Feed 流的存储比较简单 ，非常粗暴的使用 大JSON 进行存储 并且 数据表并没有做定期归档\n写入服务还可以再优化，根据判断用户是否是活跃用户，判断是否要使用写扩散保证活跃用户的使用体验\n作为面试官，按「2 年经验、能讲清一个完整 Feed 项目」来打分。\n总分：62 / 100 维度 得分 满分 说明 需求与业务理解 12 15 关注/点赞/收藏/发文通知说清楚了 架构与方案设计 14 25 有推拉混合，但细节和取舍不够 数据与存储 8 15 表结构过简，缺容量与演进 工程实现（Kafka/异步） 10 15 有削峰思路，缺可靠性细节 难点与亮点表达 10 15 方向对，面试官追问容易露怯 自我反思与边界 8 15 有缺点意识，但深度不够 优点 业务闭环清晰：从「谁要被通知」到「Feed 读写服务」再到「文章大 V 定制」，简历点能对上故事。 推拉混合有意识：大 V 写发件箱、普通人写收件箱，这是 Feed 面试的核心考点，方向正确。 知道自己的短板：大 JSON、无归档、可按活跃用户优化写扩散——自我认知加分。 工程上有基本取舍：用 Kafka 换 1–10s 延迟，比「全都要实时」更真实。 缺点（面试里容易被打穿） 「无缓存」说得太绝对\n实时性 ≠ 不能缓存。常见做法是：收件箱最近 N 条 Redis ZSet、发件箱热 Key、读路径短 TTL。直接说「无」会被追问「QPS 上去怎么办」。\n表结构过于简陋\n单表 Content 大 JSON：如何按 Type 过滤、分页、清理？ uid + ctime 索引是否联合？大 V 粉丝千万级写入怎么分库分表？ 发件箱表结构没写出来，推拉混合缺一半。 推拉切换规则模糊\n「按粉丝数判断」——阈值多少？阈值变化怎么办？粉丝从 999 涨到 1001 历史数据怎么处理？面试官一追问就虚。\n读路径描述含糊\n「业务定制优先 → 通用逻辑 → 降级跳过 → limit 再聚合」——\n多 Handler 如何保证全局时间序？每个源 limit 多少？聚合复杂度？降级后一致性？这些才是 2 年候选人该补的。\nKafka 只停在「削峰」\n缺：幂等、顺序（按 uid 分区？）、失败重试/死信、重复投递、消费者并发与乱序。\n亮点偏「做过什么」而非「解决了什么」\n「用了 Kafka」「接了点赞收藏」——面试官更想听量化效果或具体坑（延迟、重复、空洞 Feed）。\n文档结构略乱\n功能列表编号重复、发件箱模型缺失、流程图依赖图片但文字没补关键路径。\n建议（按优先级） 立刻补齐，面试可讲 3 分钟版 画清两条写路径 + 两条读路径\n写：普通用户 → 写扩散进粉丝收件箱；大 V → 只写发件箱。 读：收件箱时间线 ∪ 关注列表里大 V 发件箱 → merge 按时间排序 → 分页。 把「粉丝阈值」说死\n例：粉丝 ≥ 1万走拉模式；并说明阈值变更时只影响新内容、或异步回填策略。\n缓存改成「有边界的有」\n例：用户 Feed 首屏 200 条 ZSet；未命中再查 DB；写路径异步更新。强调与「可容忍秒级延迟」一致。\nKafka 准备 4 个追问答案\n幂等（业务唯一键）、分区键（authorId/uid）、至少一次语义下的去重、消费失败策略。\n亮点改成「问题 → 方案 → 结果」\n例：「大 V 发文写扩散导致写放大 → 粉丝阈值切拉模式 → 峰值写 QPS 从 X 降到 Y」（没有真实数字就说「预期/压测方向」）。\n建议你能背下来的追问清单 分页用 offset 还是 cursor？空洞/重复怎么处理？ 取消关注、删文、隐藏后 Feed 如何失效？ 点赞/收藏也写 Feed，和文章 Feed 如何统一 Type？ 单用户收件箱无限增长怎么办（归档/TTL）？ 降级某个业务 Handler 后，用户看到的 Feed 如何保证「还能刷」？ 面试官结论 能过初级/中级偏初的筛选，但撑不起「独立设计过 Feed」的强叙事。\n架构骨架有了（推拉混合 + 异步削峰 + 业务定制 Handler），但存储、缓存、一致性、容量、读聚合这几块偏薄，和 2 年「做过但未深挖」的画像吻合。\n把推拉边界、读合并算法、Kafka 可靠性、缓存策略补成可口述的闭环后，有机会提到 75–80；再补上容量估算和一次真实故障/压测故事，才更像能打的中级候选人。\n","date":"2026-08-07T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/feed-0x02/","title":"feed one"},{"content":"简历 preview 简历上 : 设计并实现了 用户的 评论功能, 支持多层级的评论回复\n流程设计 需求分析 :\n需要支持 用户 某篇文章进行评论\n需要支持 用户 给评论 进行评论\n需要支持 获取评论，以及获取子评论，创建评论，删除评论\n模块分析 :\n用户 可以给文章进行 评论 也可以给 用户的评论 进行评论 ， 可以抽象为 用户给某个资源进行评论 因此将评论 单独抽象成一个服务 评论服务模块 支持的功能 获取第一页的评论\n删除评论\n创建评论\n获取单条评论\n获取子评论\n表结构设计 设计 \u0026lt;PID, id\u0026gt; 外键, 因为我们不允许子评论出现在一个不存在的父评论上 。 并且我们希望在删除父评论的时候 同步删除子评论。\n引入 RootID, 因为是通过 邻接表创建的 评论结构 。额外创建一个 RootId 列 能够优化查询顶级评论下全部评论的时间\nPID 和 RootID 的类型是 Sql.NullInt64 因为顶级节点的父节点是空 。 这里考虑优化成 -1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Comment struct { ID int64 Uid int64 Biz string `gorm:\u0026#34;index:biz_type_id\u0026#34;` BizID int64 `gorm:\u0026#34;index:biz_type_id\u0026#34;` PID sql.NullInt64 `gorm:\u0026#34;index\u0026#34;` // 外键指向的也是同一张表 ParentComment *Comment `gorm:\u0026#34;ForeignKey:PID;AssociationForeignKey:ID;constraint:OnDelete:CASCADE\u0026#34;` RootID sql.NullInt64 `gorm:\u0026#34;index:root_ID_ctime\u0026#34;` Ctime int64 `gorm:\u0026#34;index:root_ID_ctime\u0026#34;` // 评论的内容 Content string Utime int64 } 获取评论列表功能设计 我们需要获取对应业务的 第一页所有评论，并且返回前3条子评论\n直接根据 biz 和 bizID 进行查询\n考虑到并发问题 使用 minID 进行偏移控制 。 如果使用 offset 可能会频繁变化\n在降级到时候 考虑不去读取子评论 。 遍历所有顶级节点 然后并发的去查询子评论\n其他细节 创建和删除 的时候 通过外键进行约束\n查询顶级评论的时候 根据 RootId 进行优化\n难点 \u0026amp; 亮点 问题 :\n对于多级评论的表结构设计\n方案 :\n使用邻接表的方案进行设计评论表 。设计 外键 \u0026lt;pid, id\u0026gt; 控制 插入子评论的时候 不应该插入到不存在的父评论 。 以及删除父评论的时候应该同步删除子评论\n亮点 :\n在查找第一页评论的时候 需要返回对应的前3条子评论 。 考虑在服务降级的时候，不走这部分慢查询 。\n考虑 用户可以评论 别人的评论 和文章 将评论服务单独拆成一个服务 并且使用 biz + biz_id 进行通用化处理\n使用 minId 进行第一次分页查询，避免 offset 在高并发插入下 会漏\n表结构设计 冗余 RootID 列，在查询顶级评论的时候能够直接查询全部回复 ，而不需要递归一整颗树\n缺点 没有较好的缓存方案，例如加载第一页的时候可以考虑 维护一个热榜评论，并且缓存第一页 定时刷新 总分：68 / 100 对标 2 年 Golang、简历点「多层级评论」：能讲清主干，但深度、完整性和面试表达还不够稳，容易被追问卡住。\n分项（满分） 维度 分 说明 需求分析 / 边界 14/20 功能列全了，缺非功能（QPS、延迟、一致性、审核） 表结构 / 建模 16/20 邻接表 + RootID + FK 方向对，细节经不起深挖 查询 / 分页 12/20 知道 minID、降级，论证不严谨，实现路径模糊 亮点可讲性 14/20 有点，但缺「为什么 / 怎么权衡 / 踩过什么坑」 表达与材料质量 12/20 结构尚可，笔误多、描述空、像草稿 优点 服务抽象合理：biz + biz_id 把「评文章 / 评评论」统一成资源评论，符合可扩展拆分，面试官一般认可。 树存储选型正确：邻接表 + 冗余 RootID 是常见方案；外键约束父子存在性、级联删也说得通。 有工程意识：第一页带前 3 条子评、降级不查子评、minID 代替 offset，说明想过热路径和并发插入。 有自省：主动写「缺缓存」比硬吹完整方案更加分。 缺点（面试里会被追的） 「多层级」与实现深度不匹配\n简历写多层级，正文几乎是「顶级 + 若干子评」。要明确：产品是无限嵌套还是只展示两级；邻接表如何保证插入时 PID/RootID 一致。\n分页说法不严谨\n高并发下 offset 的问题主要是重复/跳过、深分页成本，不是简单「漏数据」。minID（游标）要讲清排序键（id 还是 ctime）、同时间戳、方向（上滑/下滑）。\n级联硬删很脆\n真实产品多为软删、子评保留并标「父评已删」。只说 OnDelete:CASCADE 会被问：误删恢复、审核下架、计数怎么维护。\n「前 3 条子评」实现空洞\n按时间还是热度？一次 SQL 还是 N+1？并发查子评的限流、错误、超时？降级开关与指标？这些不补，亮点站不住。\n空值方案半吊子\nsql.NullInt64 vs -1 只提「考虑」，没有索引、查询条件、GORM 写入的结论。\n缺面试常问块\n创建时事务与 RootID 赋值、内容长度/敏感词、计数（回复数）、鉴权（谁可删）、幂等、缓存一致性——几乎空白。\n材料完成度低\ndescription: xxxx、错别字、口语化、无接口草图/时序，显得准备不充分。\n建议（按优先级） 准备 3 分钟口述稿：需求 → 为何独立评论服务 → 表（PID/RootID）→ 第一页查询（含前 3 条子评）→ 降级 → 已知不足（缓存）。控制在 2～3 分钟。 画清两种删除语义：硬删级联 vs 软删；选一种并说清对列表、计数、子评展示的影响。 把分页讲死：排序字段、WHERE id \u0026lt; ? ORDER BY id DESC LIMIT、为何不用 offset、边界 case。 补「前 3 条」实现：例如按 root_id 批量取 + 应用层截断，或窗口函数；说明为何不用「每条顶级再查一次」做默认路径。 补一条缓存方案（哪怕未做）：第一页热评 ZSet / 本地+Redis、失效（写删、TTL）、穿透与击穿——对应你自己写的缺点。 对齐简历用词：若只做两级展示，简历改成「支持回复与两级展示」；若真支持 N 级，补插入校验与查询深度限制。 准备 5 个追问答案：并发插评、父评不存在、删父留子、深分页、评论热点文章打爆 DB。 一句话结论 68 分：方向和关键词对，够「做过评论」的入门讲述；要过认真的 Golang 一面/二面，需要把分页、删除语义、子评批量查询和缓存权衡讲扎实，并把材料从草稿修到能直接口述的版本。\n","date":"2026-08-07T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/comment-0x02/","title":"评论 one"},{"content":"简历 preview 简历上 : 实现多实例部署下的 热榜 异步计算和查询\n流程设计 需求分析 :\n需要能够获取到最近7天 前100点赞的数据\n模块分析 :\n设计一个 RankService 支持获取 TopN 的操作\n模块设计 提供一个基于 本地缓存 + Zset 实现的热榜设计 并且通过 分key 热点信息\n提供一个离线计算的热榜服务, 使用小根堆进行维护数据 。使用分布式锁 启动时固定实例进行计算热榜\n难点 \u0026amp; 亮点 问题 :\n热榜服务要求高可用 高性能 以及处理较大的数据 怎么做到 ?\n方案 :\n使用 本地缓存 + Redis 的方式进行 处理高可用 。 如果本地缓存和redis 缓存都实效 加载本地已过期的进行兜底\n较大的数据 可以采用 分key 的方式进行处理 。 或者通过业务折中的方式，例如 只考虑最近7天的 top 数据，最近 7 天可能最多也就100w条\n问题 :\nZset 不可能进行较大数据量大计算, 这会给机器带来很大的压力\n方案 :\n采用定时任务 离线计算 。\n使用 优先队列维护一个小根堆，固定 TopN 的数量。 每次分批进行获取数据加载计算\n亮点 :\n考虑在多实例部署的情况下, 可能会存在 多个实例同时计算一个热榜，并且每个热榜都会有细微的偏差。 使用分布式锁+自动续约的机制 控制同一时间只有一个 节点 在进行计算\n使用 Hacknews 模型 Score = (p - 1) / (T + 2)^G 公式进行计算热点信息。防止点赞数量被老贴市场霸占\n在 Redis 存 TopN 的时候晴空 Content 避免占用较大的内存\n缺点 缓存方式比较简单\n强一致性不足\n总分：62 / 100 按「2 年 Golang 后端」面试标准看：方向对、有架构意识，但深度不够、表述不严谨、经不起追问。能过初筛讲清故事，很难稳住深挖。\n维度 分数 简评 问题定义与业务抽象 70 TopN + 近 7 天边界清楚 架构设计完整性 65 多级缓存 + 离线计算主链路合理 技术细节与落地 50 缺关键实现、参数、边界条件 难点亮点说服力 68 锁、堆、HN 公式有亮点，但讲不透 表达与专业度 55 错别字、占位符多，像草稿 面试可追问准备 48 几乎没有「被问住」时的备用料 优点 主路径合理：查询走本地缓存 → Redis → 兜底；计算走离线 TopN + 小根堆，符合热榜常见做法，不是乱堆关键词。 有业务折中意识：用「近 7 天 / 约 100 万」控数据量，比硬扛全量排序更像有生产经验。 亮点选得对：多实例分布式锁、Hacker News 时间衰减、ZSet 不存 Content——这些正是面试官能追问的点。 主动暴露缺点：缓存简单、强一致不足，至少说明你知道系统边界，比只会吹「高可用高性能」好。 有架构图：比纯文字更容易在面试里讲清读写两条链路。 缺点（面试会被卡住的地方） 简历描述太空：「多实例部署下的热榜异步计算和查询」没有规模、指标、职责边界，面试官第一问就会要数字。 方案停在名词层：分 key、小根堆、分布式锁、自动续约都点到了，但缺： 分 key 规则（按天？按互动类型？怎么合并 TopN？） 堆的维护时机与批大小 锁用 Redis/etcd？续约失败怎么办？持锁实例挂了呢？ 本地缓存过期后仍兜底，跨实例一致性怎么说？ 公式只写了不解释：Score = (p - 1) / (T + 2)^G 里 p/T/G 含义、G 怎么选、和「点赞数排序」差在哪，不讲等于没亮点。 图文对不齐：图里有并发 Queue、分区、forceGetLocal，正文几乎没展开，面试官对着图一问，你会空。 「高可用」论证弱：本地 + Redis 失效后用过期数据，是降级不是高可用；没有说明降级 SLA、空榜、穿透、击穿。 表达不专业：xxxx、实效→失效、晴空→清空、Hacknews，会直接拉低可信度。 缺点写完就停了：「强一致性不足」没有影响面和为何可接受，像没想完。 建议（按面试准备优先级） 补一组可说的数字：数据量、TopN、计算周期、单次计算耗时、QPS、缓存命中率、锁超时/续约周期。没有真实数也要给合理量级。 准备 3 层回答：一句话结论 → 架构步骤 → 细节/权衡。例如「为何小根堆」：只要 Top100，堆复杂度 O(n log k)，比全量排序或大 ZSet 更合适。 把追问清单写进稿子，至少覆盖： 多实例本地缓存不一致怎么办？ 计算中途失败，半成品会不会写进 Redis？ 分 key 后如何得到全局 Top100？ 锁续约失败 / 时钟漂移 / 脑裂 缓存击穿（热榜 key 过期瞬间） 讲清 HN 公式：老帖高赞为何会被新帖追上；G 调大/调小的业务含义。 把图里的并发 Queue 讲成 Go 实现：channel + worker pool、batch size、背压，对应 2 年经验该有的落地感。 润色表述：去掉占位符和错别字；简历改成「近 7 天 Top100 热榜：离线小根堆计算 + Redis ZSet + 本地缓存，分布式锁保证单实例计算」。 缺点改成「已知取舍」：例如强一致不必要，因为热榜允许分钟级延迟；用计算周期 + TTL 把最终一致说清楚。 面试官视角一句话 作为 2 年经验候选人，这份材料证明你做过热榜、懂分层和折中，但目前更像设计草稿，不像能扛 20–30 分钟深挖的面试稿。把「分 key / 锁续约 / 公式参数 / 并发队列 / 降级语义」补实，并清掉错别字，分数大概能到 75–80；再补上指标和失败场景，才比较稳。\n","date":"2026-08-07T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/rank-0x02/","title":"热榜 one"},{"content":"简历 preview ","date":"2026-08-07T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/contract-0x01/","title":"用户关系 detail"},{"content":"什么是 FunctionCall 我们已经知道 一个 Tool 是 函数本体+名称+描述+参数 定义的\n有一个关键问题 : 大模型在 作出 要调用这个工具 的决策之后, 它怎么告诉 Agent 去执行\n大模型只会说话 , 输出的永远只是文字\nAgent 要调用工具,必须知道 调哪个工具，传什么参数，格式是什么 。 这中间的信息交换，靠什么来保证正确\n没有 Funcition Call 之前 开发者 将 全部自然语言 写进 system prompt 系统提示词里面\n1 2 3 4 5 6 7 8 9 10 11 12 你是一个智能助手，你可以使用以下工具： 工具名：check_weather 功能：查询菜个城市的天气 参数： - city：城市名，中文，比如\u0026#34;上海\u0026#34; - date：日期，格式必须是 YYYY-MM-DD，比如\u0026#34;2026-03-19\u0026#34; - date 是可选的，不填默认查今天 当用户需要查天气时，你要调用这个工具，告诉我工具名和参数。 你让他查 [上海明天的天气] ，可能会返回\n散文式回复 :, \u0026ldquo;好的，我来帮你查询上海明天的天气\u0026rdquo;\n参数顺序颠倒 :\nTBD\n","date":"2026-08-06T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-agent-0x05/","title":"[all_in_ai]  AI 名词扫盲 FunctionCall"},{"content":"canal TBD\n","date":"2026-08-06T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/canal-0x01/","title":"canal"},{"content":"ELK TBD\n","date":"2026-08-06T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/elk-0x01/","title":"ELK"},{"content":"简历 preview 简历上 : 设计并实现了 搜索\n需求分析 平台里面都会有一个搜索栏 大体上的目的是 :\n方便用户快速找到所需的内容\n增加广告收入\n支持搜索主要是支持 :\n搜索具体的用户\n搜索某篇文章\n因为需要考虑使用使用 ES 进行实现, 所以需要把这两个服务原有的数据 额外的 写入到 ES 中 。\nQ : 为什么 不是用户直接写入到 ES\nA : 从 DDD 的角度来说 , es 并不考虑 user 的索引怎么确定 也不处理一些业务相关的内容 。 所以只能通过 search 服务\n搜索流程设计 推送接口 支持业务定制化的结构 和 统一处理的结构\n提供统一处理接口 和 定制化接口\n1 2 3 4 5 6 7 service SyncService { rpc InputUser (InputUserRequest) returns (InputUserResponse); rpc InputArticle (InputArticleRequest) returns (InputArticleResponse); rpc InputAny(InputAnyRequest) returns(InputAnyResponse); // 同步标签 // rpc InputTags() } 推送接口实现 唯一需要注意的一点就是,我们需要传入 user.Id 保证是 updateSert 语意\n1 2 3 4 5 6 7 func (h *UserElasticDAO) InputUser(ctx context.Context, user User) error { _, err := h.client.Index(). Index(UserIndexName). Id(strconv.FormatInt(user.Id, 10)). BodyJson(user).Do(ctx) return err } es 表结构设计 文章表结构 mapping :\ntitle : type : text\ncontent : type : text\nid : type long ; (贯穿业务的id)\nstatus : type : interger\n用户表结构 mapping :\nnickname : type : text\nemail : type : text (不使用keyword，正常人很难记住邮箱)\nphone : type : keyword\nid : tyoe long\n(这里要不要处理,理论上来说前端是不支持的 。 用户层面应该是看不到的，但是客服预计能看到) 搜索接口设计 1 2 3 4 service SearchService { // 这个是最为模糊的搜索接口 rpc Search(SearchRequest) returns (SearchResponse); } 搜索接口的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func (s *SearchServiceServer) Search(ctx context.Context,request *searchv1.SearchRequest,)(*searchv1.SearchResponse, error) { resp, err := s.svc.Search(ctx, request.Uid, request.Expression) if err != nil { return nil, err } return \u0026amp;searchv1.SearchResponse{ User: \u0026amp;searchv1.UserResult{ Users: slice.Map(resp.Users, func(idx int, src domain.User) *searchv1.User { return \u0026amp;searchv1.User{ Id: src.Id, Nickname: src.Nickname, Email: src.Email, Phone: src.Phone, } }), }, Article: \u0026amp;searchv1.ArticleResult{ Articles: slice.Map(resp.Articles, func(idx int, src domain.Article) *searchv1.Article { return \u0026amp;searchv1.Article{ Id: src.Id, Title: src.Title, Status: src.Status, Content: src.Content, } }), }, }, nil } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func (h *UserElasticDAO) Search(ctx context.Context, keywords []string) ([]User, error) { queryString := strings.Join(keywords, \u0026#34; \u0026#34;) query := elastic.NewBoolQuery().Must( elastic.NewMatchQuery(\u0026#34;nickname\u0026#34;, queryString), ) resp, err := h.client.Search(UserIndexName).Query(query).Do(ctx) if err != nil { return nil, err } res := make([]User, 0, len(resp.Hits.Hits)) for _, hit := range resp.Hits.Hits { var ele User err = json.Unmarshal(hit.Source, \u0026amp;ele) if err != nil { return nil, err } res = append(res, ele) } return res, nil } 推送消息 我们在推送消息那个模块引入 3个 kafka 进行异步的推送消息\n为不同的业务定义不同的 event , 而后业务方朝特定的 Topic 发送消息\n定义一个统一的 Event 的格式\n标签流程设计 标签功能 提升用户体验\n提高搜索引擎优化\n社交分享\n个性化推荐\n全局标签、个人标签、通用标签\n用户创建标签\n用户对某个资源打上标签\n表结构设计 创建两张表\nTag 表 , 索引 uid . 主要是为了解决 加载个人的全部标签的 内容\nTagBiz 表, 记录某个人对某个资源打的标签 。\n理论上我们可以通过 TagBiz 查询一次 Tag 进行获获取 Uid。 但是会多一次自查询 。 尤其是我们在覆盖标签的写法的时候 删除的时候很麻烦\n1 2 // delete from tag_biz where tid IN // (select distinct id from tag where uid = ?) AND biz = ? AND biz_id = ? 我们为什么要外键 。 Tid 必须对应完整的 Id 字段 。 避免脏关系 。 并且设置级联删除，删除标签的时候 关联的 tag_bizs 一起清理掉 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 type Tag struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34;` // 我要不要在这里创建一个唯一索引\u0026lt;uid, name\u0026gt; Name string `gorm:\u0026#34;type=varchar(4096)\u0026#34;` // 要在 uid 上创建一个索引 // 因为你有一个典型的根据 uid 来查询的场景 Uid int64 `gorm:\u0026#34;index\u0026#34;` Ctime int64 Utime int64 } // TagBiz 某个人对某个资源打了标签。 type TagBiz struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34;` BizId int64 `gorm:\u0026#34;index:biz_type_id\u0026#34;` Biz string `gorm:\u0026#34;index:biz_type_id\u0026#34;` // 冗余字段，加快查询和删除 // 这个字段可以删除的 Uid int64 `gorm:\u0026#34;index\u0026#34;` //TagName string Tid int64 Tag *Tag `gorm:\u0026#34;ForeignKey:Tid;AssociationForeignKey:Id;constraint:OnDelete:CASCADE\u0026#34;` Ctime int64 `bson:\u0026#34;ctime,omitempty\u0026#34;` Utime int64 `bson:\u0026#34;utime,omitempty\u0026#34;` } 缓存方案 对于获取用户的全部 tags 我们使用 redis-list 进行缓存预加载\n提供一个 PreloadUserTags 在程序启动的时候 获取全量的tags 分为多个不同的 uid list 进行插入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 func (repo *CachedTagRepository) PreloadUserTags(ctx context.Context) error { offset := 0 const batch = 100 for { dbCtx, cancel := context.WithTimeout(ctx, time.Second) // 在这里还有一点点的优化手段，就是 GetTags 的时候，order by uid tags, err := repo.dao.GetTags(dbCtx, offset, batch) cancel() if err != nil { return err } for _, tag := range tags { rctx, ccancel := context.WithTimeout(ctx, time.Second) err = repo.cache.Append(rctx, tag.Uid, repo.toDomain(tag)) ccancel() if err != nil { continue } } if len(tags) \u0026lt; batch { return nil } offset += batch } } func (r *RedisTagCache) Append(ctx context.Context, uid int64, tags ...domain.Tag) error { data := make([]any, 0, len(tags)) for _, tag := range tags { val, err := json.Marshal(tag) if err != nil { return err } data = append(data, val) } key := r.userTagsKey(uid) // 利用 pipeline 来执行，性能好一点 pip := r.client.Pipeline() pip.RPush(ctx, key, data...) if r.expiration \u0026gt; 0 { pip.Expire(ctx, key, r.expiration) } _, err := pip.Exec(ctx) return err } 标签 + 搜索 使用 kafka 发送一个通用的信息到 any里面 。 注意这里需要保证有序性 因此需要设置key\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func (svc *tagService) AttachTags(...)error { err := svc.repo.BindTagToBiz(ctx, uid, biz, bizId, tags) if err != nil { return err } // 异步发送 go func() { ts, err := svc.repo.GetTagsById(ctx, tags) if err != nil { svc.logger.Error(\u0026#34;查询标签失败\u0026#34;, logger.Error(err.Error())) return } // 这里要根据 tag_index 的结构来定义 // 同样要注意顺序，即同一个用户对同一个资源打标签的顺序， // 是不能乱的 pctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() err = svc.producer.ProduceSyncEvent(pctx, events.BizTags{ Uid: uid, Biz: biz, BizId: bizId, Tags: slice.Map(ts, func(idx int, src domain.Tag) string { return src.Name }), }) if err != nil { svc.logger.Error(\u0026#34;发送标签搜索事件失败\u0026#34;, logger.Error(err.Error())) } }() return nil } \u0026amp;sarama.ProducerMessage{ Topic: \u0026#34;search_sync_data\u0026#34;, Key: sarama.StringEncoder(fmt.Sprintf(\u0026#34;%d_%s_%d\u0026#34;, tags.Uid, tags.Biz, tags.BizId)), Value: sarama.ByteEncoder(data), } 标签索引定义 :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;tags\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;uid\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;long\u0026#34; }, \u0026#34;biz\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;biz_id\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;long\u0026#34; } } } } 关联两个查询 一个问题 ：\n我们标签和文章是两个索引, 我们需要解决一个类似 mysql 一样的 JOIN 查询 ES 提供了两种方式\n内嵌文档\n父子关系\n这两种性能很差，我们采用多次查询的方式\n先查询标签，找到对应的 biz id\n查询 article ,子 title 和 conent 的查询基础上进一步叠加是否在第一次查询到的 biz id\n先去 tags 查询出符合条件的 artclie - id 然后再去 article 查询对应的 文章。并且设置文章命中的 权重更高 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ids, err := a.tags.Search(ctx, uid, \u0026#34;article\u0026#34;, keywords) if err != nil { return nil, err } // 加一个 bizids 的输入，这个 bizid 是标签含有关键字的 biz_id arts, err := a.dao.Search(ctx, ids, keywords) if err != nil { return nil, err } // .. func (h *ArticleElasticDAO) Search(ctx context.Context,tagArtIds []int64,keywords []string,) ([]Article, error) { queryString := strings.Join(keywords, \u0026#34; \u0026#34;) // 标签命中 tagArtIdAnys := slice.Map(tagArtIds, func(idx int, src int64) any { return src }) title := elastic.NewMatchQuery(\u0026#34;title\u0026#34;, queryString) content := elastic.NewMatchQuery(\u0026#34;content\u0026#34;, queryString) or := elastic.NewBoolQuery().Should(title, content) if len(tagArtIds) \u0026gt; 0 { tag := elastic.NewTermsQuery(\u0026#34;id\u0026#34;, tagArtIdAnys...).Boost(2.0) or = or.Should(tag) } query := elastic.NewBoolQuery().Must( or, elastic.NewTermQuery(\u0026#34;status\u0026#34;, 2), ) // .. } 面试 你有没有用过 ES ？ 用它来解决什么问题？\nES 中的倒排索引是什么？ 为什么叫做倒排索引 ？\nES 是如何组织倒排索引的 ？ 核心是利用了 FST 结构\nES 的节点类型有哪些？ 他们的作用是什么 ？\nES 的写入过程是怎么样的 ？ 为什么说他是近实时的\n什么是缓存预加载？\n怎么判定一个场景要不要缓存？ 缓存时间多久？\n同一个资源，短时间内不会被重复访问 就不需要缓存 理论上缓存时间应该根据用户的习惯来定 如何控制 ES 返回的结果\n怎么在 ES 中解决类似 Mysql Join 的查询场景 ？\n","date":"2026-08-06T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/select-0x01/","title":"搜索 detail"},{"content":"简历 preview 简历上 : 设计并实现了 搜索\n流程设计 需求分析 :\n支持用户创建 标签, 查看已有标签, 删除标签\n支持用户给指定文章打标签\n支持用户查询 对应关键字的文章 并且把 打过标签的文章优先返回\n模块分析 :\n考虑标签应该是一个通用的功能 所以将标签单独拆分一个模块\n并且不希望业务直接写入ES，单独设计一层 搜索和插入服务。 将读和写拆成两个服务\n标签服务 模块 支持功能 覆盖标签\n创建标签\n获取用户的全部标签\n获取用户给某篇文章打的全部标签\n表结构设计 设计 Tag 表, 和 TagBiz 表 用于统计用户创建的 tag 和 用户给文章标记的 Tag\n索引设计 :\n在 Tag 表上设计了 uid 为 索引，保证 获取用户的全部标签的时候能够命中\n在TagBiz表设计了 biz_id,biz 和 uid 设计了索引, 同时tagBiz 设置 target_id 外键连接 tag 表\n考虑 uid 的不可变性, 设计 tagBiz 表的时候冗余了 uid 的设计，从而在查寻用户给某篇文章打的标签的时候 不需要去Join Tag表 加速查询\n缓存设计 在获取用户的全部标签的时候, 考虑使用缓存预加载进行优化\n在程序启动的时候，提前全量扫描一次数据库\n将数据分key 加载到 redis-list 中\n在获取全部标签的时候 预先读取缓存再去操作数据库\n插入搜索模块 支持\n通用的插入逻辑 content_id,content\n业务定制化的 user 和 article 的插入逻辑\nEs 索引设计 文章表结构设计 :\ntitle , content , id ,status . 这里引入 status 主要是为了后续 用户可以查看到仅自己可以见的文章处理 用户表设计 :\nnickename, email(text), phone, id . email设计称为 text 而不是keyword是因为正常人很难记住邮箱 数据插入处理 使用 Kafka 开启 3个 Consumer 分别监听 文章, User, Any 的插入\n因为我们在索引中引入了 id 字段, 保证了在并发场景下是 upsert 语意 查询搜索模块 支持查询有关用户的信息\n支持查询有关文章的信息\n并且将用户打过标签文章的内容优先返回 联合 Tag 查询 Es 支持通过 父子关系 和 内嵌文章进行联合查询 但是性能过差 。 我们这里采用二次查询的方式实现联合查询\n先查询 Tag 里面的内容， 查询当前 用户已经打过标签的 文章 id\n再去查询有关keywords的文章信息，并且使用 TermQuery Boost 增加在第一次查询出来的权重\n难点 \u0026amp; 亮点 问题 :\n用户在给某个文章打标签的时候 都会加载一次自己已有的全部标签, 预计这里会是一个高频的全量扫表过程 方案 :\n使用 Redis-list 预加载一次全部用户的全部标签内容\n每次查询优先查询缓存 再查询数据库,对于未命中的内容 再次更新缓存\n问题 :\n在获取用户给某篇文章的所有标签内容处理时 都需要去 Join 一次 Tag 表 . Join操作会变慢 方案 :\n因为 Uid 的不可变性。我们设计的 tag 是基于 Uid 的 。不存在 用户A给 文章 A打了标签。 突然这个标签变成用户 B 的情况 。 所以在 TagBiz 冗余了 Uid 字段加速查询 问题 :\n用户对一篇文章重复打标签可能会有并发问题 。 因为我们使用 kafka 进行发送Es信息 方案 :\n在对应 producer 的时候, 使用 key 绑定对应的uid 保证同一个uid 的内容只会发送到同一个分区 亮点 :\n实现了 两个 Tag 内容之前的查询 , 使用二次查询的方法 实现 用户查询有关文章的时候, 优先返回已打标签的文章 亮点 :\n搜索服务拆分成 sync 和 search ,读写分离 。 业务方面不直连 Es . 并且 支持 InputAny 方便业务快速上线 亮点 :\n预加载的时候 使用 pipeline 进行打包发送 redis 指令 缺点 仅实现了 文章和标签的相关服务 。 实现内容过少，并没有聚合点赞收藏等业务\nKafka 部分少了兜底机制\n总分：61 / 100 定位：2 年经验、项目深挖准备材料。能讲清主流程和模块边界，但多处方案经不起追问，Golang 工程深度偏弱。面试中「能讲」可以，扛不住连环追问。\n维度 分数 说明 需求与模块拆分 72 标签独立、读写拆分思路清楚 表结构 / 索引 65 有索引与冗余意识，外键与命名易被挑 缓存设计 42 启动全量预加载是明显硬伤 ES / 搜索 60 二次查询 + Boost 合理，缺量化与一致性 Kafka / 并发 48 问题与解法错位 难点亮点表达 68 有结构，但方案可信度不足 自我认知 70 知道功能少、缺兜底 Golang 工程深度 35 几乎看不到语言/并发/落地细节 优点 需求 → 模块 → 表 → 缓存 → MQ → ES 叙事完整，面试官能跟着走。 标签独立模块 + sync/search 读写分离 + 业务不直连 ES，符合中级对边界的基本要求。 TagBiz 冗余 uid 避免 Join、ES 二次查询 + Term Boost（相对 nested/父子文档）是可讲的取舍。 有难点/亮点/缺点框架，并提到 Kafka 缺兜底、功能未聚合，比只会吹亮点强。 提到 pipeline 批量写 Redis、id upsert，说明接触过落地细节。 缺点（面试里最容易被打穿的点） 缓存方案危险\n启动全量扫库、把「全部用户全部标签」塞进 Redis List：冷启动、内存、扩缩容、局部失效都没讲。2 年经验里这是减分点，不是加分点。List 也不适合标签的增删改；一致性（写标签后如何更新缓存）几乎空白。\n并发问题与解法错位\n「重复打标签」应落到：DB 唯一约束 / 事务 / 幂等。Kafka 按 uid 分区只保证同 uid 有序，解决不了重复打标、更解决不了跨服务幂等。面试官一追问就会露馅。\nES 细节经不起问\nemail 用 text：理由偏弱，应讲分词、模糊、精确匹配、keyword 多字段。 status 字段只点到，未见权限/过滤怎么做。 upsert、同步延迟、最终一致性、失败重试几乎没准备。 表设计表述不严谨\nTagBiz target_id 外键、索引 (biz_id, biz) + uid 缺查询路径说明；「覆盖标签」语义不清。\n缺量化与 Golang 深度\n无 QPS、延迟、数据量、缓存命中率。无接口抽象、超时取消、consumer 并发、错误处理、压测——对 Golang 岗这是硬伤。\n材料完成度\n「其他细节」空、序号重复、resume「搜索」与正文「标签+搜索」焦点略散。\n建议（按优先级） 1. 重写缓存故事（必改）\n改成：按 uid 懒加载 / 热点预热；Hash 或 String+JSON；打标/删标时删缓存或局部更新；讲穿透、击穿、雪崩之一即可。删掉「启动全量预加载所有用户」。\n2. 对齐「重复打标」叙事\n主答：唯一索引 (uid, biz, biz_id, tag_id) + 幂等；Kafka 分区 key 只作为「同步有序/同用户串行」的补充，不要当并发正解。\n3. 补 3 个可量化数字\n例：标签人均条数、搜索 P99、sync 延迟、日增量。没有真实数据就给设计目标量级，比没有强。\n4. 准备 5 个追问答案\n标签写后搜索多久可见？ sync 失败怎么办？（重试、DLQ、对账） 缓存与 DB 不一致怎么办？ Boost 权重怎么定、如何验证效果？ 为何不用 nested / join / 宽表？ 5. 补 Golang 落地半页\nConsumer 并发模型、context 超时、接口分层、ES client 错误处理、本地单测怎么 mock——面试官常从这里区分 1 年和 2 年。\n6. 收束简历表述\n简历写「标签 + 搜索（读写分离）」或主讲一条线，避免「实现了搜索」却大段讲标签预热。\n面试官一句话结论 能过初筛讲项目，过不了深挖。 模块拆分和二次查询+Boost 可以撑住 10–15 分钟；缓存全量预加载和「Kafka 解决重复打标」会把印象从「还行」拉到「基础不扎实」。把这两处改掉，材料有机会到 75+；再补量化与 Golang 细节，更接近 2 年合格线。\n","date":"2026-08-06T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/select-0x02/","title":"搜索 one"},{"content":"Es 提供一种简单、高效的方式 来存储、搜索和分析大量数据\nES 支持近实时的搜索\nES 支持极大数据量\nES 提供 Resful API\n主要核心就是 : 搜索\n搜索引擎的搜索并不依赖 ES，因为搜索引擎优先出现 。 但是他们底层都是 倒排索引\n搜索、广告、金融\n数据的组织方式 :\n索引: 类比 Mysql 的表\n文档 : 类比 Myssql 的表的数据\n数据的部署方式 :\n分片: 类比关系数据库的分库分表\n副本 : 类比主从同步中的从库\n索引与倒排索引 索引 : 数据本身\n倒排索引 : 从属性出发，找到这些属性的数据\nES 写入流程 : 文档首先写入到 buffer 里面\n定时刷新到 page cache 中 ，这个过程叫做 refresh\n刷新到磁盘中\nES HTTP API 创建索引 Es 暴露了 HTTP API 。 可以通过 HTTP 请求来操作整个 CRUD\n我们在下面的操作进行了 :\n创建了一个索引\n制定了 3个分片 和 2个副本\n设置了3个字段，并且设置了每个字段的类型\n写入数据 \u0026amp; 查询数据 通过 指定 index 和 _doc 表示我们在 user_index 写入了数据\n我们通过 index 和 search 指定我们查询的数据\n虽然我们并没有指定完整的 email 但是还是返回了预期的结果 ES 支持的查询 Match Query（匹配查询）：根据字段中的内容进行全文匹配查询。 • Term Query（精确查询）：根据字段中的精确值进行查询，适用于 keyword 类型或者已经执行过分词器的字段。\n• Range Query（范围查询）：根据字段中的范围值进行查询，可以用来查询数字或日期范围。\n• Bool Query（布尔查询）：通过逻辑运算符（must、must_not、should）组合多个查询条件，实现更复杂的查询逻辑。\n• Match Phrase Query（短语匹配查询）：根据字段中连续的短语进行查询，适用于需要保持短语顺序的查询。\n• Prefix Query（前缀查询）：根据字段中的前缀进行查询，适用于需要按照前缀匹配查询的场景。\n• Wildcard Query（通配符查询）：根据通配符模式进行查询，支持通配符符号（*和?）进行模糊匹配。\n• Fuzzy Query（模糊查询）：根据字段中的模糊匹配进行查询，可以通过设置 fuzziness 参数来控制模糊程度。\n• Nested Query（嵌套查询）：根据嵌套对象进行查询，以便查询嵌套在文档中的相关信息。\n• Aggregation Query（聚合查询）：用于计算、统计和分析数据，包括求和、平均值、最小值、最大值、分组等 操作\n查询例子 下面分别进行了\n一次精确匹配\n查询范围 大于等于 的日期\nES \u0026amp; Go 创建索引 这里可以看到，实际上我们还是需要 构造一个 如 http 一样的 jsonBody\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func (s *ElasticSearchTestSuite) TestCreateIndex() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() // 这是一个链式调用，你可以通过链式调用来构造复杂请求。 // 重复创建会报错，所以你可以换一个名字 resp, err := s.es.Indices.Create(\u0026#34;user_idx_test\u0026#34;, s.es.Indices.Create.WithContext(ctx), s.es.Indices.Create.WithBody(strings.NewReader(` { \u0026#34;settings\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 3, \u0026#34;number_of_replicas\u0026#34;: 2 }, \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;email\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34; }, \u0026#34;phone\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;birthday\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;date\u0026#34; } } } } `))) require.NoError(s.T(), err) assert.NotNil(s.T(), resp) assert.Equal(s.T(), 200, resp.StatusCode) } 写入文档 通过 client.index 指定对应的 index 然后放入文档\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (s *ElasticSearchTestSuite) TestPutDoc() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() resp, err := s.es.Index(\u0026#34;user_idx_test\u0026#34;, strings.NewReader(` { \u0026#34;email\u0026#34;: \u0026#34;john@example.com\u0026#34;, \u0026#34;phone\u0026#34;: \u0026#34;1234567890\u0026#34;, \u0026#34;birthday\u0026#34;: \u0026#34;2000-01-01\u0026#34; } `), s.es.Index.WithContext(ctx)) require.NoError(s.T(), err) require.NotNil(s.T(), resp) assert.Equal(s.T(), 201, resp.StatusCode) } 搜索文档 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func (s *ElasticSearchTestSuite) TestGetDoc() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() resp, err := s.es.Search(s.es.Search.WithContext(ctx), s.es.Search.WithBody(strings.NewReader(` { \u0026#34;query\u0026#34;: { \u0026#34;range\u0026#34;: { \u0026#34;birthday\u0026#34;: { \u0026#34;gte\u0026#34;: \u0026#34;1990-01-01\u0026#34; } } } } `))) require.NoError(s.T(), err) assert.Equal(s.T(), 200, resp.StatusCode) } ","date":"2026-08-05T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/es-0x01/","title":"Es"},{"content":"简历 preview 简历上 : 支持用户使用微信支付进行文章的打赏功能 以及 用户对账流程\n流程设计 需求分析 :\n用户能够选择一篇文章 通过 微信支付 进行打赏\n支付成功后 用户能够在自己的余额里面看到扣款 以及 被打赏的金额，同时可以看到对应的流水\n模块设计 :\n考虑支付是一个通用的模块, 并且支付可以额外对接审计，账号，反洗钱等模块 所以把支付模块单独拆分出来\n设计 支付、用户资产、打赏 三个模块\n功能分析 :\n用户模块支持 :\n查看业务流水 更新用户资产 打赏模块支持 :\n查看文章打赏记录 发起支付请求 支付模块支持 :\n向第三方发起支付请求 接受微信支付的calback 查询发起的支付信息 难点 \u0026amp; 亮点 问题 :\n微信支付回调后如果成功那么不会在此推送消息, 我们使用 MQ进行通知下游,如果MQ发送失败那么将丢失该信息\n方案 :\n引入 本地消息表\n使用 本地事务 开启 支付表 和 本地消息表的写入 。 并且设置 消息表 init\n只有当MQ发送成功之后才会设置 本地表 success 。\n定时任务 定时轮询 init 的任务进行补偿\n问题 :\n由于本地消息表的引入, 可能带来重复消费的问题\n方案 :\n在下游账户处理模块， 设置了 biz_id + biz + account + account_type 的联合唯一索引\n并且引入 redis 进行查询优化, 预先判断 redis 的状态\n问题 :\n可能存在 微信回调消息发送失败, 微信回调只会默认会在 30分钟之后不再回调 不管成功或是失败\n方案 :\n定时任务 定期扫描 超过约30分钟的未完结订单 主动查询微信状态 并写回 问题 :\n获取打赏信息的时候 ,我们会先去查询 打赏表的数据,如果打赏表数据还未完成 。会通过慢路径查询支付表的信息进行更新 用于加快状态的轮询。\n方案 :\n考虑限流时候降级, 对于限流的时候不走慢路径\n在 账户资产更新 MQ 的时候 会一并更新对应的 状态\n亮点 :\n模块拆分 与 边界 + gRPC\n支付只关心第三方与支付单状态\n打赏只关心 业务单和分账意图\n账户只关心余额与流水\n缺点 缺少定时自动对账的机制。 涉及 打赏-支付-记帐 三个流程\n在 MQ 更新 账户资产 和 打赏表的时候 。 并不是强一致的, 我们优先更新 打赏表, 如果之后调用 rpc 处理 账户资产失败了 。 那么会发送告警 由人工手动修复\n其他细节 支付消息必然发送成功 支付的一个核心步骤是由 微信来回调的 . 我们原先的处理是\n微信回调\n更新支付表状态\n发送消息给 MQ 通知账户变更\n如果这里 MQ 发送失败,那么将会丢失这笔消息 。但是支付消息可以认为是一个非常关键的功能 。\n所以考虑优化引入了 本地消息表\n表结构设计\n主要字段 content 和 status\n我们通过 content 记录原有的 MQ 信息 使用 status 维护 MQ 的状态 分为 init,success,failed\n1 2 3 4 5 6 7 { id: \u0026#34;id\u0026#34; content: \u0026#34;\u0026#34; stauts: \u0026#34;\u0026#34; Ctime: \u0026#34;\u0026#34; Utime: \u0026#34;\u0026#34; } 流程优化设计 :\n使用 事务控制 更新 支付表状态 和 写入本地事务表 并且初始化 init\n只有当我们成功发送 MQ 之后才把 本地事务表设置 success\n这里可能会造成重复发送 开启一个定时任务，批次获取 init 的状态, 然后发送 MQ 消息 。 如果半小时内都发送失败 设置 failed . 发送成功之后设置 success\n接口幂等性分析 由于改造了本地事务之后，可能会产生重复消费数据 . 我们在下游 账户更新的时候\n通过 redis+ 本地索引来处理 。\n我们设置 账户流水表的 唯一索引 biz,biz_id,account,account_type 进行过滤\nbiz_id 就是 微信回调里面带的 tradeNoId\n使用 redis 进行初步过滤 使用 唯一索引进行兜底\n打赏详情降级处理 我们打赏的时候 会先查询我们打赏表 如果打赏已经是支付状态 会直接返回\n否则会去查询一遍支付表\n对于限速的状态,我们会抛弃这个慢路径。 这条慢路径，由 MQ 进行处理, MQ 会较慢一点时间 更新到支付表的状态 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 func (s *WechatNativeRewardService) GetReward(ctx context.Context, rid, uid int64) (domain.Reward, error) { // 快路径 r, err := s.repo.GetReward(ctx, rid) if err != nil { return domain.Reward{}, err } if r.Uid != uid { // 说明是非法查询 return domain.Reward{}, errors.New(\u0026#34;查询的打赏记录和打赏人对不上\u0026#34;) } // 有可能，我的打赏记录，还是 Init 状态 // 已经是完结状态 if r.Completed() || ctx.Value(\u0026#34;limited\u0026#34;) == \u0026#34;true\u0026#34; { // 我已经知道你的支付结果了 return r, nil } // 这个时候，考虑到支付到查询结果，我们搞一个慢路径 // 你有可能支付了，但是我 reward 本身没有收到通知 // 我直接查询 payment， // 只能解决，支付收到了，但是 reward 没收到 // 降级状态，限流状态，熔断状态，不要走慢路径 resp, err := s.client.GetPayment(ctx, \u0026amp;pmtv1.GetPaymentRequest{ BizTradeNo: s.bizTradeNO(r.Id), }) if err != nil { // 这边我们直接返回从数据库查询的数据 s.l.Error(\u0026#34;慢路径查询支付结果失败\u0026#34;, logger.Int64(\u0026#34;rid\u0026#34;, r.Id), logger.Error(err.Error())) return r, nil } // 更新状态 switch resp.Status { case pmtv1.PaymentStatus_PaymentStatusFailed: r.Status = domain.RewardStatusFailed case pmtv1.PaymentStatus_PaymentStatusInit: r.Status = domain.RewardStatusInit case pmtv1.PaymentStatus_PaymentStatusSuccess: r.Status = domain.RewardStatusPayed case pmtv1.PaymentStatus_PaymentStatusRefund: // 理论上来说不可能出现这个，直接设置为失败 r.Status = domain.RewardStatusFailed } err = s.repo.UpdateStatus(ctx, rid, r.Status) if err != nil { s.l.Error(\u0026#34;更新本地打赏状态失败\u0026#34;, logger.Int64(\u0026#34;rid\u0026#34;, r.Id), logger.Error(err.Error())) return r, nil } return r, nil } 总分：72 / 100 对应 2 年经验中级偏上：系统设计与可靠性意识不错，能讲清主链路和几个关键问题；但深度、闭环和对面试追问的准备还不够稳。\n评分拆解 维度 分数 说明 需求与业务理解 14/20 主流程清楚，但对账、分账、退款几乎空缺 模块拆分与边界 16/20 支付 / 打赏 / 账户划分合理，亮点明确 可靠性设计（消息必达、补偿） 18/20 本地消息表 + 定时补偿是加分项 幂等与一致性 12/20 有唯一索引思路，但一致性模型讲得偏浅 降级与工程细节 8/10 慢路径 + 限流降级有工程味道 表达与面试可讲性 4/10 结构散、术语错误多，容易被追问打穿 优点 边界意识好\n「支付只关心第三方与支付单」「打赏关心业务单」「账户关心余额流水」——这是面试里很吃香的表述，说明做过拆分，不是堆接口。\n抓住了支付域真难点\n微信回调只推一次成功、MQ 可能丢、回调有时效——都点到了，再配本地消息表 + 定时扫 init 补偿，对 2 年经验来说已经超过很多只会 CRUD 的候选人。\n幂等有兜底思路\nbiz + biz_id + account + account_type 唯一索引 + Redis 前置过滤，主路径对、兜底也对。\n会主动说缺点\n缺自动对账、账户与打赏非强一致靠告警人工修——面试官通常加分，显得务实。\n有可落地的代码片段\nGetReward 快慢路径 + limited 降级，能证明不是纯理论。\n缺点（面试里容易被打穿的点） 「对账」名不副实\n简历写「对账流程」，正文却写「缺少定时自动对账」。2 年经验可以不做完整对账，但简历与口述必须对齐，否则会被认定夸大。\n一致性模型说不清\n「优先更新打赏表，RPC 失败靠人工」——面试官会问：为何不先账后业务 / 同事务 / Outbox 统一出口？最终一致还是人工一致？超时重试与幂等如何配合？这里准备不足。\n本地消息表细节偏薄\nfailed 后怎么办？（死信、告警、人工重放） 发送成功但改 status 失败 → 重复投递，下游如何保证？ 定时任务并发扫同一批 init 如何防重？ content 序列化格式、版本兼容？\n这些是「本地消息表」题的标配追问。 Redis 幂等设计不严谨\n「预先判断 Redis 状态」太模糊：key 是什么？TTL？先 Redis 还是先 DB？Redis 挂了怎么办？正确叙事应是：DB 唯一约束是真相，Redis 只是加速，不能单独当幂等源。\nGolang / 工程深度弱\n全文几乎没有：gRPC 超时与重试、context 传递、回调验签、并发安全、事务边界、错误码。2 年 Golang 岗会被追问这些。\n表达与硬伤\ncalback、stauts、ctx.Value(\u0026quot;limited\u0026quot;)（类型不安全、难测）、JSON 伪结构不规范——细节会拉印象分。\n退款 / 部分失败 / 重复打赏 几乎未覆盖\n支付面试常问：重复回调、金额篡改校验、订单超时关单、退款与冲正。\n建议（按优先级） 1. 先改简历表述（立刻做）\n改为：「实现微信支付打赏；用本地消息表保证支付结果可靠投递；账户侧幂等入账」 不要写「完整对账」，除非补一版：按日对 支付成功单 vs 账户流水 vs 打赏状态 的差异报表 + 告警。 2. 准备一张 2 分钟口述主链路\n发起打赏 → 建业务单/支付单 → 微信下单 → 回调验签 → 本地事务写支付状态+Outbox → 发 MQ → 账户幂等入账 → 更新打赏状态；失败靠定时补偿 + 主动查单。\n3. 把三个「追问答案」写死\n为何本地消息表而不是直接依赖微信重试？ 重复消息如何保证只加一次钱？ 打赏已成功、账户失败如何发现与修复？（最好从「人工」升级到「对账任务 + 可重放」） 4. 补齐技术细节清单（半页纸即可）\n回调验签、金额与 out_trade_no 校验 Outbox 状态机：init → sending → success / failed 定时任务：分页、FOR UPDATE SKIP LOCKED 或租约，避免多实例抢同一批 幂等：先插流水（唯一索引）再改余额；冲突当成功返回 主动查单：状态机只允许 Init→Success/Failed，禁止回退 5. 代码展示再打磨\n限流标记不要用 ctx.Value(\u0026quot;limited\u0026quot;)，改成明确参数或中间件注入的 Limiter/Degrade 标志 慢路径更新打赏状态时说明：是否只更新、是否再触发入账（避免查询接口变成写副作用） 6. 面试话术升级一句\n不要只说「模块拆了」；说：「支付可复用到会员/订单，打赏是支付的一种业务形态，账户是账本，三者用 MQ 最终一致，用唯一键兜底。」\n面试官结论 这份材料说明候选人做过真实支付链路，懂拆分和消息可靠性，对 2 年经验够用；但若现场只能复述文档、答不上一致性与对账追问，分数会掉到 60 左右。把「对账诚实化 + Outbox/幂等细节 + 2 分钟口述」补上，冲击 80+ 更稳。\n","date":"2026-08-05T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/payment-0x02/","title":"打赏 one"},{"content":"简历 preview 简历上 : 实现了一套完整的评论系统\n需求分析 :\n评论本身是评价一类资源，例如评价文章、视频\n评论本身是可以被回复的\n评论本身是可以被点赞的\n更加进一步考虑,我们会发现 查询评论本身也是一个高并发场景,打开任何资源都需要考虑加载评论\n评论设计 回复评论的层级 因为评论也是可以被评论的，所以考虑维护一个树形结构\n邻接表存储 :\n我们可以使用 邻接表存放树形结构，通过设计一个 partent_id 来表示自己的父节点\n根节点的 partent_id 可以表示为 null 或者是 -1\n缺点 :\n很难找出全部的评论 , 需要找出 paretn_id 然后找出评论 再找出评论\n而且删除操作不太好处理\n查找的时候是 join 操作\n分段式 path设计 :\n一个列维护了从根节点到当前节点的路径\n例如 path a/b/c\n如果路径包含当前节点 那么当前节点就是 c 他的父节点就是 b 根节点就是 a\n如果当前路径不包含当前节点,那么c就是当前节点的父节点\n查找的时候是 like 操作\n确定结构 我们考虑以下几个场景\n加载资源的第一页评论\n加载资源的第一页评论的直接评论 ，分批加载其他评论\n查找评论的更多评论\n第一个场景可以认为是根据 biz+biz_type 查询\n第二第三个场景可以认为是 查询某个节点的子节点\n我们发现邻接表 和 path 的设计 都可以很好的查询出来。 但是使用 path 的情况下是使用 like 查询 因此最终采用邻接表\n表结构设计 字段结构设计 :\n引入 rootId 来优化批量查询的问题 索引设计 :\nbiz、biz_id 肯定要建立索引 第一个场景\npid 找父节点一定要增加索引\n这个列的 null值会非常多 rootId 加载整个评论也需要索引\n所有的索引设计都是根据 where,order by, select 如果有 join 那么需要考虑 on\n在没有遇到更新、查询性能瓶颈之前，不需要过于担忧维护索引的开销\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct { id int uid int64 biz string bizId int64 Pid sql.NullInt64 // 所有顶级评论的 id RootId sql.NullInt64 Content string Ctime int64 Utime int64 } 异步写评论 使用kafka 怎么保证 容错和可用性\n分kafka和分topic\n删除评论的设计 删除评论: 是否需要删除其子节点\n目前主流的做法就是 : 会把子评论一并删了\nPG :\nDELETE from comments where pid = 4 return id\nmysql Select * from comments where pid = 4 Delete * from comments where pid = 4\n能否使用非关系型数据库 ？\n查询接口 热度评论 业务折中 :\n90%都不会超过10个评论的话，那么就没必要计算热度\noffset 有并发问题，如果有新id出现，那么可能会导致重复取\n缓存处理 可以考虑 , 缓存第一页\n面试 怎么在数据库里面设计一个支持树形结构的表，你知道哪些方法 ？ 用过哪些 ？ 各有什么优缺点\n什么是外键 ？ 你用过没有。？为什么大厂不推荐使用外键\n会对数据库性能有很大的影响 外键约束是一个很强的约束 如何提高评论系统的性能？\n各种缓存 要不要缓存，长尾不缓存， 如何提高评论系统的可用性？\n写 引入kafka 读 各种降级 读实例和写实例分离 ","date":"2026-08-05T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/comment-0x01/","title":"评论 detail"},{"content":"简历 preview 简历上 :\n实现了一套完整的打赏支付流程和简单对账\n需求分析 :\n为了激励创作者，在文章模块支持打赏功能 主要用例如下 :\n读者 : 发起支付\nwebook : 记录打赏，调用第三方服务进行支付\n创作者 : 提现 (这里不实现，因为微信提现涉及审计相关的内容)\n第三方支付 : 实际完成支付的地方\n设计过程 支付过程 实际支付流程如下 :\n读者点击打赏\nwebook 创建一个支付订单\nWebook 跳转第三方支付\n读者扫码支付\n第三方支付回调给 webook\nwebook 得到支付结果，记录结果并更新系统状态\n说一下这里和微信登陆的不同\n支付回调 登录重定向回调 是什么 服务端 → 服务端 的结果通知 浏览器 OAuth 回跳 谁在请求你 微信支付服务器 用户浏览器 你在干什么 更新支付状态、通知业务 换身份、建用户、发登录态 有没有用户页面 通常没有 有，整条链路围着浏览器转 失败了怎么办 微信重试；你们还有主动查单兜底 用户重新扫码登录 微信登陆我们校验之后 会自动重定向\n而支付回调是一个异步的，仅仅发送通知\n模块划分 打赏功能 我们不假思索的会认为是一个模块 。 但是实际上可以进行额外的拆分\n支付本身 。 和第三方支付打交道\n利用支付模块实现的打赏模块\n因此实现一个打赏模块，实际上要处理两个微服务 支付 和 打赏\n接口分析 可以看到必传的参数有\nmchid, appid, sub_mchid, sp_appid, mcc 这里都是 微信那边给的对应 id\ndescripition, notify_url, out_trade_no, trade_type 分别是 商品描述,通知地址,商户系统内部id,交易类型\n微信下单接口\n返回结果如下 :\n我们可以使用在线的二维码生成，展示这个二维码\n接口设计思路 根据接口请求和返回, 我们采用最小化原则 可以定义出 下列接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type PaymentService interface { // Prepay 预支付，对应于微信创建订单的步骤 Prepay(ctx context.Context, pmt domain.Payment) (string, error) } type Payment struct { Amt Amount // 代表业务，业务方决定怎么生成， // 我们这边不管。 BizTradeNO string // 订单本身的描述 Description string Status PaymentStatus // 第三方那边返回的 ID TxnID string } 问题1 BizTradeNO BizTradeNO 理论上是业务方传递过来的，但是存在一个问题\n对于 在打赏的情况下，如果用户第一次点击打赏 的时候，没有支付。后续要再次打赏 。\n业务方应该要 考虑：缓存前一次的二维码，或者直接生成一个新的 BizTradeNO\n如果不缓存，每一次打赏并且取消支付再次打赏，会call多次 wechat 以及插入 init 数据到数据库中\n表结构设计 仅考虑保留 currency ,amt , tradeNo ,status,txnId 后续如果有其他字段可以考虑使用 extra_blob 进行维护\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type Payment struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34; bson:\u0026#34;id,omitempty\u0026#34;` Amt int64 Currency string // 可以抽象认为，这是一个简短的描述 // 也就是说即便是别的支付方式，这边也可以提供一个简单的描述 // 你可以认为这算是冗余的数据，因为从原则上来说，我们可以完全不保存的。 // 而是要求调用者直接 BizID 和 Biz 去找业务方要 // 管得越少，系统越稳 Description string `gorm:\u0026#34;description\u0026#34;` // 后续可以考虑增加字段，来标记是用的是微信支付亦或是支付宝支付 // Type uint8 // 微信支付或者支付宝支付 // 也可以考虑提供一个巨大的 BLOB 字段， // 来存储和支付有关的其它字段 // ExtraData // 业务方传过来的 BizTradeNO string `gorm:\u0026#34;column:biz_trade_no;type:varchar(256);unique\u0026#34;` // 第三方支付平台的事务 ID，唯一的 TxnID sql.NullString `gorm:\u0026#34;column:txn_id;type:varchar(128);unique\u0026#34;` Status uint8 // Utime 上面要创建一个索引 Utime int64 `gorm:\u0026#34;index\u0026#34;` Ctime int64 } 接收支付通知 对于微信那边会通知一个回调到 notifyURL 上 。 但是有一个问题\n这个地址通常是 线上地址 。 我们开发环境和测试环境怎么接受这个 http 请求 解决办法 :\n考虑转发请求\n考虑配置多个回调域名，特定域名就打到特定的环境上\n考虑在回调路径上做一些特殊的标记，比如 /test 就打到测试环境\n处理支付通知 更新数据库状态 和 TxnId\n同时传递消费数据到 kafka 中\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func (n *NativePaymentService) updateByTxn(ctx context.Context, txn *payments.Transaction) error { status, ok := n.nativeCBTypeToStatus[*txn.TradeState] if !ok { // 这个地方，要告警 return fmt.Errorf(\u0026#34;%w, %s\u0026#34;, errUnknownTransactionState, *txn.TradeState) } // 核心就是更新数据库状态 err := n.repo.UpdatePayment(ctx, domain.Payment{ BizTradeNO: *txn.OutTradeNo, Status: status, TxnID: *txn.TransactionId, }) if err != nil { return err } // 发送消息，有结果了总要通知业务方 // 这里有很多问题，核心就是部分失败问题，其次还有重复发送问题 err1 := n.producer.ProducePaymentEvent(ctx, events.PaymentEvent{ BizTradeNO: *txn.OutTradeNo, Status: status.AsUint8(), }) if err1 != nil { // 加监控加告警，立刻手动修复，或者自动补发 n.l.Error(\u0026#34;发送支付事件失败\u0026#34;, logger.String(\u0026#34;biz_trade_no\u0026#34;, *txn.OutTradeNo), logger.Error(err1.Error())) } return nil } 对账处理 和微信交互的场景主要有两个\n调用 prepay 接口, 创建预支付订单 。 比较容易出现的情况就是 超时，如果超时没办法确认成功还是失败\n处理回调的时候失败了，你同样不知道用户是支付了还是没支付\n我们能够想到的办法就是让客户端重试\n处理回调失败的问题 微信在回调通知我们的时候 也可能会失败 。 当失败多次之后，微信就会放弃该条消息\n所以我们需要一个兜底机制，来使用 tradeNo 定时的去请求微信，更新订单状态\n定时的去获取 还在 init 定任务，并且已经过期的任务 。 然后查询 微信获取状态进行更新 。\n这里的定时任务交由我们是之前实现的 分布式任务实现 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func (s *SyncWechatOrderJob) Run() error { offset := 0 // 也可以做成参数 const limit = 100 // 三十分钟之前的订单我们就认为已经过期了。 now := time.Now().Add(-time.Minute * 30) for { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) pmts, err := s.svc.FindExpiredPayment(ctx, offset, limit, now) cancel() if err != nil { // 直接中断，你也可以仔细区别不同错误 return err } // 因为微信没有批量接口，所以我们这里也只能单个查询 for _, pmt := range pmts { // 单个重新设置超时 ctx, cancel = context.WithTimeout(context.Background(), time.Second) err = s.svc.SyncWechatInfo(ctx, pmt.BizTradeNO) if err != nil { // 这里你也可以中断，不过我个人倾向于处理完毕 s.l.Error(\u0026#34;同步微信支付信息失败\u0026#34;, logger.String(\u0026#34;trade_no\u0026#34;, pmt.BizTradeNO), logger.Error(err.Error())) } cancel() } if len(pmts) \u0026lt; limit { // 没数据了 return nil } offset = offset + len(pmts) } } 打赏设计过程 需求分析 \u0026amp; 功能设计 用户希望能够在某篇文章的最后，有一个打赏按钮按钮\n用户点击这个按钮，就会弹出微信的支付二维码，即 prepay 的过程\n可以查看支付的结果和状态 getStatus\n表结构设计 文章模块 和 文章 id 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Reward struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34; bson:\u0026#34;id,omitempty\u0026#34;` Biz string `gorm:\u0026#34;index:biz_biz_id\u0026#34;` BizId int64 `gorm:\u0026#34;index:biz_biz_id\u0026#34;` BizName string // 被打赏的人 TargetUid int64 `gorm:\u0026#34;index\u0026#34;` // 直接采用 RewardStatus 的取值 Status uint8 // 打赏的人 Uid int64 Amount int64 Ctime int64 Utime int64 } 状态更新 状态更新有两个流程\n异步的消费 kafka 进行更新\n获取detail的时候进行更新\n这里在限流的时候，就不进行慢路径的查询。等待 kafaka 进行补充 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 func (s *WechatNativeRewardService) GetReward(ctx context.Context, rid, uid int64) (domain.Reward, error) { // 快路径 r, err := s.repo.GetReward(ctx, rid) if err != nil { return domain.Reward{}, err } if r.Uid != uid { // 说明是非法查询 return domain.Reward{}, errors.New(\u0026#34;查询的打赏记录和打赏人对不上\u0026#34;) } // 有可能，我的打赏记录，还是 Init 状态 // 已经是完结状态 if r.Completed() || ctx.Value(\u0026#34;limited\u0026#34;) == \u0026#34;true\u0026#34; { // 我已经知道你的支付结果了 return r, nil } // 这个时候，考虑到支付到查询结果，我们搞一个慢路径 // 你有可能支付了，但是我 reward 本身没有收到通知 // 我直接查询 payment， // 只能解决，支付收到了，但是 reward 没收到 // 降级状态，限流状态，熔断状态，不要走慢路径 resp, err := s.client.GetPayment(ctx, \u0026amp;pmtv1.GetPaymentRequest{ BizTradeNo: s.bizTradeNO(r.Id), }) if err != nil { // 这边我们直接返回从数据库查询的数据 s.l.Error(\u0026#34;慢路径查询支付结果失败\u0026#34;, logger.Int64(\u0026#34;rid\u0026#34;, r.Id), logger.Error(err.Error())) return r, nil } // 更新状态 switch resp.Status { case pmtv1.PaymentStatus_PaymentStatusFailed: r.Status = domain.RewardStatusFailed case pmtv1.PaymentStatus_PaymentStatusInit: r.Status = domain.RewardStatusInit case pmtv1.PaymentStatus_PaymentStatusSuccess: r.Status = domain.RewardStatusPayed case pmtv1.PaymentStatus_PaymentStatusRefund: // 理论上来说不可能出现这个，直接设置为失败 r.Status = domain.RewardStatusFailed } err = s.repo.UpdateStatus(ctx, rid, r.Status) if err != nil { s.l.Error(\u0026#34;更新本地打赏状态失败\u0026#34;, logger.Int64(\u0026#34;rid\u0026#34;, r.Id), logger.Error(err.Error())) return r, nil } return r, nil } 对账模块设计 对于每一条付款记录，我们都维护到 用户余额里面并且记录一笔流水 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func (s *WechatNativeRewardService) UpdateReward(ctx context.Context, bizTradeNO string, status domain.RewardStatus) error { rid := s.toRid(bizTradeNO) err := s.repo.UpdateStatus(ctx, rid, status) if err != nil { return err } // 完成了支付，准备入账 if status == domain.RewardStatusPayed { r, err := s.repo.GetReward(ctx, rid) if err != nil { return err } // webook 抽成 weAmt := int64(float64(r.Amt) * 0.1) _, err = s.acli.Credit(ctx, \u0026amp;accountv1.CreditRequest{ Biz: \u0026#34;reward\u0026#34;, BizId: rid, Items: []*accountv1.CreditItem{ { AccountType: accountv1.AccountType_AccountTypeReward, // 虽然可能为 0，但是也要记录出来 Amt: weAmt, Currency: \u0026#34;CNY\u0026#34;, }, { Account: r.Uid, Uid: r.Uid, AccountType: accountv1.AccountType_AccountTypeReward, Amt: r.Amt - weAmt, Currency: \u0026#34;CNY\u0026#34;, }, }, }) if err != nil { s.l.Error(\u0026#34;入账失败了，快来修数据啊！！！\u0026#34;, logger.String(\u0026#34;biz_trade_no\u0026#34;, bizTradeNO), logger.Error(err.Error())) // 做好监控和告警，这里 return err } } return nil } 表结构设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // Account 账号本体 type Account struct { Id int64 `gorm:\u0026#34;primaryKey,autoIncrement\u0026#34; bson:\u0026#34;id,omitempty\u0026#34;` // 对应的用户的 ID，如果是系统账号 Uid int64 `gorm:\u0026#34;uniqueIndex:account_uid\u0026#34;` // 账号 ID，这个才是对外使用的 Account int64 `gorm:\u0026#34;uniqueIndex:account_uid\u0026#34;` // 一个人可能有很多账号，你在这里可以用于区分 Type uint8 `gorm:\u0026#34;uniqueIndex:account_uid\u0026#34;` // 账号本身可以有很多额外的字段 // 例如跟会计有关的，跟税务有关的，跟洗钱有关的 // 跟审计有关的，跟安全有关的 // 可用余额 // 一般来说，一种货币就一个账号，比较好处理（个人认为） // 有些一个账号，但是支持多种货币，那么就需要关联另外一张表。 // 记录每一个币种的余额 Balance int64 Currency string Ctime int64 Utime int64 } 面试 微信的支付流程 ， prepay 和 处理回调\n怎么处理支付的回调 ？ 关键是，怎么分发/区别 不同环境收到的支付回调\n金额应该怎么存 ？\n怎么防止重复支付？\n如果微信支付的回调处理失败怎么办 ？\n如何保证消息一定发出去？ 怎么确保一定发送了数据？如何确保消息不丢失 ？\n怎么保证消息丢有序性 ？？\n如果 topic 增加分区，怎么保证有序性？ 如果保证消息有序性，消息积压了，怎么半？ 开goroutine 批量提交 如果在自己业务里面，有明显的部分失败的场景，怎么补偿\n怎么保证幂等？ 你有什么方案 ？ 能不能撑住高并发\n唯一索引处理 二话不说先插入一个数据，保证坐在一个本地事务里面，如果有冲突那么就说明做过了 不拢过滤器去重 ","date":"2026-08-04T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/payment-0x01/","title":"打赏 detail"},{"content":"简历 preview 简历上 : 支持一套通用的同构数据不停机迁移方案, 支持全量和增量校验 使用 kafka 进行减轻数据库压力 并且动态的根据数据库状态进行切换挂载和运行\n方案描述 :\n由于我在拆分了 点赞、阅读、收藏相关的微服务, 考虑 微服务中 一个服务一个数据库进行数据隔离。从而实现了一套数据迁移方案 。 因为不修改原有数据存储架构，所以采用同构的方案 , 并且考虑这个服务属于关键服务，因此选用不停机迁移方案\n考虑在 100w条数据的情况下,如果进行 校验后立即进行修复会给数据库带来很大的负担因此使用 kafka 进行削峰的处理\n另外通过额外重写 gorm 中 connPool 的exec、prepare、query Func 实现 双写\n并且通过重写 Commit 和 Rollback 实现双写的事务控制\n困难点和改进 :\n引入 kafka 进行异步的优化 全量校验与修复的过程 。\n优化单条校验的逻辑 采用批量校验的逻辑。 并且考虑 硬删除 的业务场景，在校验 原表和目标表的 条件下，同步进行 目标表和原表的 差异校验 防止硬删除产生的不一致问题\n支持使用者动态切换 数据迁移 原表only,读原表写目标表，读目标表写原表，目标表only 的配置\n支持根据数据库状态，判断是否高负载从而判断是否要挂载校验任务 。 并且通过 context cancel() 实现，尽可能只有同一个 全量/增量 校验程序在运行\n缺点 :\n因为 ConnPool 返回值 sql.Result 并不暴露 Error 的设置，所以对于双写的错误并不能很好的暴露出来，只能进行打log和监控\n事务控制的实现并不能保证一致性，只能做到主表最大可控，但是对于目标表如果失败了 只能等待全量校验与修复\n不停机迁移数据 相比较于 停机迁移数据， 主要的困难点在于 一边迁移 一边有新数据产生\n并且因为我们切换 原表和目标表 的时候，如果运气不好会导致出现一些数据不一致。 不停机数据迁移很难做到 最终数据一致性\n数据校验 为了防止重复写一些 dirty code, 放弃在 dao层维护两个数据库的思路\n重建了一套通用的双写逻辑。 由业务方提供 equal 的方法 ， 对于某些校验不严格的业务方可以考虑\n不需要 time 就可以认为 equal 以及自定义精度误差等等\n流量控制 和 性能优化 在数据库高负载的情况下会自动挂起全量校验 只有当数据库低负载的时候才会进行\n尽可能多的进行 异步处理 和 并发处理 。 对于全量数据与校验 采用异步处理， 对于 目标表和原表的校验 以及 目标表和原表的差异校验 采用异步处理的方式进行\n面试题 Q: 不停机迁移的基本步骤是什么\n数据初始化，完成首轮数据的同步 和 数据库表的建立 。 对于 mysql 可以使用 mysqldump 进行同步，如果是异构数据的话，可以考虑内部批量轮询的方式进行首次同步\n这时候可以考虑启用全量校验进行一次数据的校验与修复\n读取并写入原表 同时 写入目标表 。 同时进行 全量校验与修复\n写入并读取目标表 同时写入原表 。 同时进行全量校验与修复 如果业务稳定可以考虑只运行增量校验\n完全使用目标表\nQ : 你的数据校验方案是什么\n支持 全量的数据校验和增量的数据方案 。 通过用户是否传递 utime + sleepTime 考虑走的是全量校验还是增强校验\n同步的进行 原表和目标表的校验 以及 目标表和原表的差异 检查 。 批量获取原表的数据 并且批量的获取目标表的数据，使用业务方提供的 equal method 进行比对 ，如果存在不想等的 在轮询完成之后批量通过 kafka 发送给下游修复 。 批量获取目标表的数据，并且检查是否在原表存在，如果不存在同时发送对应的 event 给下游\nQ : 你的数据修复方案是什么 ？ 如何保证数据正确性？ 怎么解决并发问题 ？\n使用 kafka 进行异步的通知修复 。 kafka 仅仅只是做触发器，并不存放detail信息用于实际修复 。\n对于数据修复情况只会有3种场景，target 不存在 那么需要insert，not equal 需要进行 update, base_missing 则需要删除 。 考虑 insert \u0026amp; update 的并发问题，使用 upsert 进行控制\n并不能够在单词修复保证数据的正确性，数据的正确性最终交由多次校验与修复来控制\nQ : 在数据迁移的每个阶段，你是怎么考虑保护着数据库的\n尽可能的进行 批量操作 。 批量的查询数据库，本地机器进行比对\n使用 kafka 进行异步的校验与修复\n考虑 调度时机进行 校验任务的运行 ， 对于数据库高负载的情况 挂载正在运行的任务\nQ : 如果 kafka 瓶颈了怎么办，消息积压了怎么办？\n优化消费者。 尽可能改成批量消费\n并行处理数据库修复流程\nQ : 为什么 你要使用 Kafka 直接校验之后修复数据不行吗\n直接进行修复数据 , 会给数据库带来很大的负担 。 Mysql 单库的 读写有瓶颈，尽可能保证 大量的读和写操作不同时进行 Q: 增量校验和修复、业务写数据、全量校验和修复同时进行，有什么并发问题 ？ 怎么解决？\n同时进行会带来更多的数据库操作，带来负担 。 并且我们在事务以及双写操作上必然会有并发问题，在我们切换 目标表和原表的时候 最终总会有不一致的情况 。\n采用超时控制, 优化任务调度时间 。所有的不一致问题，最终都是由反复校验反复修复解决的\nQ : 怎么保证在数据迁移的时候 不影响业务\n调度时间的控制 。 在低负载的情况下进行\n可以考虑在内部数据跑完几次全量校验之后 只开启增量校验，每次仅校验一周之内的数据\n总分：62 / 100 按「2 年经验、能讲清一个核心项目」来评：能过初筛，但扛不住深挖。\n评分拆解 维度 分数 说明 项目背景与动机 12/15 微服务拆库、同构不停机，动机清楚 方案完整性 14/25 四阶段迁移有框架，关键细节和边界含糊 技术深度 10/20 提到 ConnPool 双写、Kafka 削峰，但原理讲不透 问题意识与权衡 12/15 能主动说缺点，这点加分 面试表达与结构化 8/15 有 Q\u0026amp;A，但表述乱、有笔误、答不到位 与 2 年经验匹配度 6/10 选题够大，但「最终靠反复校验」像在回避设计 优点 选题合适：不停机迁移 + 双写 + 校验修复，是面试官愿意追问的「有难度」项目。 有诚实的缺点：ConnPool 错误难暴露、事务只能保主表——比空谈「强一致」强很多。 有业务抽象意识：业务方提供 Equal，比硬编码字段对比更像工程实践。 有保护库的意识：批量、Kafka 削峰、高负载挂起任务，方向对。 准备了面试题清单：说明有意识在「被问什么」。 缺点（面试里会被打穿的点） 核心一致性说不清\n「切换时总会不一致」「最终靠反复校验」可以当结论，但不能当唯一答案。面试官会问：双写失败怎么回滚？阶段切换的窗口期如何缩小？源表为准 / 目标表为准时，修复方向怎么定？\nKafka 用法站不住脚\n「只做触发器、不带 detail」——那消费者靠什么修？再查一次库？查到的是哪一侧？积压时只说「批量消费 / 并行」，没有分区键、幂等、重试、死信、顺序。\n并发问题答得空\n业务写、全量校验、增量修复同时跑，只提「超时 + 调度」不够。应有：同一主键的冲突策略、upsert 的版本/utime 条件、校验批次与写路径的可见性。\n双写实现太薄\n重写 ConnPool / Commit / Rollback 是亮点，但文档几乎没展开：嵌套事务？只读语句？Prepare 缓存？目标库延迟？失败是「打日志继续」还是「业务失败」？\n表达与严谨性差\n「挂载」应为「挂起」；「增强校验」应为「增量」；步骤编号重复；句子碎、逻辑跳。面试口述时会显得准备乱。\n缺可量化结果\n100 万量级提了，但没有：迁移耗时、校验一轮多久、峰值 QPS、不一致率下降、切换窗口多长。2 年经验项目，数据是加分项。\n建议（按优先级） 1. 把故事讲成一条线（30 秒版）\n背景（拆库）→ 约束（不停机、同构）→ 四阶段 → 双写怎么做 → 校验/修复怎么兜底 → 已知局限。每阶段说清「读谁、写谁、以谁为准」。\n2. 补齐 5 个必追问答案（写到能口述）\n双写失败：主成功副失败怎么办？是否影响接口返回？ 事务：主提交、目标失败时数据状态与补偿路径 Kafka：消息体设计、幂等键、消费失败重试、为何不直接同步修 硬删除：双向 diff 的具体算法（按主键批量 in-query） 切换：如何判断可以进下一阶段（不一致率、连续 N 轮全量通过） 3. 准备一组数字\n例如：数据量、双写延迟、全量一轮时间、Kafka 吞吐、挂起阈值、最终不一致量级。\n4. 把「反复校验」升级成「有界收敛」\n说明：在「源为准」阶段修复方向固定；切换前要求不一致 \u0026lt; 阈值；切换后短窗口只开增量；为何认为会收敛而不是越修越乱。\n5. 润色简历与口述\n简历：一句话写清「四阶段双写 + 全量/增量校验 + Kafka 异步修复 + 负载感知调度」 去掉口语和错别字；每个 Q 控制在「结论 → 做法 → 权衡」三层 6. 区分「你做了什么」和「方案通用知识」\nmysqldump 初始化、四阶段迁移是通识；面试官更想听你改 ConnPool、Equal 抽象、双向校验、挂起任务这些「你亲手做的」。\n面试官结论 材料证明你做过这件事，也知道难点在一致性和库压力，作为 2 年经验的项目素材够用。但当前深度停在「知道概念 + 知道会有问题」，还不足以在中高级追问里稳住。\n把一致性边界、Kafka 契约、双写失败语义、切换判定补全后，这份准备可以冲到 75–80；再加可量化结果和一次完整口述演练，才更接近「能让面试官点头」的水平。\n","date":"2026-08-03T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/migrate-0x02/","title":"不停机数据迁移 one"},{"content":"简历 preview 简历上 :\n使用 xxx 进行了服务的注册与框架 服务注册与发现 在微服务架构里面，我怎么知道哪些机器上部署了我需要的服务\nIP + 端口 最原始的形态就是 : IP+端口 . 不过这个不能称为 服务注册与发现 ， 因为既没有注册 也没有发现\n缺点 :\nIP 是会变的 不好维护 域名 + DNS 另外一个思路 : 使用域名 + DNS . 因为 IP 经常变动 所以考虑使用域名代替\n考虑解析域名的开销，所以 一般在客户端这边缓存解析域名的结果\n优点 :\n简单好用 ，不需要额外的组建 缺点 :\n如果客户端不缓存 DNS 结果，每次调用都会多一次调用\n如果客户端缓存 DNS 结果，那么就可能无法及时更新本地可用节点列表\n域名解析的问题是不能及时得到通知，那么能不能让域名服务器主动通知一下节点变化\n一个域名 可以有多个 IP 地址 。 域名可以认为是一个 连锁品牌 而 每一个 IP 可以认为是一家分店\n看来眼代码，我们 Gin 写的 HTTP 内容其实并没有牵扯到域名注册的逻辑 。\n而且我工作的代码，域名注册也是由运维负责的\n注册中心 注册中心的原理就是 ：\n服务端在启动的时候 主动在注册中心注册一下\n客户端在第一次发起调用之前，先查询注册中心，而后缓存住可用节点\n注册中心在服务端节点发生变动的时候 ,主动通知 客户端\n缺点 :\n注册中心在大规模集群下，会成为瓶颈 注册中心自省 :\ndubbo 提出来的概念， 随着集群规模增长 加上 微服务框架复杂度上升，\n导致注册中心的 数据越来越多, 更新越来越频繁 于是有了服务自省\n保留注册中心, 但是注册中心里面只有最小化的数据\n剩余跟服务有关的元数据，通过一个元数据服务来暴露\n服务注册 问题 :\n什么时候进行服务注册 ？ 暴露端口的时候注册 ？\n健康检查通过就注册 ？\n所有前置计算完成之后再注册\n注册什么数据 ？ 定位信息 : IP + 端口\n其他信息. 一般和微服务框架等具体功能有关系\n服务端和注册中心保持心跳 服务端主动保持心跳，服务端每隔一段时间就朝着注册中心发送一个心跳\n注册中心主动 。 注册中心主动朝着所有服务端节点发送心跳\n主要讨论 ：\n心跳间隔多长 如何判定节点连不上 ？ 一次心跳连不上还是多次心跳连不上 ？ 服务下线 如果服务端关闭了，就需要通知注册中心，而后注册中心通知客户端\n客户端就会把该节点从可用列表里面挪走\n服务端优雅下线 :\n问题 : 服务端 A 不能告诉注册中心自己下线了之后就立刻退出\n需要先通知注册中心，下线\n而后 服务端不在接受新请求。（在网络中读取一般的请求，也会直接拒绝）\n中间件类似的机制 服务端需要等待正在处理的请求结束\n等服务端已经接受的请求处理完毕，服务端结束运行\n还需要额外考虑 定时任务，分布式事务，数据库事务\n接入服务注册与发现 gprc 时候注册中心 gpc 在客户端提供 Resolver 进行对服务的解析\n但是他并没有在 服务端提供 register 的功能， grpc 根本不知道你使用的是不是注册中心\ngrpc 使用注册中心 etcd 服务注册与发现的高可用 高可用 高可用围绕三个点三条边来思考 :\n服务端崩溃怎么办？\n注册中心崩溃怎么办 ？\n客户端崩溃 ？。不需要处理\n三条边 :\n注册中心和服务端之间无法通信怎么办 ？\n注册中心和客户端之间无法通信怎么办？\n客户端和服务端之间无法通信怎么办？\nfailover 服务端崩溃 :\n服务端崩溃 -\u0026gt; 注册中心发现 -\u0026gt; 通知客户端, 中间有时间间隔\n简单来说 如果服务端崩溃，那么我们就要切换一个节点来重试 也就是 failover\n注册中心崩溃了怎么办 :\n先描述很难崩溃\n启用注册中心高可用方案, 例如部署一个集群，多活方案\n双注册中心方案, 注册的时候同时注册两个注册中心，在一个注册中心崩溃之后可用使用离国内外i啊一个注册中心\n按照业务拆分多个注册中心\n然后在解决真的崩溃的问题\n如果真的崩溃了，客户端怎么办\n使用本地缓存的可用节点信息 如果真的崩溃了，服务端怎么办\n如果是新节点，注册失败直接退出服务，如果是老节点继续提供服务 注册中心与服务端无法通信:\n继续保持服务，并且告警，并且考虑是否要退出 面试 服务注册与发现，有哪些组件\n客户端、服务端、注册中心 监控平台 什么是注册中心 ？ 为什么要使用注册中心 ？ 直接使用 IP 或域名行不行？\n一种中间件 用于注册支持的服务的内容 注册的时候，注册了什么数据\n定位信息 和 其他信息 （分组信息，权重信息） 在使用注册中心的整个流程是什么\n注册中心怎么知道服务端已经崩溃\n注册中心和服务端怎么保持心跳\n心跳的间隔怎么设置 ？ 有什么影响\n服怎么避免偶发性心跳失败\n服务端在下线的时候需要注意什么？ 具体的步骤是什么\n客户端和注册中心需要保持心跳吗\n","date":"2026-08-01T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/server-reg-discover-0x0x1/","title":"服务注册与发现 detail"},{"content":"简历 preview 简历上 : 点赞、阅读、收藏模块拆分成微服务之后 进行了数据库迁移实现微服务的单独存储\n为什么要做数据迁移 :\n经过微服务化之后，我们的代码已经拆除去了，但是实际上还是共享一个数据库 。 我们认为 某个微服务的数据也是单独存储的 因此还需要做 数据分离 迁移 分类 :\n异构数据迁移\n原表和目标表的结构不一样 如果数据库都不一样，也算异构数据迁移 问题 :\n数据转换 : 不同数据类型可能转出不同的兼容问题，如果是数字会有精度问题 完整性校验 : 数据之间的业务关系很难校验 同构数据迁移\n方式 :\n停机迁移\n数据迁移比较困难的就是，我们在迁移过程中会有新的数据插入，老的数据更新/删除，导致一致性问题 停机迁移可以很好的规避这部分内容，但是缺点就是应用停了下来 不停机迁移\n需要考虑 新老数据的更新 需要考虑不能对数据库造成太大的压力 不停机迁移 难点 : 数据始终是变动的\n不停机迁移的方案 :\n整个不停机迁移可以分成四个阶段：\n• 第一阶段：业务读写源表。在这个阶段，你要完成目标表的数据初始化过程。\n• 第二阶段：双写阶段，以源表为准。在这个阶段，数据会被双写到源表和目标表中，并且读是读源表，如果数据不一致，也是以源表的数据为准。\n• 第三阶段：双写阶段，以目标表为准。在这个阶段，数据也是保持双写，但是读以目标表为准，并且修复数据的时候，是以目标表为准。\n• 第四阶段：业务读写目标表。\n数据初始化 对于同构迁移\n我们可以使用 mysqldunmp 进行导出数据库数据，然后使用 source命令执行 sql 进行数据的插入\n对于异构数据的话\n我们只能够\n批量读取原数据库的数据 转换为 SQL 在代码中利用 ORM 进行插入 校验与修复 不停机数据迁移没办法做到完全保证一致性，所以需要引入数据的校验 从而尽可能大概率的保证一致性\n因为在切换目标表或者是原表的过程，总会有一些时间空隙\n数据校验 全量校验从方案来说, 就是 一条一条去比对\n但是如果 数据量特别大，怎么尽快完成校验与修复\n场景题 :\n在一些大厂的核心业务，一个表可能会有数十亿条数据，如何要求在一天内完成数据同步 答案就是 并发\n整个全量校验和修复可以看作两个步骤 : 校验 , 如果发现不一致 则修复\n能够想到的做法有下面几个\n立刻修复\n使用 channel 通知 goroutine 去修复\n使用 消息队列 进行异步修复\n考虑需要保护住目标表,因此这里引入 kafka\n校验基本思路 校验的基本思路就是\n从原表获取数据\n根据主键去目标着找出对应数据\n比较字段是否相等\n这里根据业务折中进行处理是否相等的过滤 校验逻辑 我们通过 offset 获取 base 中的第一条数据\n然后去 target 找对应 id 的数据，如果没有找到 那么通知修复，如果找到了那么比较，否则通知修复\n需要额外考虑的点 :\n超时控制，手动取消 数据库连接错误 剩余问题 :\n我们可以批量获取一部分数据进行处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 func (v *Validator[T]) validateBaseToTarget(ctx context.Context) { offset := 0 for { if v.highLoad.Load() { // 挂起 } // 找到了 base 中的数据 // 例如 .Order(\u0026#34;id DESC\u0026#34;)，每次插入数据，就会导致你的 offset 不准了 // 如果我的表没有 id 这个列怎么办？ // 找一个类似的列，比如说 ctime (创建时间） // todo 改成批量，性能要好很多 src, err := v.fromBase(ctx, offset) switch err { case context.Canceled, context.DeadlineExceeded: // 超时或者被人取消了 return case nil: // 你真的查到了数据 // 要去 target 里面找对应的数据 var dst T err = v.target.Where(\u0026#34;id = ?\u0026#34;, src.ID()).First(\u0026amp;dst).Error switch err { case context.Canceled, context.DeadlineExceeded: // 超时或者被人取消了 return case nil: if !src.CompareTo(dst) { // 不相等 // 这时候，我要干嘛？上报给 Kafka，就是告知数据不一致 v.notify(ctx, src.ID(), events.InconsistentEventTypeNEQ) } case gorm.ErrRecordNotFound: // 这意味着，target 里面少了数据 v.notify(ctx, src.ID(), events.InconsistentEventTypeTargetMissing) default: // 这里，要不要汇报，数据不一致？ // 你有两种做法： // 1. 我认为，大概率数据是一致的，我记录一下日志，下一条 v.l.Error(\u0026#34;查询 target 数据失败\u0026#34;, logger.Error(err.Error())) // 2. 我认为，出于保险起见，我应该报数据不一致，试着去修一下 // 如果真的不一致了，没事，修它 // 如果假的不一致（也就是数据一致），也没事，就是多余修了一次 // 不好用哪个 InconsistentType } case gorm.ErrRecordNotFound: // 比完了。没数据了，全量校验结束了 // 同时支持全量校验和增量校验，你这里就不能直接返回 // 在这里，你要考虑：有些情况下，用户希望退出，有些情况下。用户希望继续 // 当用户希望继续的时候，你要 sleep 一下 if v.sleepInterval \u0026lt;= 0 { return } time.Sleep(v.sleepInterval) continue default: // 数据库错误 v.l.Error(\u0026#34;校验数据，查询 base 出错\u0026#34;, logger.Error(err.Error())) // 课堂演示方便，你可以删掉 time.Sleep(time.Second) // offset 最好是挪一下 // 这里要不要挪 } offset++ } } 同时我们还需要反向考虑一个点，如果 target 表存在，但是base表没有怎么办\n因为可能存在硬删除操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 func (v *Validator[T]) validateTargetToBase(ctx context.Context) { // 先找 target，再找 base，找出 base 中已经被删除的 // 理论上来说，就是 target 里面一条条找 offset := 0 for { dbCtx, cancel := context.WithTimeout(ctx, time.Second) var dstTs []T err := v.target.WithContext(dbCtx). Where(\u0026#34;utime \u0026gt; ?\u0026#34;, v.utime). Select(\u0026#34;id\u0026#34;). Offset(offset).Limit(v.batchSize). Order(\u0026#34;utime\u0026#34;).Find(\u0026amp;dstTs).Error cancel() if len(dstTs) == 0 { // 没数据了。直接返回 if v.sleepInterval \u0026lt;= 0 { return } time.Sleep(v.sleepInterval) continue } switch err { case context.Canceled, context.DeadlineExceeded: // 超时或者被人取消了 return // 正常来说，gorm 在 Find 方法接收的是切片的时候，不会返回 gorm.ErrRecordNotFound case gorm.ErrRecordNotFound: // 没数据了。直接返回 if v.sleepInterval \u0026lt;= 0 { return } time.Sleep(v.sleepInterval) continue case nil: ids := slice.Map(dstTs, func(idx int, t T) int64 { return t.ID() }) // 可以直接用 NOT IN var srcTs []T err = v.base.Where(\u0026#34;id IN ?\u0026#34;, ids).Find(\u0026amp;srcTs).Error switch err { case context.Canceled, context.DeadlineExceeded: // 超时或者被人取消了 return case gorm.ErrRecordNotFound: v.notifyBaseMissing(ctx, ids) case nil: srcIds := slice.Map(srcTs, func(idx int, t T) int64 { return t.ID() }) // 计算差集 // 也就是，src 里面的咩有的 diff := slice.DiffSet(ids, srcIds) v.notifyBaseMissing(ctx, diff) default: // 记录日志 } default: // 记录日志，continue 掉 v.l.Error(\u0026#34;查询target 失败\u0026#34;, logger.Error(err.Error())) } offset += len(dstTs) if len(dstTs) \u0026lt; v.batchSize { if v.sleepInterval \u0026lt;= 0 { return } time.Sleep(v.sleepInterval) } } } 异构数据的校验:\n对于异构数据，我们没办法使用 id 进行查找，所以只能考虑其他 唯一键去寻找相同数据\n数据修复 数据修复的基本逻辑 target_missiong : 那么就是 insert。 获取原表数据进行插入\nneq : 那么就是更新，使用原表数据来覆盖\nbase_missiong : 代表目标表多了数据应该删除目标表数据\n并发问题 :\nQ : base找到了数据， 但是更新的时候 base 删除了数据 。 target 的更改会失败\nA : 重新开启一轮修复\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (o *OverrideFixer[T]) Fix(ctx context.Context, id int64) error { var src T // 找出数据 err := o.base.WithContext(ctx).Where(\u0026#34;id = ?\u0026#34;, id). First(\u0026amp;src).Error switch err { // 找到了数据 case nil: return o.target.Clauses(\u0026amp;clause.OnConflict{ // 我们需要 Entity 告诉我们，修复哪些数据 DoUpdates: clause.AssignmentColumns(o.columns), }).Create(\u0026amp;src).Error case gorm.ErrRecordNotFound: return o.target.WithContext(ctx). Where(\u0026#34;id = ?\u0026#34;, id).Delete(new(T)).Error default: return err } } 双写 我们经历了全量的校验与修复之后就可以考虑进行双写了\n由于是两个数据库 开启不了 数据库本地事务\n分布式事务 : 性能很差\n需要考虑 双写 + 流量切换。 即 考虑读写哪个数据源\n方案 1 引入原子类进行并发控制, 监听配置变更进行切换 数据源\ndao层面，双写两张表，如果源表失败则退出，如果目标表失败 等待 校验与修复\n问题是\n增删改都需要写一份代码 方案 2 ConnPool Prepare : 预编译语句\nSelect 都用于 Query\n增删改用于 Exec\nQuery返回的结构体，error并没有暴露出来，所以处理 error 的话，只能 panic 额外在增加一个双写的 DAO\n然后根据 pattern 进行控制 1 2 3 4 5 6 7 8 9 10 11 type DoubleWriteDAO struct { src InteractiveDAO dst InteractiveDAO pattern *atomicx.Value[string] } func NewDoubleWriteDAOV1(src *gorm.DB, dst *gorm.DB) *DoubleWriteDAO { return \u0026amp;DoubleWriteDAO{src: NewGORMInteractiveDAO(src), pattern: atomicx.NewValueOf(patternSrcOnly), dst: NewGORMInteractiveDAO(dst)} } 通过实现以下几个接口进行实现双写的操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // ConnPool db conns pool interface type ConnPool interface { PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row } type TxBeginner interface { BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) } // ConnPoolBeginner conn pool beginner type ConnPoolBeginner interface { BeginTx(ctx context.Context, opts *sql.TxOptions) (ConnPool, error) } // TxCommitter tx committer type TxCommitter interface { Commit() error Rollback() error } 面试要点 不停机迁移的基本步骤\n你的数据校验方案是什么\n你的数据修复方案是什么 ？ 如何保证数据正确性 ？ 怎么解决并发问题 ？\n不使用 MQ 来进行数据的更新只当作触发器 在数据迁移的每个阶段，你是怎么考虑保护着数据库的\n调度时机考虑 查询改成批量，修复改成批量。 修复的批量该成 kafka 批量消费 如果 kafka 瓶颈了怎么办，消息积压了怎么办？\n为什么 你要使用 Kafka 直接校验之后修复数据不行吗\n增量校验和修复、业务写数据、全量校验和修复同时进行，有什么并发问题 ？ 怎么解决？\n在主从同步下，校验和修复有什么注意事项\n怎么保证在数据迁移的时候 不影响业务\n你使用了哪些优化手段来加速迁移数据的过程\n","date":"2026-07-31T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/migrate-0x01/","title":"不停机数据迁移 detail"},{"content":"简历 preview 简历上 : 将单体业务中的点赞服务拆出一个微服务 并且使用 grpc 进行通行管理\n微服务 微服务架构 : 将功能分解到各个离散的服务中实现对解决方案的解耦。\n每个服务可独立进行 开发，管理 和 迭代 为什么要使用微服务 为了 分而治之 :\n降低复杂度\n易于开发、测试、部署 和 扩容\n模块化 对比 微服务 整合全部模块于一个单体应用，这个过程比较复杂\n从运维角度看，实例才是基本单位，微服务化的管理力度更细 。\n微服务独立性更强\n这里说一下我的理解 :\n我前司大概有 6-8个业务组 。 按照 模块化单体+进程角色部署 进行开发\n不过单独拆出了两个业务， 一个 qass 跨环境的行情中心 和 配置中心 , 一个 apitest 用于进行维护项目的api稳定\n为什么我们不全部拆成微服务\n一次下单 会串着 账户、仓位、ledger、风控。 这一套流程 强事务、强一致\n如果拆成微服务 需要考虑 事务 和 最终一致\n那么拆微服务能给企业带来什么收益 :\n发版时间\n我们知道单体的发版需要依赖整体发版 ， 例如 如果我账户模块改动，想要提前发版，这不可能。 只能发在上一个版本 然后发一次全局的重构\n但是如果拆成微服务，我们可以每个服务 自己控制发版时间\n官方话就是 ，需要考虑 团队边界、发布节奏、技术异构\n成本\n我们现在单体是 文字+用户+登陆+点赞 , 但是如果我们点赞的性能跟不上, 在单体阶段我们只能考虑增加全部机器的性能\n如果我们部署一台 文字+用户+登陆+点赞 的服务 需要 1w ，那么如果我们想要单独增加点赞的性能 需要重新部署一台完整的服务 也需要 1w\n但是如果我们把点赞拆出来，单独部署一个点赞服务只需要 2k, 那么 1w 可以部署 5 台\nRPC PRC : 远程过程调用， 允许程序在本地计算机上 调用远程计算机上的字程序，而无需程序员额外变成\nRPC 协议可以简历在很多协议基础之上\n基于 TCP 的 RPC 协议，典型的国内大厂自研的协议， 比如说 Dubbo 协议。\n基于 HTTP 的 RPC 协议，比如说 gRPC 协议。而 HTTP 本身又是可以基于 TCP 协议或者 UDP 协议的。\n为什么要引入 rpc\n我们可以看一些接口的文档，他们暴露了一些接口可以让我们使用，我们带上对应的 apikey + apisecret 即可\n但是为什么我们还需要使用 rpc\n口语里的 HTTP（REST） 口语里的 RPC（如 gRPC） 你怎么写代码 拼 URL、method、body client.Like(ctx, req) 契约 OpenAPI / 约定 JSON 字段 .proto / 接口定义，强类型生成 载荷 多为 JSON 文本 多为 Protobuf 二进制 传输 HTTP/1.1 或 HTTP/2 常见 HTTP/2（多路复用） 风格 资源导向（/users/1） 方法导向（UserService.GetUser） 调试 浏览器、curl 极友好 要 grpcurl 等工具，稍麻烦 流式 有限（SSE/WebSocket 另说） 双向流较自然 对开发者的体感：RPC 更像「远程函数调用」；HTTP/REST 更像「发一次 Web 请求」。\nGRPC 特点 :\n高性能 : 基于 QUIC 协议，利用 HTTP2 的双向流特性\n跨语言 :\n开源 :\nGrpc 使用 protobuf 来作为自己的 IDL 语言\nIDL : 接口描述语言\nprotobuf 基本原理 :\n使用二进制格式进行序列话和反序列化\n定义一种标准的消息格式 。 用于表示结构化数据\n每个字段都有唯一的标签和类型\nGRPC 服务端与客户端 服务端 :\n通过 Server Listen 启动服务\n通过 Register 注册对应的服务\n1 2 3 4 5 6 7 8 9 10 11 12 func TestServer(t *testing.T) { s := grpc.NewServer() // 这个是生成的代码 RegisterUserServiceServer(s, \u0026amp;Server{}) l, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;:8090\u0026#34;) assert.NoError(t, err) // 启动 if err = s.Serve(l); err != nil { // 启动失败，或者退出了服务器 t.Log(\u0026#34;退出 gRPC 服务\u0026#34;, err) } } 客户端 :\n通过 Dial 来进行通信连接\n使用 NewClient 创建客户端\n过程调用直接调用 GetById\n1 2 3 4 5 6 7 8 9 10 11 12 13 func TestClient(t *testing.T) { conn, err := grpc.Dial(\u0026#34;:8090\u0026#34;,grpc.WithTransportCredentials(insecure.NewCredentials())) assert.NoError(t, err) client := NewUserServiceClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() resp, err := client.GetById(ctx, \u0026amp;GetByIdReq{ Id: 123, }) assert.NoError(t, err) t.Log(resp.User) } DDD 基本理论 • 限界上下文（Bounded Context）\n描述 我们所要解决的问题上下文 描述微服务的边界 • 实体（Entity）\n标记业务的唯一 ID • 值对象（Value Object）\n一堆属性的集合 • 聚合体（Aggregate）\n一个实体 + N个值对象的 集合 • 工厂（Factory)\n普通工厂方法在 DDD 中的应用 • 仓库（Repository）\n数据存储的抽象 • 事件（Domain Event)\n就是我们说的 MQ event • 服务（Domain Service）\n微服务拆分 按照 DDD 的理论拆分微服务， 一个领域就是一个微服务\n微服务拆分路线 :\n单体应用 :\n完善单元测试。\n先抽取公共部分，如utils、helper 等。\n引入聚合层解除模块间循环依赖。\n按照业务对象划分模块，分到不同的包里。\n模块化 :\n创建不同的代码仓库，将公共部分、业务模块逐个挪到别的代码仓库。\n开始准备微服务环境和服务框架选型。\n搭建好 CI 和集成测试环境。\n模块依赖化 :\n业务模块逐个服务化，解决微服务开发、测试、部署中所遇到的问题。\n搭建自动部署和回滚平台。\n调研服务治理和网关。\n引入消息队列。\n引入分布式事务解决方案。\n引入分布式任务调度。\n搭建可观测性平台：logging、tracing、metrics，以及对应的告警系统\n微服务化：\n引入服务治理 引入网关 引入回归测试 按照业务分库 拆分 拆分方案 :\n选定模块 原则 : 先易后难 检测覆盖率 代码拆分 确定技术选型 改造微服务 在原先的应用中 同时使用 本地调用 和微服务调用 。 使用开关控制流量 并且允许回滚 逐步调整流量 抹除原先的本地调用 流量控制 我们将原先的本地服务 使用 装饰器 重新抽象成一个接口\n这个接口支持使用 threshold 控制流量，通过随机数控制 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func (g *GreyScaleInteractiveServiceClient) GetByIds( ctx context.Context, in *intrv1.GetByIdsRequest, opts ...grpc.CallOption, ) (*intrv1.GetByIdsResponse, error) { return g.client().GetByIds(ctx, in, opts...) } func (g *GreyScaleInteractiveServiceClient) UpdateThreshold(newThreshold int32) { g.threshold.Store(newThreshold) } func (g *GreyScaleInteractiveServiceClient) client() intrv1.InteractiveServiceClient { threshold := g.threshold.Load() // [0-100) 的随机数 num := rand.Int31n(100) // 举例来说，如果要是 threshold 是 100， // 可以预见的是，所有的 num 都会进去，返回 remote if num \u0026lt; threshold { return g.remote } // 假如说我的 threshold 是 0，那么就会永远用本地的 return g.local } 面试 什么是微服务架构 ？ 为什么要使用微服务架构。？\n模块化后为什么要微服务化 ？\nRestful 和 微服务架构是什么关系 ？\n可以用 HTTP 协议来说实现 微服务架构吗 ？\n什么是 RPC ？ RPC 和 HTTP 是什么关系\n什么是 DDD ？\n微服务拆分，怎么拆 ，拆分的具体步骤\n微服务拆分有哪些难点\n怎么保证微服务拆分没有引入 bug\n怎么在线上做灰度发布 ？\n阈值+随机数 ","date":"2026-07-31T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/migrate-0x01/","title":"微服务 detail"},{"content":"本篇面经来自 : https://www.nowcoder.com/feed/main/detail/e28ee3b477754207a97a79516d7e7101?sourceSSR=search\n牛客上面最新的 golang 社招没有了，不确定是不是没有岗位面试 还是 工作了之后大家都不爱写面经导致的\n随便找一个实习或者是校招的看看\n开场基础 自我介绍 ( 我是xxx, 毕业 xxx, 曾经工作于 xxx 负责 xxx, 拥有 xxx 经验 )\n当初为什么会选择做这两个核心项目？ (我这边预计一个是公司项目，一个手写的项目，一个AI项目)\n对于 AI 项目，AI发展是趋势，而且在公司的时候参加过黑客松，之前开过一个头，后面有空了之后就打算把他完善一下 由于在之前的公司 少了一部分 微服务,高并发相关的业务需求,所以单独写一个项目用于完善该部分的经验 项目1 项目中用到了分片上传、断点续传，具体如何保证最终上传的文件是完整正确的？\n如何保证所有分片拼接后绝对正确，不只是简单判断无缺片？\n这两个不会，预计后续补充该部分知识点 。不过可以根据题目思路发散一些\n保证文件完整正确, 先保证分片存在\n如果分片存在 , 可以考虑根据 后端存放的 metaData即 后端存放每次分片发送过来的数据 。 然后每个分片单独比较该分片的数据。\nAI 编程能力认知 日常 Go 开发中，是依赖 AI 编程更多，还是自己手写代码更多？ 基本上使用 AI, AI 占比率 99% 。工作的时候主要使用 codex 和 cursor 这种具有高安全性 并且性能够用的 Agent. 有时候会考虑使用 GLM 进行跑涉及多模块的需求 。 平时使用 AI 编程，用过哪些实用的 Skill/能力？ 写了一个 skills用于排查对账问题,我们服务客户60%的时间都是要解决客户的对账问题，排查是否缺少数据，是否有数据没有参与对账。通过写了一个 skill 把之前人为排查的顺序告诉 AI 让他来进行排查 。 最终效率提升了 50%\n将内部接口暴露出来供外部使用，组内封装过过个MCP接口, 通过使用各个环境的 Apikey 进行访问每个环境的数据 。\n你用过的这些 AI 编程 Skill，存在哪些不好用、有局限的地方 局限的地方 : 对于较为明显的问题，实际上人一眼就能看出来,但是跑AI的话预计要等5-10分钟。 不好用: 因为部分接口还没有封装暴露成为MCP,导致现有 skills需要的数据需要人手动传入 项目1 项目使用 Kafka 做任务调度，重试机制、失败恢复的具体实现方案是什么 这两个也没有接触过,不过可以根据感觉来回答\n在点赞服务，使用 kafka 进行异步统计。如果在任务调度中 失败了,我们会尝试 3次重试，如果重试也失败之后,会把该数据放到死信队列中, 死信队列的数据单独启动一个定时任务来进行处理这部分数据。 如果多次失败，会有对应的告警和日志，并且可以在监控中观测到，这时候就需要人为到定位，是不是第三方服务不稳定 或者是传入了错误的请求导致的 。 当然我们也可以考虑业务折中的方案，我们对于点赞服务，只需要确保用户能够正常的维护是否点赞这个功能，而对于点赞数在业务上允许他可以不稳定更新 重试、退避策略、失败恢复，是否依赖 MySQL 状态表 + 定时扫表 实现调度？具体流程是什么。 如上 项目 2 详细介绍项目中的长会话上下文管理方案。\n你项目中做过受限子任务编排，具体展开讲讲实现思路与作用。\n这两个完全是知识盲区了\n六、场景题 设计异步任务创建接口，如何保证接口幂等，避免重复创建任务？ 根据业务字段进行重复性校验。例如一个异步任务需要 时间范围,操作对象 . 那么就查看是否已经有 \u0026lt;操作对象,时间范围\u0026gt; 的组合存在,如果存在则不创建\n使用进程级的限流, 防止前端一秒内多次点击从而频繁发送相同的请求\n生产环境接口原本响应几十毫秒，突然耗时飙升至数秒：如何快速排查问题？如何紧急止损？ 一般这种异常都会有对应的告警 同时会有完整的tracing. 如果没有的话可以考虑 通过监控图观察异常情况的发生时间,然后去对应时间点查询日志 。 并且根据对应接口最近改动进行比较\n如果接口特别影响使用, 那么可以考虑使用本地缓存 cover一下实际请求 实现降频 . 或者是回退最近改动的代码。如果在可以接受范围内的话，可以考虑定位出来之后再解决\n若问题在版本更新后出现，优先从哪些方向排查根因？ 最近代码的改动，大概率是 如果涉及第三方服务，可以排查第三方服务是否稳定 哪些具体代码改动，会直接导致接口耗时从几十毫秒飙升至数秒？ 数据库层面, 开发环境和线上环境数据不一致，可能新增了某些 大量查询 或者是 更新的操作，但是后端没有做限制和处理 缓存层面, 可能引入了某些大型配置文件,但是这些配置文件的 json.unmarshal没有使用缓存进行控制或者说未命中缓存导致频繁 unmarshal 网络层面, 代理或者是某些三方接口网络不稳定。 导致等待http请求，并且http timeout设置的很大，导致等待了很长 硬件层面, 其他进程的占用飙升导致我们服务只能拥有少部分资源导致速度变慢 项目主要使用 MySQL 吗？有没有实际做过慢查询优化、SQL 调优相关工作？ 用的 pgSQL .有过 慢查询优化 的经历\n最主要常见的就是，业务需要支持 账户+时间的 查询。但是历史表结构并没有 \u0026lt;account + timestamp\u0026gt;的索引。 主要优化就是在其上面创建索引\n其他优化方向也大同小异。 主要是根据业务方向进行调整，使用 ESR 原则进行优化\n综合评分：58 / 100\n按 2 年 Golang 社招标准看：方向对、有真实业务痕迹，但项目深度、技术闭环和面试表达都偏弱，目前更接近「能聊几句」而不是「能扛住追问」。\n分项打分 维度 分数 简评 项目深度与闭环 45 分片上传、Kafka、AI 长会话多处空白，硬伤 场景题 / 排查能力 68 有思路，但缺具体手段和分层排查 AI 工程实践 75 Skill/MCP 有真实落地，是亮点 数据库 / SQL 70 有索引优化经历，但深度不够 表达与面试策略 55 「不会」「根据感觉答」会直接扣印象分 岗位匹配（深信服向） 60 偏业务+AI，缺系统/网络/高并发细节 优点 AI 工程有真实产出：对账 Skill、MCP 暴露内部接口、效率提升 50%，比空谈「会用 Cursor」强很多，2 年经验里这是差异化点。 知道业务折中：点赞数允许最终一致，说明有业务取舍意识，不只堆组件。 慢查优化有落点：account + timestamp 索引、提到 ESR，说明碰过真实 PG/MySQL 问题。 耗时飙升题有分层意识：库 / 缓存 / 网络 / 机器都想到了，方向基本正确。 缺点 项目答不上核心题\n分片完整性、长会话、子任务编排直接写「不会 / 盲区」。面试官会默认：项目是挂上去的，不是你做的。\n技术答案停在「感觉层」\n分片：只说到 metaData 比对，没提分片 hash、整文件 MD5、ETag、合并校验。 Kafka：重试/死信方向对，但缺幂等、offset、重试是否可重入、和「定时扫表」方案的对比。 幂等：业务唯一键有思路，但「进程级限流」解决不了分布式重复创建，应用层锁 / Redis SETNX / DB 唯一索引没讲。 AI 占比 99% 很危险\n面试官会追：没有 AI 时你怎么 debug？怎么读 pprof？怎么自己写并发？建议改成「生成 + 审查 + 自测」比例，强调你负责设计和验收。\n排查题缺「可操作清单」\n耗时飙升应落到：QPS、P99、DB 慢查、连接池、GC、锁等待、下游 RT、最近发布；止损应有限流、降级、回滚、熔断，而不是只说「本地缓存 cover」。\n准备姿态偏被动\n多处「这两个不会，后续补充」「根据感觉来回答」——面试现场不能这样说，要说「这块我负责的是 X，完整方案是 Y，边界是 Z」。\n建议（按优先级） 1. 先补项目闭环（最高优先级） 每个项目至少能讲清：背景 → 你的职责 → 关键路径 → 数据一致性 → 失败怎么处理 → 指标。\n分片上传建议补齐并能口述：\n上传前：文件 hash / uploadId 分片：chunkIndex + chunkHash，落盘后校验 续传：查已上传分片列表 合并：校验分片齐全 + 按序合并 + 整文件 hash 可选：秒传（文件 hash 已存在则跳过） Kafka / 异步任务建议补齐：\n至少一种可靠方案：本地消息表 / Outbox，或 Kafka + 幂等消费 重试：有限次 + 指数退避 + 死信 + 告警 明确：为什么不用「只扫 MySQL 状态表」，或两者如何配合 AI 项目（长会话 / 子任务）若写在简历上：\n上下文：滑动窗口 / 摘要 / 向量检索，各自取舍 子任务：状态机或 DAG，超时、取消、部分失败怎么处理 写了却讲不清，比不写更伤。\n2. 场景题改成「标准答题模板」 幂等：客户端 requestId + Redis/DB 去重表 + 业务唯一键；限流只防刷，不是幂等。\n耗时飙升：监控定位时段 → 看发布/依赖 → 拆 RT（本机/DB/下游）→ 止损（回滚/限流/降级）→ 再查根因。\n版本后出问题：优先 diff 发布内容、配置、SQL、缓存 key、依赖版本。\n3. 调整 AI 叙事 建议口径：\nAI 提效写样板和排查脚本；核心协议、一致性、并发、线上问题由我设计并验证；Skill 是把可重复排查流程固化，不是替代判断。\n准备 1～2 个「没有 AI 也搞定」的案例（慢 SQL、死锁、goroutine 泄漏等）。\n4. 把「不会」改成「边界诚实」 可以说：「合并校验是同事做的，我负责分片落盘和续传列表；完整方案我了解是 hash 校验这条链路。」\n不要说「根据感觉答」。\n5. 针对深信服多补一块 这类公司常挖：网络、并发、资源占用、稳定性。建议再准备：\ngoroutine / channel / context 取消 连接池、超时、熔断 pprof 怎么用 一次真实线上/准线上问题的复盘（现象→排查→根因→防再发） 结论 评价 说明 当前水平 业务感 + AI 落地尚可，系统与项目深度不够 面试风险 项目关键题答空时，很容易被判定「包装项目」 提分空间 把 3 个项目各自补成 10 分钟可讲、5 分钟可追问的闭环，分数有望到 75+ 如果你愿意，我可以按这份面经，直接帮你改一版「可上场口述」的标准答案（分片上传 + Kafka 重试 + 幂等 + 耗时排查）。\n","date":"2026-07-30T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/interview_0x03/","title":"[深信服] 面经每日一看"},{"content":"需求分析 对于一个大文件 \u0026gt;= 50mb 的上传, 走单个文件上传的接口很容易超时\n单个请求时长过长，容易出发 网关/前端axios 超时\n中间失败就要整个文件重传\n也可能遇见中间件请求大小限制\n业务流程 前端拆分文件\n通过 /post-init . 初始化本次文件的基本信息\n1 2 3 4 5 6 { file_name // 文件名 file_size // 文件大小 totalChunks // 总分片数 // .. } /post-init 为每个分片创建临时目录, 然后将本次的 uploadItem 存放到 进程级 map中并且返回upload-id\n前端分片带上 upload-id 和 chunk_index 以及本次的数据到 post-chunk 接口\n所有数据上传完之后前端调用 post-merge接口进行数据到合并，通过 upload-id 遍历每一个分区\n一些问题 数据上传信息记录由进程级 map 进行记录，重启之后会丢失 , 并且不支持 跨实例续传\n业务上是允许的，因为客户环境只有每次升级才会进行重启，升级前后的上传操作 从业务上看确实需要客户重新操作一次\n项目是分环境分数据库部署的，没有多实例要求\n优化点 业务没有断点续传。 如果失败的话需要整体重传\n如果上传失败了，我们需要重新传整改文件 旧的文件也不会主动清理掉，磁盘上会一直留着，直到运维来清理他 并不能保证 拼接后绝对正确。\n","date":"2026-07-30T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/big-file-upload-0x01/","title":"大文件上传"},{"content":"高频面试题：如果要你找出按照点赞数量前 N 个数据，怎么做？\n设计一个高性能方案, 要求 :\n综合考虑可以怎么利用缓存，包括 Redis 和本地缓存。要想清楚，你这个缓存方案拿出去面试究竟有没有竞争力，有没有让面试官眼前一亮的点\n允许业务折中，但是你要说清楚你准备怎么折中\nZSet 存储 我们在用户请求点赞 URL 之后, 使用 ZSET 进行维护一个点赞集合 。 使用 lua 脚本控制 check-dothing 的并发问题\n如果请求的 key 不存在, 则查找数据库将已有的 value 写入到 zset 中\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 local zsetName = KEYS[1] -- 有序集合的名称 local memberToIncrement = ARGV[1] -- 要增加分数的元素 -- 获取指定元素的当前分数 local currentScore = redis.call(\u0026#34;ZSCORE\u0026#34;, zsetName, memberToIncrement) if currentScore then -- 将分数加1 local newScore = currentScore + 1 -- 更新有序集合中指定元素的分数 redis.call(\u0026#34;ZADD\u0026#34;, zsetName, newScore, memberToIncrement) return newScore else -- 指定元素不存在 return 0 end 如果不存在那么需要把 value 写入到 zset 中\n先判断 set-key 是否存在\n如果存在 判断 key 是否有统计，如果有则 +1 否则 设置 value\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 local zsetName = KEYS[1] -- 有序集合的名称 local memberToIncrement = ARGV[1] -- 要增加分数的元素 local specifiedScore = tonumber(ARGV[2]) -- 指定的分数，如果未指定则默认为1 -- 检查有序集合是否存在，如果不存在则创建并设置指定元素的分数 local exists = redis.call(\u0026#34;EXISTS\u0026#34;, zsetName) if exists == 0 then redis.call(\u0026#34;ZADD\u0026#34;, zsetName, specifiedScore, memberToIncrement) return specifiedScore end -- 获取指定元素的当前分数 local currentScore = redis.call(\u0026#34;ZSCORE\u0026#34;, zsetName, memberToIncrement) if currentScore then -- 如果元素存在，将分数加1 local newScore = currentScore + 1 redis.call(\u0026#34;ZADD\u0026#34;, zsetName, newScore, memberToIncrement) return newScore else -- 如果元素不存在，将分数设置为指定的分数 redis.call(\u0026#34;ZADD\u0026#34;, zsetName, specifiedScore, memberToIncrement) return specifiedScore end 读取 通过使用 ZRevRangeWithScores 来获取前 100 的数据进行获取 topN 的数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (r *RedisInteractiveCache) LikeTop(ctx context.Context, biz string,) ([]domain.Interactive, error) { var start int64 = 0 var end int64 = 99 key := r.rankingKey(biz) res, err := r.client.ZRevRangeWithScores(ctx, key, start, end).Result() if err != nil { return nil, err } interactives := make([]domain.Interactive, 0, 100) for i := 0; i \u0026lt; len(res); i++ { id, _ := strconv.ParseInt(res[i].Member.(string), 10, 64) interactives = append(interactives, domain.Interactive{ Biz: biz, BizId: id, LikeCnt: int64(res[i].Score), }) } return interactives, nil } 优化 :\n我们业务上只需要 top100 可以考虑只维护 前1000的数据。 使用 ZREMRANGEBYRANK 删除 \u0026gt; 1000的内容\n最坏情况下，无非就是冷门文章爆火，需要查一次数据库 。 但是爆火的冷门文章并发并不是很高\n问题 前 100 名是一个高频数据\n如果有 一亿个 数据怎么维护 ？\n我们假设 100b 一条数据， 100w 条就是 100mb , 1000w 条就是 1gb . 1亿条就是 10gb 这很夸张了\n前 100 名的维护 可以结合一个本地缓存，使用定时任务更新本地缓存。 例如 每 5s 调用一次 topN 函数，放进本地缓存中\n本地缓存的实现\n1 2 3 4 5 6 7 type cacheImpl[T any] struct { result sync.Map // [string]cacheResult[T] successTimeout time.Duration failTimeout time.Duration lock sync.Mutex lockMap map[string]*sync.Mutex } 一亿个数据 分 key 写入 . 我们在写入数据的时候 按照每 100个元素一个。set 进行分 key 处理\n业务折中 。 并不是真的维护一亿，而是维护近期的点赞数据 例如 3天内的 。\n这里可以考虑算法过期 例如 likecnt / (age + 2) ^ gravity\n或者是考虑整个 key 过期\n1 2 3 4 5 6 7 8 9 10 11 12 13 func (r *RedisInteractiveCache) IncrRankingIfPresentV1(ctx context.Context, biz string, bizId int64, error { h := fnv.New32a() _, _ = h.Write([]byte(bizId)) key := fmt.Sprintf(\u0026#34;top_100_%d_%s\u0026#34;, h.Sum32()%100, biz) res, err := r.client.Eval(ctx, luaRankingIncr, []string{key}, bizId).Result() if err != nil { return err } if res.(int64) == 0 { return RankingUpdateErr } return nil } 分key 版本的读取\n每次从同一个业务中的 100 个分key 中获取前100的数据，然后再合并出前100的数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func (r *RedisInteractiveCache) LikeTopV1(ctx context.Context, biz string,) ([]domain.Interactive, error) { // 我从 100 个 key 里面，各取前 100 // 然后，合并再取前 100 interactives := make([]domain.Interactive, 0, 100*100) for i := 0; i \u0026lt; 100; i++ { var start int64 = 0 var end int64 = 99 key := fmt.Sprintf(\u0026#34;top_100_%d_%s\u0026#34;, i, biz) res, err := r.client.ZRevRangeWithScores(ctx, key, start, end).Result() if err != nil { return nil, err } for j := 0; j \u0026lt; len(res); j++ { id, _ := strconv.ParseInt(res[j].Member.(string), 10, 64) interactives = append(interactives, domain.Interactive{ Biz: biz, BizId: id, LikeCnt: int64(res[j].Score), }) } } // 进一步排序，然后取前 100 return interactives, nil } 其他 借助定时计算，每次计算1000名，使用 Zset 来维护 1000 名的分数\n总结 Q : 你是如何设计一个高性能的 排行榜服务\n我们使用 Zset 进行维护, 每次用户点赞的时候 会去操作一次 Zset 如果 Zset 不存在会获取数据库中的数据进行补充 。\n对外提供一个 TopN 的函数用于获取 topN 的数据。\n使用 本地缓存 + 使用定时任务每5s 维护 Zset TopN 的任务 。\n对于大数据结构，我们采用了 分 key 的方案 , 并且考虑业务折中的方案 我们只维护最近3天的数据 。 对于超过 3 天的数据，我们会进行整 key 过期 。 如果有业务需求，可以考虑使用算法过期\n对于数据的读取，每次获取同一个业务中 100 个key内的 前100的数据，合并后再去前 100 的数据\n由于我们业务只需要前 100 的数据。 所以我们对于每一个 topN 维护一个 1000 数量的 Zset 。通过使用。ZREMRANGEBYRANK 进行裁剪优化\n总分：62 / 100（按「2 年 Go 后端」面试深度） 能讲清 ZSet + 本地缓存 + 业务折中，达到中级入门；但并发正确性、分片收益、读路径细节经不起追问，离「眼前一亮」还有距离。\n分项（供对照） 维度 分 说明 问题建模 14/20 点到高频读、亿级内存，缺一致性与降级 方案设计 16/25 ZSet / 本地缓存 / 分片方向对，收益与边界说不清 正确性与并发 10/20 Lua / 冷启动 / 裁剪与回写有明显漏洞 缓存竞争力 12/20 有本地缓存+定时刷新，缺亮点与可落地细节 表达与代码 10/15 结构尚可，代码未完成、笔误多 优点 选型合理：点赞排行用 Redis ZSet + ZRevRangeWithScores，是面试官期望的标准答案。 有业务折中意识：只维护 Top1000、近 3 天、时间衰减公式，比死磕「全量精确」更像有线上经验。 意识到读热点：Top100 用本地缓存 + 定时刷新，方向正确。 有规模感：用「100B × 条数 ≈ 内存」估算，说明想过容量，而不是只背 API。 分片合并思路：每分片取 TopK 再归并，全局 Top100 在理论上是成立的（全局 Top100 一定落在各分片 Top100 里）。 缺点（面试深挖会挂的点） 1. 写路径 / Lua 正确性不足\n第一段脚本：member 不存在就 return 0，和「key 不存在回源 DB」混在一起，边界说不清。 EXISTS + 回源 + ZADD 有典型竞态：多请求同时发现 key 不存在，会重复回源、互相覆盖。 直接 ZINCRBY 通常更简单；自己做 ZSCORE + ZADD 却没讲清为何不用原子自增。 冷门突爆「并发不高」不能当结论，面试官会问：缓存击穿 / 单飞 / 互斥回源怎么做？ 2. 分 Key 没有真正解决「10GB」问题\n分 100 个 key 只是摊内存到多实例/多槽，总量仍约 10GB，除非配合「每分片只留 Top1000 + 时间窗」。 文档里「分 key」和「裁剪 1000 / 只保 3 天」缠在一起，容易被问成：分片到底解决什么？裁剪丢了的 member 再点赞怎么回写？ 3. 读路径代码与叙述不一致\n1 2 // ... 注释写合并再取前 100 return interactives, nil // 实际未排序、未截断 100 次串行 ZRevRange，没提 Pipeline / 并发，高 QPS 下这是减分项。 h.Write([]byte(bizId))：bizId 是 int64，这样写编译都过不了，暴露准备粗糙。 4. 「眼前一亮」的缓存方案偏薄\n本地缓存只给了结构体字段，没有：过期策略、失败降级、单飞、多实例一致性、Redis 挂了怎么办。 5s 定时刷新可以，但要主动说清楚：最多 5s 脏读、是否可接受、写后是否主动失效。 5. 总结段有自相矛盾\n一边「只维护近 3 天」，一边「每个 topN 维护 1000 的 ZSet 裁剪」——没串成一条清晰主路径（写什么、裁什么、读什么、何时过期）。 建议（按面试表述优先级改） 先定一条主链路（30 秒版本）\n点赞写：ZINCRBY（或 Lua 原子自增）→ 可选裁剪到 TopK → 读：本地缓存 Top100（定时/主动刷新）→ Redis 兜底 → 业务只保证近 N 天、允许秒级延迟。\n补齐会被追问的 5 个点\n冷启动：key 不存在时 singleflight / 分布式锁回源，防止击穿。 与点赞主链路一致性：双写失败怎么补偿（异步对账 / 消息队列重放）。 分片收益：说清是「内存分散 + 裁剪」还是「近似排行」；读用 Pipeline。 本地缓存：成功/失败 TTL、刷新抖动、降级返回旧数据。 降级：排行挂了不影响点赞主流程（熔断、开关、返回空或历史快照）。 代码准备\n把 LikeTopV1 补完：归并堆 / sort + 截断 Top100。 分片 key 用 strconv.FormatInt；Lua 收敛成一种语义讲透。 修标题笔误 signle → single，避免细节减分。 想加分的「亮点」（选 1～2 个讲深）\n读写分离：写只碰分片 ZSet，读只碰「聚合后的 Top100 专用 key」（定时任务算好写入），线上读路径 O(1)。 或：消息异步更新排行，点赞接口不直连大 ZSet，用最终一致性换写吞吐。 面试官一句话结论 作为 2 年经验的准备稿：骨架有了（约及格偏上），深度和正确性不够。把「并发回源、分片真实收益、读合并实现、一致性与降级」补成能闭环口述的一条故事，分数可以到 75～80；再补上「聚合 Top key / 异步更新」一类可落地亮点，才接近「眼前一亮」。\n","date":"2026-07-30T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/signle-rank-0x01/","title":"单机热榜"},{"content":"消息队列 消息队列的作用是 : 实现 生产者 和 消费者解耦\n例如 :\n点赞服务聚合推送，批量插入数据库\nkafka 基本概念 生产者 : producer\n消费者 : consumer\nbroker : 消息服务器 。 逻辑上的概念 , 实践中 一个 broker 就是一个消息队列进程\ntopic 与 分区 (parition)\n消费者组 与 消费者\nTopic 与 分区 topic : 可以认为是 消息队列上代表不同业务的 东西 。 简单的认为 一个 业务就是一个 topic\n一个 topic 可以有多个分区\nKafka 为了保证 高可用 和 数据不丢失 分区有 主分区 和 从分区\n当 发送消息 到 kafka 上的时候， kafka 会把消息写入 主分区之后 再同步到从分区\n分区 与 broker 的关系 主分区之间, 不会在同一个 broker 上\n同一个分区的 主从分区 也不会在同一个 broker 上\n分区 与 生产者的关系 正常情况下，一个 topic 都会有多个分区 。 所以发送者在发送消息的时候 需要选择一个目标分区。\n比较常见的有三种 :\n轮询 : 一个分区发送一次\n随机 : 随机挑选一个\nhash : 根据 消息中的 key 来筛选一个 目标分区\n分区和消息有序性 Kafka 中的消息有序性 保证是以分区为单位的 。 也就是说 一个分区内的消息是有序的\n因此如果要做到 全局有序, 那么只能有一个分区\n如果要做到 业务有序, 需要保证业务的消息都丢在同一个分区里面\n分区与消费者组、消费者的关系 一个消费者组可以认为是关心这个 topic 的一个也无妨\n同一个消费者组里面，一个分区最多只能有一个消费者。\n一个分区最多有一个消费者 :\n如果一个 topic 有 N 个 分区，那么同一个消费者组最多有 N 个消费者\n如果消费者性能很差，那么并不能通过 无限增加消费者来提高速率\n消息队列相关问题 Q : 一个分区只有一个消费者，那么怎么做到 两个业务消费同一条信息。 例如账户更新发送了一个 MQ, 需要 计算服务 和 聚合服务共同消费这次改动\n如果两个不同的服务需要消费同一条消息，需要属于不同的 消费者组，从而能够从 同一个 topic 获取相同的信息进行消费\nQ : 死信、消息堆积\n死信 : 消费多次失败，无法被正常处理的消息，会单独丢进一个 死信队列 DLQ 。 目的是为了把坏消息 隔离出来，避免反复重试拖垮消费者 。\n消息堆积 : 生产速度大于消费速度 导致 topic 的消息越堆越多 。\n面试题 有没有用过 kafka ？ 用来解决什么问题 用过。 对于 点赞、阅读业务的时候，需要将用户点击 和 数据库操作进行解耦.\n在用户获取文章接口的时候，会进行一次数据库自增操作，这会使得接口变慢。 如果考虑使用 go 异步出来 。那么每次点击都会产生一个 goroutine 不安全。并且希望能够做到 自动聚合一部分相同业务的内容增量，所以考虑使用 kafka\n什么是 broker , broker 和 分区的关系是什么 broker 是一个逻辑上的概念，可以认为是一个 MQ 的进程 。 一个 broker 拥有一个主分区和多个从分区 。\n什么是 topic , topic 和 分区的关系是什么 一个 Broker 可托管多个 Topic 的多个分区（Leader / Follower 都有）。应说：分区副本尽量打散到不同 Broker；同一分区的 Leader 与 Follower 不在同一 Broker。\nKafka 中一个分区可以有多个消费者吗 同一消费者组内，一个分区同一时刻只被一个消费者消费；不同组可并行消费同一分区。Q4 按现有答案答，追问就会挂。\n消息积压了怎么办 ？\n增加 topic\n异步消费\n批量消费\n怎么保证消息的顺序 ？ 怎么保证全局有序 ？ 怎么保证业务有序。？\nkafka 支持 分区上的有序性，一个分区是消息是有序的。\n全局有序性需要保证所有消息都发送到同一个分区\n业务有序性需要保证同一个业务都发送到同一个分区\n可以使用 hash + biz-key 来选择一个目标分区进行发送\n","date":"2026-07-29T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/kafka-0x01/","title":"MQ kafka"},{"content":"Saram 入门 发送消息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func TestProducer(t *testing.T) { cfg := sarama.NewConfig() cfg.Producer.Return.Successes = true producer, err := sarama.NewSyncProducer(addrs, cfg) assert.NoError(t, err) p, offset, err := producer.SendMessage(\u0026amp;sarama.ProducerMessage{ Topic: \u0026#34;test_topic\u0026#34;, Value: sarama.StringEncoder(\u0026#34;hello,这是一条消息\u0026#34;), Headers: []sarama.RecordHeader{ {Key: []byte(\u0026#34;header1\u0026#34;), Value: []byte(\u0026#34;header1_value\u0026#34;)}, }, Metadata: map[string]any{\u0026#34;metadata1\u0026#34;: \u0026#34;metadata_value1\u0026#34;}, }) assert.NoError(t, err) t.Log(p, offset) } saram 指定分区 我们观察上面的内容，发现消息发送在来 partition : 0 上面\n我们可以看到 partitioner 支持\nrandom : 随机挑一个\nRoundRobin : 轮询\nHashkey : 根据 key 的哈希值塞选一个\nManual : 根据 Message 中的 partition 来选择\n异步发送 saram 支持 aysnc 和 sync 两种发送 其中支持异步发送 async\n初始化 producer 后，从 producer 获取 input 的 channel 并且发送一个消息 。 通过 select 监听 success 和 error\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func TestAsyncProducer(t *testing.T) { cfg := sarama.NewConfig() cfg.Producer.Return.Successes = true cfg.Producer.Return.Errors = true producer, err := sarama.NewAsyncProducer(addrs, cfg) assert.NoError(t, err) msgCh := producer.Input() msgCh \u0026lt;- \u0026amp;sarama.ProducerMessage{ Topic: \u0026#34;test_topic\u0026#34;, Value: sarama.StringEncoder(\u0026#34;hello,这是一条异步消息\u0026#34;), // 会在 producer 和 consumer 之间传递 Headers: []sarama.RecordHeader{ {Key: []byte(\u0026#34;header1\u0026#34;), Value: []byte(\u0026#34;header1_value\u0026#34;)}, }, Metadata: map[string]any{\u0026#34;metadata1\u0026#34;: \u0026#34;metadata_value1\u0026#34;}, } // 在实践中，一般是开另外一个 goroutine 来处理结果的 select { case err := \u0026lt;-producer.Errors(): // 这边是出错了 val, _ := err.Msg.Value.Encode() t.Log(err.Err, string(val)) case msg := \u0026lt;-producer.Successes(): // 这边是成功了 val, _ := msg.Value.Encode() t.Log(\u0026#34;成功了\u0026#34;, string(val)) } } 指定 acks 生产者在发送数据的时候 需要设置一个关键参数 ack\n0 ; 客户端发送一次，不需要服务端的确认\n1 ; 客户端发送，并且服务端写入主分区\n-1 ; 客户端发送，并且需要服务端同步到所有的 ISR 上 。(ISR 即 所有跟上主分区的从分区)\n1 2 3 4 5 6 7 8 9 10 11 type RequiredAcks int16 const ( // NoResponse doesn\u0026#39;t send any response, the TCP ACK is all you get. NoResponse RequiredAcks = 0 // WaitForLocal waits for only the local commit to succeed before responding. WaitForLocal RequiredAcks = 1 // WaitForAll waits for all in-sync replicas to commit before responding. // The minimum number of in-sync replicas is configured on the broker via // the `min.insync.replicas` configuration key. WaitForAll RequiredAcks = -1 ) ConsumerGroupHandler 业务方通过指定 consumer_group 和 topic 进行消费处理。 消费 interface 需要实现 setup, cleanup,consumeclaim\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func TestConsumer(t *testing.T) { cfg := sarama.NewConfig() cg, err := sarama.NewConsumerGroup(addrs,\u0026#34;test_group\u0026#34;, cfg) assert.NoError(t, err) //... ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() //... err = cg.Consume(ctx,[]string{\u0026#34;test_topic\u0026#34;}, \u0026amp;ConsumerHandler{}) assert.NoError(t, err) } // ... type ConsumerGroupHandler interface { // Setup is run at the beginning of a new session, before ConsumeClaim. Setup(ConsumerGroupSession) error // Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited // but before the offsets are committed for the very last time. Cleanup(ConsumerGroupSession) error // ConsumeClaim must start a consumer loop of ConsumerGroupClaim\u0026#39;s Messages(). // Once the Messages() channel is closed, the Handler must finish its processing // loop and exit. Handlers should also return when ConsumerGroupSession.Context() // is done; Messages() alone can block while the partition consumer is retrying // (e.g. after a broker disconnect). See examples/consumergroup. ConsumeClaim(ConsumerGroupSession, ConsumerGroupClaim) error } 异步消费 批量提交 kafka 要求消费后的数据必须提交 。如果不提交，在下一次重启之后 会出现重复提交的情况\n下面是一个异步消费的写法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func (c *ConsumerHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { ch := claim.Messages() batchSize := 10 for { var eg errgroup.Group msgs := make([]*sarama.ConsumerMessage, 0, batchSize) ctx, cancel := context.WithTimeout(context.Background(), time.Second) done := false for i := 0; i \u0026lt; batchSize \u0026amp;\u0026amp; !done; i++ { select { case \u0026lt;-ctx.Done(): // 这一批次已经超时了， // 或者，整个 consumer 被关闭了 // 不再尝试凑够一批了 done = true case msg, ok := \u0026lt;-ch: if !ok { cancel() // channel 被关闭了 return nil } msgs = append(msgs, msg) eg.Go(func() error { log.Println(\u0026#34;offset\u0026#34;, msg.Offset) // 标记为消费成功 time.Sleep(time.Second * 3) return nil }) } } err := eg.Wait() if err == nil { // 这边就要都提交了 for _, msg := range msgs { session.MarkMessage(msg, \u0026#34;\u0026#34;) } } else { // 这里可以考虑重试，也可以在具体的业务逻辑里面重试 // 也就是 eg.Go 里面重试 } cancel() } } 面试题 kafka 中 producer 中的 ack 有哪些取值 ？ 分别是什么含义？你用了哪些 0, -1, 1\n0 表示客户端发送之后 不需要服务端验证\n1 表示客户端发送之后 并且同步到服务端主分区\n-1 表示客户端发送之后 并且同步到所有 ISR 上\nKafka 中 ISR 是什么意思 ？ 所有跟得上主分区节奏的从分区\n总分：58 / 100 按「2 年经验、Golang + Kafka」面试标准看：能证明你用过 Sarama，覆盖了发送/分区/acks/消费组的入门面，但深度、完整度和可追问的生产经验都偏弱，深挖很容易露怯。\n优点 有可运行的 Sarama 代码：Sync / Async Producer、ConsumerGroupHandler，比纯背概念强，能证明动手过。 Producer 关键点抓得对：Partitioner（Random / RoundRobin / Hash / Manual）和 acks（0 / 1 / -1）是常考项。 消费侧有「批量 + 提交」意识：知道要 MarkMessage，也提到失败可重试，方向对。 结构适合当笔记骨架：从发消息 → 分区 → 异步 → acks → 消费，学习路径清晰。 缺点 面试题太薄：只有 2 道，且第二题「ISR」写成「解咒」，表述不严谨，面试官一追问就挂。 缺少语义与可靠性主线：至少一次 / 至多一次 / 精确一次、幂等 Producer、事务、重平衡、Consumer Lag、死信、重试，几乎没写。 示例代码有明显硬伤（面试官很爱抠）： eg.Go 里闭包捕获循环变量 msg，Go 经典坑； 没用 session.Context()，rebalance / 关闭时可能卡死； 只 MarkMessage、未说明 auto-commit / 手动 Commit 策略； 批次超时 1s、业务 Sleep 3s，逻辑自相矛盾。 「你用了哪些」没答完：acks 题只背取值，没结合业务说选了什么、为什么、出过什么问题。 表达与细节粗糙：Saram/Sarma、aysnc、塞选等；acks=-1 与 min.insync.replicas 的关系没讲清。 建议（按优先级） 优先级 建议 P0 把面试题扩到 15～20 道：acks+ISR、分区与顺序、消费组重平衡、offset 提交、至少一次如何做幂等、Sync vs Async 选型、Lag 排查 P0 修掉消费示例：闭包、session.Context()、失败不提交/重试/死信策略写清楚 P1 每道题加「项目经历」：业务场景、选型原因、踩过的坑（重复消费、消息丢失、rebalance 卡住） P1 补齐：幂等 Producer、enable.idempotence、批量参数（linger/batch.size）、压缩、监控指标 P2 对照官方注释精修术语；和 kafka_0x01 基础篇打通，形成「原理 → Sarama → 生产落地」一条线 分项参考 维度 得分 说明 Producer 基础 70 acks / partitioner / sync·async 有，深度不够 Consumer 基础 55 知道接口，提交与生命周期不扎实 可靠性语义 35 面试高频区几乎空白 代码可追问性 45 能演示，但经不起抠细节 面试题准备 30 题量与质量都不足 表达与准确度 50 笔误和概念表述拖后腿 面试官结论：当前水平大约是「跟过教程、能写 Demo」；以 2 年经验去面中级岗，过 HR/浅技术面有希望，过深挖 Kafka 生产问题的技术面风险较大。把可靠性语义 + 修代码坑 + 项目故事补上，目标可冲到 75～80。\n","date":"2026-07-29T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/sarma-kafka-0x01/","title":"Sarma Kafka"},{"content":"简历 preview 简历上 :\n设计了一个 热榜\n需求分析 :\n一些问题 ：\n什么样的才算是热点 ？\n如何计算热点 ？\n热点必然带来高并发，怎么保证性能\n如果热点功能崩溃了，怎么降低对整个系统的影响\n算法模型 Hacknews 模型 Score = (p - 1) / (T + 2)^G\n模型认为 : 得票数最重要，而后热度随着时间衰减\nReddit 模型\n数据计算 考虑一点 : 是否需要 实时计算\n实时计算的难点 要扫描全表，找出所有帖子的点赞数和发表时间\n计算每个帖子的 score 并且全局排序\n难点在于 : 全表扫描 + 全局排序\n异步计算 由于全表计算的困难，我们考虑使用异步定时计算\n解决方案包括 :\n每隔一段时间就计算一次热榜\n在异步的情况下，计算的时间可以比较长，但是依旧不能太长\n在这个基础上考虑\n怎么设计缓存，保证有极好的查询性能\n怎么保证可用性，保证在任何情况下都能拿到热榜数据\n计算热榜的算法实现 从数据库中拉取一批文章（batchSize），再找到对应的 点赞数，计算 score。\n使用一个数据结构来维持住 score 前 100 的数据。如果 该批次中有 score 比已有的前 100 的还要大，那么就从 数据结构中淘汰热度最低的。\n加入更高 score 的。\n全部数据计算完毕之后，数据结构中维护的就是热度前 100 的\n将这些数据装入 Redis 缓存\n这里使用的数据结构可以使用已经封装好的优先队列 也就是小根堆\n放入缓存有两个关键点 :\n热榜数据不需要保存在数据库中。 一些公司会把这些内容保存到数据库里面用于大数据分析\n在 Redis 的过期时间，要比计算间隔长，最好留有足够的重试时间 。\n查询接口的处理 使用 atomicx.Value 进行本地缓存\n在操作的时候优先操作本地缓存，如果本地缓存找不到数据，再去查找 redis 缓存 。然后回写 本地缓存\n可用性问题 一个降级策略，对于 数据库和Redis 都崩崩溃的情况下 。 如果本地缓存也失败了\n那么可以增加一个 ForceGet 的流程，即 不管本地缓存是否已经过期 我们都获取数据 。\n极致的缓存方案 在大多数时候，追求极致性能的缓存方案，差不多就是本地缓存 + Redis 缓存 + 数据库。\n那么：\n查找的时候，就要先查找本地缓存，再查找 Redis，最后查找数据库\n更新的时候，就要先更新数据库，再更新本地缓存，最后更新（或者删除）Redis。核心在于一点，本地缓存的操作几乎不可能失败\n高级的亮点在于：\n本地缓存可以预加载。也就是在启动的时候预加载，或者在快过期的时候，提前加载\n本地缓存可以用于容错。也就是如果 Redis 崩溃，这时候依旧可以使用本地缓存。例如，正常过期时间是三分钟，但是本地缓存会设置五分钟。如果数据已经超过了三分钟，那么会尝试刷新缓存，如果刷新失败，那么就继续使用这个已经“过期”的本地缓存。在部分场景下，可以考虑让本地缓存永不过期，同时异步任务刷新本地缓存。好处是可以在 Redis 或者 MySQL 崩溃的时候，依旧提供缓存服务。\n当下的问题 如果我们部署了多个实例，那么多个实例会同时执行这个计算热榜的任务。\n理论上来说 同一时刻计算的结果应该是一样的，但是大多数都不是同一时刻，这会导致 在实例1看上的 和 实例2看上的结果会有细微的差距\n因此我们需要考虑 控制任务只在一个节点上进行运行\n分布式锁方案 使用分布式锁来控制 goroutine 的计算方式\n因为我们知道任务的运行 timeout 时间。 所以不需要开启续约\n另外解锁失败的话，最多 r.Timeout 就会自动释放 也不需要担心\n但是上述方案会出现一个问题， 这个只能控制同一时刻 只有一个 goroutine 计算，但是控制不住 计算了一次之后别的机器就不要计算热榜了\n所以我们可以考虑\n在启动的时候拿到锁，而后不管几次都不会释放锁 分布式锁的具体实现如下\n定时任务每次运行 ，尝试拿分布式锁\n如果拿到 异步开启自动续约\n下一次正常运行，不需要额外拿锁\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 func (r *RankingJob) Run() error { r.localLock.Lock() defer r.localLock.Unlock() if r.lock == nil { // 说明你没拿到锁，你得试着拿锁 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 我可以设置一个比较短的过期时间 lock, err := r.client.Lock(ctx, r.key, r.timeout, \u0026amp;rlock.FixIntervalRetry{ Interval: time.Millisecond * 100, Max: 0, }, time.Second) if err != nil { // 这边没拿到锁，极大概率是别人持有了锁 return nil } r.lock = lock // 我怎么保证我这里，一直拿着这个锁？？？ go func() { r.localLock.Lock() defer r.localLock.Unlock() // 自动续约机制 err1 := lock.AutoRefresh(r.timeout/2, time.Second) // 这里说明退出了续约机制 // 续约失败了怎么办？ if err1 != nil { // 不怎么办 // 争取下一次，继续抢锁 r.l.Error(\u0026#34;续约失败\u0026#34;, logger.Error(err1.Error())) } r.lock = nil // lock.Unlock(ctx) }() } ctx, cancel := context.WithTimeout(context.Background(), r.timeout) defer cancel() return r.svc.TopN(ctx) } 基于 mysql 的分布式任务调度 基本思路就是\n在数据库中创建一张表， 里面是等待运行的定时任务\n所有的实例都试着从这个表里面 抢占 等待运行的任务 , 抢占到了就执行\n抢占但是崩溃 如果一个实例抢占了，但是还没执行完毕，直接崩溃了怎么办 ？\n引入 续约机制 , 实例0 要不断更新数据库的更新时间证明自己还活着\ntbd ","date":"2026-07-29T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/rank-0x01/","title":"热榜 detail"},{"content":"简历 preview 简历上 :\n实现了一个通用的 阅读、点赞、收藏 服务。使用 Kafka 进行解耦\n数据库表设计 :\n通用表设计 :\nid,biz,bizid,readCnt,CollectCnt,LikeCnt\n为 bizid 和 biz 创建一个联合唯一索引\n点赞表设计 :\nid,biz,bizid,uid,status\n为 bizid 和 biz和uid 创建一个联合唯一索引, 用 status进行软删除\n数据库处理 对于 阅读、点赞、收藏 操作都会涉及到一个 check-dothing 的并发问题\n即 : 当用户点赞的时候 文章是否已经点赞过了，如果点赞过需要+1，否则创建 ，这里统一采用 do on conflict update 处理\n不过需要注意的是 : update 的时候我们需要保证自增 所以需要使用 gorm.Expr 来获取数据库运行的值，而不是直接 select-update\n缓存处理 对于写入方，优先写入数据库，后写入redis\n对于查询方，优选查redis 然后再查数据库\n对于写入的过程，因为我们使用的 redis key 是一次缓存了 3个 like,read,collect 三个字段\n所以我们自增的时候使用的是 HINCRBY 指令，对于 key 不存在的情况下会有歧义 。所以使用 lua 控制了一下 仅只有 key 存在的时候才会自增\n消息队列 新增 统计事件 引入 kafka 给 阅读、点赞服务解耦\n在阅读文章服务，使用 kafka 异步发送，设置 10次每批，1s超时进行聚合发送消息\n消费端同样设置 10条每批，1s超时来进行组合消费\n每次消费 使用一个 map 进行聚合 同个biz+biz_id的内容，优化之前一次点赞就需要一次 insert 的 操作\n并且通过开启事务进行消费，防止 MVCC 中的 刷盘操作，将原先要进行10次的刷盘降低到1次。\n问题 一致性问题的处理 不处理\n从业务上来说，没有必要要求阅读数、点赞数或者收藏数量一定是严格准确的，因为即便不准确，也不会对用户 产生什么不好的影响。\n更进一步说，只有高并发的文章才会有并发问题。而高并发的文章，阅读数、点赞数或者收藏数本身就很多，你 少一点点无所谓。\n亮点 大量使用了事务，来保证在同时操作两个表的时候，保持住 ACID 语义。\n大量使用了 Upsert 语义 , 因为很多情况下我们都不知道数据库中是否已经存在数据\n大量使用 lua 脚本来保证缓存中的数据是正确的, 但是没有彻底解决缓存一致性的问题\n在查询接口中，只缓存总数数据，, 并不会缓存个人是否收藏或者点赞的数据。\n总分：62 / 100 按「2 年 Go 后端」面试标准看：方向对、有工程意识，但表达粗糙、关键点浅、经不起追问。面试里能撑 15–20 分钟开场，深入一追容易露怯。\n分项打分 维度 分数 说明 业务建模与表设计 14/20 通用计数表 + 点赞明细表思路正确，索引方向对 并发与数据正确性 12/20 知道 Upsert / Expr，但 check-then-act、事务边界讲不清 缓存设计 12/20 Cache-Aside + Lua 防空 key 自增有亮点，一致性论证弱 消息队列与解耦 13/20 Kafka 批量、聚合、减刷盘有工程味道 表达与面试叙事 6/10 口语化、错别字多，「大量使用」空洞，像笔记不像口述稿 亮点与深度 5/10 亮点空泛，缺少权衡、失败场景、可观测性 优点 表设计合理：biz + biz_id 抽象通用互动能力，点赞用 (biz, biz_id, uid) 唯一 + status 软删除，符合常见业务。 踩过真实坑：ON CONFLICT + gorm.Expr 自增、Redis HINCRBY 空 key 用 Lua 护栏，说明写过代码而不只是背概念。 有吞吐意识：Kafka 批量生产/消费、按 biz+biz_id 聚合、事务合并刷盘，对 2 年经验来说是加分项。 一致性取舍有业务理由：承认最终一致、高热内容允许误差，比死磕「强一致」更成熟——前提是能讲清边界。 缺点（面试官会盯的） 「不处理一致性」太绝对：面试官会问：DB 成功 Redis 失败？Kafka 重复消费？取消点赞与计数对不上？需要有策略（补偿、对账、幂等），不能只说「少一点点无所谓」。 亮点写法很伤：连续「大量使用事务/Upsert/Lua」像模板，没有场景、没有代价。事务多 ≠ 好，还可能拖慢吞吐。 技术细节易被打穿： 写库再写缓存：失败怎么回滚/补偿？ Lua「仅 key 存在才 incr」：key 不存在时计数会丢吗？靠查询回源还是异步重建？ Kafka 聚合：幂等键、乱序、部分失败重试怎么做？ 「防止 MVCC 刷盘」表述不严谨，容易被纠正。 缺个人态查询设计：只说不缓存「是否点赞」，但列表页如何批量查个人态？N+1？位图/本地缓存？没写。 表达与文档质量差：优选/check-dothing、front matter 的 xxxx、标点混乱，会降低「能讲清楚」的印象。 建议（按面试口述改） 改成 STAR 口述结构：背景（互动服务压力）→ 方案（表 + Kafka + Redis）→ 难点（空 key、重复消费）→ 结果（QPS/延迟/错误率，哪怕是压测数字）。 准备 3 个追问标准答： DB/Redis 不一致：异步对账 + 定时校准热点 key Kafka 至少一次：消费幂等（唯一键 + Upsert） 取消点赞：status 翻转 + 计数 GREATEST(cnt-1,0)，防负值 删掉「大量使用」，改成「在点赞写明细+改计数时用事务；计数更新用 Upsert；缓存用 Lua 保证 key 存在才 HINCRBY」。 补一张口述链路：读请求 → Redis Hash → miss 回源 DB 回填；写请求 → DB Upsert →（可选）发 Kafka → 消费者聚合写计数 → 更新缓存。 亮点换成可量化的 2–3 条：例如「批量聚合把热点文章写放大降到约 1/N」「Lua 避免幽灵 key」「个人态与总数分离降低缓存体积」。 面试官一句话结论 材料有「做过互动域」的骨架，适合当提纲；按现在写法直接上面试，深度和表达大概卡在 及格偏上。把一致性策略、幂等、失败路径和口述结构补齐，冲到 75–80 比较现实。\n","date":"2026-07-29T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/interactive-0x02/","title":"阅读、点赞、收藏 one"},{"content":"channel 语法 var ch chan T. 声明 channel\nmake(chan T) 和 make(chan T, size) 声 不带容量 和 带容量的 channel\nch \u0026lt;- data 发送消息到 channel 里面\nval := \u0026lt;- ch 从 channel 里面读取数据\nclose(ch) 关闭 channel\nchannel 的 close 问题 close 不是幂等的\n当被 close 之后\n继续写入数据，会 panic\n从 channcel 读取的数据 会读到 0 值 。 val, ok := \u0026lt;- ch ; ok = 0\n再次 close 会 panic\n在实践中坚持一个原则 : 谁创建的 channel 谁来关, 可以避免很多 channel 没有正确关闭的问题\nchannel Loop 我们可以使用 for range 进行读取 channel 的数据\n但是需要注意 close 如果不 close channel 会 panic . 因为接收方会一直接受数据，但是发送方没有数据了 所以会一直阻塞\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func LoopChannel() { ch := make(chan int) go func() { for i := 0; i \u0026lt; 3; i++ { ch \u0026lt;- i time.Sleep(time.Second) } close(ch) }() for val := range ch { fmt.Println(val) } fmt.Println(\u0026#34;done\u0026#34;) } channel \u0026amp; 阻塞 如果接受者读不到数据，就会阻塞\n如果发送者写不了消息，就会阻塞\n有缓冲和 无缓冲的 channel 无缓冲 Channel :\n容量: 大小为0，不能存储任何数据\n同步性：是同步的。发送者发送数据时，必须有接收者准备好接收，否则发送方会一直阻塞；反之亦然。\n作用：提供强同步保证，确保两个 goroutine 在某一时刻完成了数据交接\n缓冲 Channel ：\n容量：大小大于 0，可以存储指定数量的元素。\n同步性：是异步的。只要缓冲区没满，发送数据就可以立刻返回而不阻塞；只要缓冲区不空，读取数据就可以立刻返回值。\n作用：解耦生产者和消费者，能够平滑突发流量，提高程序的运行效率。\n有缓冲的写法 :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func BufferChannel() { ch := make(chan int, 3) defer func() { close(ch) }() for i := 1; i \u0026lt;= 3; i++ { ch \u0026lt;- i } for i := 1; i \u0026lt;= 3; i++ { val := \u0026lt;-ch fmt.Println(val) } } 无缓冲的写法 :\n要写进行接收，然后才可以进行发送\n并且必须同时处理\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func NoBufferChannel() { ch := make(chan int) defer func() { close(ch) }() for i := 1; i \u0026lt;= 3; i++ { go func() { val := \u0026lt;-ch fmt.Println(val) }() ch \u0026lt;- i } //for i := 1; i \u0026lt;= 3; i++ {// panic //\tval := \u0026lt;-ch //\tfmt.Println(val) //} } channel 与 select select-case 用于控制从不同的 channel 中读写数据 ，只运行一次\n语法\n1 2 3 4 5 select { case ch \u0026lt;- val: // 写入 case val := \u0026lt;- ch2 // 读取 default : } 每一个 case 都可以是读取或者是写入数据到 channel\n没有 default 分支的时候, select 就会阻塞\n如果有多个分支同时满足 会随机执行一个\ncase 后面不需要break\n应用 输出 0 - 100 使用 goroutine, 交替 输出 0 - 100\n写法如下 :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func Print100() { time := time2.Now() even := make(chan int) odd := make(chan int) wg := sync.WaitGroup{} wg.Add(2) go func() { for i := 0; i \u0026lt;= N; i += 2 { \u0026lt;-even fmt.Print(\u0026#34;p1:\u0026#34;, i, \u0026#34;\\n\u0026#34;) if i == N { wg.Done() close(odd) return } odd \u0026lt;- i } }() go func() { for i := 1; i \u0026lt;= N; i += 2 { \u0026lt;-odd fmt.Print(\u0026#34;p2:\u0026#34;, i, \u0026#34;\\n\u0026#34;) if i == N-1 { wg.Done() close(even) return } even \u0026lt;- i } }() even \u0026lt;- 0 wg.Wait() fmt.Println(\u0026#34;cost:\u0026#34;, time2.Now().Sub(time)) } 我们也可以使用一个 channel 进行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func print100() { time := time2.Now() ch := make(chan int) wg := sync.WaitGroup{} wg.Add(1) go func() { for { val, ok := \u0026lt;-ch if !ok { return } fmt.Print(\u0026#34;p1:\u0026#34;, val, \u0026#34;\\n\u0026#34;) if val == N { wg.Done() close(ch) return } val += 1 ch \u0026lt;- val } }() go func() { for { val, ok := \u0026lt;-ch if !ok { return } fmt.Print(\u0026#34;p2:\u0026#34;, val, \u0026#34;\\n\u0026#34;) if val == N { wg.Done() close(ch) return } val += 1 ch \u0026lt;- val } }() ch \u0026lt;- 0 wg.Wait() fmt.Println(\u0026#34;time:\u0026#34;, time2.Now().Sub(time)) } 在 1e7 的数据下 :\n2 个协程 : cost: 9.692623833s\n顺序遍历 : cost : 6.417589125s\n失败的写法 我这里使用使用 for 直接进行输出，但是这里没办法保证是 交替 输出\n因为我们这里只是保证了能够存入 channel 而没办法 fmt.Print\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func NewPrinter2() *Printer2 { p := \u0026amp;Printer2{ ch: make(chan int), } return p } func (p *Printer2) Print() { for val := range p.ch { fmt.Println(\u0026#34;p2:\u0026#34;, val) } } func SelectChange100() { p1 := NewPrinter1() p2 := NewPrinter2() defer func() { close(p1.ch) close(p2.ch) }() go p1.Print() go p2.Print() for i := 1; i \u0026lt; 100; i++ { if i%2 == 1 { p1.ch \u0026lt;- i } else { p2.ch \u0026lt;- i } } } ","date":"2026-07-27T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/channel_0x01/","title":"channel"},{"content":"简历 preview 简历上 :\n设计并实现了一个高并发的 阅读、点赞、收藏服务\n需求分析 :\n阅读数，每次打开都会加1，但是关掉不会 -1\n点赞和收藏都有取消功能\n技术分析 :\n从使用场景考虑 :\n当我们点开一篇文章的时候，会看到这篇文章的阅读、点赞和收藏数量 。 从这里考虑 全部都写在一个服务上会更好处理\n用户可以单独查看自己收藏的文章 。 这个角度考虑 收藏单独做成一个服务更合适\n从性能的角度考虑 :\n拆分成三个服务，三个服务可以独立治理，对于分散读写压力有好处 从研发效率角度来看 :\n拆分三个服务对应代码就有三套，研发效率不高 因此最终选择研发合并在一起\n阅读计数 业务处理 : 当点击文章服务的 GetPubslishId 的时候 进行增加阅读计数\n先操作数据库 再更新缓存 如果更新缓存失败会导致不一致，但是其实不用管他，因为阅读数不准确从业务角度考虑没什么问题 数据库操作 : 同样需要考虑是否一个情况\n如果新帖子没计数，那么应该 create\n否则的话 update\n因此这边也是 upsert 的语义 . 另外我们不能给使用取出计数然后+1 的做法会有并发问题 。 因此使用 gorm.Expr(read_cnt+1)\n缓存处理 缓存的设计也会有和数据库一样的问题\n如果 Redis key 不存在 和 redis 存在 的情况 因此需要引入 LUA 脚本解决这种情况 点赞 业务处理 需要支持\n点赞\n统计点赞数量\n某个用户是否点赞过，并且用户可以取消点赞\n对于点赞计数，同样和阅读处理一样 。\n点赞和阅读不同，需要支持取消点赞， 因此需要额外维护一张用户点赞表，聚合 user_id.\n考虑软删除策略，因此需要额外加入一个 status 字段 。 因为取消点赞和点赞是一个很频繁的业务，如果使用硬删除，数据库会有很多 gap\n收藏 收藏的处理同上述问题\n其他 查询接口 对于 阅读、点赞和收藏 的查询接口 这三个是互相独立的，所以可以考虑使用 goroutine 进行异步的获取，使用 error_group 进行等待\n缓存一致性 对于 更新 DB 和 数据库操作会存在 缓存一致性问题 如下\n考虑业务情况\n阅读数、点赞数 或者是 收藏数 并不一定要严格准确，即使不准确也不会对用户产生什么不好的影响\n只有高并发的文章才会有并发问题，而高并发的文章，阅读数、收藏数、点赞数本来就很多，少一点也无所谓\nKafka kafka 的基本概念\n生产者 、消费者、broker、topic 、partition\nbroker 可以认为是中间人，是一个逻辑概念 。 broker 可以认为是一个消息队列进程 。\n可以简单的认为 一个 业务就是一个 topic . 一个 topic 有多个分区 。\n当我们讨论 topic 有多少个分区的时候，我们主要讨论的是有多少个主分区\n点赞解耦 我们上面设计的点赞系统是每次请求都进行一次点赞操作，每次都会访问我们的 DB\n因此我们需要引入 Kafka 来进行异步的解耦\n在调用 调用获取文章相信的时候 发送对应的阅读事件\nmQ消费者端，使用 ctxTimeout + channel 控制批量消费\n消费插入数据库中使用map对biz进行一次聚合。 然后插入数据库 ，使用事务进行插入 。\n主要优化\n多个业务读取文章 是多行sql，现在改成 N 行 ,n \u0026lt;= 多行sql\n开启事务，进行一次提交。 少了 MVCC 等操作。\n亮点 批量消费 和 批量生产 面试题 有没有用过 kafka ？ 用来解决什么问题\n你为什么要使用 MQ，使用MQ 等好处是什么 。 异步，解耦，消峰\n介绍一下 kafka :\n什么是 broker , broker 和 分区的关系是什么\n什么是 topic , topic 和 分区的关系是什么\nkafka 中 producer 中的 ack 有哪些取值 ？ 分别是什么含义？你用了哪些\nKafka 中 ISR 是什么意思 ？\nKafka 中一个分区可以有多个消费者吗\n消息积压了怎么半 ？\n增加 topic 异步消费 批量消费 怎么保证消息的顺序 ？ 怎么保证全局有序 ？ 怎么保证业务有序。？\n全局有序 : 只使用一个分区 业务有序 : 使用 hash类的算法制定生产者发送消息的分区 ","date":"2026-07-27T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/interactive-0x01/","title":"阅读、点赞、收藏 detail"},{"content":"限流 单机限流 令牌桶算法 算法思路设计\n设计一个带有容量的桶\n给定速率的进行生成可用令牌\n每次请求消耗对应的令牌，如果没有令牌可用则失败\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type TokenBucket struct { rate float64 // 令牌生成速率（每秒生成的令牌数） capacity float64 // 令牌桶容量 tokens float64 // 当前令牌数 lastRefill time.Time // 上次填充令牌的时间 mutex sync.Mutex // 保护令牌桶的互斥锁 } func (tb *TokenBucket) Allow() bool { tb.mutex.Lock() defer tb.mutex.Unlock() now := time.Now() elapsed := now.Sub(tb.lastRefill).Seconds() tb.lastRefill = now // 计算新增的令牌数 tb.tokens += elapsed * tb.rate if tb.tokens \u0026gt; tb.capacity { tb.tokens = tb.capacity } if tb.tokens \u0026gt;= 1 { tb.tokens -= 1 return true } return false } 漏桶算法 漏桶算法是令牌桶算法的反向思路看着 , 令牌桶是生成一定容量的令牌\n漏桶算法则是 以固定速度进行消耗令牌(漏水), 如果请求过来发现水溢出了则禁止访问\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type LeakyBucket struct { rate float64 // 漏出速率（每秒处理请求数） capacity float64 // 桶容量 water float64 // 当前水量（排队请求数） lastLeak time.Time // 上次漏水时间 mutex sync.Mutex // 保护内部状态 } func (lb *LeakyBucket) Allow() bool { lb.mutex.Lock() defer lb.mutex.Unlock() now := time.Now() elapsed := now.Sub(lb.lastLeak).Seconds() lb.lastLeak = now // 按时间漏水 lb.water -= elapsed * lb.rate if lb.water \u0026lt; 0 { lb.water = 0 } if lb.water+1 \u0026gt; lb.capacity { return false } lb.water++ return true } 固定窗口限流 顾名思义\n我们维护一个固定时间的窗口, 在此期间 如果访问请求超过窗口大小则失效 否则成功 。 如果窗口超出设置的窗口时间 则清空窗口\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type FixedWindow struct { limit int64 // 窗口内最大请求数 window time.Duration // 窗口大小 count int64 // 当前窗口计数 windowStart time.Time // 当前窗口起始时间 mutex sync.Mutex // 保护内部状态 } func (fw *FixedWindow) Allow() bool { fw.mutex.Lock() defer fw.mutex.Unlock() now := time.Now() if now.Sub(fw.windowStart) \u0026gt;= fw.window { fw.count = 0 fw.windowStart = now } if fw.count \u0026gt;= fw.limit { return false } fw.count++ return true } 滑动窗口限流 同滑动窗口，无非是动态维护了一下窗口的请求，不额外补充\n算法对比 算法 文件 特点 令牌桶 token_bucket.go 恒定投放令牌，允许突发 漏桶 leaky_bucket.go 恒定漏出，输出更平滑，不允许突发 固定窗口 fixed_window.go 实现简单，窗口边界可能双倍突发 滑动窗口 sliding_window.go 日志法精确统计，避免边界问题 基于 Redis 限流 对于多实例部署的应用如果要做到 单IP 限流, 需要考虑使用 Redis\n根据 Redis 单机单线程的特性可以使用 lua + 滑动窗口进行处理\n算法 lua脚本 :\n计算 min 即窗口的左边界\n然后根据 ZREMRANGEBYSCORE 删掉左边的请求\n如果大于阈值，那么返回限流\n否则增加计数, 并且设置过期时间\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 -- 基于 Redis ZSET 的滑动窗口限流脚本。 -- KEYS[1]: 限流对象 key -- ARGV[1]: 窗口大小（毫秒） -- ARGV[2]: 阈值 -- ARGV[3]: 当前时间戳（毫秒） -- 返回 \u0026#34;true\u0026#34; 表示应限流，\u0026#34;false\u0026#34; 表示放行。 local key = KEYS[1] local window = tonumber(ARGV[1]) local threshold = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local min = now - window redis.call(\u0026#39;ZREMRANGEBYSCORE\u0026#39;, key, \u0026#39;-inf\u0026#39;, min) local cnt = redis.call(\u0026#39;ZCOUNT\u0026#39;, key, \u0026#39;-inf\u0026#39;, \u0026#39;+inf\u0026#39;) if cnt \u0026gt;= threshold then return \u0026#34;true\u0026#34; else -- member 加入 cnt，避免同一毫秒内多次请求互相覆盖 redis.call(\u0026#39;ZADD\u0026#39;, key, now, tostring(now) .. \u0026#39;-\u0026#39; .. tostring(cnt)) redis.call(\u0026#39;PEXPIRE\u0026#39;, key, window) return \u0026#34;false\u0026#34; end Go这边的处理\n1 2 3 4 5 6 7 8 //go:embed slide_window.lua var slideWindowLua string func (r *RedisSlideWindowLimiter) Limited(ctx context.Context, key string) (bool, error) { return r.cmd.Eval(ctx, slideWindowLua, []string{key}, r.interval.Milliseconds(), r.rate, time.Now().UnixMilli()).Bool() } 熔断 熔断的理解 :\n当一个服务已经出现问题的时候, 需要控制住访问该服务的请求。 对于 Gin 层面, 我们要做到的就是, 对于下游一个服务崩溃了，能够在请求发送之前就快速返回错误\n详细的说就是 :\n例如账户服务如果过载了, 数据库请求很慢. 为了防止大量请求 导致请求到数据库上从而崩溃. 需要进行熔断 更简单的就是 :\n对于 手机号登陆的功能 , 如果腾讯云失连续超时，那么需要熔断腾讯云30s 防止多次请求连续超时 参考 :\nhttps://www.liuvv.com/p/2ca9d630.html\n实现 这里不详细说出具体思路, 对于 Gin 层面的熔断\n我们可以通过使用 middleware 进行判断, 在请求访问之前 判断是否允许访问, 如果不可用则 abort()\n否则 next() 进入下一个 中间件\n记入我们请求成功和失败的次数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func CircuitBreakerMiddleware(cb *CircuitBreaker, opts ...CircuitBreakerOption) gin.HandlerFunc { o := \u0026amp;circuitBreakerOptions{ isFailure: defaultIsFailure, fallback: defaultFallback, } for _, opt := range opts { opt(o) } return func(c *gin.Context) { generation, err := cb.Allow() if err != nil { o.fallback(c) c.Abort() return } c.Next() if o.isFailure(c) { cb.Failure(generation) return } cb.Success(generation) } } 降级 Gin 层面的限流确实没看懂，不过可以就 短信服务 谈一下限流\n对于 默认使用腾讯云的 短信服务，如果发生了熔断，那么可以考虑使用 阿里云进行 backup 这个过程就是降级\n同样的\n对于热榜服务，如果热榜服务失效。 可以考虑去使用历史的热榜来代替现有的热榜\n","date":"2026-07-25T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/web-control-0x01/","title":"Web 治理"},{"content":"简历 preview 简历上 :\n设计并实现了一个 发帖 服务\n需求分析 :\n帖子状态分析 : 未发表 -\u0026gt; 发表 -\u0026gt; 被删除 如果做到一边修改一边可查看\n分开存储, 设计一个制作库 一个线上库 制作库和线上库的事务问题 我们从 service 的角度考虑，其实并不知道 repository 层面存储的是什么数据 。 所以就别谈开启事务了\n因此我们能做的最好的一点就是 重试 . 对于创作者来说就算线上库失败了其实影响也不大\n如果看到发表失败，他可以考虑重试\n读者完全看不到这个帖子\n如果是站在 repository 的角度考虑 就要分 同库不同表 和 不同库 的问题\n对于 不同库 的情况， 可以考虑 分步更新 find - update -\u0026gt; upsert\n对于 同库不同表 可以采用数据库事务进行处理\n建表\nMongodb 雪花算法 因为 系统内增加了 MongoDB 但是我们在 repo 层定义的是 int64 的 ID，因此需要使用雪花算法\nOSS 通过 OSS 来维护 content\n查询接口 作者的查询接口 :\n列表接口 : 看到自己发表的所有文章, 这是一个分页接口\n详情接口 : 当作者选中了某篇文章的时候，能看到文章的全部内容\n读者的查询接口 :\n搜索接口 : 接入搜索模块之后设计\n推荐接口 : 再设计\n阅读文章接口 : 当选中某篇文章的时候, 可以看到文章的全部内容\n分页接口处理 分页接口的三种形态 :\nOffset + Limit 的形式\n之直接定义一个 Page 的形式 。 前端传递了 page 之后需要后端计算出对应的 offset 和 limit\n直接定义一个 Cursor 的形式 。 每次查询后，后端控制返回一个 Cursor,让前端使用 。\n缓存设计 PreCache 缓存第一页 大部分没有什么人会缓存分页结果 。 但是很多业务场景，用户只看列表内容的第一页，很少会往后翻 。 因此可以考虑 只缓存第一页\n实现逻辑 :\n查询接口的时候。优先查缓存 然后再查数据库。 判断 offset == 0 和 limit \u0026lt; 100 则走缓存\n更新/新增操作 : 在对于新写文章、编辑文字、发表文章都要清楚相关的 首页缓存\n缓存第第一条数据 对于创作者查看文章详情接口，很大概率。创作者在拿到列表之后，只会访问第一条数据 。 所以我们可以考虑进行缓存预加载 。只缓存第一条数据\n特点 ：\n因为伴随着预测性质，所以这个过期时间设置的很短 。避免内存的开销 改进点 :\n不缓存大文档 。 避免浪费内存 面试 什么是 TDD\nTDD 和 DDD 的关系\nDDD 适合战略规划，TDD 适合落地具体功能。 TDD 有什么好处\n理清思路、查漏补缺、方便重构、测试完善。 有没有用过 MongoDB ？ 用来解决什么问题 ？\n为什么要用 Mongodb 用 mysql 行不行\n解释一下 mongodb 中集合和文档的关系\n如何设计 Mongobd 上的索引 ESR 原则\n如何数据存储在 MongoDB上 怎么生成 ID\nMongoDB 是否支持事务，是否支持跨文档事务，是否支持 ACID\n缓存方案 : 淘汰策略\nLRU(最近最少使用) , LFU (最近不频繁使用)\n优先淘汰普通创作者的数据，尽可能保留大 V 的数据。 LFU 能够达到近似效果 ，LRU 效果会差一些\n优先淘汰大对象\n优先淘汰小对象\n","date":"2026-07-25T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/publish-service-0x01/","title":"发帖服务 detail"},{"content":"简历 preview 简历上 : 设计并实现了一个 文章 服务\n考虑文章的业务属性, 创作者支持保存草稿和发布文章。 设计了 线上库 和 制作库 两个库表设计 。 采用 ESR 原则，考虑会有 用户查看自己的文章，用户查看某篇文章详情,以及热榜查看对应时间的文章 。设定过了 _create,auth_id . 这两个索引 文章服务支持 制作库 保存草稿, 设置隐私文章, 发布制作库内容, 按照 ID 查看文章,或许作者文章列表, 获取发布文章列表 支持 MongoDB 和 OSS 的扩展\n设计合理的缓存服务\n考虑并发场景\n细节 数据库索引设计 主要涉及到的业务有 : 作者查看当前文章，和最近一周的热榜设计\n对于作者查看当前文章预计查询是 select from where auth_id = '' and statuts = '' 因此设计了 auth_id 的索引\n对于最近一周的热榜设计，因为热榜计算是一个频繁的操作，预计查询是 select from where _create \u0026gt;= now-7week 因此设计了 _create 的索引\n数据库并发处理 隐私文章功能 :\n需要同步改动 制作库和 线上库的 状态 。\n系统设计的时候选择的是同库不同表的流程因此采用数据库事务进行一致性处理，对于更新失败的操作进行回滚 。从业务角度考虑，就算更新失败了也只是让用户多重试几次。\n如果是不同库的情况，可以考虑在接口层面增加多次重试。\n发布功能 :\n对于发布功能, 线上库存在两个状态,线上没有文章需要create,线上有文章需要 update\n防止数据库层面的同一篇文章多次竞态问题使用upsert进行操作。\n缓存设计 对于列表接口 :\n根据习惯习惯缓存第一页内容,大部分用户只会访问第一页的内容。 细节: 缓存之前需要清空文章的content 节省内存资源。\n在更新，删除，隐私的接口增加对应的删除缓存的操作 。\n对于查看detail接口 :\n根据用户习惯，往往只会查看列表第一篇文章的内容。 因此考虑在拉取列表后，缓存第一篇文章的内容。 仅在 content \u0026lt; 1M 的时候进行缓存。 缓存设置了一个很短的过期时间，防止浪费。\n对于发布接口 :\n考虑作者发布文章后就会有人阅读，因此在发布的时候就设置缓存。 其他细节 MongoDB 尝试接入 MongoDB 进行存储文章数据。 接入 MongoDB 之后发现，MongoDB 返回的 ID 是 String 类型 。破坏了原先接口的定义。 因此引入了 雪花算法 进行对其接口\nOSS 接入 OSS，在发布文章之后，对于线上库的文章内容进行优化。线上库不保存文章内容，文章内容全部存储在 OSS 中\n对接阅读统计服务 使用 channel + ctxWithTimeout 进行批量发送阅读统计给 MQ。 供下游消费\n作为 Golang 面试官，我按「2 年经验、能独立负责一个业务模块」的标准，对这份 发帖/文章服务 面试准备稿打分如下。\n综合评分：68 / 100 维度 得分 说明 业务理解与架构设计 18/25 制作库/线上库分离思路正确，有业务驱动意识 数据库与索引 12/20 有索引意识，但 ESR 理解偏浅，热榜查询设计不完整 并发与一致性 13/20 提到事务、upsert、重试，但缺少边界与失败场景 缓存设计 14/20 第一页缓存、去 content、失效策略有亮点，缺 key 设计与一致性细节 工程化与 Golang 表达 6/15 几乎无 Go 实现细节，MQ 只一笔带过 面试表达与完整性 5/10 结构尚可，错别字多，placeholder 未填，缺 Q\u0026amp;A 演练 结论：作为 2 年经验候选人，项目经历是真实的、方向也对，但这份准备稿更像「项目笔记摘要」，还不足以支撑 45–60 分钟的深度技术面。能过初筛，若在索引、一致性、缓存、Go 并发等点被追问，容易失分。\n优点 1. 业务建模有思考 「制作库 + 线上库」分离是内容类系统的常见做法，能解释：\n草稿编辑 vs 线上可读 发布时从制作库同步到线上库 这比只会说「CRUD 增删改查」高一档。\n2. 缓存策略贴近真实访问 只缓存第一页 —— 符合大部分用户行为 列表缓存去掉 content —— 说明考虑过内存成本 写操作删缓存 —— 知道 Cache Aside 的基本思路 详情预缓存第一条 + 短 TTL + \u0026lt; 1M 限制 —— 有「预测性缓存」意识 3. 并发场景有触点 发布用 upsert 防竞态 隐私状态双库同步用事务 不同库场景考虑接口层重试 说明不是只写 happy path。\n4. 扩展能力有落地 MongoDB 接入 + 雪花 ID 对齐 int64 接口 OSS 存正文、DB 存元数据 channel + ctxWithTimeout 批量发 MQ 有「存储分层 + 异步解耦」的工程感。\n缺点 1. ESR 原则理解不准确（高风险扣分点） 文档写「采用 ESR 原则，设计了 _create 和 auth_id 两个索引」。\nESR（Equality / Sort / Range）说的是 复合索引字段顺序，不是两个单列索引。面试官很可能追问：\n作者查文章：auth_id = ? AND status = ? → 更合理的是 (auth_id, status) 或 (auth_id, status, ctime DESC) 热榜：ctime \u0026gt;= now-7d 且可能要 status = published → 单列 _create 往往不够，还要考虑排序字段（阅读量、score） 若被问到「为什么不用复合索引」，目前文档撑不住。\n2. 热榜设计过于粗糙 now-7week 应为 7 天（笔误会减印象分） 没说明热榜指标（UV、阅读量、点赞、时间衰减） 没说明是实时算还是定时任务 + 缓存 频繁计算却只提 _create 索引，逻辑链不完整 3. 一致性问题讲不深 隐私文章「制作库 + 线上库同步」只说了事务和重试，缺：\n同库不同表 vs 跨库：最终一致性怎么做 事务失败后用户看到什么状态 OSS 与 DB 不一致怎么办（DB 更新了，OSS 上传失败） 是否需要 Saga / 补偿 / 幂等键 2 年经验不要求精通分布式事务，但要能讲清 自己的方案边界。\n4. 几乎没有 Golang 相关内容 这是 Golang 面试准备，但全文没有：\ncontext 怎么用、超时怎么传 goroutine 泄漏怎么防 errgroup / worker pool 怎么做批量 MQ 接口设计、依赖注入、分层（handler/service/repo） 单元测试、压测数据 channel + ctxWithTimeout 只一句，深度不够。\n5. 表达与细节问题 statuts、auth_id/author_id 混用、_create 命名不规范 「习惯习惯」重复 description 仍是 xxxx 相比同系列的 register_login tidy，这篇 没有 Q\u0026amp;A 脱稿演练，面试时容易散 6. 缺少可量化的结果 没有 QPS、P99 延迟、缓存命中率、发布失败率等，「设计合理」缺少说服力。\n改进建议 一、把每个技术点补成「STAR + 追问应答」 建议每个模块固定四句话：\n背景：为什么这么做 方案：具体怎么实现 取舍：为什么不用别的方案 结果：数据或线上表现 示例（索引）：\n作者列表查询是 author_id + status + 按创建时间倒序分页。我用复合索引 (author_id, status, ctime DESC)，遵循 ESR：等值条件在前，排序字段在中间。热榜是近 7 天已发布文章按 score 排序，定时任务每 5 分钟算一次，结果放 Redis ZSet，读路径不走 DB。\n二、重点补强 5 个高频追问 追问 建议准备内容 为什么制作库/线上库要分开？ 读写隔离、草稿不影响线上、发布可审核 发布接口如何保证幂等？ 幂等键、upsert 条件、版本号 / status 机 缓存一致性怎么保证？ key 设计、先更 DB 再删缓存、延迟双删要不要 为什么用 MongoDB？MySQL 行不行？ 文档结构、字段变化、大正文；MySQL 可以但 OSS 后 MySQL 更合适 MQ 批量发送失败怎么办？ batch 大小、重试、dead letter、ctx 超时 三、补 Golang 实现细节（至少 3 点） 发布流程：ctx 超时、errgroup 并行写 DB + 上传 OSS 阅读统计：带 buffer 的 worker，select + ctx.Done() 优雅退出 接口层：统一错误码、middleware 鉴权、作者只能改自己的文章 能画一张流程图或贴 20 行核心代码，比纯文字强很多。\n四、OSS + DB 一致性单独成段 建议明确：\n先发 OSS 还是先写 DB？ 发布失败如何回滚 / 清理孤儿文件？ 读路径：DB 存 content_url，CDN /cache 怎么处理？ 五、参考 register_login tidy 的结构改版 那篇有「脱讲稿 + 问题列表 + 标准答法」，这篇建议对齐：\n1 2 3 4 5 6 ## 30 秒 elevator pitch ## 架构图（制作库/线上库/OSS/缓存/MQ） ## 核心接口（Publish / SaveDraft / SetPrivate / List / Detail） ## 5 个技术亮点（每个 1 分钟） ## 10 个高频面试题 + 参考答案 ## 踩坑（MongoDB ObjectId、缓存大对象、双库不一致） 六、修正文档硬伤 统一术语：author_id、status、ctime 修正「7 week」→「7 day」 填满 front matter 的 description 通读去掉错别字 面试官视角的一句话评价 「候选人做过真实的文章发布模块，有制作库/线上库、缓存、OSS、MQ 等完整链路意识；但对索引、一致性、Go 并发和面试表达深度不足。若能在 ESR、发布幂等、缓存 key 设计、双库一致性边界上准备充分，可从『大概做过』提升到『能独立负责』，分数有望到 80+。」\n如果你愿意，我可以按面试官口吻，直接把这篇 tidy 文档改成「可背诵版」（含 10 道追问 + 参考答案），或对照 register_login tidy 的结构帮你重写一版。\n","date":"2026-07-25T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/publish-service-0x01/","title":"发帖服务 one"},{"content":"脱讲稿 简介 : 设计并实现了 完整的 高安全性,高可用,高扩展的 登陆与注册 服务\n自述 :\n我给系统设计了一个支持 手机号码、微信登陆、邮箱注册 的登陆与注册服务。 使用 长短JWT + Session 控制用户的登陆登出。 处理了跨域和限流问题,并且对于手机登陆支持切换云服务商 支持 failover 策略。功能拥有完整的链路控制，并且使用 wrk 进行压测。\n高安全性 :\n做了限流, 手机验证, 设备验证 高可用 :\n支持切换多个云服务商 高扩展的 :\n基于面向接口编程. 并且采用 DDD 设计 支持 repo 层切换多个存储方案，支持 SMS 和 微信登陆切换各种服务 问题 1 什么是 Gin 的 middle-ware ？ 能解决什么问题 ？ Gin 框架提供的一种 AOP 编程机制, 支持实际业务逻辑前后进行插入额外处理 。 能够解决以下几类问题\nWeb治理 : 实现 http 层面的熔断、限流、降级\n可观测性 : 可以聚合日志，metrics，tracing\n身份的认证与鉴权\n什么是跨域问题，怎么解决 ？ 跨域的问题本质是 请求发送方和接收方 host 和 端口不匹配的问题 。 对于跨域问题，浏览器会预先发送一个 preflight option方法，询问允许访问的范围 。\nGin 框架支持使用 Cros 中间件进行处理跨域问题，设置对应的 allow-origins,allow-headers,allow-method即可\n跨域问题需要设置哪些头部 ？ 上面的已经回答 什么是 cookie，什么是 session ? http 本身是无状态协议，为了保护用户的登陆状态，浏览器支持本地存储一个 kv 数据, 这个数据可以理解为 cookie\n由于 cookie 的安全性问题，隐私信息一般不存放在 cookie中，因此引入 session 机制\nsession 机制允许 绑定一个 session_id 给前端，具体身份校验逻辑可以存放在后端避免隐私信息泄漏\ncookie 和 session 比起来有什么缺点 ？ cookie 不依赖后端存储，并且本身可以设置一些安全性配置 例如 domain,expires,secure\n缺点的话就是 不允许存放敏感信息，容易丢失\n\u0026ndash;\nSession ID 可以放在哪里？ 如果Cookie禁用 cookie, header, redis,本地缓存,sql 等各种地方\n用户密码加密算法的选取 尽可能选择高安全系数的,不宜破解的 。 技术选型的时候考虑过 md5,pbkdf2,bcrypt\n因为 md5 需要在数据库中额外存储盐值的原因最终选择 bcrypt\n使用 wrk 压测的时候，发现 密码加密和不加密的情况下 性能相差10倍, 但是这是不可避免的\n怎么做登陆校验 ? 正确性校验 :\n在登陆的时候 获取对应的账号和密码，去数据库中查找，并且使用 bcrypt 进行比对 正确的话 则返回对应的 jwt 用于保护用户状体啊 状态维护:\n每次获取对应的 短token进行 jwt解析，解析失败则登陆失败 安全性处理:\n在 JWT 中维护一个 user-agent字段，每次比对该字段是否相同\n使用 Redis 限流控制 每次的登陆\n刷新 Session 过期时间的几种方案\n快要过期的时候刷新 ; 这个可能会有 gap\n每次访问都刷新 。会频繁的读redis\n固定时间刷新 ， 增加 updateAt字段\n怎么保护 session_id ?主要还是启用 https协议，设置 cookie 的 secure\n怎么做到 在session_id 和 jwt-token泄漏之后保护着客户 ？ 记录登陆的额外信息\n如何保护 Web 服务 ？ 针对 IP限流、整个集群限流\n这几个问题主要在问题 8 会引申\n问题 2 你用 Redis 解决过什么问题？ 业务上 :\n用来标记某些 handle 是否完成。 例如某些需要在全环境修复的任务\n分布式锁 控制针对于 IP 限流的一些账户创建\nStream 实现的轻量化 mq 队列 用于实现异步的消费\n存储一些实时性不高的 例如快照的信息\n使用 Zset 进行热榜功能的排序\n使用 Redis 单机 线程安全的特性 实现限流\n你知道 Redis 支持哪些数据结构，用过哪些 ？ 用来解决什么问题？ string,list,hash,set,zset . 问题 1 的扩展\n各个数据结构的底层实现 ？ 不知道 ，有对应的 memo 。 预计会单独开辟一个文章\n当你更新数数据的时候 先更新数据库还是先更新缓存，有没有一致性问题 先更新数据库，然后删除缓存。 对于更新操作 采用 Cahce-Aside 的方式 会有一致性问题\n我们只有在 Get 操作的时候才会重新写入缓存\n(这里说实话，忘记了开发的时候的想法了，创建一个memo处理一下)\n如何解决一致性问题 这里主要是 check-do thing 的模型吧 。 尽可能使用原子操作 ，例如 lua 或者是 atomic\n（同样单独使用 memo 进行处理） 问题 3 什么是依赖注入，如何在 Go 实现 依赖注入 依赖注入，就是 A 要调用 B 上面的方法, A 在构造的时候要求传入构造好的 B 而不是自己初始化一个 B 。\n通过引入 wire包进行依赖注入\n什么是面向接口编程 ？ 为什么要面向接口编程 ？ 业务之间 组件和组件的通信必须依赖接口 。 即 如果 A 调用 B，那么 B 必须是一个接口 。\n面向接口编程最主要的一点就是提升扩展性，避免代码堆积 。\n什么是长短 token ？ 为什么要用长短两个 token ？ 长 token 用于控制 短 token 的刷新\n短 token 用于进行登陆的校验\n长短token的设计是为了安全性， 如果只有一个 token 有效期设置的很短 用户频繁登陆，很长的话会有安全问题\n长短 token 的过期时间应该怎么设置 ？ 根据业务需求和产品经理来判断，长 token 一般会很长 例如半年，一个月 。 短token 可以是 2小时 或者是 1天\n怎么保证长token的安全性，万一泄漏了怎么办 ？ 携带一些设备信息，例如 user-agent 手机设备号，在对于敏感操作的时候，增加手机号验证\n使用 JWT token 怎么退出登陆,使用 长短 token 之后怎么退出登陆 增加一个 session_id 进行维护，在 Redis 中维护一个黑名单\n扩展问题 Q : 对于系统支持 微信、手机号、邮箱注册，数据库表是怎么设计的\nA : 对于这三个字段单独设计了唯一索引,我们认为任意一个渠道注册进来就是单独的一个账号\nQ : 那么如果我后续想要实现 手机号和微信和邮箱 是一个账号怎么办\nA :\n看一下业务层面是否要迁移历史数据\n如果不迁移历史数据的话 :\n创建部分联合索引 加上 _create 限制，保证就新的数据需要遵守该索引\n业务层面强制 这三类业务注册的时候 就一起绑定 手机号 或者是 邮箱\n如果要迁移历史数据的话\n优先保证内部数据迁移跑完\n并且合并多个账户之间的数据。 这里可能复杂点 预计需要强制用户绑定手机号才可以进行功能操作 xxx\n创建索引\n改善业务层面的登陆与注册服务\nQ : Gin 实现了哪些限流 降级 熔断\n遗留 尝试把 SMS 服务单独在拆一个场景出来，增加异步的内容\n可以设置一个 同步转异步的 容错机制 。 当第三方不可用的时候，将当前请求存储到数据库中，后续再单独启动一个 gouroutine 异步的发送出去\nAI Judge 1 你现在是 Golang 面试官，下面是一个拥有2年经验的求职者的一个面试准备 。 请你结合现有系统实现 和 xxx.md 给其中的内容进行打分 满分100分，给出 优缺点和建议 。 对照 webook 实现后，这份准备材料按 2 年经验后端面试 标准评分如下。\n总分：62 / 100 维度 分数 说明 项目叙事与亮点提炼 14/20 有多通道登录、长短 Token、限流、failover，但「三高」略夸大 与代码一致性 10/15 大方向对，细节与代码不一致，易被追问穿帮 安全与鉴权深度 12/20 知道 bcrypt / JWT / ssid，但机制说不清、有概念混用 Go 工程能力（DI/接口/分层） 12/15 这块相对扎实，和代码匹配 Redis / 中间件深度 7/15 用过什么能说，底层与一致性大量空白 表达与完整度 7/15 错别字多、多题空答、遗留太多 优点 项目选型对得上代码：邮箱密码、短信、微信；长短 JWT + ssid 黑名单；User-Agent 校验；SMS failover；Wire + 接口；Redis 滑动窗口限流；CORS；wrk 压测——这些在仓库里都能找到对应实现。 能讲清楚「为什么做」：长短 Token 的安全/体验权衡、接口便于换 SMS/Repo、bcrypt 有压测体感——比只会背概念强。 扩展题有业务意识：多渠道账号合并、历史数据迁移，这是面试官爱听的「产品 + 工程」题。 工程分层叙事清晰：依赖注入、面向接口，和 wire.go、UserCache 注释里的约束一致。 缺点（面试里会扣分的点） 1. 概念混用，容易被打穿 实际是 JWT 放 Header（x-jwt-token / x-refresh-token）+ Redis users:ssid 黑名单，不是经典 Cookie Session。 Cookie / Session 答得偏教材，和项目实现脱节；面试官一问「你们项目 Session 存在哪」，容易答乱。 「Session ID 可放 redis/本地缓存/sql」把 服务端存储 和 客户端携带位置 混在一起了。 2. 与代码不一致 讲稿说法 代码现实 短 Token 2h / 1 天，长 Token 月/半年 短 30 分钟，长 7 天（redis_jwt.go） 设备验证 仅比对 User-Agent，不是设备指纹 完整高可用 SMS 有 failover，但 SendV1 有逻辑问题；真实多厂商未必接好 DDD 更接近分层 + 接口，不是完整 DDD 短信登录流程本身也不完整：LoginSMS 校验成功后没发 Token，Token 却在 SendLoginSMSCode 里设置——若被问「短信登录时序」，会很难圆。\n3. 关键答案有硬伤 bcrypt vs md5：选 bcrypt 主要因为自适应哈希/抗暴力破解，不是「md5 要额外存盐」。bcrypt 自带盐，理由说偏了。 跨域：不只是 host/port，还有 scheme；还缺 Credentials、ExposeHeaders（你们代码里刚好有 ExposeHeaders 暴露 JWT Header）。 缓存一致性：写了「忘了」——2 年经验被问 Cache-Aside / 双写，这是硬伤。 Gin 限流/降级/熔断：空题；项目有限流，没有完整熔断降级，却标题写了「高可用」。 4. 准备完整度不够 Redis 底层、一致性、Gin 治理、遗留 SMS 异步都标了 memo/空答。面试不是写博客进度，空题 = 扣分。\n5. 自我包装过满 「完整的高安全性、高可用、高扩展」对学习项目偏重。2 年经验更稳妥的说法是：\n练手项目，覆盖多登录方式、长短 Token、限流与 SMS 容错，重点验证接口化扩展。\n建议（按优先级） 立刻改讲稿 用代码里的真实数字讲：AT 30min、RT 7 天；退出时 ssid 写入 Redis，TTL 与 RT 对齐。 统一术语：说「JWT + 会话标识（ssid）黑名单」，少说「Session 存在 Cookie」。 删掉或降级「设备验证」：改成「绑定 User-Agent，防简单 Token 盗用，不能防 UA 伪造」。 补齐空题，至少能答 2 分钟： Cache-Aside：先写 DB 再删缓存；并发下的短暂不一致 Redis String/Hash/ZSet 各举一个本项目例子（验证码、用户信息、热榜） 限流：滑动窗口；熔断降级诚实说「项目未做，可讲思路」 对着代码能画一张时序图 邮箱登录 → 发 AT/RT → 中间件校验 AT + UA + ssid Refresh：校验 RT → 查黑名单 → 发新 AT Logout：ssid 入黑名单 面试官最爱顺着这条链路追问。\n主动暴露已知坑（加分） 短信登录时序问题 Failover 主备不均 / SendV1 错误处理 FindOrCreate 主从延迟注释 「我知道问题在哪、怎么改」比「系统很完美」更像有 2 年经验。\n表达 错别字（Cros、状体啊、Cahce）和口语化「不知道有 memo」直接改掉；面试可以诚实说「这块我回去补过」，但讲稿里不该留白。\n面试官视角的一句话 项目素材够用（约 70 分的料），讲解与掌握大约 55–60 分；合在一起 62。\n能过初筛讲项目，但深挖安全、缓存一致性、短信登录细节时风险大。把术语对齐代码、补空题、主动讲缺陷，冲到 75+ 比较现实。\n","date":"2026-07-24T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/register-login-desin-0x01/","title":"高可用 高扩展 高安全 的 登陆与注册的设计 one"},{"content":"简历 preview 简历上 :\n设计并实现了 完整的 高安全性,高可用,高扩展的 登陆与注册 服务 面试中 :\n系统支持 邮箱注册,短信验证登陆,系统扫码登陆的功能, 使用 JWT + Cookie 的形式维护登陆状态, 采用 长短Token 进行续期. 采用 Lua+滑动窗口 实现非访问登陆的限流\n短信验证登陆支持 阿里云和腾讯云的切换, 使用负载均衡算法进行处理切换 同时设计对应的降级策略 failover 保证服务崩溃也能使得部分功能正常\n对接微信开发平台 实现微信扫码登陆 处理2次回调内容\n难点 :\n一开始使用 cookie + session 实现的注册与登陆，在使用 wrk 压测的时候 发现 redis 有很大的压力. 决定牺牲一点性能 采用 JWT\n用户表设计的时候 由于密码是隐私内容 需要考虑加密, 在选择加密算法的时候进行了选择, 经历了xxx,最后选择 bcrypt\n使用 wrk 压测的时候发现，登陆接口并没有限制会引入大量异常流量，于是自己设计了一个限流中间件，利用 Redis 本身的单线程特性，采用滑动窗口实现\n在对接微信扫码登陆时, 因为需要本地测试，但是 微信api返回的是 注册域名的 跳转链接 。 最后改了本地的 hosts实现测试。 另外因为之前表设计缺陷，实现该功能后额外增加了一列微信，并且重新设计了唯一联合索引\n全部开发使用 DDD 模式架构进行整理，使用 TDD 业务开发 整个开发过程比较繁琐，另外引入装饰器模式 和 面向接口编程在牺牲开发效率的情况下 保证系统的高扩展性\n跨域问题 跨域问题 是因为 : 发请求的域名 + 端口 和 接收请求的域名 + 端口 对不上 。 比如 前端 localost:3000 发到 localhost:8080 上\n对于跨域问题 : 浏览器会提前发送一个 preflight 询问自己愿意接受的请求\n解决的办法 : Gin 提供了解决问题的 中间件来处理\n通过 middle 设置 :\nAllowCrendentials : 允许带上用户认证信息 AllowHeader : 业务请求可以带上的头 AllowOrigin : 哪些来源是可以被允许的 1 2 3 4 5 6 cors.New(cors.Config{ AllowOrigins: []string{\u0026#34;http://localhost:3000\u0026#34;, \u0026#34;http://localhost:3001\u0026#34;}, AllowHeaders: []string{\u0026#34;Content-Type\u0026#34;, \u0026#34;Authorization\u0026#34;}, AllowCredentials: true, ExposeHeaders: []string{\u0026#34;x-jwt-token\u0026#34;, \u0026#34;x-refresh-token\u0026#34;}, }) 加密 选择加密算法的标准就一个, 难破解. 需要考虑以下的问题 :\n相同的密码\n难以通过碰撞来破解\n常见的加密算法 :\nmd5\n在 1 的基础上引入 salt 这里需要额外存储\n使用 PBKDF2,BCrypt 随机盐值的加密算法进行加密\nCookie \u0026amp; Session \u0026amp; JWT Cookie 浏览器存储一些数据到本.\n不安全 : 可以通过 cookie-editor插件进行获取\n一些设置 :\nDomain: cookie 可以用在什么域名下\nPath: Cookie 可以用在什么 path 下\nmax-Age: 过期时间\nHttp-only: 如果设置为true, js代码无法使用这个 cookie\nSecure: 只能使用于 https 协议\n一些有意思的问题 Q : 为什么有一些网站喜欢禁用 Cookie\nA : 没看懂 说实话，安全相关的内容 难理解\nSession 我的理解 就是一个 key-string , 不过他是由后端存储\n但是他还是强依赖 cookie , 无非最重要的一点就是 cookie如果存放敏感信息会被获取,所以引入session,传入 session_id 用于后端校验\n如何让客户端懈怠 session_id :\n最佳的方式就是放在 cookie中, 毕竟 cookie还可以设置 secure 如果禁用了 cookie 可以考虑放在 header 和 sess_id 中\nJWT JWT 由 header+playload+sign 组成\nJWT 与 Session 比较 :\n优点 :\n不依赖第三方存储\n适用于分布式环境\n提高性能 (没有 Redis 访问)\n缺点 :\n对加密依赖非常大\n最好不要在 JWT 放置敏感信息\n退出问题 Session 的退出，需要先把 cookie 删了 然后把 session本身删了\n但是 JWT 本身是无状态的，所以旧的token在没有过期 其实也是能用的\nJWT 如果要实现退出登陆，只能够考虑使用 redis 黑名单的机制\n但是我们已经实现了 长短token, 如果记录黑名单需要在登陆校验的时候 也要传入黑名单 。这导致长token会被频繁调用\n所以考虑 JWT 中存放 session-id 使用 session-id进行黑名单控制\n安全性问题 Q : 我们实现登陆与注册系统之后 有一个最大的问题就是\n任何人都可以注册 任何人都可以登陆 因此我们需要考虑 限流\n这里采用 Redis+lua 滑动 窗口限流\nQ : 如何保证 JWT 或者是 Session 泄漏 攻击者假冒你\n采用验证码二次验证，对比上一次的信息 例如 user-agent\n性能问题 使用 wrk 进行性能压测\nwrk -t1 -d1s -c2 -s ./script.lua urlpath/signup\n参数解释如下. :\n-t: 线程数量\n-d: 持续时间\n-c: 并发数量\n-s: 测试脚本\n压测下来预计会发现两个问题\n加密算法 . JWT 在注册和登录后会进行加密和解密，预计相差 10倍\n数据库查询\n优化数据库查询 我们常见的一种写法是 cache-db-cache\n1 2 3 4 5 6 7 func FindById() { cache.Get() db.Get() _ := SetDB() } 场景问题决策 Q : Session 的 过期时间如何刷新 ?\n用户每次访问, 都刷新. 如果 session 部署在 redis 上，那么影响很大\n快要过期了我再刷新 。 有 gap\n固定时间刷新，引入 updateAt 参数 , 用户每次访问的时候 如果超过固定的时间 那么刷新\nQ : 长短 token\n短 token : 用于访问资源\n长 token : 短 token 过期之后，生成一个新的短 token\n这里其实是依赖的前端，我们在登陆接口的时候返回两个 token\n然后提供一个 refresh 接口,如果 短token 失效，让前端用长token请求刷新一下\n第三方服务治理 整体思路 :\n尽量不要搞崩第三方\n万一第三方崩了，你的系统还能够稳定运行\nfailover failover (失效转移)\n如果第三方崩了 那么就直接换一个服务商\n策略有 :\n轮询\n缺点每次都从头开始，负载不均衡 如果 sms svc 很多, 轮询很慢 动态轮询\n每次使用 i%length的方式进行轮询 不过这里需要注意并发问题 需要引入 atomic 动态判断第三方\n真的计算服务商是否还在运作正常 . 可以从错误率 响应时间判断 OAuth2 微信 微信扫码登陆 看着更像是 TCP 链接的三次握手 分为\n请求登陆 返回确认请求 确认 带上 uuid 进行请求 返回 access token 配置 之前我们一开始使用的是 build tag 进行配置控制\n我们如果要优化配置可以从下面的方式进行考虑\n启动参数 ，命令行工具传入的参数 。 例如 mysqlroot:password\n环境变量\n配置文件。 使用 viper 制定 path 进行读取\n远程配置中心 。 etcd\n配置文件的缺点 :\n不够灵活 权限控制 实时更新 日志 使用 zap 进行日志处理, zap 本质上是维护了一个全局 logger\n可以考虑使用 适配器模式封装一下 zapper\nGin 可以考虑使用 middleware 进行打印日志\n通过在 ctx.Next()前后处理对应的数据 但是我们无法获取到 resp,我们需要自己重新实现一个 respWriter Gorm 可以考虑使用 config.Logger实现\n其他的地方只能考虑从 Service 插入 logger 或者是维护全局 logger的形式进行输出com\n面试题 什么是 Gin 的 middle-ware ？ 能解决什么问题 ？\n什么是跨域问题，怎么解决 ？\n跨域问题需要设置哪些头部 ？\n什么是 cookie，什么是 session ?\ncookie 和 session 比起来有什么缺点 ？\nSession ID 可以放在哪里？ 如果Cookie禁用\n用户密码加密算法的选取\n怎么做登陆校验 ? 利用 Gin 的 middleware\n刷新 Session 过期时间的几种方案\n增强登陆的安全性\n怎么保护 session_id ?主要还是启用 https协议，设置 cookie 的 secure 怎么做到 在session_id 和 jwt-token泄漏之后保护着客户 ？ 记录登陆的额外信息 如何保护 Web 服务 ？ 针对 IP限流、整个集群限流 Redis :\n你用 Redis 解决过什么问题？\n你知道 Redis 支持哪些数据结构 ？ 用过哪些 ？ 用来解决什么问题？\n各个数据结构的底层实现 ？\n当你更新数数据的时候 先更新数据库还是先更新缓存，有没有一致性问题 (这在登陆与注册超纲了吧)\n如何解决一致性问题\n什么是依赖注入，如何在 Go 实现 依赖注入\n什么是面向接口编程 ？ 为什么要面向接口编程 ？\n什么是 Ioc 控制反转\n微信登陆 (TBD)\n微信扫码登陆的流程\n为什么微信的回掉地址，域名必须是你预先注册的\n如果我的临时授权码(code)被黑客拿到了会怎么样\nstate有什么作用 ？ 如何使用 state ?\n什么是长短 token ？ 为什么要用长短两个 token ？\n长短 token 的过期时间应该怎么设置 ？\n怎么保证长token的安全性，万一泄漏了怎么办 ？\n使用 JWT token 怎么退出登陆\n使用 长短 token 之后怎么退出登陆\n使用 JWT 还需要再使用 Session 吗\n亮点提升 Gin :\nWeb治理 : 熔断、限流、降级\n限流实现 ：\n单机限流 :\n令牌桶算法\n漏桶算法\n滑动窗口算法\n固定窗口算法\n基于 Redis 限流\n基于 Redis IP 限流\nGin插件库\nGorm :\n为 Gorm 提供可观测性的插件实现\n实现读写分离插件\n提供 BeforeFind 函数\n第三方服务治理 :\n可以设置一个 同步转异步的 容错机制 。 当第三方不可用的时候，将当前请求存储到数据库中，后续再单独启动一个 gouroutine 异步的发送出去 考虑布隆过滤器进行黑名单过滤\n","date":"2026-07-23T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/register-login-desin/","title":"高可用 高扩展 高安全 的 登陆与注册的设计 detail"},{"content":"本篇面经来自 : https://www.nowcoder.com/feed/main/detail/ac0efc95550947c0a55693799dbfa380?sourceSSR=search\n个人背景与项目挖掘 Q : 简单介绍一下目前在职的情况，以及寻找新机会（离职）的原因是什么？\nQ : 介绍一下你在做的项目，核心思想和解决的痛点是什么？\n两个很普通的问题 : 总结\n问题 2 : 预计到时候要准备一篇 500 字的演讲稿方便背诵 。 已添加到 Memo 中 ddl 预计下个月\nGo 语言基础 常识题 Q : 项目里用到了 Gin 框架，请问 Gin 第一层的参数绑定（Parameter Binding）底层是怎么实现的？\nGin 支持 Bind 和 ShouldBind 两种调用\n1 2 3 4 5 6 7 8 9 func (c *Context) ShouldBind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) return c.ShouldBindWith(obj, b) } func (c *Context) Bind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) return c.MustBindWith(obj, b) } 其中通过 Method 来分发对应的实现\n1 2 3 4 5 6 7 8 9 10 11 func Default(method, contentType string) Binding { if method == http.MethodGet { return Form } switch contentType { // ... return BSON default: // case MIMEPOSTForm: return Form } } shouldbind 不额外封装 http-code，但是bind额外封装 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (c *Context) ShouldBindWith(obj any, b binding.Binding) error { return b.Bind(c.Request, obj) } func (c *Context) MustBindWith(obj any, b binding.Binding) error { err := c.ShouldBindWith(obj, b) if err != nil { var maxBytesErr *http.MaxBytesError // ... switch { case errors.As(err, \u0026amp;maxBytesErr): c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck default: c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck } return err } return nil } 具体更核心的实现，预计就是读取 http 的请求根据各种字段进行处理。预计只要回答到这一步即可\ntodo : 整理这部分内容单独到 Gin 中\nQ : Go 语言中的 Channel 有什么特点？在使用它的时候需要注意什么？\n吐槽一下 : 说实话实际工作中用到 channel 的地方真的很少\n目前的印象来说 :\nchannel 的发送操作和接收操作 会相互阻塞直到双方准备完毕\n使用它有什么需要注意的 ： 记得 close\ntodo : 这个后续预计会在系统整理 高并发那一块处理，或者是在补充《协程池》这篇文章后进行针对学习\nQ : 如果一个 Channel 已经关闭了，再往里面写数据会发生什么情况？\nWrongAnser : 从上面的回答来看，如果接收方的channel关闭，但是发送方的没关闭，预计是会一直阻塞，然后程序运行后会出错 all sleep goroutine, deal lock 印象中是这样\nTrue : panic: send on closed channel\ntodo : 会一并同上面的问题一起处理\n代码输出题 1 2 3 有一个切片 s := make([]int, 3)， 然后往里追加数据 s = append(s, 1, 2, 3)， 最终 fmt.Println(s) 打印输出的结果是什么？ 这个问题是默认补0吧，println不是sprintf(%v)感觉会输出地址？ 不过预计答案是 0,0,0,1,2,3\n纠正 : fmt.Println 对 slice 用的就是类似 %v 的默认格式，打印的是元素内容：[0 0 0 1 2 3]\nRedis Q : Redis 的分布式锁怎么实现的 ?\n不知道，印象中是 setNx 但是没实际实现过\ntodo : 补在memo中\nQ : 如果现在想做一个排行榜功能，应该用 Redis 的哪种数据结构？\nZset, 不过如果要处理 100w 的排行的话 需要在业务上额外处理一下，例如定时排序+Zset 不过这个作业目前还没完成 只是知道一个概念\ntodo : 补在memo中\nQ : Redis 的字符串类型中，有没有哪个指令能够对已存储的数字进行累加？\n没看懂这个什么问题，字符串类型的累加？ IncrBy() 吗，需要注意的是 如果没有key和有key的情况。这个在 查询和缓存计数的时候有考，预计在下周总结出来\nQ : 了解 Redis 的缓存雪崩、缓存穿透和缓存击穿吗？分别是什么以及怎么解决？\n吐槽 : 梦会实习面经\n缓存雪崩指的是 : 大量数据没有命中缓存 导致都打到数据库上\n缓存穿透 : 指的是数据在缓存和数据库中都不存在\n缓存击穿 : 热点 key 正好过期，此时又有大量数据引入\n解决办法忘记的差不多了，不过从现在的业务理解上面看\n给数据库进行限流降级，保住一部分用户的使用。 另外使用其他可用的Redis 都不存在，那就用 布隆过滤器过滤一下，本质上就是给不存在的key增加一个缓存 和方法2差不多，也是缓存一下这个过期的 大V key Kafka Q : Kafka 是怎么保证消息消费的有序性的？\n没用过 Kafka 不知道\ntodo : 推进 kakfa 的学习\nSQL 1 2 假设有一个部门表 department，包含以下四列：ID（主键）、UID、age（年龄）、dept_id（部门 ID）。 需求：查询平均年龄在 20 岁以上的部门 ID。 吐槽 : 没想到 26年的面试也是这么存粹，还有手撕sql的环节\n这里简单手撕一下，估计牛客上面有原题输入输出什么的预计，这周末去刷一下\n1 2 3 select dept_id from ( select dept_id,avg(age) as avg from department where avg \u0026gt; 20 group by dept_id ) 猜测是这样\n手撕代码 1 golang语言用协程交替打印1-100 这个炫技做法就是 参考我这里的实现吧协程池 ，用协程池开2个goroutine 输出就行了？不过输出的时候全局变量要上锁\n感觉不用搞这么麻烦，直接手动开启两个 gouroutine ，然后用 atomic并发安全的基本类也行？\n不过怎么实现交替，用管道通信？一个携程输出完 然后传给另外一个 直到 100 然后close ?\n写了一下没写出来，看着是 写入方和接收方 不能够同时操作 \u0026hellip;\ntodo: 实现 1-100交替输出\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 type printer struct { numerChan chan atomic.Int64 changeFlag atomic.Bool } func main() { wg := sync.WaitGroup{} wg.Add(3) p := printer{ numerChan: make(chan atomic.Int64), changeFlag: atomic.Bool{}, } printNum := atomic.Int64{} go func() { p.numerChan \u0026lt;- printNum }() trueGoroutine := func(x atomic.Int64) { fmt.Println(x) } falseGoroutine := func(x atomic.Int64) { fmt.Println(x) } select { case printNum = \u0026lt;-p.numerChan: if printNum.Load() == 100 { defer close(p.numerChan) wg.Done() wg.Done() wg.Done() } else if p.changeFlag.Load() { go trueGoroutine(printNum) printNum.Add(1) p.numerChan \u0026lt;- printNum } else { go falseGoroutine(printNum) printNum.Add(1) p.numerChan \u0026lt;- printNum } } wg.Wait() } AI Summary 总评 答对：4 / 11（另有 2 道半对） 评分：48 / 100\n按技术题计（不含自我介绍/项目介绍）。能答对的多是「知道用什么」，卡在「怎么做对 / 边界 / 落地」。\n逐题核对 # 题目 判定 说明 1 Gin 参数绑定 ✅ Bind/ShouldBind、按 Method/ContentType 分发，方向对，面试够用 2 Channel 特点与注意 ⚠️ 「互相阻塞」只覆盖无缓冲；「记得 close」不准确 3 往已关闭 channel 写 ❌ 你自己标了 Wrong；正确是 panic: send on closed channel 4 make([]int,3) + append ✅ [0 0 0 1 2 3] 正确 5 Redis 分布式锁 ❌ 只想到 setNx，缺过期、value、续期、释放校验 6 排行榜用啥 ✅ ZSet 正确 7 字符串累加 ✅ INCR/INCRBY 正确 8 雪崩/穿透/击穿 ⚠️ 定义大致对，方案混、不完整 9 Kafka 有序性 ❌ 空白 10 SQL 平均年龄 ❌ 应用 HAVING，不能 WHERE avg \u0026gt; 20 11 协程交替打印 ❌ 没写出；现有代码结构也不对 优点 会追源码：Gin 那题不是背概念，而是顺着 ShouldBind → Default → MustBindWith，面试里加分。 知道错在哪：closed channel、slice 打印都能自纠，学习闭环在。 选型直觉还行：排行榜 ZSet、计数 INCR，方向不偏。 有复盘习惯：todo / memo 落点清楚，比「看完就过」强。 缺点 基础边界不熟：channel close、SQL HAVING、手撕交替打印 —— 都是高频送分题。 中间件偏「听过没用过」：Redis 锁、Kafka 几乎空。 缓存三兄弟：定义能背一半，方案和场景对不上（击穿该用互斥/singleflight，不是「再缓存一下」）。 手撕偏复杂：交替打印绕 atomic + channel of atomic，反而写挂；面试要最小正确实现。 建议（按优先级） 本周先补这 4 个，能直接拉到 ~65：\nSQL\nSELECT dept_id FROM department GROUP BY dept_id HAVING AVG(age) \u0026gt; 20\nChannel\n无缓冲/有缓冲、close 后读零值、写 panic、nil channel 永久阻塞、谁负责 close\n交替打印（双 channel 或一个 channel + 两个 goroutine 即可）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func main() { ch1, ch2 := make(chan struct{}), make(chan struct{}) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() for i := 1; i \u0026lt;= 100; i += 2 { \u0026lt;-ch1 fmt.Println(\u0026#34;A:\u0026#34;, i) ch2 \u0026lt;- struct{}{} } }() go func() { defer wg.Done() for i := 2; i \u0026lt;= 100; i += 2 { \u0026lt;-ch2 fmt.Println(\u0026#34;B:\u0026#34;, i) if i \u0026lt; 100 { ch1 \u0026lt;- struct{}{} } } }() ch1 \u0026lt;- struct{}{} wg.Wait() } Redis 锁一句话版\nSET key uuid NX EX 30 → 业务 → Lua 校验 uuid 再 DEL；进阶再提 Redlock / 看门狗续期。\n下周： Kafka「单分区有序 + 同 key 进同分区」；缓存三兄弟各配 1 个标准方案（随机 TTL / 布隆或空值 / mutex）。\n面试表达： Gin、ZSet 这类题用「结论 → 一句原理 → 一句坑」三段式，比贴大段源码更稳。\n一句话： 选型感和复盘习惯不错，但基础题失分多；先把 SQL / Channel / 手撕 / Redis 锁补实，分数会比补 Kafka 涨得更快。\n","date":"2026-07-22T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/interview_0x02/","title":"面经每日一看"},{"content":"我做了什么 可以说, 我持续学习了 一周半(今天是周三)\n上周学习了 7天, 没什么公司的事 无非就是开几个同步会\n总共学习时间 39h ，那么一天学习时间大概在 5.7h 左右\n大部分实现在 golang 语言上面 ，少部分在算法和总结上面\n一些想说的话 承认我很累:\n不得不承认一天学习 6h 很累，超他妈的超级累\n我回想一下我每天的生活\n早上\n9:00 起床 - 9:30 开始学习 - 10:40左右休息\n11:00 学习 - 11:40 休息\n12:00 吃饭 - 13:00午休结束\n早上总共预计 1h + 40min = 1h40min 的学习时间\n下午\n13:10 学习 - 14:30 结束\n14:50 学习 - 15:50 结束\n16:00 - 18:00 开始发散\n下午的有效学习可能只在 2h 另外2h 印象中不是那么有效\n晚上\n18:00 - 19:00 期间做饭，无氧锻炼\n19:00 - 20:00 洗澡吃饭\n20:00 - 21:00 休息，玩游戏\n21:00 - 22:00 (可能会跑步或者是散步，跑步的话会要一个小时，散步的话只要半个小时)\n22:00 - 23:00 (之前这段时间会去学习算法，但是最近一直在和女朋友聊天)\n23:00 - 00:00 (写算法题，总结，这段时间最近也在和女朋友聊天)\n00:00 - 0:30 睡觉\n晚上的有效学习时间预计也只在 2h ，但是最近一直在和女朋友聊天。\n就算是我算上 下午的不高效学习时间 和 女朋友聊天的时间 那么一天也有 7h40min 的时间,但是这好累，就是为了多出这 4h\n从利益的角度考虑，确实一天多学习了 4h 确实比一天学习 3h 的多一天 但是这样好累\n另外吐槽一下\n自从不上班之后，他妈的周六周天都没了，一直在他妈的学\n我方向歪了？\n我不知道是不是我方向歪了，说实话我在 Go上面已经投入了 52h\n按我之前实习准备来说这52h 都够我写3个项目了\n一个 博客系统 。 亮点在于 点赞服务的设计，缓存的控制，ES，Grafana QPS的处理 一个 geeRPC 框架 一个 基于 RPC的聊天室 但是反观我现在得到了什么\n一个只完成了 1/3 的项目，目前只完成了 1 2 3 4 5 6 使用 DDD 进行项目架构, TDD 进行业务开发 支持 微信、SMS、邮箱注册登陆的方式, 使用多种限流算法进行无登陆态的限流 . 自选型通过 cookie+JWT 进行维护登陆权限和状态控制 使用装饰器以及面向接口设计,封装和二次开发 日志组件 和 监控组件 以及配置组件 使用 K8S 进行线上环境的部署 支持分级数据展示,设计线上表和开发表. 实现多台实例上的一致性问题 支持 MySQL 平滑迁移到 Mongodb . 设计并实现 SMS 的熔断以及限流业务 后面还有\n1 待实现 Kafka 点赞、榜单设计、微服务拆分、分布式任务调度、ELK、支付、秒杀、Feed流设计 说实话有点后悔了，我是随便找了一个 24年的训练营的 课程走的 。 当然也不能说是随便 只能说是最全面当下最好的系统课了\n总共 21周的 视频，一周平均 3 个 3h 的视频 总共时长 189h\n说实话 这比 当初规划 翻了好几倍\n而且说实话这个课程有我只能给到中规中矩的评价\n优点\n资料比较详细 核心场景，面试考点，实际开发中的情况都有介绍 缺点\n时长太长了，内容体量很大 多个模块之间可能没什么关联，例如 引入了 Mongodb 和 k8s 也只是为了实现 dao层级的切换，以及简单的部署。但是讲完之后后面就再也没用到了 有一些代码并没有在课上完成，也就是就算189h学习完成后，可能还需要多花90h用来处理其他内容 说实话 不后悔是假的，如果我真想要找下一份工作\n我应该把实习的项目捡起来，花20h记住，然后再花30h-40h的时间准备AI Agent那个仙骨\n然后再花 40h准备面经，但是现在好像上了贼船\n小总结 我不知道应该怎么走，但是我要继续走\n这里简单算一笔账 ， 课时总共 21周，那么一周我学习7周 的内容 3周就能解决\n这要求我一天基本上就要解决掉一周的学习内容 大概 9h ,算上倍数 4.5h\n感觉是可以的\n那么我就需要把每天学习的时间 [3h40min , 7h40min] 改成 6h40min\n这样子我可以使用多出来的 2h 进行很好的复习和总结\n那么如果我把玩游戏的1h拿出来呢？如果我把晚上睡前的1h拿出来呢？\n或许学生时代的我有这个精力\n总结 简单理清了一下现状\n第一个月 :\n完成 21周 学习的内容 保证每周平均每天 6h40min的学习时间 ，其中 4h 放在 go上面，当然如果一天学习了一周的内容，可以考虑往后吞并一周 或者是进行总结 其余2h用于算法的学习 以及总结 第二个月 :\n把原先的精力放在 熟悉八股文 模拟面试 面经 算法题 第二个月底+第三个月 尝试开始投递\n写给我自己 :\n我承认这很难，但是计算下来看着也不是没有完成的希望 。 最近学习破防了好几次，总感觉来不及，但是计算下来发现是可以完成的\n我清楚的知道这两个月会很枯燥 也会很难 可能没有结果 也能会有坏结果 但是我当下能做的也只有这个\n","date":"2026-07-22T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/upgrade-study-0x01/","title":"战略性调整"},{"content":"池化技术 我们对于一个批量操作，例如 批量暂停账户 或者是 批量删除数据\n我们很常规的做法就是 接口开放 []business_list 然后通过 for 进行处理对应的对应的业务逻辑\n1 2 3 4 5 6 7 func Handler(req) error { // ... for _, item := range business_list{ handle(item) } // ... } 但是这种在数据量大的时候会有一个问题 那么就是 前端等待超时, 这个超时 我上班的时候看着是 nginx还是前端那边特定组件控制的\n这时候可能就会想到\n我们只提交 tasks 然后放入到数据库中，单独开启一个 go backend_job 进行处理，并且处理对应的状态流转。 这种思路在我们 批量处理数据的地方 屡见不鲜\n这里就需要考虑 状态的流转，展示 如果进程重启，还需要恢复 如果数据量很大很大，是不是要考虑 分批分次进行，还需要进度控制 使用 long-task 接口，前端发起req请求，后端返回一个 task-id 然后每次都适用 task-id进行请求 查看任务状态 。\n这种做法，我能想到的问题就是，用户会被硬控在前端页面一直loading 池化技术貌似并不能很好的解决 第一个方案，如果数据量真的很大很大，感觉还是要进行分批分次进行 ，并且加上进度控制 。 顶多优化一下每轮的次数\n更多的是优化第二个内容，优化接口的响应和返回\n1 [AI Asking]: 池化技术在业务中 最多解决哪一类问题？我们什么时候用到池化技术 突然想起来了 :\n这里的池化技术 特指 并发池 而不是 连接池 对于 连接池的技术不熟悉 GMP 现在我是一个小白, 例如如果我们要实现 清理 100w条数据 那么是否是 开 100w 个 gorutine 更好呢，毕竟每一个 gourtine 单独处理最快\n我么可以从 GMP设计图 中很直观的看到\n全局队列 : 存放等待运行的 G\nP的本地队列 : 存放的也是等待运行的 G 。 本地队列的限制是 256\nP列表 : 可以认为是 逻辑上的 CPU . 数量可以认为是 CPU 的核数\nM : 线程 ; 想要运行任务就需要获取 P . 并且通过内核线程 Kernel Thread 进行 CPU 的相关调度请求\n一些调度关系 :\n如果 当前 M 绑定的 P 的 G 阻塞了，那么会重新调度一个新的 M 进行后续 G 的运行\nG 如果运行完之后 会产生对应的 GC 占用一定的内存空间\n如果当前本地队列 P 没有可运行的 G, 那么会去全局队列进行寻找\n如果 当前本地队列 P 没有可运行的 G 并且 全局队列为空 ，那么会去其他队列进行偷取 . (降低性能)\n大量创建 go 协程的代价 内存开销:\n初始阶段 goroutine 大概只有 2k 的内存开销 . ps : 一个线程 大概需要 2M 的开销\n/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.4.darwin-arm64/src/runtime/runtime2.go:422\n调度开销:\n我们可以从上面的图中看到 ，一个G 需要先放到全局队列，然后到本地队列，最后到P 又需要根据M进行绑定最后跑到内核线程 。\n并且一个很坏的结果是，当我们的G 阻塞后，会创建新的M 来进行执行，导致内核的线程开销也会变大\ngc开销:\n协程占用的内存最终需要 Gc 来回收 协程池 我们知道\n并发可以提高处理请求的速度 但是并发数量并不是越多越好 这是一个 规则怪谈 ，所以这时候 引入了 协程池，即固定数量的协程\n例如 nginx 最多支持 每秒钟并发 5 个请求，那么我们就可以启动一个数量为5的协程池进行批量工作 实现 1 架构 work 实际运行的 gourtine 用于并发处理 func()\nJobsChannel 内部任务的队列用于分发任务到 work()中\nEntryChannel 对外处理任务的队列，用于对接 JobsChannel 的数据\n代码剖析 Task 定义 Task 其中值字段包含一个 func value()\n并且支持一个函数用于执行 Execute()\n1 2 3 4 5 6 7 8 9 10 11 type Task struct { f func() error } func NewTask(f func() error) *Task { return \u0026amp;Task{f: f} } func (t *Task) Execute() error { return t.f() } Pool 整个 pool 定义如下\n1 2 3 4 5 type Pool struct { EntryChannel chan *Task // 对外的任务入口 JobsChannel chan *Task // 内部的队列 workerNum int // 协程池最大的数量 } 支持的 Func 如下\n单独解释一下 Run 过程\n这里先通过 遍历workerNum 启动 gourtine\n启动之后会进入到 JobsChannel 此时数据里面没有,会进行阻塞\n然后下方的数据会进行获取数据并且传入\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func (p *Pool) worker(workerId int) { for task := range p.JobsChannel { task.Execute() fmt.Println(\u0026#34;worker ID \u0026#34;, workerId, \u0026#34; \u0026#34;) } } func (p *Pool) Run() { defer close(p.JobsChannel) for i := 0; i \u0026lt; p.workerNum; i++ { go p.worker(i) } for task := range p.EntryChannel { p.JobsChannel \u0026lt;- task } } 完整运行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func Test_ExamplePool(t *testing.T) { a := atomic.Int64{} task := NewTask(func() error { fmt.Println(\u0026#34;task :\u0026#34;, a.Add(1)) return nil }) pool := NewPool(3) go func() { for i := 10; i != 0; i-- { pool.EntryChannel \u0026lt;- task } defer close(pool.EntryChannel) }() pool.Run() } 实现 2 手写协程池 协程池的应用 其他 ","date":"2026-07-20T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/gouroutine_pool_0x01/","title":"协程池"},{"content":"如果想让 AI 给你输出一套方案 或者是分类规则 是非常容易的\n但是想让他给你查看今天天气，或者是预定明天机票一般来说都不能很完美的进行\n而 Tool 就是为了实现这些内容\nAgent 和 Tool Agent在上一篇文章中指出 是负责作出决策和调用工具，所以 Agent主要负责的就是 调度和决策\n用户 : 提供需求\n大模型 LLM : 负责决策，先做什么 后做什么\nAgent : 负责 Tool 的调度，和 LLM 调度\nTool : 实际处理解决 问题的操作\n具体来说，当你给 Agent 下一条指令：「帮我读取 C 盘目录下的 hello_world.cpp 文件，移动到 D 盘目录下，最后给我总结一下这个文件的核心内容」，整个执行流程是这样的：\nAgent 把需求 + 工具清单打包，发给大模型 ：「用户想做这件事，你有这些工具可以用，第一步该怎么做？」 大模型做决策，返回指令 ：「调用【读取文件】工具，路径是 C://hello_world.cpp」 Agent 执行指令，调用工具 ：真正去磁盘上读文件 Agent 把结果回传给大模型 ：「文件读取成功，内容是……」 大模型根据结果，决定下一步 ：「好，现在调用【移动文件】工具，把它移到 D 盘」 循环执行，直到任务完成 ：所有步骤跑完，大模型生成最终总结 Agent 把结果反馈给你 ：任务完成 你看，这整个过程里，「决策」始终在大模型，「执行」始终在工具，而 Agent 就是那个来回传话、推进任务的协调员。 Tool 我们在写 skills 的时候，经常会用到 python 处理脚本\n那么 python 代码就是 tool 吗\nTool 的四要素 函数本体 函数本体 ： 真正干活的代码\nname (名称) : 大模型的识别标签\ndescription (描述) ： 整个工具定义最重要的字段\nparameters (参数定义) : 告诉大模型我们应该怎么填参数\n函数本体 :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # ===== 第一部分：函数本体（大模型看不到，Agent 负责执行）===== import requests def get_weather(city: str, date: str) -\u0026gt; dict: \u0026#34;\u0026#34;\u0026#34;调用天气 API，查询指定城市指定日期的天气\u0026#34;\u0026#34;\u0026#34; response = requests.get( \u0026#34;https://api.weather.com/v1/forecast\u0026#34;, params={\u0026#34;city\u0026#34;: city, \u0026#34;date\u0026#34;: date} ) data = response.json() return { \u0026#34;city\u0026#34;: city, \u0026#34;date\u0026#34;: date, \u0026#34;weather\u0026#34;: data[\u0026#34;condition\u0026#34;], # 晴/多云/雨 \u0026#34;temperature\u0026#34;: data[\u0026#34;temp_c\u0026#34;], # 摄氏度 \u0026#34;humidity\u0026#34;: data[\u0026#34;humidity\u0026#34;] # 湿度百分比 } 工具说明书 包含 name + description + parameters\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // ===== 第二三四部分：工具说明书（大模型看到的部分，靠这个决定要不要调用）===== { \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;查询指定城市在指定日期的天气情况。当用户询问某地天气、出行建议、是否需要带伞等问题时使用此工具，返回天气状况、气温和湿度信息。\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;city\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;要查询天气的城市名称，例如：北京、上海、广州\u0026#34; }, \u0026#34;date\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;查询日期，格式：YYYY-MM-DD，例如：2025-03-21\u0026#34; } }, \u0026#34;required\u0026#34;: [\u0026#34;city\u0026#34;, \u0026#34;date\u0026#34;] } } ","date":"2026-07-18T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-agent-0x04/","title":"[all_in_ai]  AI 名词扫盲 Tool"},{"content":"Gin 入门 Gin-Engine 在 Gin 中, 一个 Web 服务器被抽象成为 Engine\nEngine 承担了路由器注册、接入 middleware 的核心职责\n1 2 3 4 5 6 7 8 9 func main() { server := gin.Default() } type Engine struct { // ... RouterGroup // ... } Gin-Context Gin 封装的上下文, 其中 Request 和 Writer 负责 处理请求 和 返回相应\n1 2 3 4 5 6 7 8 9 type Context struct { writermem responseWriter Request *http.Request Writer ResponseWriter Params Params handlers HandlersChain // .. } 对于 GET 的参数我们使用 query 进行获取参数\n1 code := ctx.Query(\u0026#34;code\u0026#34;) 对于 POST 的参数 我们使用 Bind 进行获取参数\n1 2 3 4 5 6 type Req struct { Code string `json:\u0026#34;code\u0026#34;` Phone string `json:\u0026#34;phone\u0026#34;` } var req Req if err := ctx.Bind(\u0026amp;req); err != nil { Gin 路由注册 在 Engine 的 RouterGroup 支持注册 Restful 的接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // ... func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPost, relativePath, handlers) } // GET is a shortcut for router.Handle(\u0026#34;GET\u0026#34;, path, handlers). func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodGet, relativePath, handlers) } // DELETE is a shortcut for router.Handle(\u0026#34;DELETE\u0026#34;, path, handlers). func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodDelete, relativePath, handlers) } // ... 基本路由 跨域请求 前端端口是 localhost:3000 请求到了后端 localhost:8080\n对于域名和端口任意一个不同都是跨域请求\n跨域请求问题 是浏览器那边进行控制的\n每次浏览器请求前，会提前发送一个 Option 的方法 访问 allow-origins,allow-headers,allow-method\n如果不在允许范围内就会失败\n中间件 对于请求 发送到 实际业务逻辑 。 前后插入的额外处理即是 middleware\n也就是 AOP编程 。\n最常见的 AOP 就是\n1 2 3 4 5 beforeHook() do() afterHook() 限流start --\u0026gt; 熔断start --\u0026gt; handler --\u0026gt; 熔断end --\u0026gt; 限流end Gin 插件支持使用 Use 进行中间件的注册\n1 2 3 4 func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() } 面试 什么是 Gin 的 middleware ？ 能用来解决什么问题 ？ 什么是跨域问题 ，怎么解决？ 跨域问题需要设置哪些头部？ 最后 设计并实现一个 Gin插件库\n使用 middleware 机制实现 Gin 插件库支持\nWeb 治理 ： 熔断限流降级 可观测性 : 包括日志、metrics、tracing 身份认证与鉴权 : ","date":"2026-07-17T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/webook-summary-0x02/","title":"[webook-tidy] Gin \u0026 基本路由"},{"content":"Gorm Orm (Object Relational Mapping) : 对象关系映射 。 Orm框架 提供 不需要关注 实体对象 与 数据库之间的操作关系\nDocker Compose docker compose 启动 mysql8\n基本语法 :\nservices : 服务列表 image : 镜像 command : 启动命令 (启动命令行参数) volumes : 挂载文件 ports : 端口映射关系 1 2 3 4 5 6 7 8 9 10 11 12 version: \u0026#39;3.7\u0026#39; services: mysql8: image: mysql:8.0.29 command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: root volumes: - ./script/mysql/:/docker-entrypoint-initdb.d/ ports: - \u0026#34;13316:3306\u0026#34; 基础命令 :\ndokcer compose up : 初始化 dokcer-compose 并且懂 docker compose down : 删除 docker-compose 里面创建的容器\n项目结构 Service : 代表 领域服务 完整的业务的处理过程\nRepository : 按照 DDD 的说法 是代表领域对象的存储 ，可以认为是存储数据的抽象\nDao : 代表数据库操作\ndomain : 代表领域对象\n1 2 3 4 5 6 7 8 9 ├── domain │ ├── user.go ├── repository │ ├── cache │ ├── dao │ └── user.go ├── service 这里简单提一下 :\n我们公司项目的调用逻辑是 :\n每一层都有自己相同的 user , 从这里看\nhtv2-db 操作代表 dao exgv2 层代表 domain (和 db 弱关联，但是算是底层数据) cefiacc 即是 Repository ec代表 service 封装给其他组进行调用 1 ec --\u0026gt; cefiacc --\u0026gt; exgv2 --\u0026gt; htv2(db) 一些疑问 为什么有了 repository 之后 还要有 dao ?\nrepository 是一个整体抽象，本质上可以考虑用 ES或者是Mysql或者是 mongoDB 并不代表数据库 HTTP 无状态协议 HTTP HTTP 是无状态协议，对于连续发送的两次请求, HTTP 并不知道两次都是同一个人发的\n只能引入 cookie 和 session 进行记录\nCookie 浏览器存储一些数据到本地，这些数据就是 Cookie\n因为 Cokkie 是放在浏览器本地的，所以很不安全\n一些关键配置 :\nDomain : cookie 可以用在什么域名下\nPath : cookie 可以用在什么路径下\nMax-age 和 Expires : 过期时间\nSecure : 只用于 HTTPS 协议\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func SetCookie(w http.ResponseWriter, r *http.Request) { cookie := http.Cookie{ Name: \u0026#34;access_token\u0026#34;, MaxAge: 3600, Path: \u0026#34;/\u0026#34;, HttpOnly: true, Secure: true, } // send cookie via header http.SetCookie(w, \u0026amp;cookie) fmt.Fprintln(w, \u0026#34;Cookie has been set!\u0026#34;) } Session 由于 Cookie 本身不安全的特性，所以大部分 我们不把一些关键信息放在 Cookie\n关键数据我们希望放在后端，这个存储的东西就是 session\n1 2 3 4 5 6 sess := sessions.Default(ctx) sess.Set(\u0026#34;userId\u0026#34;, user.Id) sess.Options(sessions.Options{ MaxAge: 20, }) sess.Save() 个人理解 :\nsession也离不开本地化存储，大部分都是放在 cookie中 如果禁用了cookie也会考虑放在header或者是query中 使用session无非是把敏感信息的相关校验放在了后端 面试 什么是 cookie 什么是 session ? cookie 和 Session 比起来有什么缺点 Session ID 可以放在哪里 ？ 用户密码加密算法选取有什么注意事项 怎么做登陆校验 后续 增强扩展 GORM 功能\n为 GORM 提供可观测性的插件实现 为 GORM 提供读写分离插件 为 GORM 提供 BeforeFind 功能 为 GORM 提供辅助方法 ","date":"2026-07-17T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/webook-summary-0x03/","title":"[webook-tidy] Gorm \u0026 用户基本功能 \u0026 docker"},{"content":"场景问题 刷新登陆状态 对于一个设置了 10分钟过期时间的 session，我们应该如何采用刷新策略才能够保证 用户在用着用着就要重新登陆\n用户每次访问，我们都刷新\n性能差，对 Redis 之类的影响很大 。 小公司看不出来，但是如果是 10w or 100w用户级别 快要过期了我在刷新，例如 10分钟过期，用户9分钟过来访问的时候 刷新\n如果用户第9分钟没有过来 那就寄了 因此只能考虑使用 固定时间间隔刷新,比如 每分钟内的第一次访问我们都刷新\n即\n在sesssion中维护一个 updateAt 的参数 然后在 middleware 中进行更新 JWT JWT (Json WEB Token) : 很常见的一种机制，主要用于身份认证\n基本原理就是通过 加密生成一个 token ,每次访问都带上这个token\n组成部分 :\nHeader : JWT 的元数据 Payload : 数据内容 Signature : 签名 基本流程如下 :\n使用 NewWithClaims 设定指定的算法 并且传入对应的 userCliams 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (j *jwtHandler) setJWTToken(ctx *gin.Context, uid int64) error { userClaims := UserClaims{ RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 30)), }, UserId: uid, UserAgents: ctx.Request.UserAgent(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims) tokenStr, err := token.SignedString([]byte(\u0026#34;secret\u0026#34;)) ctx.Header(\u0026#34;x-jwt-token\u0026#34;, tokenStr) //. .. } 校验方通过同样的 sign-method 和 secret 校验是否正确\nJWT 的优缺点 和 Session 比起来 优点 :\n不依赖第三方存储 适合用于分布式环境 提高性能 缺点 :\n对加密依赖非常大 最好不要在 JWT 中存储敏感信息 保护系统 如果有一个人，使用脚本进行大量的发送注册、登陆请求 ，系统负载会提高 所以这时候需要考虑保护系统\n最简单的一个办法就是 限流\n限流 如何在登陆之前确定限流\n我应该怎么认定 是哪个用户发送了什么请求 ？ 我应该怎么确定我限流的阈值时多少 ？ 限流对象 IP 是最容易获取的限流方式之一\n其中更好的选择还有 MAC 地址 以及 CPU序列号 但是在 web端很难获取\nAPP端端话可以考虑设备序列号\n不过 IP 并不能实际意义上代表一个人，因为两个人可以共用一个 ip 但是这已经是较好的一个选择了\n限流阈值 限流阈值 时通过 压测 得到的 。 例如 压测 了整个系统，发现最多只能撑住每秒 1000个请求，那么阈值就是 1000\n安全 不管是 JWT 还是 Session 一旦被攻击者拿到 关键的 JWT 或 SSid 攻击者就会假冒你\nHTTPS 可以有效的阻止攻击者拿到你的 JWT 或者是 ssid\n但是如果你电脑中了病毒，那 HTTPS 也无能为力\n最好的解决办法是做一个 二次校验，即增加 发邮件, 发短信 的形式进行控制\n或者是增加一些额信息 例如 HTTP 的User-Agent头部，或者是手机硬件信息\n面试 刷新 Session 过期时间的几种可行的办法\n增强登陆的安全性\n怎么保护 session id ? 主要还是开启 HTTPS 协议，把 cookie 的 Secure 和 HttpOnly 设置为 true 怎么做到在 Session id 或者是 JWT token 泄漏之后保护住用户？ 要在登陆的时候记录一下 登陆的附加信息，例如 user-Agent 在登陆校验的时候统一校验 如何保护 Web 服务 ？ 针对 IP 限流、整个集群限\n后续 支持 Gin 的限流插件 :\n单机限流 :\n令牌桶限流 漏桶限流 滑动窗口限流 固定窗口算法 分布式限流 :\n基于 Redis 限流 给予 Redis 的 IP 限流 ","date":"2026-07-17T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/webook-summary-0x04/","title":"[webook-tidy] Session \u0026 JWT \u0026 Security"},{"content":"欧拉函数 什么是欧拉函数\n对于一个数 N, phi(N) = x 其中 x 代表 1~n 中与n 互质的个数\n互质的意思就是 n 与 a 没有公因子\n算法思路 我们知道一个数 n = p1^a1 * p2^a2 ... pn^an\n即对于 p1 有 x1 个数在 1~n中不是互质的因为他是因子，那么不互质的个数就是 y1 = n - n/p1 算个倍数\n同理可得其他质因子的算法 从而推出 phi = n * (1 - 1/p1) * (1- 1/p2)... (1-1/pn)\n代码 1 2 3 4 5 6 7 8 9 10 11 12 int phi(int x) { int res = x ; for(int i = 2; i \u0026lt;= x/ i ; i ++ ) { if(x % i == 0 ) { res = res - res/i; while(x % i == 0 ) x/=i ; } } if(x \u0026gt; 1) res = res - res / x ; return res; } 时间复杂度 sqrtN\n","date":"2026-07-16T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x11/","title":"[Acwing] 基础算法(十一) 欧拉函数"},{"content":"约数 约数的概念 : 能被整除的数叫做约数 即 d|n ,d是 n 的约数\n试除法求约数 算法思路 我们在 试除法求质数 的时候，知道 如果d|n那么d/n | n 因为一个数的约数最小都是 2倍\n所以我们可以通过 for(int i = 2; i \u0026lt;= x/i ; i ++ ) 的方式寻找 约数\n不过需要注意的是 对于 1 可以是任何数的约数\n所以我们这里的循环需要从 1 开始取\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 vector\u0026lt;int\u0026gt; getDiv(int x){ vector\u0026lt;int\u0026gt; ans ; for(int i = 1; i \u0026lt;= x/ i ; i ++ ) { if(x % i == 0) { ans.push_back(i); if(i != x /i ) { ans.push_back(x/i); } } } sort(ans.begin(),ans.end()); return ans ; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin\u0026gt;\u0026gt;n; while(n -- ) { int x;cin\u0026gt;\u0026gt;x; auto ans = getDiv(x); for(auto res : ans) { cout\u0026lt;\u0026lt;res\u0026lt;\u0026lt;\u0026#34; \u0026#34;; } cout\u0026lt;\u0026lt;endl; } return 0; } 时间复杂度 O sqrtN\n约数个数 算法思路 我们知道一个数可以拆分成 n = p1^a1*p2^a2*p3^a3...pn^an 这是质因数分解的章节学到的\n同时我们可以知道 对于 p1 的幂，取值有 0~a1 种，因为剩下的p2...pn可以被整除\n所以根据排列 我们知道约数个数 为 (a1+1)*(a2+1) ... (an+1)\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int n; cin\u0026gt;\u0026gt;n; unordered_map\u0026lt;int,int\u0026gt; primes;//映射函数 while(n--) { int x; scanf(\u0026#34;%d\u0026#34;,\u0026amp;x); for(int i=2;i\u0026lt;=x/i;i++) while(x%i==0) { primes[i]++; x/=i;//方便求得约数的数量 } if(x\u0026gt;1) primes[x]++;//x的最大公约数可能大于sqrt(x); } long long res=1; for(auto p:primes) res=res*(p.second+1)%mod;//将统计出来的数按照由图中公式所得出来的结论得出答案 printf(\u0026#34;%lld\\n\u0026#34;,res); 时间复杂度 o sqrtN\n约数之和 算法思路 题目求的是 所有约数乘积的和\n即 (p0^a1 + p1^a2 .... pn^an+1) * (p?1^b1 + p?2^b2 ... p?n^bn)\n根据乘法原理我们可以提取出来\n(p0^0 + .. p0 ^(a1 + ???) ) * (pk^0 + ...) 的形式\n因此我们只需要做一次质因数分解，然后根据次数进行求质数幂次的乘积和即可\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include \u0026lt;bits/stdc++.h\u0026gt; #define LL long long using namespace std; const int N = 1e6+10, mod = 1e9+7; int n; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin\u0026gt;\u0026gt;n; unordered_map\u0026lt;int,int\u0026gt; mp ; while(n -- ) { int x;cin\u0026gt;\u0026gt;x; for(int i = 2; i \u0026lt;= x / i; i++) { while(x % i == 0) { x /= i; mp[i]++; } } if(x \u0026gt; 1) mp[x]++; } LL ans = 1; for(auto x : mp) { LL t = 1; int a = x.second ; while(a -- ) { t = (t * x.first + 1) % mod ; } ans = ans%mod * t %mod ; } cout\u0026lt;\u0026lt;ans\u0026lt;\u0026lt;endl; return 0; } 时间复杂度 o sqrtN\n最大公约数 算法思路 我们知道 如果 x | a 并且 x | b 那么 x | a * y + b * k\n同理对于 gcd(a,b) = gcd(b, a % b) 为什么等式成立 ？ 因为 x | b 那么 x | a + c * b 这里的 c 就是 -a/b\n因为 x | a + c *b - c*b 等式成立 所以 x|a成立\n代码 1 2 3 int gcd(int a,int b) { return b ? gcd(b , a %b) : a; } 时间复杂度 on\n","date":"2026-07-15T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x10/","title":"[Acwing] 基础算法(十) 约数"},{"content":"名词扫盲\nAgent 对于 llm 和 prompt 他都只能进行 ask 操作，并不能干，只是能输出\n什么是 Agent Agent (智能体) : 能够自主感知环境，作出决策，调用工具，完成多步骤任务的程序\n自主（Autonomously） ：不需要人在每一步发指令。你给它一个目标，它自己判断下一步做什么 多步骤（Multi-step） ：一个任务可能要走 5 步、10 步，Agent 自己把这些步骤串起来 工具调用（Tool use） ：调用真实的函数，查数据库、搜网页、发通知、写文件，不只是\u0026quot;说说\u0026quot;，而是真的去做 一句话总结： Agent = 大模型（大脑）+ 工具（双手）+ 执行循环（思考-行动-观察-再思考）\n为什么需要 Agent 之前我们说过大模型的三大短板：\n没有执行能力 ：只能生成文字，不能操作外部系统\n知识有截止日期 ：不知道最新数据，看不到你的私有系统\n没有持久记忆 ：每次调用对它都是全新的，不能积累任务中间状态\nAgent 的核心组成 大模型（LLM），大脑 负责理解任务、分析当前状态、决定下一步做什么。所有的\u0026quot;推理\u0026quot;都在这里发生，该查什么、查完之后怎么解读、下一步往哪走，全是大模型来判断。\n工具（Tools），双手 一个个可以被调用的真实函数：查数据库、调用搜索引擎、读写文件、发通知、调用 API…… 大模型告诉 Agent \u0026ldquo;调用这个工具、传这些参数\u0026rdquo;，代码层面真正去执行。工具的结果会返回给大模型，供它继续推理。大模型不\u0026quot;直接\u0026quot;操作外部系统，它通过工具来\u0026quot;动手\u0026quot;。\n记忆（Memory），记事本 短期记忆 ：当前任务里的对话历史 + 每次工具调用的结果，存在 context window 里。Agent 执行第 5 步时，\u0026ldquo;记得\u0026quot;第 1 步查到了什么，靠的就是这个 长期记忆 ：跨会话需要记住的信息，存在外部数据库，需要时检索出来注入 context 为什么需要记忆？多步骤任务里，每一步的结果都是下一步判断的依据。没有记忆，每步都是\u0026quot;从零开始\u0026rdquo;，Agent 根本没法串联多步骤任务。\n执行循环（Loop），节拍器 Agent 的\u0026quot;引擎\u0026quot;。不断重复\u0026quot;思考 → 行动 → 观察\u0026quot;这个循环，直到任务完成。没有这个循环，Agent 只是一次性的问答，无法串联多步骤。这个循环是 Agent 和普通大模型调用之间最根本的区别。\nAgent 是怎么工作的. ReAct 循环 目前主流的 Agent 工作范式叫做 ReAct 即 Reasoing+Acting 即 推理 + 行动 + 观察 + 在推理 ..\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 用户：帮我调查今晚 23:00 的数据库报警 [第 1 轮] Think：需要先拿到具体的报警详情 Act：调用 get_alarm_details(time=\u0026#34;23:00\u0026#34;) Observe：返回\u0026#34;连接数突增，连接池耗尽，持续 8 分钟后自动恢复\u0026#34; [第 2 轮] Think：连接数突增，需要看是哪些查询造成的 Act：调用 query_slow_log(timerange=\u0026#34;22:50-23:10\u0026#34;) Observe：发现 3 条全表扫描的慢查询，来自同一个用户 ID [第 3 轮] Think：根因已经清楚，生成分析报告 Act：输出最终分析结论（Final Answer） 几个关键机制：\n结果回流 ：每一轮的工具调用结果都会追加到 context window 里，大模型下一轮能\u0026quot;看到\u0026quot;所有历史。所以它能基于已有发现继续推理，而不是每次都从零开始。这就是为什么前面\u0026quot;记忆\u0026quot;那个组件如此重要。\n动态决策 ：每一步该怎么走，是大模型实时推理出来的，不是你提前写好的。发现是慢查询，它就去查慢查询日志；如果发现是网络问题，它就会去查网络相关的工具。路径是活的，不是死的。\n何时停止 ：大模型判断任务已完成时输出\u0026quot;最终答案\u0026quot;，或者达到预设的最大轮数上限。两个条件任意满足一个，循环结束。 这就是\u0026quot;自主\u0026quot;的含义，你给任务目标，它自己决定怎么走到终点。\nAgent 的另一个工作模式 Plan and Execute ReAct 很好用，但有一个先天的弱点。\nReAct 是「边想边做」，每一步只看眼前，这一步的结果决定下一步的方向。对于 2-3 步能解决的短任务，这完全没问题。但如果任务很复杂、步骤很多呢？\n问题一：长任务容易「迷路」\nReAct 跑了 8 轮之后，context window 里已经堆满了历史操作记录。大模型要在这一大堆信息里同时维持「当前在第几步」「最终目标是什么」「还缺哪些信息」，注意力越来越稀释，越往后越容易偏离最初的目标。\n问题二：容易绕弯路\n没有全局规划，每步只看眼前，就像在迷宫里随机探索，走了 5 步之后才发现方向不对，只能回头重来，白白消耗了好几轮 LLM 调用。\n问题三：token 消耗滚雪球\nReAct 每一轮都要把完整的对话历史带上。步骤越多，每轮的 context 越长，后期每次 LLM 调用的 token 消耗都很高，成本直线上升。\n这三个问题催生了另一种工作模式： Plan and Execute（规划-执行模式） 。\n核心思路用一句话说清楚： 先让大模型把整个任务规划成一份清单，再逐步按清单执行，而不是走一步看一步。\nPlan and Execute 的两个阶段 第一阶段：Planner（规划阶段）\n大模型拿到任务，先不调任何工具，专注做一件事： 把任务完整拆解成一份有序的子任务清单 。\n1 2 3 4 5 6 7 8 用户：「帮我调查今晚 23:00 的数据库报警，写一份完整的故障分析报告」 Planner 输出的计划： 步骤 1：获取 23:00 报警的详细信息（报警类型、持续时间、影响范围） 步骤 2：查询报警时间段内的慢查询日志，定位异常 SQL 步骤 3：查询报警时间段内的数据库连接数、CPU、内存指标 步骤 4：关联步骤 2 和步骤 3 的结果，分析根因 步骤 5：生成完整的故障分析报告，包含时间线、根因和改进建议 第二阶段：Executor（执行阶段）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 执行步骤 1： → 调用 get_alarm_details(time=\u0026#34;23:00\u0026#34;) ← 返回「连接数突增，连接池耗尽，持续 8 分钟后自动恢复」 执行步骤 2： → 调用 query_slow_log(timerange=\u0026#34;22:50-23:10\u0026#34;) ← 发现 3 条全表扫描的慢查询，来自同一个用户 ID 执行步骤 3： → 调用 get_db_metrics(timerange=\u0026#34;22:50-23:10\u0026#34;) ← CPU 正常，连接数峰值达到上限 500，内存无异常 执行步骤 4： → 大模型综合步骤 2、3 结果进行分析 ← 结论：慢查询导致连接长时间占用，连接池耗尽触发报警 执行步骤 5： → 生成故障分析报告 ← 完整报告输出，任务完成 Re-planning （重新规划） 执行中途如果遇到了计划没预料到的情况，比如步骤 2 查不到慢查询，但发现了大量锁等待，Executor 可以把这个新信息反馈给 Planner，重新生成后续步骤的计划，而不是一条路走到黑。\n1 2 3 4 5 6 7 8 9 10 11 12 13 用户任务 ↓ [Planner] 大模型一次性生成完整计划 ↓ 执行计划 = [步骤1, 步骤2, 步骤3, 步骤4, 步骤5] ↓ [Executor] 按顺序执行每个步骤 步骤1 → 调工具 → 结果 步骤2 → 调工具 → 结果 ← 遇到意外？触发 Re-planning，重新调整后续计划 步骤3 → 调工具 → 结果 ... ↓ 最终结果汇总 → 输出给用户 ReAct vs Plan and Execute：怎么选？ 对比维度 ReAct Plan and Execute 决策时机 每一步实时决策 开始前一次性规划 全局视野 弱（只看当前这步） 强（提前看到全貌） 灵活性 强（随时根据结果调整） 弱（计划一旦生成，调整成本高） 适合任务步骤 少（3 步以内） 多（5 步以上的复杂任务） token 消耗 随步数线性增长，后期很贵 规划阶段集中消耗，执行阶段可控 实现难度 低（结构简单） 中（需要管理计划状态和 Re-planning 逻辑） 简单记忆：\n任务步骤少、目标模糊、需要随机应变 → 用 ReAct\n任务步骤多、目标明确、需要全局把控 → 用 Plan and Execute\n系统复杂、两者都需要 → Plan and Execute 做外层框架，每个子任务内部跑 ReAct\n最后这种「外层 Plan and Execute + 内层 ReAct」的搭配，在实际生产系统里最常见，用规划保证大方向不跑偏，用 ReAct 保证每个子任务执行时足够灵活。\nAgent vs Workflow Workflow（工作流）是你提前写好所有步骤和分支逻辑的流程：先做 A，再做 B，如果 B 的结果是 X 就走流程 C，否则走流程 D。大模型只是其中某个步骤里被调用一次，整体流程是硬编码的。\nAgent 是你只给任务目标，大模型自己实时决定每一步做什么、调用什么工具、根据结果决定下一步。执行路径是动态的，每次运行可能走不同的路径。\n对比维度 Workflow Agent 决策者 你（写死的代码逻辑） 大模型（实时推理） 执行路径 固定 动态 适合场景 步骤明确、重复性高 任务开放、情况多变 可预测性 高 低 成本 低 高（多轮 LLM 调用） 两者没有高下之分，适合不同的场景：\n流程能写清楚、步骤是固定的 → 优先用 Workflow ，更稳、更省钱、更可预期\n任务是开放式的、需要根据中间结果动态决策 → 用 Agent\n最常见的实际架构： Workflow 做骨架，Agent 负责其中需要判断的环节 ，两者结合，不是非此即彼\n一个真是的 agent 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def run_agent(user_message): messages = [ {\u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: SYSTEM_PROMPT}, {\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_message}, ] while True: # 1. 调用大模型，让它思考下一步 response = llm.call(messages) # 2. 如果大模型说\u0026#34;我完成了\u0026#34;，退出循环 if response.is_final_answer: return response.content # 3. 否则，执行大模型指定的工具 tool_result = execute_tool(response.tool_name, response.tool_args) # 4. 把工具结果追加到 messages，供下一轮参考 messages.append({\u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34;, \u0026#34;content\u0026#34;: tool_result}) 总结 整理一下这一章的核心认知：\nAgent 是什么 ：以大模型为大脑，能自主调用工具、完成多步骤任务的程序。让大模型从\u0026quot;只能说\u0026quot;变成\u0026quot;能干\u0026quot;。\n核心组成 ：大模型（大脑）+ 工具（双手）+ 记忆（记事本）+ 执行循环（节拍器）\n核心机制：Think → Act → Observe 循环，每轮工具结果都回流给大模型，直到完成\n和 Workflow 的本质区别 ：决策者不同，Workflow 是你提前写好的代码逻辑，Agent 是大模型实时推理。两者不是竞争关系，实际系统里经常结合使用\n","date":"2026-07-15T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-agent-0x03/","title":"[all_in_ai]  AI 名词扫盲 agent"},{"content":"名词扫盲\nPrompt Prompts (提示词) ; 对于同一个 LLM , 不同的人用 结果天差地别，而 prompt 就是为了 统一 。\n可以是 :\n具体的指令 具体的问题 具体的背景 具体的格式要求 1 2 3 4 messages = [ { role: \u0026#34;system\u0026#34;, content: \u0026#34;你是一个 OnCall 助理，回答必须简洁\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;昨晚数据库报警是什么原因？\u0026#34; }, ] system和user 都是 prompt 的一部分\n为什么 prompt 很重要 llm 本质上是一个 next-token 的预测及其 。 对于模糊的上下文，他往哪里走的方向就越多，输出的就越随机\n但是给的上下文越精确，它搜索的范围就越窄\nuser prompts 大部分 Ai 对话 都是 user protmps\n好的 User prompts 的三要素 :\n目标明确 ：要做什么？分析、总结、改写、生成代码，得明说。\u0026ldquo;帮我看看这个日志\u0026quot;不是目标，\u0026ldquo;帮我分析这段日志里的报错原因并给出排查方向\u0026quot;才是。 背景充足 ：大模型不知道你的业务、你的系统架构、你的团队惯例，它只能靠你给的信息推断。背景给得越充分，它越不需要乱猜，幻觉越少。 输出要求 ：格式、长度、风格，不说，它随心所欲。你想要 Markdown 列表？想要 JSON？想要 3 条以内？一定要在 Prompt 里讲清楚。 场景对比 :\n❌ 简单版 ：「数据库怎么了？」 大模型不知道你说的是哪个数据库、什么时间发生的、有什么报警信息、你想要什么样的答案。\n✅ 完整版 ：\n1 2 3 4 5 以下是今天凌晨 2 点的数据库报警日志： [日志内容] 请分析可能的根本原因，并给出 3 条排查建议，以 Markdown 列表格式输出。 System Prompt 用户层面看不到 system prompt 这是开发提前写进去的一个 底层指令\nsystem prompts 的三个核心用途 :\n身份设定 ：给模型一个具体的角色 1 2 你是一个 OnCall 助理，专门帮助工程师排查和分析系统故障。 你只处理与系统稳定性、报警分析、故障排查相关的问题。 行为规则：定义模型\u0026quot;能做什么、不能做什么\u0026rdquo; 1 2 3 4 不允许回答与系统故障无关的话题。 如果你不确定某个判断，必须说明\u0026#34;我不确定，建议进一步验证\u0026#34;。 不要自行假设任何背景信息，只根据用户提供的内容作答。 输出格式约束 ：让模型每次都按固定格式输出 1 2 3 4 你的回答必须是 JSON 格式，包含以下字段： summary：对问题的一句话概括 root_cause：可能的根本原因列表 action_items：建议的排查步骤列表 如何写好一个 pormpts 给模型\u0026quot;角色\u0026rdquo; 正向约束 \u0026gt; 负向约束 ❌不要太啰嗦 ✅回答控制在3句话以内 喂足够的信息 指定输出格式 Few-shot . 给几个参考例子 让模型先思考再回答 请先逐步分析，再给出最终结论 ","date":"2026-07-15T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/ai-prompt-0x02/","title":"[all_in_ai]  AI 名词扫盲 prompt"},{"content":"名词扫盲\n大模型 什么是大模型 LLM (large language Model) 大语言模型。平时说的 [AI 对话] [AI 助手] 都是大模型\n大的体现 :\n参数量大 : 模型内部有数千亿个参数 训练数据大 根据给的上下文，预测接下来最合理的文字是什么\n大模型读文字的基本方式 Token 是大模型处理文本的 基本单位 。LLM 会把一句话拆分成多个 token 进行读取\n对于一句话 hello world ，通常会被切分为 [\u0026quot;hello\u0026quot;, \u0026quot;world\u0026quot;] ，大约一个单词对应一个token\nToken :\n决定模型能处理的文本长度上限 : 最常见的就是 200k 决定 API 调用花费 上下文窗口 上下文窗口 ，限制了 token 的量。 跟达模型聊的越久，越容易忘记前面发生的事\nGPT-4 有 128K token 窗口，大约 10w个汉字 Claude 3.5 有 200k 的 token 窗口 不过上下文窗口并不只是包含你询问的数据，它包含 你发出去的所有消息+模型的所有回复+调用工具的结果\n大模型是无状态的 LLM 本身没有任何记忆，每次调用 APi 对他来说都是第一次见面\n为什么我们在网页端 Gemini 和 Gpt 上面聊天都能记住前面的话，是因为在应用层，代码已经处理了记忆\n每次我们发送消息，他都会把历史对话打包成为一个列表传给大模型\n这就引入了两件事 :\n为什么上下文窗口会写满 : 历史对话越多，上下文窗口会更快的被占满 为什么 Agent 需要管理记忆 : Agent 在执行长任务时，必须主动决定 把哪些历史传进去，不然窗口很快就会塞满 对话的角色结构 : system,user,assistant 既然多轮对话是靠 传历史列表实现的，那么列表长什么样\n调用大模型 API 时，你传入的不是一段裸文字，而是一个有结构的消息列表，每条消息都有一个 角色（role） ：\nsystem ： 系统提示 ，给模型的\u0026quot;身份设定和行为规则\u0026quot;。比如「你是一个 OnCall 助理，只能回答和系统故障相关的问题，回答必须简洁」。这部分用户看不见，但模型会严格遵守 user ： 用户的输入 ，就是你说的话 assistant ： 模型之前的回复 ，多轮对话时，把历史回复也打包进来，模型才知道\u0026quot;之前说了什么\u0026quot; 用伪代码表示，大概长这样： 1 2 3 4 5 6 messages = [ { role: \u0026#34;system\u0026#34;, content: \u0026#34;你是一个 OnCall 助理，回答必须简洁\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;昨晚数据库报警是什么原因？\u0026#34; }, { role: \u0026#34;assistant\u0026#34;, content: \u0026#34;是慢查询导致的连接池耗尽。\u0026#34; }, { role: \u0026#34;user\u0026#34;, content: \u0026#34;怎么预防？\u0026#34; }, // 当前问题 ] https://cloud.tencent.com/developer/article/2643849\n大模型能帮我们做什么 使用自然语言，分析意图 会进行逻辑思考，拆解任务和做决策 生成符合要求的内容 看懂规则并且执行 大模型做不到什么 没有执行能力, 只能通过 agent+tool的方式进行调用 知识有保质期，容易瞎编 上下文窗口有限，记不住太长的内容 开发场景 在理解完上面的概念之后，我们可以知道 agent 开发 就是给 llm 增加 Tool 调用,funciton call ,MCP 并且 加上 RAG (知识库) 进行应用开发\n","date":"2026-07-15T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/al-llm-0x01/","title":"[all_in_ai]  AI 名词扫盲 大模型"},{"content":"质数 质数的概念\n对于大于 1 的数，其因数只有他自己和1 那么这个数就是质数 试除法判定质数 算法思路 根据质数的定义出发我们可以很简单的想到 下面的形式，进行枚举来判断出来是否是质数\n1 2 3 4 5 for(int i = ; i \u0026lt;= n ; i ++ ) { if(n%i == 0) { return false ; } } 但是这种算法是 on 的\n我们根据 d | n 那么 d/n | n 可以得到 d \u0026lt; n^2\n这个怎么理解，例如 2 | 8 那么 4 | 8 因为最小的质数是 2 因此肯定存在这种情况\n所以我们可以考虑 优化 i 的枚举, 从而达到 sqrt(n) 的时间复杂度\n1 2 3 4 5 for(int i = ; i \u0026lt;= n/ i ; i ++ ) { if(n%i == 0) { return false ; } } 时间复杂度 On\n分解质因数 算法思路 我们需要知道对于一个数 n 他可以拆成多个质数p幂的乘积 即 n = p1^a1 * p2^a2.. pn^an\n我们需要分解质因数，只需要从小到大枚举出能够整除的数即可,因为我们肯定会先遇到2，3这个质数 ，除完之后发现剩下的数也都是质数\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void SplitPrime(int x) { for(int i = 2 ;i \u0026lt;= x / i ; i ++ ) { if(n % i == 0 ) { int s = 0 ; while(x % i == 0 ) { x /= i ; s ++ ; } cout\u0026lt;\u0026lt;i\u0026lt;\u0026lt;\u0026#34; \u0026#34;\u0026lt;\u0026lt;s\u0026lt;\u0026lt;endl; } } if(x \u0026gt; 1) { cout\u0026lt;\u0026lt;x\u0026lt;\u0026lt;\u0026#34; \u0026#34;\u0026lt;\u0026lt;1\u0026lt;\u0026lt;endl; } } 时间复杂度 logn ~ sqrt(n) 之间\n筛质数 埃及筛 算法思路 我们知道一个质数 i ，他的倍数 2*i,n*i 的因数肯定是 i\n所以我们可以根据这个性质过滤掉 i 的倍数\n代码 1 2 3 4 5 6 7 8 9 10 void filterPrime() { int cnt = 0 ; for(int i = 2; i \u0026lt;= n ; i ++ ) { if(!st[i]) { for(int j = i ; j \u0026lt;= n ; j += i ) st[j] = 1; cnt ++ ; } } cout\u0026lt;\u0026lt;cnt\u0026lt;\u0026lt;endl; } 时间复杂度 o (nlnln)\n线性筛 线性筛是对埃及筛的一个优化，我们每次都用 最小质因子进行优化\n代码 1 2 3 4 5 6 7 8 9 10 11 void filterPrime() { int cnt = 0 ; for(int i = 2; i \u0026lt;= n; i ++ ) { if(!st[i]) prime[++cnt] = i ; for(int j = 1 ; prime[j] \u0026lt;= n / i ; j ++ ) { st[prime[j] * i] = 1 ; if(i % prime[j] == 0) break; } } cout\u0026lt;\u0026lt;cnt\u0026lt;\u0026lt;endl; } 时间复杂度 on\n","date":"2026-07-14T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x09/","title":"[Acwing] 基础算法(九) 质数"},{"content":"函数式编程 Golang 支持定义 方法作为变量即 myFunc = func(){}, myFunc() 的形式\n同样方法也可以作为返回值\n1 2 3 4 5 func Say() func(name string) string { return func(name string)string { return \u0026#34;hello,\u0026#34; + name } } 闭包 closure ： 方法+它绑定的上下文\n组成如下 :\n方法 运行时上下文 : 即 name 变量 1 2 3 4 5 func Closure(name string) func() string { return func() string { return \u0026#34;hello \u0026#34; + name } } 闭包如果使用不当会造成内存泄漏, 因为外层 Func 的name变量被直接使用传递出去了 。 如果一个对象被闭包引用，他是不会被垃圾回收的\n不定参数 Golang 支持不定参数传递即,使用...进行 ，不定参数可以当作切片进行使用\n1 2 3 func hanlder(name string ,alias ...string) { println(alias[0]) } 其中 Option模式 大量应用了不定参数\nDefer Golang 允许你从方法返回前一刻执行一段逻辑\nDefer 的运行逻辑类似于栈即 后进先出\n1 2 3 4 5 6 7 8 func Defer() { defer func() { println(\u0026#34;第一个defer\u0026#34;) }() defer func() { println(\u0026#34;第二个defer\u0026#34;) }() } 输出如下\n1 2 第二个 defer 第一个 defer 问题 defer 有一个 catch 的机制，主要考 for 中的内容，如果是通过闭包传递或者是通过拷贝变量进行copy 他会catch到当前值而不是最后值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func DeferClosureLoopV1() { println() for i := 0; i \u0026lt; 10; i++ { defer func() { print(i) print(\u0026#34; \u0026#34;) }() } } // 预计是 10,10,10,10 拿到的是最后一个 func DeferClosureLoopV2() { println() for i := 0; i \u0026lt; 10; i++ { defer func(val int) { print(val) print(\u0026#34; \u0026#34;) }(i) } } // 预计是 1,2,3... 10 // 实际是 9,8 ,7 ,6 ,5,4,3,2,1 func DeferClosureLoopV3() { println() for i := 0; i \u0026lt; 10; i++ { j := i defer func() { print(j) print(\u0026#34; \u0026#34;) }() } } // 预计是 10，10，10 ？ // 实际上是 9,8,7,6,5,4,3,2,1 基本数据结构 切片 语法 []type ,数组的话是 [int]type\n初始化方式 :\n1 s1 := []int{1,2,3} //创建了一个 3个亚孙的切片 1 2 s1 := make([]int,3,4) // 初始化 3个元素，这时候访问 s1[0] = 0 s2 := make([]int,4) // 创建一个 4个元素的切片， 这时候访问 s1[0] panic 推荐使用 s1 := make([]type,0,cap) 的方式进行创建\n子切片 切片可以通过 [start:end] 的形式获取子切片，其中范围是 左闭右开 的\n内存共享问题 :\n在没有发生扩容的情况下， 子切片 和 切片 是共享一个数组的 即如果 子切改变了一个数，那么原本的切片也会改变 map 初始化方法\n1 2 3 m1 := map[string]string { \u0026#34;key\u0026#34;: \u0026#34;value\u0026#34; } 对于 for k,v := range map 来说，每次结果都是不一样的\n接口 \u0026amp; 结构体 接口是 一组行为的抽象\ntype name interface{}\n结构体\n1 2 3 type a struct { } 衍生类型 如果我们想使用第三方库，但是又没办法修改代码，又想扩展这个库的结构体，我们会使用到这个\n衍生类型只共享字段 并不共享实现的方法\n即 typeB 拥有的方法 , typeA并不能够调用\n1 type typeA typeB 类型别名 不同于 衍生类型， 可以说是一个别名 类型本质上是没有变的\n1 type typeA = typeB 泛型 对于下面这个例子，其中 T 就是泛型可以是任意类型\n1 2 type List[T any] interface { } 泛型语法\n对于 Number 来说就是一个 泛型约束\n1 2 3 4 5 6 7 8 9 10 11 func Sum[T Number](vals ...T) T { var res T for _, val := range vals { res += val } return res } type Number interface { int | int64 } 面试重点 [✅] 什么是闭包？闭包有什么缺陷\n[✅] 什么情况下会栈溢出\n自己循环调用自己 [✅] 什么是不定参数 ？调用方法的时候 不定参数可以传入 0个值吗？方法内部怎么使用不定参数？\n函数参数以 \u0026hellip; 方式进行传递就是不定参数 0值预计是个nil ,会自动初始化 和切片一样使用即可 什么是 defer ? 能解释一下 defer 的运行机制吗 ？ Go 语言提供的一种延迟调用机制。它通常用于处理成对出现的操作，比如打开/关闭连接、加锁/释放锁。无论函数是正常执行结束还是发生了 panic，defer 注册的函数都会被确保执行，这能有效防止资源泄漏。 [已删除] 一个方法内部 defer 能不能超过 8个？\n[待定] defer 内部能不能修改返回值? 怎么改？\n数组和切片有什么区别？\n类型与长度限制：\n数组是定长的，长度是其类型的一部分（[3]int 和 [4]int 是不同类型） 切片是动态的，长度不属于类型的一部分（类型统称为 []T） 底层数据结构：\n数组是一段连续的内存空间，直接存储了所有的元素。 切片本质上是一个引用类型（描述符），它的底层结构包含三个字段：一个指向底层数组的指针（Pointer）、切片的长度（Len）和容量（Cap）。 函数传参的性能开销:\nGo 语言是值传递。如果把数组作为参数传递，会触发整个数组的深拷贝，对于大数组来说内存和 CPU 开销很大。\n传递切片时，只会拷贝切片头部的三个字段（指针、长度、容量），开销非常小。并且在函数内部修改切片的元素，会直接影响到底层的真实数组。\n扩容机制：\n数组创建后不可扩容。切片支持使用 append 函数追加元素，当容量不足时，Go 运行时会自动分配一块更大的新内存，将旧数组的数据拷贝过去，并让切片的指针指向新数组。 切片怎么扩容 ？\n在 256 以下进行双倍扩容 在 256 以上进行 (原容量 + 3 * 256) / 4 即 1.25倍的扩容 最后 了解函数式编程在在业务中的使用 了解内存泄漏，业务上的内存泄漏，排查方法，解决办法 了解垃圾回收的机制 了解 Option 模式 slice 的底层实现 map 的底层实现 defer 的实现原理 和机制 闭包 写出一个 泛型工具支持\n切片的辅助方法 : 添加、删除、查找、求并集、交集、map reduce API map 辅助方法 扩展 map 实现 : 接受任意类型的 HashMap, TreeMap, LinkedMap List 实现 : LinkedList, ArraryList 和 SkipList Set: 包括 HashSet 和 TreeSet, SortedSet 队列: 普通队列,优先队列 bean 操作操作辅助类 : 高性能高扩展的 bean copier机制 并发扩展工具 : 包括并发队列,并发阻塞队列,并发阻塞优先队列 协程池 ","date":"2026-07-14T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/webook-summary-0x01/","title":"[webook-tidy] Golang 基础语法"},{"content":"基础概念 闭包 : 引用了自由变量的函数，即 函数 + 引用环境\n例子 :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func intSeq() func() int { i := 0 return func() int { i++ return i } } func main() { nextInt := intSeq() fmt.Println(nextInt()) fmt.Println(nextInt()) nextInt2 := intSeq() fmt.Println(nextInt2()) } 输出如下\n1 2 3 1 2 1 我们发现，对于程序内定义的变量 i, 他被外部函数引用了 。 并且对于 nextInt 和 nextIn2 是两个不同的环境，他们拥有不同的 i\n怎么理解这个过程 :\n函数的局部变量是分配在栈上面的 但是闭包上的变量会逃逸到堆上 原理如下 :\n对于一个以函数返回到形式的 Func，我们称为 FuncValue 其分配是在堆上 当我们通过其他变量进行接收的时候，会先去堆上找到该 FuncValue 的地址，由于闭包 Capture 了局部变量 所以会在堆上额外分配一块地址 垃圾回收 以下是一些概念\n逃逸 :\n变量生命周期超出声明它的函数栈帧，编译器不得不把它放到 堆 上 GC回收 :\n堆上对象在没人引用后，由 GC 回收；程序结束进程退出时也会一起释放 对于下面这种形式，因为 nextInt 已经输出完成了，虽然main函数没有退出，但是已经没有引用了，虽然产生了逃逸 但是同样会被回收\n1 2 3 4 5 6 7 8 9 10 func main(){ nextInt := intSeq() fmt.Println(nextInt()) fmt.Println(nextInt()) nextInt2 := intSeq() fmt.Println(nextInt2()) for {} // 卡住不让 main 退出 } 只有在循环内一直被引用，以及包级变量引用的时候才不会被回收 。\n1 2 3 for { _ = nextInt() // 循环里还在用 → 仍存活 } 1 2 3 4 5 6 7 var keep func() int // 包级变量 nextInt := intSeq() fmt.Println(nextInt()) keep = nextInt // 挂到全局 → 一直可达 for {} 包级变量理论上来说是和 当前进程一起存活的 。但是如果在引用期间，主动进行解引用同样也会被回收\n1 2 3 4 5 6 7 8 9 10 11 var keep func() int func main() { keep = intSeq() fmt.Println(keep()) keep = nil // 断掉引用 runtime.GC() // 演示用；生产里不必手动调 for {} // 之后闭包就可能已被回收 } 应用场景 隔离数据 如果不想让其他人访问该数据,例如生成一个 斐波那契数列，但是不想让其他人更改其 value\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main() { gen := makeFibGen() for i := 0; i \u0026lt; 10; i++ { fmt.Println(gen()) } } func makeFibGen() func() int { f1 := 0 f2 := 1 return func() int { f2, f1 = (f1 + f2), f2 return f1 } } 封装函数 和 创建中间件 Go 的函数是一等公民，如果我们想要实现一个，接口在运行前后的请求耗时中间件的话\n我们需要使用到闭包进行处理，因为 handleFunc 需要接受一个 FuncValue\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main() { http.HandleFunc(\u0026#34;/hello\u0026#34;, timed(hello)) http.ListenAndServe(\u0026#34;:3000\u0026#34;, nil) } func timed(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() f(w, r) end := time.Now() fmt.Println(\u0026#34;The request took\u0026#34;, end.Sub(start)) } } func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, \u0026#34;\u0026lt;h1\u0026gt;Hello!\u0026lt;/h1\u0026gt;\u0026#34;) } 不过这并不会产生 OOM ，因为端口一直监听，但是堆上的逃逸只产生了一次\n同理可以分析下面这两个情况，对于 第一个场景，我们只产生了一个新的环境，而对于第二个场景我们每次都产生了新环境\n1 2 3 4 5 6 7 8 9 10 nextInt := intSeq() for { nextInt() } for { nextInt := intSeq() // 每次新环境 _ = nextInt // 若再塞进全局 cache / 切片 → 环境越堆越多 → 可能 OOM } sort 包 1 2 3 4 5 people := []string{\u0026#34;Alice\u0026#34;, \u0026#34;Bob\u0026#34;, \u0026#34;Dave\u0026#34;} sort.Slice(people, func(i, j int) bool { return len(people[i]) \u0026lt; len(people[j]) } ) fmt.Println(people) 面试 什么是闭包 ？ 闭包有什么缺陷？ 闭包的本质是 引用了自由变量的函数，即 函数+引用环境 是闭包\n闭包有什么缺陷 : 在我们反复使用新环境引用闭包变量的时候，会产生大量的内存逃逸 从而使得CPU上涨和OOM\n闭包的优点 :\n可以规避一些函数定义的接收器不允许传入的参数, 例如中间件只允许传入 Write 和Request .但是我们如果想要引用其他 Param，那么就需要定义一个 FuncValue的返回， func(otherParm) FuncValue 做数据的隔离 防止一些不想让其他人访问该数据。例如 Sort 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 🛠️ 改进项与“满分回答”建议 (Improvements) 为了在面试中拿到 9-10 分，你可以将回答按照“定义 -\u0026gt; 常见应用(优点) -\u0026gt; 底层代价(缺陷) -\u0026gt; 语言特有陷阱”的逻辑来组织。 你可以参考以下改进后的表述： 1. 什么是闭包？（保持你的精准定义） 闭包的本质是函数体加上它所引用的外部环境（自由变量）的结合体。在 Go 中，当一个匿名函数引用了它外部作用域的变量时，就形成了闭包。 2. 闭包的优点（优化你的实战案例） 函数签名适配与状态保持（柯里化）：在 Web 开发中，标准库的 http.HandlerFunc 只接受 ResponseWriter 和 Request。我们可以利用闭包将外部配置（如 DB 连接池、日志记录器）封装进去，返回一个符合签名的函数。 数据封装与隔离：闭包可以用来隐藏状态，实现类似面向对象中“私有变量”的效果。比如实现一个计数器生成器，每次调用返回的函数都能修改外层的局部变量，而这个变量对全局是不可见的。同时，在使用 sort.Slice 时，闭包能直接捕获并操作外部的切片，免去了繁琐的参数传递。 3. 闭包的缺陷与坑（重点重构） 性能代价（内存逃逸与 GC 压力）： 闭包引用的外部变量，其生命周期会超越原本的函数调用栈。因此，Go 编译器会通过逃逸分析将这些变量分配到堆上，而不是栈上。如果高频创建闭包，会导致堆内存分配增多，加重 GC（垃圾回收）的负担，进而可能引发 CPU 占用率上升。 经典陷阱：循环变量捕获问题： (重点展示你的技术深度和对 Go 版本更迭的了解) 在 Go 1.22 之前，for 循环中的迭代变量是共享同一个内存地址的。如果在循环里启动 Goroutine 并使用闭包引用该变量，往往会导致所有 Goroutine 最终打印的都是最后一个元素的值。 补充加分项：不过，自 Go 1.22 起，官方已经修改了 for 循环的语义，每次迭代都会创建一个新的变量，从而从根本上解决了这个经典的闭包陷阱。 面试官点评：如果你能在面试中流利地按照上述逻辑回答，不仅展现了你扎实的理论功底和实战经验，还顺带秀了一把底层（逃逸分析、GC）原理，以及对 Go 语言最新版本（1.22+）演进的关注。这绝对是一个一锤定音的高分回答！ 其他 多协程快排的实现 闭包 ","date":"2026-07-14T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/closure-0x01/","title":"闭包"},{"content":"Heap 算法思路 我们使用一维数组来表示一个堆,堆的性质是一个完全二叉树，其中根是最小值，左右节点均小于父节点\n我们使用 x表示根节点，则 2*x表示左节点 , 2*x+1 表示右节点\n考虑\n插入一个数 删除最小值 求最小值 对于插入一个数 :\n因为是使用一维数组进行存储的，所以插入的数会在末尾，我们需要考虑进行 up 操作，将其和其父节点进行比较，如果比父节点小那么交换 1 2 3 4 5 6 void up(int u ) { while(u/2 \u0026amp;\u0026amp; h[u/2] \u0026gt; h[u]) { heap_swap(u/2,u); u /= 2 ; } } 对于删除最小值 因为最小值在 h[1] 所以考虑删除的时候，应该交换h[size] ，然后size-- 最后将 size 进行一次down操作\ndown操作也是很简单就能想到是和 左右节点进行比较 ,如果比左右节点大那么进行交换\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int n,m; int h[N],sz ; void down(int x) { int t = x; if(x * 2 \u0026lt;= sz \u0026amp;\u0026amp; h[t] \u0026gt; h[x * 2]) t = x * 2; if(x * 2 + 1 \u0026lt;= sz \u0026amp;\u0026amp; h[t] \u0026gt; h[x*2 + 1] ) t = x*2+ 1; if(h[x] != h[t]) { swap(h[x],h[t]); down(t); } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin\u0026gt;\u0026gt;n\u0026gt;\u0026gt;m; for(int i = 1; i \u0026lt;= n ; i ++ ) cin\u0026gt;\u0026gt;h[i]; sz = n ; for(int i = n/2; i ; i -- ) down(i); while(m -- ) { cout\u0026lt;\u0026lt;h[sz]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; h[1] = h[sz]; sz -- ; down(1); } return 0; } 时间复杂度分析 建树的时候是 o(n) 的 遍历树的时候 是 ologn\n模拟堆 在普通堆的基础上增加了\n删除第 k 个数 修改第 k 个数 对于删除 第 k 个数\n我们需要考虑存储 , 第k个数 对应在 heap中的下标是什么 即 ph[k] 同时因为涉及到交换，我们需要在 o(1) 的时间复杂度知道 堆下标i = 2*x? 2*x+1 上堆元素，是第几次插入的 hp 因为如果不知道 hp 元素\n我们在交换的时候会遇到\n1 2 3 4 5 原来：第 m 次插入在位置 a → ph[m] = a 第 n 次插入在位置 b → ph[n] = b 交换后：第 m 次插入到了 b → ph[m] = b 第 n 次插入到了 a → ph[n] = a 这种奇异的问题，原本第m次插入的a 变成了 第n次插入的\n除非我们进行 oN 的处理\n1 2 3 4 // 没有 hp 时，每次 swap 都要扫一遍 for (int k = 1; k \u0026lt;= m; k++) if (ph[k] == a) ph[k] = b; else if (ph[k] == b) ph[k] = a; 但是我们如果引入了 hp 后只需要进行 更换即可\n1 2 3 4 5 6 void heap_swap(int a, int b) { swap(ph[hp[a]], ph[hp[b]]); // ph[hp[a]]=a, ph[hp[b]]=b → 交换后变成 b 和 a swap(hp[a], hp[b]); swap(h[a], h[b]); } 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 int n,m; int h[N],sz , hp[N], ph[N]; void heap_swap(int a,int b) { swap(h[a],h[b]); swap(ph[hp[a]], ph[hp[b]]); swap(hp[a],hp[b]); } void down(int x) { int t = x; if(x * 2 \u0026lt;= sz \u0026amp;\u0026amp; h[t] \u0026gt; h[x * 2]) t = x * 2; if(x * 2 + 1 \u0026lt;= sz \u0026amp;\u0026amp; h[t] \u0026gt; h[x*2 + 1] + 1) t = x*2+ 1; if(x != t) { heap_swap(x,t); } down(t); } void up(int u ) { while(u/2 \u0026amp;\u0026amp; h[u/2] \u0026gt; h[u]) { heap_swap(u/2,u); u /= 2 ; } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n;cin\u0026gt;\u0026gt;n; while(n -- ) { string op;cin\u0026gt;\u0026gt;op; if(op == \u0026#34;I\u0026#34;) { int x;cin\u0026gt;\u0026gt;x; h[++sz] = x ; m ++ ; ph[m] = sz ; hp[sz] = m ; up(sz); }else if(op == \u0026#34;PM\u0026#34;) { cout\u0026lt;\u0026lt;h[1]\u0026lt;\u0026lt;endl; }else if(op == \u0026#34;DM\u0026#34;) { heap_swap(1,sz); sz -- ; down(1); }else if(op == \u0026#34;D\u0026#34;) { int k;cin\u0026gt;\u0026gt;k; k = ph[k]; heap_swap(k,sz); sz -- ; up(k); down(k); }else { int k,x;cin\u0026gt;\u0026gt;k\u0026gt;\u0026gt;x; k = ph[k]; h[k] = x; up(k); down(k); } } return 0; } 一些感想 好难，说实话 涉及到一些指针的来回跳动就完全不会了\n","date":"2026-07-13T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x08/","title":"[Acwing] 基础算法(八) 堆"},{"content":"Tire 算法思路 解决问题 :\n解决单词统计类型的问题，或者是说 有多少 prefix 是以当前字符串为前缀的串\n算法思路 ：\n由于这种题目出现会控制单词长度，所以我们考虑开一个 [N][26] 树用于存储\n对于一个单词，相当于一个单边树 对于 abc ,abcd 树会在 c 处进行分叉 每个节点单独赋值 idx 对于访问一个字符串我们可以从 根节点开始访问，对于其子串可以使用 p = tire[p][u] 的形式进行递归访问 代码展示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 int son[N][26], n, idx ; int cnt[N]; char str[N]; void insert(char str[]) { int p = 0 ; for(int i = 0 ; str[i] ; i ++ ) { int u = str[i] - \u0026#39;a\u0026#39;; if(!son[p][u]) son[p][u] = ++idx ; p = son[p][u]; } cnt[p] ++ ; } int query(char str[]) { int p = 0 ; for(int i = 0 ; str[i] ; i ++ ) { int u = str[i] - \u0026#39;a\u0026#39;; if(!son[p][u]) return 0; p = son[p][u]; } return cnt[p]; } int main() { cin\u0026gt;\u0026gt;n ; while(n -- ) { char op[2]; scanf(\u0026#34;%s%s\u0026#34;, op, str); if (*op == \u0026#39;I\u0026#39;) insert(str); else cout\u0026lt;\u0026lt;query(str)\u0026lt;\u0026lt;endl; } } ","date":"2026-07-12T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x07/","title":"[Acwing] 基础算法(七) Tire 树"},{"content":"并查集 算法思路 使用场景\n将两个集合进行合并 询问两个元素是否在一个集合中 我们很简单的可以想到判断两个元素是否在一个集合中可以使用map进行 o1 的判断，但是对于合并集合无法避免是 o(n) 的\n因此我们考虑使用树的形式存储集合\n对于一个集合，其根节点代表整个集合编号 使用 p[x] 表示其父节点 对于一个单边集合\n1 1 → 2 → 3 → 4 我们如果要找到 1 的集合，那么需要 从 p[1],p[2],p[3]一直寻找\n但是我们可以考虑进行优化，对于找到根节点的集合，我们将其全部转移到根节点，即路径压缩\n1 2 3 1 ──┐ 2 ──┼──→ 4 3 ──┘ 代码 其中 p[x] = find(p[x]) 代表路径压缩\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 int p[N] ; int find(int x) { if(p[x]!= x) { return p[x] = find(p[x]); } return p[x]; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n,m;cin\u0026gt;\u0026gt;n\u0026gt;\u0026gt;m; for(int i = 1 ; i \u0026lt;= n ; i ++ ) { p[i] = i ; } for(int i = 1 ; i \u0026lt;= m ; i ++ ) { char op;cin\u0026gt;\u0026gt;op ; int a,b;cin\u0026gt;\u0026gt;a\u0026gt;\u0026gt;b; int fa = find(a); int fb = find(b); if(op == \u0026#39;M\u0026#39;) { p[fa] = fb; }else if(op == \u0026#39;Q\u0026#39;) { if (fa == fb) { cout\u0026lt;\u0026lt;\u0026#34;Yes\u0026#34;\u0026lt;\u0026lt;endl; }else { cout\u0026lt;\u0026lt;\u0026#34;No\u0026#34;\u0026lt;\u0026lt;endl; } } } } ","date":"2026-07-12T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x07-01/","title":"[Acwing] 基础算法（七) 并查集"},{"content":"写在前头 :\n最近生活没有了特别明确的目标，活着确实有点不自在\n反差 离职的时候写了2-3篇文章说是已经规划好了，但是实际只是简单算了一下故事点和整合内容\n实际任务编排并没有特别有规划或者是目的的进行\n当然并不是说我没有做，我确实是做了，我在整理完之后就开始寻找能够平替飞书甘特图的内容\n毕竟之前上班都是用的那个，每天上班无非就是打开甘特图，打开backlog 看一下要做什么 昨天有什么没做的，都是安排好的\n我记得我找了好久，最后使用石墨文档的甘特图，然后排了这两周的任务，但是实际排完之后第二天就没运行了\n我排了两个3个任务\n完成 Acwing AI Chat 项目 周一做完XXX模块 周二做完XXX模块 算法学习项目 周一学XXX算法 周二学XXX算法 Tidy 任务 整理个人todo 整理公司todo 整理规划这几个月发展 整理个人业务闪光点 但是最终没运行\n我的一天 昨天因为要吃一顿散伙饭所以没有特地记录，今天的话可以说是可以好好记录一下\n今天早上 10:00 左右起床的， 不过9:00就起来了，昨天在玩 堆叠大陆，买了2个DLC，然后熬了会夜看攻略\n今天早上起来还在看大概看到了 10:00 然后起床洗漱\n然后开始玩游戏玩到了中午12:30 开始吃饭，点的外卖 不想自己做饭\n然后吃完饭后，看了会抖音大概看到了 13:30. 然后好像就去看电视剧去了大概看了2个小时 看到了 15:30\n然后开始打开百度云看一个 《golang训练营 从0到1》 总共28周，我预计是想要一天学一周的内容，这样子可以控制在一个月内学完，但是因为昨天耽搁了，所以今天在赶昨天的进度\n现在还是很简单的内容今天主要学习了\n项目结构 普通的登陆和注册 错误的基本处理 数据库的连接 session 和 cookie 以及密码加密什么的 都是一些大学阶段就应该掌握的内容，我现在2年后又重新掌握一遍，但是我并没有把我的commit推送，我甚至没有把这个项目当作我的仓库项目，只是放在那里\n也就是说 需要更新到我简历的项目目前还没有开始写\n然后18:30左右开始做饭，今天正常无氧 20分钟\n然后吃饭，吃完饭后洗澡，打游戏，刷抖音。大概好像就到了21:00左右了\n出门跑步，跑了半个小时，最近拉肚子，跑步跑的不是很得劲，然后走了1.5公里回去，大概 22:10左右回到家\n然后洗澡，再清一会游戏体力\n大概到了23:00开始学习算法，学了预计半小时吧还是40分钟\n学完之后补一下 commit 和 blog 然后开始写这篇随便总结的文章\n下一步 下一周其实还有一些公司的活需要干，例如写交接文档，完成todo\n但是我的心思已经不在公司那边了\n随便写点 说实话 我原本预计一天学习8小时的，这样子可以在一个半月内追完进度\n但是实际今天只学习了4个小时，也就是说在buffer范围内，我需要花费 3个月的时间才行，这很显然对我来说不是很乐观\n随便写点，或许明天有空会继续补内容，因为正好 0:00 了 我要上床了，这是习惯\n","date":"2026-07-12T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/no-plan-time-review/","title":"2026-07-12 不定时复盘"},{"content":"栈 算法思路 我们使用 stk[N],tt 表示栈数组和栈顶指针\n对于插入操作 stk[++tt] = x 即可\n对于查询是否为空的操作判断 tt != 0 即可\n对于删除操作 tt-- 即可\n比较简单的一个模拟\n待会中午约了散伙饭先收拾去了~\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 bool checkEmpty() { return (tt == 0); } void push(int x) { stk[++tt] = x; } int pop() { int x = stk[tt]; tt -- ; return x; } int query() { return stk[tt]; } int main() { int m; cin\u0026gt;\u0026gt;m ; while(m -- ) { string op;cin\u0026gt;\u0026gt;op; if(op == \u0026#34;push\u0026#34;) { int x;cin\u0026gt;\u0026gt;x; push(x); }else if (op == \u0026#34;pop\u0026#34;) { pop(); }else if(op == \u0026#34;empty\u0026#34;) { if (checkEmpty()) { cout\u0026lt;\u0026lt;\u0026#34;YES\u0026#34;\u0026lt;\u0026lt;endl; }else { cout\u0026lt;\u0026lt;\u0026#34;NO\u0026#34;\u0026lt;\u0026lt;endl; } }else if(op == \u0026#34;query\u0026#34;) { cout\u0026lt;\u0026lt;query()\u0026lt;\u0026lt;endl; } } return 0; } 队列 算法思路 我们通过 hh,tt 表示队头和队尾, 维护一段连续区间\n对于弹出操作，我们使用 hh++\n对于插入操作, 我们使用 q[++tt]=x\n对于查询是否为空的操作，我们使用 hh\u0026lt;=tt\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int q[N]; int tt,hh; void init() { tt = -1; } bool empty() { return (hh \u0026lt;= tt); } void add(int x) { q[++tt] = x; } void pop() { hh ++ ; } int query() { return q[hh]; } int main() { init(); int m;cin\u0026gt;\u0026gt;m; while(m -- ) { string op;cin\u0026gt;\u0026gt;op; if(op == \u0026#34;push\u0026#34;) { int x;cin\u0026gt;\u0026gt;x; add(x); }else if(op == \u0026#34;pop\u0026#34;){ pop(); }else if(op == \u0026#34;empty\u0026#34;) { if(empty()) { cout\u0026lt;\u0026lt;\u0026#34;NO\u0026#34;\u0026lt;\u0026lt;endl; }else { cout\u0026lt;\u0026lt;\u0026#34;YES\u0026#34;\u0026lt;\u0026lt;endl; } }else if(op == \u0026#34;query\u0026#34;) { cout\u0026lt;\u0026lt;query()\u0026lt;\u0026lt;endl; } } return 0; } 单调栈 算法思路 顾名思义，我们维护的是一个 单调递增 or 单调递减 的一个内容\n一般情况下我们会考虑一种提醒题型,即找到 当前数左边比自己小的数是什么\n那么我们可以考虑维护一个 maxValue 是当前数的栈,对于大于自己的数都出栈，从而保证整个栈的值是递增的\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main() { cin\u0026gt;\u0026gt;n; for(int i = 1; i \u0026lt;= n ;i ++ ) { int x;cin\u0026gt;\u0026gt;x; while(tt\u0026amp;\u0026amp;stk[tt] \u0026gt;= x) { tt -- ; } if(!tt) { cout\u0026lt;\u0026lt;-1\u0026lt;\u0026lt;\u0026#34; \u0026#34;; }else { cout\u0026lt;\u0026lt;stk[tt]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; } stk[++tt] = x; } return 0; } 时间复杂度分析 对于我们想要解决这个问题，暴力做法是 n^2 的\n但是如果我们对于 a3\u0026gt;a5 的话，我们会发现我们永远用不到 a3 所以我们并不需要单独去遍历他，在最坏情况下也是 2n 的\n单调队列 算法思路 同样以 滑动窗口举例 ,我们需要快速的求出一个固定窗口的最大最小值\n我们对于 [1 3 -1 -3 5 3 6 7] 这个窗口,在[3,-1,-3] 窗口大小为 3 的时候，我们可以发现，其实-3是最小值，其中3,-1都没有用，因为后面的序列也用不到\n所以我们可以维护一个最小值的窗口\n考虑\n当队头超出窗口 那么移出队头 当队内元素大于当前数，那么移除当前元素 。 因为我们维护的是一个从小到大的序列，所以这里从队尾开始删除 然后把当前数插入进去 并且窗口有输出的时候进行输出 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int main() { cin\u0026gt;\u0026gt;n\u0026gt;\u0026gt;k; int hh = 0 ,tt = -1; for(int i = 1 ; i \u0026lt;= n ; i ++ ) cin\u0026gt;\u0026gt;a[i]; for(int i = 1; i \u0026lt;= n ; i ++ ) { if(hh \u0026lt;= tt \u0026amp;\u0026amp; i - k \u0026gt;= q[hh]) hh ++ ; while(hh \u0026lt;= tt \u0026amp;\u0026amp; a[q[tt]] \u0026gt;= a[i]) tt -- ; // 5 6 2 q[++tt] = i ; if(i-k \u0026gt;= 0) cout\u0026lt;\u0026lt;a[q[hh]]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; } hh = 0, tt = -1; cout\u0026lt;\u0026lt;endl; for(int i = 1; i \u0026lt;= n ; i ++ ) { if(hh \u0026lt;= tt \u0026amp;\u0026amp; i - k \u0026gt;= q[hh]) hh ++ ; while(hh \u0026lt;= tt \u0026amp;\u0026amp; a[q[tt]] \u0026lt;= a[i]) tt -- ; // 5 6 2 q[++tt] = i ; if(i-k \u0026gt;= 0) cout\u0026lt;\u0026lt;a[q[hh]]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; } } ","date":"2026-07-11T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x06/","title":"[Acwing] 基础算法(六) 数据结构 栈\u0026队列"},{"content":"链表 单链表 算法思路 我们使用\nhead 存储头节点\ne[i] 存储下标 i 节点的值\nne[i] 表示 i 的后继节点在哪\nidx 表示当前点位\n对于插入头部的操作\ne[idx] = x, ne[idx] = head, head = idx++;\n中间插入的操作\ne[idx] = x, ne[idx] = ne[k], ne[k] = idx++;\n删除的操作\nne[k] = ne[ne[k]];\n下面是一个演示过程 :\n头插入\n1 2 3 H 1: e[0]=1, ne[0]=-1, head=0 → 链表: 1 H 2: e[1]=2, ne[1]=0, head=1 → 链表: 2 → 1 H 3: e[2]=3, ne[2]=1, head=2 → 链表: 3 → 2 → 1 调用 Insert 3 5 在第三个数后面插入5\n1 2 3 e[3] = 5 ne[3] = ne[2] = 1 // 新节点先接上原来的后继 ne[2] = 3 // 节点 2 改指向新节点 调用 Insert 4 4\n1 2 3 e[3] = 5 ne[3] = ne[2] = 1 // 新节点先接上原来的后继 ne[2] = 3 // 节点 2 改指向新节点 最终情况\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 逻辑链表: head ↓ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ 3 │ → │ 5 │ → │ 4 │ → │ 2 │ → │ 1 │ → NULL └────┘ └────┘ └────┘ └────┘ └────┘ 下标2 下标3 下标4 下标1 下标0 数组状态: i: 0 1 2 3 4 e[i]: 1 2 3 5 4 ne[i]: -1 0 3 4 1 ↑ ↑ ↑ │ │ └─ head=2 │ └─ ne[2]=3 指向节点5 └─ ne[4]=1 指向节点2 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include \u0026lt;bits/stdc++.h\u0026gt; #define LL long long using namespace std; const int N = 1e5+10; int head, e[N], ne[N], idx ; void init() { head = -1 ; idx = 0 ; } void add_to_head(int x) { e[idx] = x; ne[idx] = head ; head = idx ++ ; } void add(int k ,int x) { e[idx] = x ; ne[idx] = ne[k]; ne[k] = idx ++ ; } // 1 , ne[0] = -1 // 2 , ne[1] = 0 // 3 , ne[2] = 1 // 5 , ne[4] = ne[3-1] = 1 ; 3, 1 (ne[3] = 5) // 4 , ne[3] = 2 void remove(int k) { ne[k] = ne[ne[k]]; } int main() { int n;cin\u0026gt;\u0026gt;n; init(); for(int i = 1 ; i\u0026lt;= n ; i ++ ) { char op ;cin \u0026gt;\u0026gt; op ; if (op == \u0026#39;H\u0026#39;) { int x;cin\u0026gt;\u0026gt;x; add_to_head(x); }else if (op == \u0026#39;D\u0026#39;) { int x;cin\u0026gt;\u0026gt;x; if (!x) head = ne[head]; else remove(x-1); }else if (op == \u0026#39;I\u0026#39;) { int k,x;cin\u0026gt;\u0026gt;k\u0026gt;\u0026gt;x; add(k-1,x); } } for(int i = head ; i != -1 ; i = ne[i]) { cout\u0026lt;\u0026lt;e[i]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; } return 0; } 双链表 算法思路 我们假设 [0] [1] 代表我们的左右端点\n初始化的时候 r[0] = 1 , l[1] = 0, idx = 2\n对于插入操作\n我们需要给当前节点赋值\n并且使得当前节点的左右指针指向上下两个数\nl[idx] = k, r[idx] = r[k] 同时修改两个被影响的指针\nl[r[k]] = idx, r[k] = idx 这是插入右端点的思路，插入左端点可以认为是在前一个节点插入右端点即inser(l[k],x)\n对于删除操作,我们需要把 当前节点的上一个节点 和 下一个节点互相建立关系\n将当前节点的左节点的右节点指向下一个节点\nr[l[k]] = r[k] 将当前节点的右节点的左节点指向上一个节点\nl[r[k]] = l[k] 另外需要注意的是，我们一开始就使用了两个节点所以对于操作的节点我们应该认为是 k+1\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 int e[N],l[N],r[N],idx; void init() { r[0] = 1, l[1] = 0 ; idx = 2; } void add(int k,int x) { e[idx] = x; l[idx] = k ,r[idx] = r[k]; l[r[k]] = idx, r[k] = idx; idx ++ ; } void remove(int k) { l[r[k]] = l[k]; r[l[k]] = r[k]; } int main() { init() ; int n;cin\u0026gt;\u0026gt;n; for(int i = 1 ; i\u0026lt;= n ;i ++ ) { string op;cin\u0026gt;\u0026gt;op; if(op == \u0026#34;L\u0026#34;) { int x;cin\u0026gt;\u0026gt;x; add(0,x); }else if(op == \u0026#34;R\u0026#34;) { int x;cin\u0026gt;\u0026gt;x; add(l[1], x); }else if(op == \u0026#34;D\u0026#34;) { int k;cin\u0026gt;\u0026gt;k; remove(k + 1); }else if(op == \u0026#34;IL\u0026#34;) { int k,x;cin\u0026gt;\u0026gt;k\u0026gt;\u0026gt;x; add(l[k+1], x); }else { int k,x;cin\u0026gt;\u0026gt;k\u0026gt;\u0026gt;x; add(k+1,x); } } for(int i = r[0] ; i!= 1; i = r[i]) cout\u0026lt;\u0026lt;e[i]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; return 0; } ","date":"2026-07-10T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x05/","title":"[Acwing] 基础算法（五) 数据结构 链表"},{"content":"写在开头 :\n本次复习主要使用 : https://www.xiaolincoding.com/network/\n个人对此上面的评价 : 应该很难找到同效益的平替内容了, 如果有人不知道这个他可能会去b站上面看考研的来复习？\nTCP/IP 网络模型有哪几层 应用层、传输层、网络层、网络接口层\n这是TCP/IP的体系结构, 之前可能学习的都是7层网络结构，内容太复杂了\n其中传输层有 :\nTCP 和 UDP 两个传输协议\nTCP 自身拥有 : 流量控制，超时重传，拥塞控制\nUDP 相对简单称为不可靠协议\n网络层 :\nIP 协议 : 组装传输层的报文加上IP包头组装成IP报文进行传递\n键入网址到网页显示,期间发生了什么 HTTP 浏览器解析 URL, 一个 URL 到组成如下 :\nhttp: URL表示访问数据的协议\n// + web服务器 + 数据源文件路径名\n对URL进行解析之后，会根据这些信息来生成 HTTP 请求报文\nDNS 查询服务器域名对应的IP地址 进行发送对应的 HTTP 消息\n域名则是上面的 web服务器 例如 www.server.com\nIP + TCP/UDP 协议栈 找到对应的 IP 地址后, 通过 IP 协议进行传输\n然后根据 TCP 和 UDP 的方式进行发送和接受数据\nTCP 链接 (三次握手) 服务端主动监听某个端口，处于 LISTEN 状态 客户端主动发起链接 SYN，处于 SYN_SENT 服务端收到发起链接，返回 SYN+ACK , 处于 SYN_RCVD 客户端收到服务端发起的 SYN+ACK 后发送 ACK 处于 ESTABLISHED 状态 感悟 基础篇讲的内容很多，但是我看了一些内容感觉都是 无用的 ，至少对工作开发无用，如果我是面试官我也不会以这种问题去刁难\n","date":"2026-07-09T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/net-work-0x01/","title":"[计算机网络] 基础篇"},{"content":"HTTP 超文本传输协议 : HyperText Transfer Protocol\n常见错误码 : 2xx 成功状态码 :\n200 OK 一切正常 204 No Content 与 200 OK基本相同，但相应没有 body 数据 3xx 重定向状态码\n301 Moved Permanently 永久重定向 302 Found 临时重定向 4xx 客户端发送的报文有误,服务器无法处理\n400 : 客户端请求的报文有误 403 : 服务器禁止访问资源 404 : 服务器请求的资源不存在, 无法提供给客户端 5xx 客户端请求报文正常,但是服务器内部处理发送了错误\n500 笼统的错误码,服务器发生了错误 501 客户端请求的功能还不支持 502 访问后端服务器错误, 网关错误 503 服务器繁忙,服务器可能正在升级 HTTP 常见字段 Content-Length : 本次回应的数据长度\nConnection : 客户端要求服务器使用 HTTP 长连接机制,以便其他请求复用\nContent-Type : 告诉客户端 本次的数据是什么格式\nGET 和 POST 的区别 GET 的语义 : 从服务器获取指定的资源\nPOST 的语义 : 根据请求负荷（报文body）对指定的资源做出处理\n我们开发的时候也是一样的 GET 一般用于只读, POST 一般用于操作, 但是 GET 可能会产生拼接的 URL 过长的情况,这时候需要改为POST进行请求\nHTTP 特性 HTTP 常见的， 感觉好无聊，不如看几个面筋去，今天先td了\n","date":"2026-07-09T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/net-work-0x02/","title":"[计算机网络] 应用层"},{"content":"学累了我就看面经，看多了就会发现都是一些什么神人在卷，然后就不想学了\n本篇面筋来自 : https://www.nowcoder.com/discuss/631514581503926272?sourceSSR=users\n店匠科技 一面 算法题 两数之和\n暴力做法 N^2 匹配 优化做法 通过 Map 进行标记 1 2 3 4 5 6 7 8 9 10 func twoSum(nums []int, target int) []int { hashTable := map[int]int{} for i, x := range nums { if p, ok := hashTable[target-x]; ok { return []int{p, i} } hashTable[x] = i } return nil } 聊项目 系统数据模型怎么设计的 说实话我自己做的项目都数据库表可以说是抄的，我们系统的呢\n对于一张数据表 historytrade_trans 我们拥有\n对于一张 Tradeacc 表 我们拥有\nTrans表中，我们把会演进的内容放到了 detail 中 ，好处是 如果增加或者是删除字段 不需要频繁更改 列\n通用设计表的步骤 :\n区分实体 :\n主数据 :\n特征 : 数据少,变，被引用 例如 : 账户表 默认倾向 : 宽表 + 局部 Json 事件 :\n特征 : 多,追加写,按时间查 例如 : 成交表 默认倾向 : 窄表 + payload 关系 :\n特征 : 关联 A-B 例如 : 关系表 默认倾向 : 极简列 + 联合唯一 监控关注的业务指标 我们只关注 IP 层级的限速 入库率 八股 Session 是什么 不清楚 前端有session，可以直接拿到apifox用 一致性 不清楚 分布式事务 不清楚 店匠科技 二面 算法题 一个文件里有40亿个数字，找出最大的10个数字\n写不下去了有点累\n","date":"2026-07-09T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/interview_0x01/","title":"面经每日一看"},{"content":"题目 给你一个整数数组 nums ，找到其中最长严格递增子序列的长度。\n子序列 是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。例如，[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。\n示例 1：\n输入：nums = [10,9,2,5,3,7,101,18] 输出：4 解释：最长递增子序列是 [2,3,7,101]，因此长度为 4 。 示例 2：\n输入：nums = [0,1,0,3,2,3] 输出：4 示例 3：\n输入：nums = [7,7,7,7,7,7,7] 输出：1\n思路 这道题的数据范围很小，可以考虑暴力，我们很想当然可以想到\nf[i] 表示当前以 i 结尾的最长子序列是是多少, 那么答案就是 f[i] = f[j] + 1 (a[j] \u0026lt; a[i] , j ~ (0~i))\n这样子的思路是 N^2 的\n我们改变思路将 f[i] 表示当前长度为i的最长子序列，结尾最小的数是f[i]\n那么对于 a[i] 来说，我们只需要找到第一个能插进去的位置即可，即找到一个 比 a[i] 大的最小的数 。这样子我们可以优化第二层的时间复杂度为O(logN)\n简单拿一组数据来说 101,102,103,104,2 , 当我们遍历到 104 的时候我们发现只能找到 f[4] 说明它可以接到 f[4]后面，即f[5] = a[i]\n但是当我们遍历到 2 的时候，发现只能找到F[0] 说明只能找到长度为 1 的序列，那么他只会更新F[1] = a[i]\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const int N = 2e5+10; int q[N]; class Solution { public: int lengthOfLIS(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int n = nums.size(); int len = 0 ; for(int i = 0 ;i \u0026lt; n ; i ++ ) { int l = 0, r = len ; while(l \u0026lt; r) { int mid = l+r+1\u0026gt;\u0026gt;1; if(q[mid] \u0026lt; nums[i]) l = mid; else r = mid - 1; } len = max(len,r + 1); q[r+1] = nums[i]; } return len ; } }; ","date":"2026-07-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/leetcode-300/","title":"[LeetCode] 300. 最长递增子序列"},{"content":"说实话没有什么比较好的思路能够一下子整理出工作中的闪光点\n目前能想到的切入点\n人脑回忆这两年的Backlog 从上到下扫描这两年全部的Backlog 根据产品季度目标 PO-backlog 来整理 预计采用的方案是\n1+3,如果数量不够可能会使用 2进行补充\n开始回忆 账户暂停 我们系统内部维护一个 Account Api 状态\n包括 只读,归档,Error,交易\n需要增加一个账户暂停的状态，把归档的状态下掉，为什么下掉归档忘记了，看着是从业务理解层面上下掉的\n暂停的账户需要停止\n资产的更新 记录的抓取 印象中是做了\n在 Account 表中增加一列 boolean 表示账户是暂停还是启动 在 Collector Account Monitor 中根据账户的状态进行控制是否要进行资产更新 在 Fetch Data Monitor 中根据账户的状态进行控制是否要进行抓取记录 这里感觉不对 :\n优化了一下流程\n写了一个 prompts 然后把对应任务的 pr丢给他 让他分析 这次改动做了什么\n通过 migration 给涉及到的3张Account数据库表增加 state 的列 处理相关下游模块的 暂停程序 collector , monitory , api hook 通过 go 一个 migrateArchivedToPaused 将历史归档状态的账户改成暂停的账户 亮点 :\n复杂领域模型的状态及设计，因为涉及到 子账户暂停母账户才可以暂停 跨模块一致性改造 存量数据迁移 个人想法 理了一下这个backlog，感觉并不是很惊艳，只能说涉及到的模块多，下游设计到的多，导致做的周期比较长比较大\n我感觉可以把 migration 单独提出来说一下，可以去挖一下我们公司 migration 的实现\nAi总结 主导 CeFi 账户 Pause/Resume 端到端设计与实现，将原先混用的「归档」语义拆分为运行态（Normal/Paused）与 API 权限态，新增 DB 三字段并完成 v2.171 migration。实现 Prop/Sub 级联状态机、恢复前 API 复检、事务与业务日志；改造 Collector 与 Monitor 共 6+ 后台 Job，暂停账户停止资产拉取与 API 巡检；完成归档账户存量迁移及多轮生产 bugfix，保障 v2.171 稳定上线。\nAsync 背景 交易所接口的实时查询接口只能够查询最近3个月的数据，如果想要获取更早的数据需要透过异步接口获取\n请求下载ID | 这一步一个月有次数限制，而且IP权重很高 根据请求下载ID 去请求下载链接 整理 整体思路\n1 2 3 4 5 6 7 8 9 REST 入口（统一限流/配额/落库） ↓ DownloadId 工厂（exg + dataType → handler） ↓ Redis 队列（解耦「申请」与「轮询下载」） ↓ 后台 Scan Job（轮询 + 下载 + 重试） ↓ MQ → Consumer（CSV 解析 → 标准 OTS 格式 → 入库） 亮点\nRedis 队列进行接藕操作\n任务每次在发版 进程重新启动的时候进行Recover\n状态机驱动 not_started → packaging_data → downloading → importing → succeed/failed，含 packaging_data_with_error\n双层配额控制\n进程级 : rate.Limiter 一分钟一次，防止误操作 账户级 : Redis 控制自然月计数 Mock 测试体系\nAI一句话总结 设计并实现 CAM 加密资产管理系统历史数据异步拉取引擎（Go / Redis / PostgreSQL / MQ）：从 0 搭建 Binance/OKX/Bybit 异步导出全链路（申请 → 轮询 → 下载 → CSV 解析入库），含双层配额控制、Redis 队列解耦、PG 状态机与发版容错恢复；支撑对账与历史回溯，模块累计 100+ 次生产迭代。\nAuth 升级 背景说明 我们的 Apikey 铭感信息 都存放到 Auth 服务器上，之前 Auth 服务器上的 APikey_1 表数据太脏了\napikey 不是存放在prop层级的，而是母子层级都可能存在 现在想要升级 apikey_1 为 apikey_2\n希望所有 apikey 都收拢到 prop层级上面 \u0026ndash;\nC交易所账户有层级结构：Prop 母账户（如 bnprop）下挂多个子账户（如 binancef、binanced）。原先每个子账户都要单独配置 API Key，运维成本高、易出错。\n这套改动实现：子账户自动复用 Prop 母账户的 API 凭证，并支持复制已有 Prop 账户创建新账户，同时完成 Auth 服务从 V1 到 V2 的平滑迁移。\n整理 存储层 : 新增 account_apikey_2 表\nAuth Server 层 : 新增 Copy Api 的功能,暴露出copy-api的接口\nAuth Client 层 : 封装 Copy 调用， V1/V2 自动切换\n业务层 : CopyHelper 统一调度：读 Key、写 Key、复制账户\n观测层 : 给每一个回退增加统计\n核心决策设计 :\n双表并存，平滑迁移\nV1（account_apikey）与 V2（account_apikey_v2）共存 支持 V1→V1、V2→V2、V1→V2 三种 Copy 路径 客户端按 Auth 版本自动路由，V1 读不到时自动 V1→V2 迁移 灰度发布\nCopyRegionControl() 控制 master 环境先上线 后续通过 Auth 版本检测自动切换 V2 个人想法 这个功能比较难做，涉及到的模块很多，而且 更新 新增 删除 apikey 暴露给了很多地方，还需要做版本控制\n导致改动的地方很多\nAi总结 负责设计并实现交易账户 API Key 共享能力，解决 Prop 母账户与子账户重复配置凭证的问题。新建 account_apikey_v2 存储层，提供 V1/V2 双版本 Copy API；在 tradeacc、auth_client、交易所 Sign 模块实现统一 CopyHelper，支持子账户自动复用 Prop 凭证及复制开户；设计 V1→V2 平滑迁移与降级回退，配合 Feature Toggle 灰度发布；新增 Copy 降级监控指标。涉及 5 个核心模块、1,000+ 行代码，6 天内分 4 个 PR 交付。\nTrans 表优化 背景 成交表的数据有很多，一个客户一天的数据甚至就能到百万级别的了，需要优化一下存储\n我们Trans表主要存储 id,business_id,account,time,detail这几个字段，之前 detail 是直接存储的交易所的原始返回\n所以主要是优化这部分内容，通过压缩存储来进行优化\n压缩算法和测试是运维那边来处理的（后续需要去看一下）\n整理 我们存在两个结构一个 是 ec.OTSTrans 供给上层业务组使用 一个是 db.Trans 用于存储\n新增 SlimOTSTrans，将 ec.OTSTrans 的 jsontag 改为短JSON名处理例如 a/b/c... 在 OTSTransToDB 的时候使用 Compressor 进行控制 新增 blob 和 blob_format 列用于表示是否是压缩内容 以及存入压缩内容 将detail字段进行私有化控制，全部业务层统一使用 GetDetails()和SetDetails()进行访问 流程 写入时：JSON → Slim 序列化 → 压缩 → 写入 blob，清空 details；读取时：按 blob_format 自动解压还原\n1 2 3 4 5 6 7 8 9 10 11 12 ┌─────────────────────────────────────────────────────────┐ │ Layer 1: JSON 字段瘦身 (SlimOTSTrans 短字段名) │ ├─────────────────────────────────────────────────────────┤ │ Layer 2: 二进制压缩 (xz / zstd / vanilla) │ ├─────────────────────────────────────────────────────────┤ │ Layer 3: 存储分离 (details 清空 → blob 存压缩数据) │ └─────────────────────────────────────────────────────────┘ ↓ blob_format 标识格式，统一读写入口 ↓ blob_format=0 读旧 details（向后兼容） blob_format\u0026gt;0 读 blob 解压（新数据） 架构与设计\n全链路存储优化：不是简单「加个 gzip」，而是从 JSON 结构、压缩算法、DB Schema、读写 API 到 20+ 下游模块的系统性改造。\n向后兼容设计：blob_format 双轨读写，支持新旧数据共存，生产可渐进迁移。\n零拷贝转换：SlimOTSTrans 与 OTSTrans 通过 unsafe.Pointer 转换，并用 init() 校验 struct layout，兼顾性能与安全。\nAi总结 设计并实现 CAM 历史交易流水存储优化：Slim JSON 瘦身 + zstd/xz 压缩 + blob 分离存储，解决核心表存储与 I/O 瓶颈。\n通过 blob_format 双轨兼容设计实现零停机迁移，重构 40+ 下游模块统一读写接口，开发 CLI 批量迁移工具并完成多环境灰度发布。\n","date":"2026-07-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/fire-workd-04/","title":"垃圾中堆找黄金"},{"content":"我去过两个很远的地方，一个是 香港的麦理浩径 一个是成都的雪山\n去成都雪山的时候\n我买了一个 25L 的背包 提前一晚上买了一点路餐 买了运动相机 买了2个登山杖 买了速干内衣 买了抓绒内衣 买了冲锋衣冲锋裤 这些不是我自己想出来的，参考的网上的攻略\n“世间上到现在所有的问题都有人有答案”\n去麦里浩径补充买了一些东西\n一个 48L 的背包 睡袋 帐篷 能量胶 我说这些是想表明，想要出发需要先整理，整理出发需要什么，现在有什么，我没有什么\n我有什么 2年的工作经验，虽然很多都是 dirty - work 。但是也有一些值得拿出来讲的事\n另外 我leader和其他同事做的事情 其实也可以拿过来让自己用 开发中使用 AI Cursor Skills prompts 的经验 Acwing 的算法基础课和提高课，以及之前刷过算法的经验\nhttps://www.acwing.com/activity/content/11/ 虽然把之前学的算法忘的差不多了，但是实际去重温 会发现比从0开始学速度更快，而且感受更不一样 一个 Java 的知识星球\nhttps://nageoffer.com/planet/ 我才发现竟然更新了 Agent 项目 ，不得不承认 Java 高性价比可以直接抄的项目有很多，路线上看的比Golang更清晰 一个 Python 的 AI Chat 课\nhttps://www.acwing.com/activity/content/4250/ ，体积小 仅 18个课时，视频形式 从0到1，到项目上线 一个 Java 的 在线匹配课\nhttps://www.acwing.com/activity/content/1877/ 依旧是体积小，视频形式从0到1，到项目上线，我记得这个项目前端占比了10个课时左右 一个完整的 Golang 课程\nhttps://u.geekbang.org/subject/go3rd 总共24周，视频时长还没统计过，预计覆盖很多内容，但是视频是24年11月份的，不涉及现在 AI 相关内容 一些简单的 Go 项目\nhttps://geektutu.com/post/gee.html https://www.bilibili.com/video/BV1op4y177iS/?spm_id_from=333.1387.homepage.video_card.click\u0026vd_source=25934349561f43f2cda2ed48ce984f9b 一套简单的Go-zero实现 我没有什么 良好的 Golang 基础 ，现在要问我 Golang 数据结构的实现，GC 什么的我都不清楚\n良好的八股文基础，同样的 计算机网络 操作系统 Redis什么都忘记了\n没有 Mysql 相关经验，工作用的是 Pg\n没有 Kafka 、etcd、docker 的经验，工作用的 Redis stream实现的MQ\n没有高并发 GRPC 等相关的经验\n市场需要什么 网易 :\n熟悉 Gin、Go-zero 熟悉 Restful API 设计规范 熟悉 Golang 生态的服务架构设计 熟悉 中间件的使用 MySQL、Redis、Kafka、RocketMQ 具有高并发业务相关经验 游卡 :\n精通 Golang 相关网络服务开发 熟悉 Python 熟练掌握设计模式以及面向对象的设计模式 熟悉网络基本原理，比如 TCP/IP，HTTP 协议 熟悉 MYSQL 数据库，了解 MYSQL索引优化，查询优化，存储优化 字节 :\n熟悉分布式系统设计、微服务系统设计、稳定性治理、常用中间件原理 腾讯 :\n熟悉 Mysql、redis、es、mongo等存储 以及常见的消息中间件 熟悉 TCP/IP 与 HTTP 等网络协议 有海量服务、高性能、分布式系统设计、开发经验者优先 AI 工程化实践 : 熟悉使用 主流 Ai coding工具，有LLM接口继承，对话系统开发或Ai Agent 工程化落地经验者优先 我应该怎么做 提炼\n提炼 这两年 dirty work 中的 （难点 ，怎么解决的，遇到的问题，闪光点） AI 在项目中的使用方法 BS设计模式 重温\n算法+刷题 Go语言的面试问题，深度理解 八股文 学习\nMysql Redis Kafka etcd docker 设计模式 需要\nAI Agent 后端 (预计魔改一下Java的那个项目) 高并发服务 （可能选择邓明的那个或者是魔改一下我实习的项目，看一下哪个ROI高点） 写在最后 我自己也看不清未来的走势，站在这个时间点 我也迷茫 恐惧 想要逃避\n但是我内心有一股劲，或者说我还年轻，我只有一双脚，能走到哪里，我也想看看\n","date":"2026-07-08T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/fire-workd-03/","title":"整理"},{"content":"写在前头 :\n周一通知的裁员，今天周二，这一周还有很多任务要做，今天可以说是这周最忙的一天\n今天一整天都没怎么摸鱼，没有写算法题没有写项目。回到家也很累，今天锻炼就锻炼了15分钟\n现在是 22：29 我想我应该做点什么\n现状 这次被裁员了 20% ，覆巢之下，安有完卵 , 我应该算是最年轻的一个\n跟我同一批的人他在做什么 :\n跟我同一批的人有4个，指的是我们公司的，至于互联网上的其他人我并不清楚\n清华哥，实习的时候和我对接，但是实习结束后没有选择转正 而是去日本工作了 equation，目前应该是单独处理一个业务组，好像是在做最新的业务 yapeng,一开始是在行情组工作，但是后面分了一半心思去做 AI Agent了主要是对接客户成功那边 反观我而是一直在组内安分守己 温水煮青蛙，我并不知道他们为什么会去接触这种新业务的，是Leader推荐还是CTO选拔，或者是自己去私聊。我是一个局外人\n跟我同一批被裁的人在做什么 :\n跟我同一批被裁的目前聊下来的大概有 4个，至少有4个有一些打算\nsunl, 我前组的同事，在我实习转正之后跳到了风控组。我很佩服他这个决定，可以说是天时地利人和而且非常大胆 我记得我们公司春初的时候让大家填了一个表，询问是否想要留在自己组，如果不想要留在自己组最想去哪个组。但是我并不思变，依旧选择自己组\n但是 sunl 是前年就转组了，没转组之前预计在我们的业务组呆了2年 。可惜的是他也被裁了。我没记错他应该是去年结婚的，估计现在经济压力负担也大，他现在的目标就就是\n下周就能到岗 远程高新工作 有点佩服，一直都在更新简历？不用准备？下周就能到岗\nxll，一位工作了很久的同事，说是这波裁员之后 都可以领退休金了，社保已经交满15年了 他现在的想法是\n先玩一个月 然后复习 最后转 AI 哦 另外两个没有表明意向，我也不清楚。我本来预计是想要gap半年的，但是看到老一辈的都这么卷，我感觉我也不能松懈\n我的想法 我现在的想法是什么 :\n从先玩再准备 ，改变成先准备在玩\n预计一天学习 9:00 到 21:00 , 12个小时 。算上buffer-time 预计是 10~12个小时 先准备一个月，大概是 300 ~ 360 h\n预计需要完成3个项目\n一个 Python 搭建的 AI Chat 项目， 这个项目来自 Acwing ; https://www.acwing.com/activity/content/4250/ 之前写到一半放弃了，18个课时，算上double，总共36个课时\n一个 Golang 的项目，这个还没有找可能是从实习简历里面捞一个出来，或者是重新写一个。预计课时也是18左右 double 36\n一个 Mit 6.824 我很可惜，我一直没有完整的思考和写过这个项目，预计课时 10 double 20\n一个 Java 项目 可能也来自 Acwing 或者是 Java 星球里面的 12306 抢票系统 。 预计课时 18 double 36\n项目花费课时 36*3 + 20 = 128\n期间还需要补充八股文\n主要复习来自 https://xiaolincoding.com/network/ 小林coding\n计算机网络 5个章节 预计一个章节 10小时，总共50个小时\n操作系统 10个章节 预计一个章节 10小时 预计100个小时\nMysql 总共8个章节 预计一个章节 10小时 预计 80个小时\nRedis 总共8个章节 预计一个章节 10小时 预计 80个小时\n100+160+50 = 310 h (看来欠的有点多 hh)\n可能还需要准备一下 Etcd，分布式，高并发，MQ 等问题 这里预计要 50个小时\n总共 360h\n剩下的时间还需要算上算法的内容，预计课时是 45，算上刷题和复习 double一下 大概90\n还有一些时间大概就是 准备一下面试题什么的\n总共算一下大概就是\n128 + 360 + 90 = 578 大概高强度 一天10个小时学习，57天 大概是两个月。 如果是下周一开始准备的话大概是9月14号后开始进行玩\n(这里简单算一笔账，如果每天回家学习1个小时，大概需要学习578天也就是2年，但是现在只需要两个月) hh , 果然上班之后时间都不是自己的\n这周因为还有工作，所以预计会在周五总结一下手上有的和手上没有的，同时决策一些方案。例如去哪里玩 什么的\n写在后面 我承认这个计划可能很难，我感觉也会失败，而且失败的代价或许是惨痛的，因为算上double 我可能会gap 4个月\n或者说找不到好的工作 。\n但是我想说的是，现在没有更好的选择了 不是吗\n我能做到的是当下最好的选择\n加油，这下真该加油了\n补充一下 刚刚写完之后 清了一下游戏的体力想到一些问题\n如果我写 Java 项目那么就需要多准备一份 Java 八股文，预计会额外增加故事点，这可能会增加压力，但是他会带来更多选择的好处这个值得决策，而且我引入 Java 项目主要是可以多一个资料全一点的高并发项目可以抄，Golang这边目前没看到\n我的时间都去哪了？ 假设一天正常工作8小时，其中摸鱼时间1小时，这一小时我会水群看知乎看小红书，这叫放松 。 那么我早上9：00起床，9：30到公司，晚上18：30下班，到睡觉0：30期间有6个小时\n锻炼，炒菜，吃饭洗澡洗碗 1个小时半\n游戏清体力过日常 1个小时\n回家刷抖音/B站 1个小时\n如果要跑步的话 1个小时\n那么还剩 1个半小时，这一个半小时就是学习的一个半小时，但是我又要提前半个小时放松大脑，所以只有1个小时\n这个小时就是 23：00到00：00 ，这个时间点之前我再记录日记，现在再写博客，闲暇的时候我会去水群，其他时间可能会去看直播\n所以我只有工作了之后我确实只有 1个小时的休闲时间，当然如果把刷抖音/b站，游戏去掉 或者说加起来控制再1个小时，那么我只会多出一个小时 可是这样事情会变得很枯燥\n而且之前我还在和女朋友聊天，再学习漫展修图 各种各种，看来我工作的时候确实没时间学习，或者说学习是一条很长的战线\n哦 还有我玩游戏1个小时只是搬砖游戏的一个小时，偶尔还会去玩其他游戏占用其他时间，另外之前炒股又花了一部分时间\n突然想起来 史铁生的《好运设计》 ， 以及某些政府政策导致某些人群 某些地区的发展变动\n我的时间被谁设计了，我的一生被谁设计了 。我选择了这个行业，但是我并没有在这个行业看到希望看到的之后绝望\n倘若我能拿 22 *15 总包也只有30，算一年存20w，十年才200w，可是有什么工作能够工作10年，或者说我有什么耐心有什么毅力能够在枯燥 动荡日子里坚持十年\n坚持十年后 买房100w 买车20w 结婚20w 总共花费 140w，赡养父母 20w 总共160w 我还剩40w 这时候我还剩下什么\n1 逆流河这条路是看不到尽头的，只有深深的绝望 ","date":"2026-07-07T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/fire-workd-02/","title":"裁员之后我该何去何从"},{"content":"写在前头\n种一棵树的最好时间是十年前，其次是现在\n我们什么都没做错，我们只是被筛选\n工作历程 在这家公司大概有 3年了, 我只能说缘分让我留在了这，而且我正好也懒不向上求。\n现在的我没有情绪上的特大波动，仿佛就好像认为自己本该走一样，无非早点晚点。\n但是我也没什么思绪能够理清现在的表达逻辑，就简单的说一下\n组内变动 从我实习到现在，我们组确切的说变动了4次\n第一次是 DEFI \u0026amp; CEFI 的拆分，这是从业务属性上进行拆分 第二次是 将 Collector Account 的组合并，负责底层调度账户的逻辑写好大概有2年了，没什么新内容。所以那个组被砍掉了，归为我们拆出来的组管 第三次是 数据组 合并到 CEFI 组，同样的，数据组 的大部分内容都很简单，无非就是处理 数据接口 API调度以及处理旧数据，大体的框架跑安稳了几年之后没什么新东西，同时处理旧数据的内容可以直接使用SOP进行处理，并且很多东西都配置化了 第四次就是我们这次了, 也可以说不是变动吧 我们组原先的 Leader 换人了，新招了一位 Leader 过来，新增了一些 快照处理 回溯 账户暂停 的需求。然后需求做的差不多,我们也被裁员了 我经历了 3次完整的业务组调整,见过很多人走,也见过很多人来\n一位组内的实习生,因为临近春招，频繁请假,然后被裁员了，裁员的很突然，实习生嘛，没什么交接流程直接就走了 还有一些实习了很久但是没转正的，说是想要回家工作去了新疆 还有一个从腾讯微宝出来的,但是他没通过转正考核 还有一位也是腾讯出来的,但是他转正后工作了几个月就跳槽去了证券 还有一些刚团建完就被裁员的 为了出国然后辞职的 好多好多, 刚毕业那会我对很多同事都很热情,同样我也经常和他们交流。但是走的人越多,我越沉默,渐渐的我不说话\n1 2 3 4 5 6 7 8 9 10 多年之前 我是这么寂寞的, 今时今日我也是这么寂寞的 ... 寂寞又怎么样？ 礁石都不说话 但是水流过去之后 礁石留下 ---- \u0026lt;\u0026lt;江南随笔\u0026gt;\u0026gt; 自身成长 1 2 我刚买了两瓶新的盐，一瓶酱油，一瓶橄榄油,我还没来得及用，但是我要走了 ---- 我自己 硬说我这两年没有成长是真的，甚至可以说我是退步了\n我忘记了之前大学那会的算法题 我忘记了我实习时候写在简历上的项目 我忘记了我当初准备面试时候背的八股文 我失去了学习的热情 我失去了这两年的时间 \u0026hellip;\n所以我可以 大大方方的承认 我失败了,我退步了\n那么你说我没改变吗？\n2024年\n我尝试拍漫展，学习修图，并且获得了不错的作品，有一些摄影的朋友 我完整的完成了我的毕设 我一个人租房 一个人换房 第一次出国 我好像就没了 2025年\n我爬了一座雪山 开始陆陆续续减肥 从 170 减到了 150 我买了人生第一辆公路车 人生第一次搬家 卖掉了我的摄影设备 开始玩期货，并且从 200u 赚到了 20W RMB .不得不承认这段时间回不去了 开始玩基金 2026年\n开始自己做饭 开始护肤 开始护发 开始锻炼, 从 150 减到了 140 开始跑步 开始价值投资 开始玩股票 我尝试了很多很多，同时也浪费了很多很多\n我知道主要让我赚钱的是工作, 但是我并没有在上面进行努力\n补充 在这3年值得提几句的点真不多，我刚得到 优秀员工 下一秒就被裁了，从任务上看\n实习期间 :\n那时候 AI还没火，我单独写了一个 异步调度任务 的需求 一整个完整的需求\n从异步调度 API 存入数据库状态，发送给MQ MQ轮询消费 处理数据 数据入库，状态更新 正式期间 :\n优化数据库存储\n这个说实话方案是 我 Leader 出的，我只是负责实行而已 Auth 服务器迁移\n这个比较难描述，我们单独把敏感的API信息放到了一个 Auth 服务器上面，但是我们之前 V1 版本维护的不好。所以我们做了 V2 升级 好像就没有了，一时半会没想起来 后面第二篇整理的时候在想吧\n后期规划和安排 我原本是想着\n去尼泊尔学一趟英语 顺便走一趟尼泊尔徒步 然后去埃及学滑雪 去菲律宾潜水 最后去趟日本旅行 但是我发现如果我这么玩下来可能要半年了，到时候我会脱节吗 我不知道，我是否又能做到在旅行的时候学习呢\n然后下午的时候想了一下发现我思路错误了\n我应该先学习 然后再投简历和面试的时候出去玩\n这次预计 gap 3个月，因为赔偿hi N+1 正好 3个月\n预计是 2个月准备 +1个月游玩\n期间的安排还没定过，预计是\n先洗一遍简历的项目 然后背八股文 和 一些中间件 期间看情况有没有时间学第二语言 我感觉这几个月会很忙\n当然还有一些其他问题\n整理工作的经验 社保要不要自己交，断交，以及失业金的领取 要不要换个房子租，去海边租 一些感悟 其实我很早就能猜到我们组要裁员的，大概是3个月前，我们组就没什么需求了\n但是那时候我还没有开始，因为 第一出差 第二 端午 51 假期规划出游\n直到最近一个月才开始重新刷算法题，但是当我开始我才发现我开始晚了\n裁员的事其实上周 ，CTO就找leader聊过了，但是名单是这周才出。\n从 CTO 的角度来看，CTO只能看到 一个人有没有写bug，他的薪资是多少，他的产出是多少\n我可以认为是我们组产出是第二高的，或者是第一 bug的话因为AI的出现很少有了 CTO 同样还需要参考 产品经理 和 Leader 的建议\nLeader 从可替代性，涉及模块出发，我确实会走。 因为我自己拥有的模块业务属性不高，而且可替代性很强。我相当于是一个工具，只是被用，而不是在培养\n从产品经理的角度来看，自从有了AI的加持，对于一个需求能不能做，合不合适做，这个问题就变的微乎其微了，所以我的护城河也没了\n其他 写累了 明天再写吧，写这么点东西竟然写了我一个小时\n","date":"2026-07-06T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/fire-workd-01/","title":"[重磅消息] 我被裁员了！"},{"content":"离散化 整数离散化 算法思路 算法解决的问题是\n值域很大, 但是实际操作区域很小 的题目 例如 :\n在 1e9 的区间范围上 操作 3e5 次操作然后进行求和 整个思路如下\n先将需要 操作的数 和 查询的数 放到一个队列里面 给队列排序去重 通过二分获取每个值应该存在的下标 核心代码展示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 typedef pair\u0026lt;int,int\u0026gt; PII ; const int N = 3e5 + 10 ; int n , m ; int a[N],s[N]; vector\u0026lt;int\u0026gt; alls ; vector\u0026lt;PII\u0026gt; add,query ; int find(int x) { int l = 0 , r = alls.size() - 1; while(l \u0026lt; r) { int mid = (l + r) \u0026gt;\u0026gt; 1; if(alls[mid] \u0026gt;= x) r = mid ; else l = mid + 1; } return r + 1 ; } cin\u0026gt;\u0026gt;n\u0026gt;\u0026gt;m; for(int i = 1; i \u0026lt;= n ; i ++ ) { int x,c; cin \u0026gt;\u0026gt;x\u0026gt;\u0026gt;c; add.push_back({x,c}); alls.push_back(x); } for(int i = 1 ; i \u0026lt;= m ; i ++ ) { int l,r;cin\u0026gt;\u0026gt;l\u0026gt;\u0026gt;r; query.push_back({l,r}); alls.push_back({l}); alls.push_back({r}); } sort(alls.begin(), alls.end()); alls.erase(unique(alls.begin(), alls.end()), alls.end()); for(auto item : add) { int x = find(item.first); a[x] += item.second; } for (int i = 1; i \u0026lt;= alls.size(); i ++ ) s[i] = s[i-1] + a[i]; for(auto item : query) { int l = find(item.first) ; int r = find(item.second) ; cout\u0026lt;\u0026lt;s[r] - s[l-1]\u0026lt;\u0026lt;endl; } 时间复杂度分析 需要遍历数组 On 感悟 这次学到了一下新玩意\ntypedef 可以定义 pair\n以及 sort 可以直接对数组进行排序\nvector.erase(unique(), vector.end()) 可以去重\n1 2 3 4 typedef pair\u0026lt;int,int\u0026gt; PII ; sort(alls.begin(), alls.end()); alls.erase(unique(alls.begin(), alls.end()), alls.end()); ```c++ ","date":"2026-07-02T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x04/","title":"[Acwing] 基础算法（四) 离散化\u0026区间合并"},{"content":"前缀和 一维前缀和 算法思路 使用 Sum 数组累加计算数值数组的和 sum[i] = sum[i-1] + a[i]\n从而可以很快的求出某个区间的和，例如\nsum[l~r] = sum[r] - sum[l-1]\n核心代码展示 1 2 3 4 5 6 for(int i = 1; i \u0026lt;= n ; i ++ ) cin\u0026gt;\u0026gt;a[i]; for(int i = 1; i \u0026lt;= n ; i ++) s[i] = s[i-1] + a[i]; while(m -- ) { int l , r ; cin \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r; cout\u0026lt;\u0026lt;s[r] - s[l-1]\u0026lt;\u0026lt;endl; } 时间复杂度分析 在只有一次区间询问的时候 和 for 循环直接计算无异 都是o(n) 但是在多次区间询问的时候可以提前把和计算出来从而使得 o(n*m) 变成 o(n+m)\n感悟 小学数学\n二维前缀和 算法思路 通过数学方法将 Sxy 代表二维空间中矩形的面积，从而根据二维关系进行处理\n不过需要注意的是 我们在处理一个区域的面积大时候， 需要保证角落上的点也被计算所以就需要 S[x1][y2-1] 以及 s[x2-1][y1] 这种处理\n由于作图的时候 只有点线，并没有画出具体的面，所以导致写代码的时候很少联想到\n核心代码展示 1 2 3 4 5 6 7 8 9 10 11 for(int i = 1; i \u0026lt;= n; i ++ ) for(int j = 1; j \u0026lt;= m ; j ++ ) { cin\u0026gt;\u0026gt;a[i][j]; s[i][j] = a[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1]; } while(q -- ) { int x1,y1,x2,y2 ; cin \u0026gt;\u0026gt; x1\u0026gt;\u0026gt;y1\u0026gt;\u0026gt;x2\u0026gt;\u0026gt;y2; cout\u0026lt;\u0026lt; s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]\u0026lt;\u0026lt;endl; } 时间复杂度分析 o(n*m) 感悟 不用死记公式，根据二维图像脑中进行想象\n差分 一维差分 算法思路 使用 Sum 数组累加计算数值数组的和 sum[i] = sum[i-1] + a[i]\n从而可以很快的求出某个区间的和，例如\nsum[l~r] = sum[r] - sum[l-1]\n核心代码展示 1 2 3 4 5 6 for(int i = 1; i \u0026lt;= n ; i ++ ) cin\u0026gt;\u0026gt;a[i]; for(int i = 1; i \u0026lt;= n ; i ++) s[i] = s[i-1] + a[i]; while(m -- ) { int l , r ; cin \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r; cout\u0026lt;\u0026lt;s[r] - s[l-1]\u0026lt;\u0026lt;endl; } 时间复杂度分析 在只有一次区间询问的时候 和 for 循环直接计算无异 都是o(n) 但是在多次区间询问的时候可以提前把和计算出来从而使得 o(n*m) 变成 o(n+m)\n感悟 小学数学\n二维前缀和 算法思路 构造数组 b[i]=a[i]-a[i-1] 是的 Sum[b[]] = a[i]\n如果我们需要对一个区间进行一次操作，如 计算 区间 [l,r] +c 之后的总和，我们很自然的可以想到 s[r]-s[l-1] + (r-l+1)*c\n但是如果我们需要对不同的区间进行多次操作,我们就没办法这样\n基于 sum[b[]] = a[i] 的性质，我们对 b[l]+c 并且对 b[r+1]-c 我们得到的数组就是\nb[l]=a[l]-a[l-1]+c, b[l+1] = a[l+1]-a[l], .... b[r+1] = a[r+1] - a[r] - a[r]\n如果计算前缀和的话 会变成\na[l] + ... a[r] + (r-l+1)*c\n核心代码展示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void insert(int l,int r,int c) { b[l] += c ; b[r+1] -= c; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin\u0026gt;\u0026gt;n\u0026gt;\u0026gt;m; for(int i = 1; i \u0026lt;= n; i ++ ) cin \u0026gt;\u0026gt; a[i]; for(int i = 1 ; i \u0026lt;= n ; i++) insert(i,i,a[i]); while(m -- ) { int l, r, c; cin\u0026gt;\u0026gt;l\u0026gt;\u0026gt;r\u0026gt;\u0026gt;c; insert(l,r,c); } for(int i = 1; i \u0026lt;= n ; i ++ ) b[i] += b[i-1]; for(int i = 1; i \u0026lt;= n; i ++ ) cout\u0026lt;\u0026lt;b[i]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; return 0; } 时间复杂度分析 差分时间复杂度 O(1) 感悟 这里说实话就有点抽象了，不实际想想或者是推导很难理解这部分代码\n二维差分 算法思路 我们在已知 一维差分的操作会因为前缀和的原因导致 操作值累加在区间上\n在不推导 差分数组的前提下 :\n我们可以直观的想想 如果我们在 x1,y1增加 c 那么剩下的范围都会被影响，即取一个差集\n所以我们如果需要给 x1,y1 ,x2,y2 增加 c 的话我们应该\nx1,y1 +c,x1,y2+1 -c, x2+1,y1 -c , x2+1,y2+1 +c\n从而使得我们的矩形能够加 c\n反过来如果我们 给 x1,y1,x1,y1 这个区间增加 c 那么最终我们得到的就是原始的数组\n核心代码展示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void insert(int x1,int y1,int x2,int y2,int c) { b[x1][y1] += c; b[x2+1][y1] -= c; b[x1][y2+1] -= c; b[x2+1][y2+1] += c; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin\u0026gt;\u0026gt;n\u0026gt;\u0026gt;m\u0026gt;\u0026gt;q; for(int i = 1; i \u0026lt;= n ; i ++ ) for(int j = 1 ; j \u0026lt;= m ; j ++ ){ int c ;cin\u0026gt;\u0026gt;c; insert(i,j,i,j,c); } while(q -- ) { int x1,x2,y1,y2; cin\u0026gt;\u0026gt;x1\u0026gt;\u0026gt;y1\u0026gt;\u0026gt;x2\u0026gt;\u0026gt;y2; int c ; cin\u0026gt;\u0026gt;c; insert(x1,y1,x2,y2,c); } for(int i = 1; i \u0026lt;= n ; i ++ ) for(int j = 1; j \u0026lt;= m ; j ++ ) b[i][j] += ( b[i-1][j] + b[i][j-1] - b[i-1][j-1]); for(int i = 1; i\u0026lt;= n ; i ++ ) { for(int j = 1; j \u0026lt;= m ;j ++ ){ cout\u0026lt;\u0026lt;b[i][j]\u0026lt;\u0026lt;\u0026#34; \u0026#34;; } cout\u0026lt;\u0026lt;endl; } return 0; } 时间复杂度分析 O n*m\n感悟 二维差分说实话一开始没反应过来，我听y总讲的时候，感觉怎么跳的这么快，连构造数组都不出，原来这里反向思考了一下\n而且对于 (x2 + 1) 我刚开始以为 x 是 列，问了 gemini 后才知道是行\n","date":"2026-06-25T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x03/","title":"[Acwing] 基础算法(三) 前缀和\u0026差分"},{"content":"二分排序 整数二分 算法思路 比较玄学的一句话目前还没懂\n二分不一定单调，但是单调一定二分\n算法思路\n对于一个区间可以拆分成 左边不满足条件 右边满足条件 的数据，可以求满足或者是不满足的数据的边界 对于边界的的定义 有 左边界 和 右边界\n整体抽象思路如下\n设定 mid check(mid) 如果 mid 符合 右区间/左区间 那么答案一定在 右区间/左区间, 将 l/r 指针移动到 mid 对于答案在右区间来说，那么需要把 l 移动到 mid 因为 mid 也可能是答案 ，否则到话 r=mid-1\n同样答案在左区间来说，那么需要把 r 移动到 mid 否则到话 l=mid+1\n需要注意的是 因为程序 下取整的原因，需要保证 在移动 l=mid 的时候 mid =l+r+1\u0026gt;\u0026gt;1\n核心代码展示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 while(l \u0026lt; r) { int mid = l+r\u0026gt;\u0026gt;1; if(a[mid] \u0026gt;= x) r = mid ; else l = mid + 1; } if(a[l] != x) { cout\u0026lt;\u0026lt;\u0026#34;-1 -1\u0026#34;\u0026lt;\u0026lt;endl; }else { cout\u0026lt;\u0026lt;l-1\u0026lt;\u0026lt;\u0026#34; \u0026#34;; l = 1, r= n ; while(l \u0026lt; r) { int mid = l+r+1\u0026gt;\u0026gt;1; if(a[mid] \u0026lt;= x) l = mid; else r = mid - 1; } cout\u0026lt;\u0026lt;r-1\u0026lt;\u0026lt;endl; } 时间复杂度分析 每次操作都将区间一分为二，最多logn次求解\n感悟 左区间调动右指针，右区间调动左指针有点反直觉 但是实际理解下来还好 这个是我下班的时候看的，一开始没想明白，回家锻炼之后然后今天突发的想要跑步，跑了个5公里回来继续看 写下的\n","date":"2026-06-23T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x02/","title":"[Acwing] 基础算法(二) 二分"},{"content":"方法 课上: 学习 主要思想\n课下:\n理解代码,背过代码 根据题目默写代码 ( 3 ~ 5 次 ) 快速排序 算法思路 寻找区间内的随机一个值, 将其当作中间值 遍历当前区间, 将区间分为 [l ~ x] [x ~l] 的形式 递归处理区间, 将大区间拆分成多个小区间进行排序 核心代码展示 1 2 3 4 5 6 7 8 9 10 11 void quick_sort(int q[], int l, int r) { if (l \u0026gt;= r) return; int i = l - 1, j = r + 1, x = q[(l + r) \u0026gt;\u0026gt; 1]; while (i \u0026lt; j) { do i++; while (q[i] \u0026lt; x); do j--; while (q[j] \u0026gt; x); if (i \u0026lt; j) swap(q[i], q[j]); } quick_sort(q, l, j); quick_sort(q, j + 1, r); } 时间复杂度分析 由于区间是对半拆分的 最多只有 logN 层\n但是每层都需要进行一次 while(i\u0026lt;j) 的遍历 每层需要 o(n) 的时间复杂度\n所以总时间复杂度是 o nlogn\n最坏情况下 :\n例子 A：已有序 + 基准取 q[l]\n数组：[1, 2, 3, 4, 5]，每次 pivot = 最左边：\n1 2 3 4 第1次: [1 | 2,3,4,5] 左 1 个，右 4 个 第2次: [2 | 3,4,5] 右 4 个继续 第3次: [3 | 4,5] ... 分区大小每次都是 n, n-1, n-2, …, 1 总工作量 n + (n-1) + (n-2) + … + 1 = n(n+1)/2 = O(n²)\n理想情况\n1 2 3 4 5 n / \\ n/2 n/2 / \\ / \\ n/4 ... 高度 ≈ log n，每层总工作量 n 最坏情况\n1 2 3 4 5 6 7 n \\ n-1 \\ n-2 \\ ... 链状，高度 n，总工作量 n+(n-1)+... = O(n²) 犯错 一开始因为不喜欢 do while 的形式，所以自己单独写了一个 while 的形式\n后续发现一直 TLE 后面看了一下 在两个相同数的时候，如果先进行判断然后再进行位移指针会卡住程序的运行\n1 2 3 4 5 6 7 8 9 初始: i=1, j=2 第 1 轮: 左扫: 67 \u0026lt; 67 ? 否 → i 停在 1 右扫: 67 \u0026gt; 67 ? 否 → j 停在 2 swap(67, 67) → 数组不变 第 2 轮: i 还是 1, j 还是 2 → 完全一样 → 永远循环 1 2 3 4 5 6 7 8 9 10 11 12 13 初始: i=0, j=3 第 1 轮: do i++ → i=1, q[1]\u0026lt;67? 否 do j-- → j=2, q[2]\u0026gt;67? 否 swap(67,67) → 数组不变 第 2 轮: do i++ → i=2 ← 强制从 1 走到 2 do j-- → j=1 ← 强制从 2 走到 1 现在 i=2, j=1 → i\u0026lt;j 为假 → 退出 递归 (1,1) 和 (2,2) → 结束 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void quick_sort(int q[], int l , int r) { if( l \u0026gt;= r ) { return ; } int x = q[l] , i = l , j = r ; while(i \u0026lt; j) { while(q[i] \u0026lt; x) i ++ ; while(q[j] \u0026gt; x)j -- ; if(i \u0026lt; j) { swap(q[i], q[j]); } } quick_sort(q, l , j ); quick_sort(q, j + 1, r); return ; } 感悟 记录一下在上班的时候 能够听满 30 分钟的课程 之前可是想都不敢想的\n第二次默写 第二次默写失败了好多次\nsignal: bus error 这个错误 指的是程序访问了不合法的内存地址 因为我代码总累加写错了 变成了 j++\n1 2 3 4 5 6 7 8 9 10 11 void quick_sort(int q[],int l , int r) { // ... do j ++ ; while(q[j] \u0026gt; x) ; swap(q[i],q[j]); } // ... } 第二个点就是 需要保证 i \u0026lt; j 才能够进行 swap 不然会把原来已经排序好的程序重新打乱 边界的选取 在竞技编程中，为了防错，大家通常死记硬背以下两套固定组合，千万不要交叉使用：\n组合一（最常用，推荐）：向下取整配 j\n基准值：x = q[(l + r) \u0026gt;\u0026gt; 1];\n划分线：j\n递归：quick_sort(q, l, j); 和 quick_sort(q, j + 1, r);\n组合二：向上取整配 i - 1\n基准值：x = q[(l + r + 1) \u0026gt;\u0026gt; 1];\n划分线：i - 1\n递归：quick_sort(q, l, i - 1); 和 quick_sort(q, i, r);\n归并排序 算法思路 递归处理大区间一分为2为小区间 双指针合并两个小区间 ， 保证数组有序 核心代码展示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void merge_sort(int q[], int l, int r) { if(l \u0026gt;= r) return ; int mid = (l + r) \u0026gt;\u0026gt; 1; merge_sort(q,l,mid) , merge_sort(q,mid + 1, r) ; int k = 0 , i = l , j = mid + 1; while(i \u0026lt;= mid \u0026amp;\u0026amp; j \u0026lt;= r) { if(q[i] \u0026lt;= q[j]) temp[++k] = q[i ++ ] ; else temp[++k] = q[j ++ ]; } while(i \u0026lt;= mid) temp[++k] = q[i ++ ]; while(j \u0026lt;= r) temp[++k] = q[j ++] ; for(int i = l , j = 1 ; i \u0026lt;= r ; i ++ , j ++ ) q[i] = temp[j]; } 时间复杂度分析 递归处理最多拆分成 logN 个区间，每个区间最多进行 N 次 最坏情况下都排序也是 nlogn 的\n感悟 归并排序的过程很好理解，但是实际写代码下来 指针很多 i,j,k,mid ，然后还加上递归回溯的处理，导致整个过程比较难理解\n","date":"2026-06-17T00:00:00Z","permalink":"https://ddlgitgzzhm.github.io/p/acwing-basic-0x01/","title":"[Acwing] 基础算法(一) 排序"},{"content":"我的初心 Q : 为什么之前我要写这个博客\n我创建 Blog 这个项目的初心 是打算日常记录 以及 学习记录 分享\n可是从毕业以及工作之后越来越没有学习的心思了\n工作内容变动 恋爱的琐事 副业的变动 牛熊转换 \u0026hellip; 各种内容充斥在生活里面，让我开始摆烂\n项目的扎手点 目前这个项目的设计逻辑是\n使用者在 IDEA 编辑对应的 Markdown 文件 如果你要发布一个文章 你需要 创建一个 package 指的是手动的右键点击创建 package 然后插入一个 title 图片 如果没有图片的话会很丑 还需要手动填写文件的日期 date 手动填写 slug 手动填写 categories 在本地运行 hugo server 进行确认 提交 \u0026amp; push 然后等 action 同步最后才上线 如果需要编辑和更新文章的话 那同样麻烦 一些吐槽 而且我的并不打算把我的文章广为流传，因为确实没有什么内容，不值得推广，如果我真想要分享 不如去 知乎上面分享\n另外 Markdown 的缺点，你并不能手动 的拖动调整图片的大小 这是我很烦的一点\n后来 我尝试了 Notion 和 Obsidian\nObsidian 可以说是本地化的 Markdown 文件，同样处理图片很麻烦\nNotion 的话对我来说很好用，但是他没有标签也不能自定义展示，我没在用 Blog 的时候 大部分时间都在用 Notion\n另外 Notion 必须联网这点很不好，而且 notion 自家的代理太烂了，时长卡顿\n经历了很多内容 最后还是打算捡起来这个 项目， 就想尝试 能不能做好，先慢慢做下去吧\n另外 Notion 预计只能记录一些生活化相关的内容，偏学习或者是技术的内容，我感觉记录在上面不太专业，而且也不可以定制化展示\n后续想法 想要考虑做成 动态页面，这样子可以在前端直接发布文章，也可以自动的打标签，可以防止一开始创建文件的繁琐步骤\n但是这样成本预计就上来了，现在使用 git-page 部署的，只能部署静态网页，边走边看了 或者是简单的版本，可以考虑写几个 scripts ， 用于自动创建 page，这个看着是效益最高的\n","date":"2026-06-11T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/blog-summary-26-06-01/blog_hu_d0bf4bd03c634dd3.png","permalink":"https://ddlgitgzzhm.github.io/p/blog-summary-26-06-01/","title":"[blog] 当前使用体验"},{"content":"分布式系统是什么 它的核心是通过网络使一群计算机相互通信来完成一些连贯的任务\n大型网站的存储 大数据计算 为什么要有分布式系统 许多关键基础架构是由分布式系统基础架构构建而成，该架构需要一台以上的计算机才能完成工作或者本质上的物理分散 高性能 ： 想要通过某种方式来实现某些东西的并行，大量的 CPU、大量的内存、大量的硬盘 可以并行的运行\n","date":"2025-12-18T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/mit-6.824-learning-1/title_hu_1e5f17695893b496.png","permalink":"https://ddlgitgzzhm.github.io/p/mit-6.824-learning-1/","title":"[mit - 6.824] First Step"},{"content":"总结 视频来源 : https://www.youtube.com/watch?v=83-jhmek0iQ\u0026list=PLv62PVNSGzGD2614LkngWETWL-fZebdja\n视频目的 :\n开单的逻辑 开单的细节 最近哪些单子做的不好，哪些单子做的好 一周下来我的问题要怎么调整 一周下来我做的好的方向要怎么坚持 心态崩的单子还原整个心理路程 不会去解答每一个问题 并不能解答明白 也没有那么多精力 第一项内容 :\n整体市场这一周来在做一个什么事情 整体市场在进行怎么样一个动作，市场的情绪是怎么样的 复盘每一个单子，开单逻辑技术细节，有什么问题避免什么问题 第二项内容 :\n财富密码，接下来重点关注的机会 主升浪的上涨中没有一笔空单 单子 :\n主升浪 上方有空间 小分歧\n尽量做低吸 不要做突破 市场青睐次新股，因为没有太多套牢盘，比较好拉盘 在分歧结束的位置去做单 ","date":"2025-12-12T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/bitlanglang-study-1/title_hu_1e5f17695893b496.png","permalink":"https://ddlgitgzzhm.github.io/p/bitlanglang-study-1/","title":"BitLangLang 视频学习1"},{"content":"踏歌行 qei h\n","date":"2025-09-09T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/system-summary-2/title_hu_1e5f17695893b496.png","permalink":"https://ddlgitgzzhm.github.io/p/system-summary-2/","title":"[登神长阶] 构建完整的交易系统 1 "},{"content":"前言 简单总结一下 目前的一个 交易系统\n可以作用于 山寨币 或者是容易出现 趋势行情 的标的中 . 不适用于大盘,因为大盘最近处于 大级别的震荡行情\n山寨季的定义 :\n当大盘在盘整区间,资金流出,流入到各个小的标的. 那么会导致 这种标的有大额涨幅 过程 1. 选标的 如何一个可能突破的标的 ？\n各种群聊群友喊单 广场 Kol 喊单 @投研看剑 @crypto 七安 or 其他 飞机群喊单 (没加过感觉听神秘的) 由他们根据市场信息，热度，板块，解锁时间，链上信息，或者是形态等多方面分析之后，可以过滤出很多标的\n涨幅榜前 30 ，不用做任何调研，我们只需要根据 市场热度 , 市场有热度就会有资金流入 2. 如何判断突破 我们需要先确定 有效压力位, 我们可以去 4小时 , 天 , 1小时 找到仍然在盘整区间\n我们看下面这个k线图，4小时级别一直都在一个箱体里面震荡，且出现一个很明显的上顶和下底\n那么我们可以认为 这个上顶 就是一个大区间的压力位\n我们画一条k线，如果他突破了压力位那么就会 大概率会直接起飞. 这是因为市场会产生合力 .简单来说就是 ( 老韭菜们都知道这里要突破了所以都会大资金进来这就是合力 )\n3. 如何确定入场点 我们从大级别 切换到小级别 30min, 15min,5min,1min 画一条 上升趋势线 当多次回踩趋势线的时候我们就可以入场 。 常见情况是第三次回踩趋势线的时候 并且这里出现了7小连阳，市场信息倍增 所以我们可以在第三次回踩趋势线的时候入场，或者是等他 突破的时候入场 即带量突破上方压力位。或者是突破压力位回踩的时候进场 4. 如何有效的止损 两个逻辑\n我们可以设置大级别震荡区间的最低点 进行止损，因为如果突破失败 他还是会回震荡区间进行下一次突破. 为了避免频繁止损 保守点可以设置在这 当然激进一点我们可以设置在 趋势线前一个回踩点, 因为如果趋势线跌破 市场会没有信息，当然也可能是来扫损的 所以这个止损有点激进 5. 如何有效的止盈 如果我们在趋势线进场, 那么止盈就是突破压力位的时候，需要止盈50% ，因为避免假突破 其他的话我们可以 在左侧压力位分批止盈，即左侧每一个阴线的收盘价止盈 6. 其他 这次写的比较匆忙很多细节未补充，可能周末补充一下～ 。但是大致逻辑是这样 希望各位看到这条博客有元人发大财 ～ 另外这只是在 大级别区间震荡 能用的做法\n如果出现极致的单边行情 如 yala, myx, a2z 这种在 第三次回踩趋势线的时候可以直接进去 止损点设置在第二次回踩的时候,所以就会出现 我插了好几次myx，止损了好几次总算进去了 这种言论 忠告 开单必设止损 交易系统因 个人性格原因 所以同一个交易系统在不同人身上也会有不一样的效果 仅个人的玩法，仅供参考，切勿大资金乱玩。 交易市场本质就是一个零和博弈，没有常胜将军除非他对冲 实战截图 ","date":"2025-08-08T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/system-summary-1/img_hu_79f77f6411c76883.png","permalink":"https://ddlgitgzzhm.github.io/p/system-summary-1/","title":"[交易系统] 交易系统初版整理"},{"content":"概念 价格行为 : 指价格动作的形态，适用各种品种，各种周期，各种图标，走势变化就是价格行为的例子，每一次价格变动都是价格行为。\n市场运动 : 市场只有 趋势运动 和 非趋势运动 两种状态\n黄色箭头都是 非趋势运动\n对于黄色的1, 是一个 区间运动 而不是 趋势运动。处于一个区间震荡没有趋势 趋势 K 线\n实体较长 影线很短，实体占主要部分 和上一根 /上几根 K 线没有太多重叠 非趋势 K 线\n实体很小 影线很长 和上一根 / 上几根 K 线没有太多的重叠 类似 十字星 没有很明显的多空力量\n信号 K 线\n入场 K线 即是信号K 线 反转 K 线\n一根多头的反转 K 线 最低要求是 收盘高于开盘 或者 收盘高于 K线中位 下影线达到 K线高度 大约 1/3 ~ 1/2， 没有上影线或很短 信号 K 线的后一根 K 线不是十字星内包 K 线， 而是强入场 K 线 （多头趋势K 线，实体较长，影线较短） ","date":"2025-07-29T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/k-price-learning-1/img_hu_79f77f6411c76883.png","permalink":"https://ddlgitgzzhm.github.io/p/k-price-learning-1/","title":"[交易认知] 裸K 先导基础"},{"content":"概念 价格行为 : 指价格动作的形态，适用各种品种，各种周期，各种图标，走势变化就是价格行为的例子，每一次价格变动都是价格行为。\n市场运动 : 市场只有 趋势运动 和 非趋势运动 两种状态\n黄色箭头都是 非趋势运动\n对于黄色的1, 是一个 区间运动 而不是 趋势运动。处于一个区间震荡没有趋势 趋势 K 线\n实体较长 影线很短，实体占主要部分 和上一根 /上几根 K 线没有太多重叠 非趋势 K 线\n实体很小 影线很长 和上一根 / 上几根 K 线没有太多的重叠 类似 十字星 没有很明显的多空力量\n信号 K 线\n入场 K线 即是信号K 线 反转 K 线\n一根多头的反转 K 线 最低要求是 收盘高于开盘 或者 收盘高于 K线中位 下影线达到 K线高度 大约 1/3 ~ 1/2， 没有上影线或很短 信号 K 线的后一根 K 线不是十字星内包 K 线， 而是强入场 K 线 （多头趋势K 线，实体较长，影线较短） ","date":"2025-07-29T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/k-price-learning-1/img_hu_79f77f6411c76883.png","permalink":"https://ddlgitgzzhm.github.io/p/k-price-learning-1/","title":"[交易认知] 裸K 先导基础"},{"content":"","date":"2025-07-29T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/k-price-learning-2/img_hu_79f77f6411c76883.png","permalink":"https://ddlgitgzzhm.github.io/p/k-price-learning-2/","title":"[交易认知] 裸K 先导基础 2"},{"content":"\n","date":"2025-07-28T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/future-leaning-2/img_hu_79f77f6411c76883.png","permalink":"https://ddlgitgzzhm.github.io/p/future-leaning-2/","title":"[交易认知] MACD 重新认知"},{"content":"前言 很高兴又能继续更新我的博客，上一篇博客停留在了 5月26号，时隔两个月经历了许多事情。现在总算又回归平静，重整旗鼓我们重新出发。\n仓位管理 ","date":"2025-07-28T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/future-learning-1/img_hu_932c7e191f998b98.png","permalink":"https://ddlgitgzzhm.github.io/p/future-learning-1/","title":"[交易认知] 仓位管理和手续费"},{"content":" 兴尽悲来,识盈虚之有数\n-《滕王阁序》\n假若经历过 亲情、爱情、友情、物质的起落。\n不会把车子、房子、外貌、金钱、学历、地位等随时变化的东西作为自己的底气。\n如果你生活中的精神底气是这些随时会变的东西，那么当其离开你的时候，你就会抽尽端骨之痛。\n若有幸的话，至此会大彻大悟。天地阴阳循环不止，这世间万物都有其运行的周期。地球自传\n","date":"2025-07-28T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/future-think-1/img_hu_79f77f6411c76883.png","permalink":"https://ddlgitgzzhm.github.io/p/future-think-1/","title":"[交易心态] 兴尽悲来,识盈虚之有数"},{"content":"某音短视频 各个视频的平均完播率 题目 :\nQ : 如何计算两个列的差值\nA : 使用 TIMESTAMPDIFF(unit, start_time, end_tiem) 或者是 使用 DATEDIFF(end_date, start_time)\n如果使用 DATEDIFF 返回的是 相差的天数 对于使用 TIMESTAMPDIFF 可以使用下列枚举值 SECOND, MINUTE , HOUR, DAY, WEEK, MONTH, YEAR 思路 :\n我们需要计算出单条的完播率 TIMESTAMPDIFF() / duration 然后需要统计出一个视频的完播率 SUM ( TIMESTAMPDIFF() / duration ) / count(tb_user_video_log) 由于我们需要计算的 duration 在另外一张表, 所以我们可以考虑将 video_info 表的信息 left join 到我们的 video_log 表中 另外我们需要注意的是 这个题目只需要处理 2021 的数据, 也就是我们只需要 join 到 where year = 2021 的数据 1 2 3 4 5 6 7 8 9 10 select tbl.video_id, ROUND(SUM(IF (TIMESTAMPDIFF(second, tbl.start_time, tbl.end_time) \u0026gt;= tbv.duration, 1, 0)) / count(*),3) as avg_comp_play_rate from tb_user_video_log as tbl left join tb_video_info as tbv on tbl.video_id = tbv.video_id where year(tbl.end_time) = 2021 group by tbl.video_id order by avg_comp_play_rate desc 平均播放进度大于60%的视频类别 题目 :\nQ : 如何展示百分比\nA : 我们可以使用 CONTRACT(value , _suffix) 的形式加上 %\n坑点 : 可能用户会超播, 导致计算的数超过 100%\nA : 我们这里需要使用 LEAST(value , limit_value) 而不是 MIN(A,B) , MIN 函数在 SQL 中是一个聚合函数, 我们应该使用 LEAST\n思路 :\n同样我们需要计算单个条的播放率 LEAST( TIMESTAMPDIFF , DURATION ) , DURATION 然后我们需要计算单个视频的播放率 LEAST( ... ) / DURATION 由于这里输出的结果是 TAG 那么我们考虑先根据 vedio_id 分组, 分组后 再聚合 video_info 的信息 重新查一遍 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 select tbv.tag, temp.avg_play_progress from ( select tbv.video_id, CONCAT( ROUND( SUM(LEAST( TIMESTAMPDIFF(SECOND, tbl.start_time, tbl.end_time) , tbv.duration) / tbv.duration) / count(*) * 100 , 2), \u0026#39;%\u0026#39;) as avg_play_progress from tb_user_video_log as tbl left join tb_video_info as tbv on tbl.video_id = tbv.video_id group by tbv.video_id having avg_play_progress \u0026gt; 60 order by avg_play_progress desc ) as temp left join tb_video_info as tbv on temp.video_id = tbv.video_id TODO ","date":"2025-05-26T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/com-practise-2/byte_dance_hu_16fdde05e3cd7079.png","permalink":"https://ddlgitgzzhm.github.io/p/com-practise-2/","title":"[SQL] 综合练习 2"},{"content":"快速幂 求 A^B 的最后三位数表示的整数 输入格式 输入数据包含多个测试实例 每个实例占一行，由两个正整数A和B组成（1 ≤ A,B ≤ 10000） 如果A=0且B=0，则表示输入结束，不做处理 输出格式 对于每个测试实例，输出A^B的最后三位表示的整数 每个输出占一行 输入输出示例 输入样例 输出样例 2 3 8 12 6 984 6789 10000 1 0 0 (无输出) 算法讲解 :\n对于 123^(234). 我们可以优化成 (123^2)^117, 再进一步优化成 ((123^2) * 123 )^116 同理往下, 我们可以发现, 我们的计算量由次幂来控制, 同时我们可以根据就性质来操作次幂的优化 3. 对于奇数 我们只需要拆成 ans = ans * a 的形式 4. 对于偶数 我们只需要拆成 a = a * a 的形式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int qmi(int a, int b) { int ans = 1 ; while(b != 0) { if(b\u0026amp;1) ans = ans * a % 1000 ; a = a * a % 1000 ; b = b \u0026gt;\u0026gt; 1; } return ans % 1000 ; } int main() { int a, b; while(1) { cin\u0026gt;\u0026gt;a\u0026gt;\u0026gt;b; if (a == 0 \u0026amp;\u0026amp; b == 0 ) { break ; } cout\u0026lt;\u0026lt;qmi(a,b)\u0026lt;\u0026lt;endl; } return 0; } vj题目链接\ntodo ","date":"2025-05-25T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/hdu-0x01-math-02/icpc-title_hu_a9d0f649c644dbf8.png","permalink":"https://ddlgitgzzhm.github.io/p/hdu-0x01-math-02/","title":"[Hdu] 数学基础 day2"},{"content":"练习 统计复旦用户8月练题情况 题目：现在运营想要了解复旦大学的每个用户在8月份练习的总题目数和回答正确的题目数情况，请取出相应明细数据，对于在8月份没有练习过的用户，答题数结果返回0。\n表结构：\n用户信息表 user_profile id device_id gender age university gpa active_days_within_30 1 2138 male 21 北京大学 3.4 7 2 3214 male 20 复旦大学 3.2 15 练习明细表 question_practice_detail id device_id question_id result date 1 2138 111 wrong 2021-05-03 2 3214 112 wrong 2021-08-01 3 3214 113 right 2021-08-15 4 3214 114 wrong 2021-08-20 输出示例：\ndevice_id university question_cnt right_question_cnt 3214 复旦大学 3 1 5432 复旦大学 0 0 **题目要求解析 : **\n需要 用户, 做题数量, 最对数量 的一张子表 cout(), sum(), group by device_id 需要 用户, university 的一张子表 where university = 我们结果集是 2 表, left join 上 1 表. 对于 NULL 的情况 4. 我们需要使用 COALESCE() 处理一下 其他考点, 日期函数 month() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 select b.device_id, b.university, COALESCE(temp.question_cnt, 0) AS question_cnt, COALESCE(temp.right_question_cnt, 0) AS right_question_cnt from ( select device_id, university from user_profile as u where u.university = \u0026#39;复旦大学\u0026#39; ) as b left join ( select device_id, count(question_id) as question_cnt, SUM(CASE WHEN result = \u0026#39;right\u0026#39; THEN 1 ELSE 0 END) AS right_question_cnt from question_practice_detail where month(date) = 8 group by device_id ) as temp on b.device_id = temp.device_id 另外一个做法, 聚合之后在做 聚合函数的计算\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SELECT u.device_id, u.university, COUNT(qpd.question_id) AS question_cnt, SUM(CASE WHEN qpd.result = \u0026#39;right\u0026#39; THEN 1 ELSE 0 END) AS right_question_cnt FROM user_profile u LEFT JOIN question_practice_detail qpd ON u.device_id = qpd.device_id AND qpd.date BETWEEN \u0026#39;2021-08-01\u0026#39; AND \u0026#39;2021-08-31\u0026#39; WHERE u.university = \u0026#39;复旦大学\u0026#39; GROUP BY u.device_id, u.university ORDER BY u.device_id; 牛客题目链接\n浙大不同难度题目的正确率 题目：现在运营想要了解浙江大学的用户在不同难度题目下答题的正确率情况，请取出相应数据，并按照准确率升序输出。\n表结构：\n用户信息表 user_profile id device_id gender age university gpa active_days_within_30 question_cnt answer_cnt 1 2138 male 21 北京大学 3.4 7 2 12 2 3214 male 20 浙江大学 3.8 15 5 25 练习明细表 question_practice_detail id device_id question_id result 1 3214 111 wrong 2 3214 112 right 3 3214 113 right 4 2138 114 wrong 题目难度表 question_detail question_id difficult_level 111 hard 112 easy 113 medium 114 easy 输出要求：\n只统计浙江大学用户 计算不同难度题目的正确率（正确答题数/总答题数） 正确率保留4位小数 按correct_rate升序排列 输出示例：\ndifficult_level correct_rate hard 0.0000 easy 0.5000 medium 1.0000 题目要求解析 :\n首先需要一张表 浙江大学表 device_id, university 然后还需要一张表 用户做题难度表, device_id, question_id, diffcult_level 所以我们先聚合上面1的 TABLE, 然后再 JOIN 2 的 TABLE 最后计算答案 这里有一个偷鸡的办法, 由于 LEFT JOIN 聚合出了 NULL 的列, 最后分组操作的时候, 使用 having 过滤了一下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 select qd.difficult_level as difficult_level, round(sum(if( temp.result = \u0026#39;right\u0026#39;, 1, 0)) / count(temp.result),4)as correct_rate from ( select qpd.device_id , qpd.question_id, qpd.result from user_profile as u left join question_practice_detail as qpd on qpd.device_id = u.device_id and u.university = \u0026#39;浙江大学\u0026#39; ) as temp left join question_detail as qd on temp.question_id = qd.question_id group by qd.difficult_level having difficult_level is not null order by correct_rate asc 其他解法 :\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 SELECT qd.difficult_level, ROUND( SUM(CASE WHEN qpd.result = \u0026#39;right\u0026#39; THEN 1 ELSE 0 END) * 1.0 / COUNT(qpd.question_id), 4 ) AS correct_rate FROM user_profile u JOIN question_practice_detail qpd ON u.device_id = qpd.device_id JOIN question_detail qd ON qpd.question_id = qd.question_id WHERE u.university = \u0026#39;浙江大学\u0026#39; GROUP BY qd.difficult_level ORDER BY correct_rate ASC; 牛客题目链接\n21年8月份练题总数 送分题 不做解释 1 2 3 select count(distinct device_id) as did_cnt , count(question_id) as question_cnt from question_practice_detail where year(date) = 2021 and month(date) = 8 牛客题目链接\n","date":"2025-05-25T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/com-practise-1/title_hu_1e5f17695893b496.png","permalink":"https://ddlgitgzzhm.github.io/p/com-practise-1/","title":"[SQL] 综合练习 1"},{"content":"文本函数 统计每种性别的人数 题目：现在运营举办了一场比赛，收到了一些参赛申请，表数据记录形式如下所示，现在运营想要统计每个性别的用户分别有多少参赛者，请取出相应结果\n示例：user_submit\ndevice_id profile blog_url 2138 180cm,75kg,27,male http:/url/bigboy777 根据示例，你的查询应返回以下结果：\ngender number male 2 female 1 新知识点\nQ : 如何拆分 一个列里面的数据, 然后进行分组\nA : SUBSTRING_INDEX(profile, ',', -1) AS gender\nprofile 需要处理的字符串字段 , 分隔符 -1 从字符串右侧开始截取, 第一个出现的分隔符后面的 所有内容 1 2 3 4 5 6 7 select SUBSTRING_INDEX(profile,\u0026#39;,\u0026#39;,-1) AS gender, COUNT(*) as number from user_submit group by gender 其他方法 : 注意famale 和 male 有重合的地方，所以不能直接like male，否则female 也会被统计进 ‘male’\n1 2 3 4 5 6 7 8 9 10 11 12 13 SELECT CASE WHEN profile LIKE \u0026#39;%,male\u0026#39; then \u0026#39;male\u0026#39; WHEN profile LIKE \u0026#39;%,female\u0026#39; then \u0026#39;female\u0026#39; else \u0026#39;其他\u0026#39; end as gender, COUNT(*) AS number FROM user_submit GROUP BY gender; SELECT IF(profile LIKE \u0026#39;%female\u0026#39;,\u0026#39;female\u0026#39;,\u0026#39;male\u0026#39;) gender,COUNT(*) number FROM user_submit GROUP BY gender; 题目链接\n送分题\n1 2 3 4 5 select device_id, SUBSTRING_INDEX(blog_url, \u0026#39;/\u0026#39;,-1) as user_name from user_submit 截取年龄 题目：现在运营举办了一场比赛，收到了一些参赛申请，表数据记录形式如下所示，现在运营想要统计每个年龄的用户分别有多少参赛者，请取出相应结果\n示例：user_submit\ndevice_id profile blog_url 2138 180cm,75kg,27,male http:/ur/bigboy777 根据示例，你的查询应返回以下结果：\nage number 27 1 25 1 \u0026hellip; \u0026hellip; 两段 substring_index 1 2 3 4 5 select SUBSTRING_INDEX( SUBSTRING_INDEX(profile, \u0026#39;,\u0026#39;, -2),\u0026#39;,\u0026#39;, 1) as age , count(*) as number from user_submit group by age 窗口函数 找出每个学校GPA最低的同学 题目：现在运营想要找到每个学校gpa最低的同学来做调研，请你取出每个学校的最低gpa。\n示例：user_profile\nid device_id gender age university gpa active_days_within_30 question_cnt answer_cnt 1 2138 male 21 北京大学 3.4 7 2 12 2 3214 male NULL 复旦大学 4.0 15 5 25 根据示例，你的查询结果应参考以下格式，输出结果按university升序排序：\ndevice_id university gpa 6543 北京大学 3.2 [id] [学校名] [最低gpa] Q : 题目要求求出一个 TopN 的问题\nA : 新知识点\n1 2 \u0026lt;窗口函数\u0026gt; over (partition by \u0026lt;用于分组的列名\u0026gt; order by \u0026lt;用于排序的列名\u0026gt;) 窗口函数 rank(), dense_rank, row_number\nrank() : 如果存在多个并列名次会占用, 即 1,1,1,4\ndense_rank() : 如果存在多个并列名次不会占用, 即 1,1,1,2\nrow_number() : 不占用,不并列. 即 1,2,3,4\n具体说明 :\n窗口函数知乎链接\n1 2 3 4 5 select device_id, university, gpa from ( select device_id, university, gpa , RANK() over (PARTITION BY university order by gpa) rk from user_profile ) a where a.rk = 1 另外由于这个只是一个 max-min 的问题, 我们同样可以是用 JOIN or 子表查询 的方法做\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 select u.device_id , u.university, u.gpa from user_profile as u inner join ( select min(gpa) as gpa, university from user_profile group by university ) as temp on temp.university = u.university and u.gpa = temp.gpa order by university SELECT device_id, university, gpa FROM user_profile u1 WHERE gpa = ( SELECT MIN(gpa) FROM user_profile u2 WHERE u2.university = u1.university ); 牛客题目链接\n计算每日累计利润 题目：在一张daily_profits表中，存储了公司每天的利润记录。请计算每一种产品每一天的累计利润，并按profit_date升序输出所有字段。\n具体要求：\n计算每一天的累计利润 输出结果按profit_date升序排列 表结构：daily_profits\nprofit_id profit_date profit 1 2024-01-01 100.00 2 2024-01-02 150.00 3 2024-01-03 200.00 输出示例：\nprofit_id profit_date profit cumulative_profit 1 2024-01-01 100.00 100.00 2 2024-01-02 150.00 250.00 3 2024-01-03 200.00 450.00 Q : 我们需要逐天的累加每个分组的结果\nA : 我们可以把整个结果集，看成一个分组。想要实现逐行累加，那么就需要使用 窗口函数 , 但是我们不需要 PARTITION BY 进行分组\n1 2 select * , sum(profit) over (order by profit_date) cumulative_profit from daily_profits 牛客题目链接\n基础数学函数 基本数学函数 题目：在一张 numbers 表中，存储了一些数值。请使用 SQL 的基本数学函数，计算每个数值的绝对值、向上取整、向下取整、四舍五入到一位小数，并输出这些计算结果。\n具体要求：\n计算每个数值的绝对值 计算每个数值的向上取整值 计算每个数值的向下取整值 计算每个数值四舍五入到一位小数 输出结果按 id 升序排列 表结构：numbers\nid value 1 3.14 2 -2.71 输出示例：\nid value absolute_value ceiling_value floor_value rounded_value 1 3.14 3.14 4 3 3.1 2 -2.71 2.71 -2 -3 -2.7 新知识点 :\nABS(), ceil(), floor(), round() 绝对值，向上取整，向下取整，精度 1 2 3 4 5 6 7 select *, abs(value) as absolute_value, ceil(value) as ceiling_value, #向上取整 floor(value) as floor_value, #向下取整 round(value,1) as rounded_value from numbers order by id ASC 牛客网题目链接\n","date":"2025-05-23T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/necessary-select-2/title_hu_1e5f17695893b496.png","permalink":"https://ddlgitgzzhm.github.io/p/necessary-select-2/","title":"[SQL] 必要函数 2"},{"content":"条件函数 计算25岁以上和以下的用户数量 以下是修复后的Markdown内容，保持引用格式并优化表格展示：\n题目：现在运营想要将用户划分为25岁以下和25岁及以上两个年龄段，分别查看这两个年龄段用户数量\n本题注意：age为null 也记为 25岁以下\n示例：user_profile\nid device_id gender age university gpa active_days_within_30 question_cnt answer_cnt 1 2138 male 21 北京大学 3.4 7 2 12 根据题目要求，你的查询应返回以下结果（注意age为null的情况应归类为25岁以下）：\nage_cut number 25岁以下 4 25岁及以上 3 新知识点\nQ : 这个 age_cut , 25岁以下 ，25岁以上 是怎么插进去的\nA : 原来 我们 SELECT CONST STRING, 那么就会输出对应的 STRING\nQ : 怎么进行判断 25 岁以下, 25岁以上\nA :\n第一种方法， 使用 CASE (WHEN ... ELSE) END 的形式 第二种方法， 使用 IF(JUDGE , TRUE, FALSE) 很明显 这种语句只支持两个结果 所以对于本体 我们的做法 是 SIWTCH ( CONST STRING ) FROM TABLE 的形式\n1 2 3 4 5 6 7 8 9 10 SELECT ( CASE WHEN age \u0026lt; 25 THEN \u0026#39;25岁以下\u0026#39; WHEN age \u0026gt;= 25 THEN \u0026#39;25岁及以上\u0026#39; ELSE \u0026#39;25岁以下\u0026#39; END ) as age_cut, count(*) as number from user_profile group by age_cut 1 2 3 4 5 6 7 select (case when age\u0026gt;=25 then \u0026#39;25岁及以上\u0026#39; else \u0026#39;25岁以下\u0026#39; end) as age_cut, count(*) as number from user_profile group by age_cut 坑点 :\n由于这里使用了 COUNT() 这个聚合函数，所以必须加上 GROUP BY 当我们涉及查询 既有聚合函数 COUNT,SUM,AVG,MAX,MIN , 又有普通列的时候，就必须用 GRUOP BY 分组后, 如果我们想要对结果进行筛选必须使用HAVING ,WHERE 是分组前筛选, HAVING 是分组后筛选 题目链接\n送分题\n日期函数 计算用户8月每天的练题数量 以下是修复后的Markdown内容，保持引用格式并优化表格展示：\n题目：现在运营想要计算出2021年8月每天用户练习题目的数量，请取出相应数据\n示例：question_practice_detail\nid device_id question_id result date 1 2138 111 wrong 2021-05-03 根据题目要求，你的查询应返回以下结果（按日期升序排列）：\nday question_cnt 13 5 14 2 新知识点\nQ : 如何获取 日期 day , month , year 的信息\nsql 支持使用 year(), month(), day() 的形式获取对应的 年月日 WEEKDAY() 返回星期索引 , 0 = 星期一 \u0026hellip; QUATER() 返回季度 , 范围1-4 MINUTE() , SECOND() 1 2 3 4 5 6 7 8 #获取当前系统的日期时间 SELECT NOW(); # 2021-12-22 13:50:58 #获取当前系统的日期 SELECT CURDATE(); # 2021-12-22 #获取当前系统的时间 SELECT CURTIME(); # 13:53:11 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #日期增加,使用函数date_add(date,INTERVAL exp type) #增加1天 SELECT DATE_ADD(\u0026#39;2021-12-22 13:50:58\u0026#39;, INTERVAL 1 DAY); # 2021-12-23 13:50:58 #增加1小时 SELECT DATE_ADD(\u0026#39;2021-12-22 13:50:58\u0026#39;, INTERVAL 1 HOUR); # 2021-12-23 14:50:58 #日期减少，使用函数date_sub(date,INTERVAL exp type) # 减少1天 SELECT DATE_SUB(\u0026#39;2021-12-01 13:50:58\u0026#39;, INTERVAL 1 DAY); # 2021-11-30 13:50:58 #其他间隔 INTERVAL 1 YEAR INTERVAL 1 MONTH INTERVAL 1 DAY INTERVAL 1 HOUR INTERVAL 1 MINUTE INTERVAL 1 SECOND 思路\n这题 在知道如何操作日期之后就很简单了 1 2 3 4 SELECT DAY(date) as day , count(*) as question_cnt from question_practice_detail WHERE YEAR(date) = 2021 and MONTH(date) = 8 group by day 计算用户的平均次日留存率 以下是修复后的Markdown内容，保持引用格式并优化表格展示：\n题目：现在运营想要查看用户在某天刷题后第二天还会再来刷题的留存率。请你取出相应数据\n示例：question_practice_detail\nid device_id question_id result date 1 2138 111 wrong 2021-05-03 根据示例，你的查询应返回以下结果（保留4位小数）：\navg_ret 0.3000 题目要求 :\n计算 用户连续两天 都做题的占比 注意一个用户一天可以做多个题 思路 :\n首先我们需要知道 如何比较 有一条数据 是 另外一条数据的 第二天 2. 我们可以使用上面学到的 DATE_ADD(DATE , INTERVAL X Y) 比较 如果 P1.DATE = DATE_ADD() 的话那么就说明相差一天 3. 因此我们肯定是 DAY 1 TABLE JOIN DAY 2 TABLE 4. 由于我们需要知道 DAY 1 的总数，所以我们需要 LEFT JOIN 1 2 3 4 5 6 7 8 9 10 11 SELECT ROUND( SUM( IF(p2.device_id IS NOT NULL ,1,0)) / COUNT(*) ,4 ) as avg_ret FROM (SELECT DISTINCT device_id, date from question_practice_detail) as p1 LEFT JOIN (SELECT DISTINCT device_id, date from question_practice_detail) as p2 ON p1.device_id = p2.device_id and p2.date = DATE_ADD(p1.date, INTERVAL 1 day) 牛客题目链接\n","date":"2025-05-08T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/necessary-select-1/sql_hu_f84849343f8d133e.png","permalink":"https://ddlgitgzzhm.github.io/p/necessary-select-1/","title":"[SQL] 必要函数"},{"content":"子查询 浙江大学用户题目回答情况 题目：现在运营想要查看所有来自浙江大学的用户题目回答明细情况，请你取出相应数据\n示例：question_practice_detail\nid device_id question_id result 1 2138 111 wrong 第一行表示：id为1的用户的常用信息为使用的设备id为2138，在question_id为111的题目上，回答错误\n示例：user_profile\nid device_id gender age university gpa active_days_within_30 question_cnt answer_cnt 1 2138 male 21 北京大学 3.4 7 2 12 第一行表示：id为1的用户的常用信息为使用的设备id为2138，性别为男，年龄21岁，北京大学，gpa为3.4，在过去的30天里面活跃了7天，发帖数量为2，回答数量为12\n根据示例，你的查询应返回以下结果，查询结果根据question_id升序排序：\ndevice_id question_id result 2315 115 right 两个表 根据 device_id 关联, 题目需要我们聚合 qpd 表的 question_id 和 result 字段 条件是 up 表的 university 是 浙江大学 第一时间没有想到 子查询 , 直接使用 JOIN ON了 1 2 3 4 5 6 7 8 select qpd.device_id,qpd.question_id,qpd.result from user_profile as up join question_practice_detail as qpd on up.device_id = qpd.device_id where up.university = \u0026#39;浙江大学\u0026#39; order by qpd.question_id 子查询代码如下\n1 2 3 4 5 6 select device_id, question_id, result from question_practice_detail where question_practice_detail.device_id in ( select device_id from user_profile where university = \u0026#39;浙江大学\u0026#39; ) 牛客题目链接\n链接查询 统计每个学校的答过题的用户的平均答题数 题目：查找每个学校用户的平均答题数目（某学校用户平均答题数量计算方式为该学校用户答题总次数除以答过题的不同用户个数）\n表结构说明：\nuser_profile 用户信息表：\ndevice_id：终端编号（每个用户有唯一的一个终端） gender：性别 age：年龄 university：用户所在的学校 gpa：该用户平均学分绩点 active_days_within_30：30天内的活跃天数 question_practice_detail 答题情况明细表：\nquestion_id：题目编号 result：答题结果 示例数据：\nuser_profile：\ndevice_id gender age university gpa active_days_within_30 2138 male 21 北京大学 3.4 7 question_practice_detail：\ndevice_id question_id result 2138 111 wrong 要求：计算每个学校用户的平均答题数目（答题总次数/答过题的不同用户数），结果保留4位小数，按university升序排序\n预期输出示例：\nuniversity avg_answer_cnt 北京大学 1.0000 题目要求\n根据 up 表的 university 聚合出 qpd 表中 每所大学的 平均答题数, 即 university 对应有的 device_id 总共有多少个记录在 qpd 表中 这里有一个逻辑坑点 , distinct up.device_id 一开始认为 device_id 必然是唯一的 。但是我们 join on 之后，由于 qpd表的 device_id 是有多个的，所以我们最后用于计算的分母需要DISTINCT 1 2 3 4 5 6 7 8 9 10 11 select up.university, round( count(qd.question_id)/count(distinct up.device_id), 4) as avg_answer_cnt from user_profile as up JOIN question_practice_detail as qd on up.device_id = qd.device_id group by up.university order by up.university 牛客题目链接\n统计每个学校各难度的用户平均刷题数 以下是修复后的Markdown内容，保持引用格式，优化了表格展示和换行：\n题目：运营想要计算一些参加了答题的不同学校、不同难度的用户平均答题量，请你写SQL取出相应数据\n用户信息表：user_profile\nid device_id gender age university gpa active_days_within_30 question_cnt answer_cnt 1 2138 male 21 北京大学 3.4 7 2 12 题库练习明细表：question_practice_detail\nid device_id question_id result 1 2138 111 wrong 表：question_detail\nid question_id difficult_level 1 111 hard 请你写一个SQL查询，计算不同学校、不同难度的用户平均答题量，根据示例，你的查询应返回以下结果(结果在小数点位数保留4位，4位之后四舍五入)：\nuniversity difficult_level avg_answer_cnt 北京大学 hard 1.0000 题目要求 :\n首先需要 up 表的 university 另外需要 qd 表的 diffcult 其次需要计算 count(same-diffcult-question) / count(same-university-device-id) 思路 :\n对于三张表的查询 , 我们先优化到 两张表的查询，拆子问题 , 我们可以先聚合 qd 和qpd 这两张表 , 知道每个 题目的难度 相当于我们聚合了一张 带有难度信息的 qpd 然后我们使用 这个new qpd 再去和 UP 聚合计算一下 对应的 question_id/device_id group by (university, diffcult) 即可 1 2 3 4 5 6 7 8 9 10 11 12 13 14 select up.university, temp.difficult_level, count(temp.question_id) / count(distinct up.device_id) as avg_answer_cnt from user_profile as up join ( select qpd.device_id,qpd.question_id, qd.difficult_level from question_practice_detail as qpd join question_detail as qd on qpd.question_id = qd.question_id ) as temp on up.device_id = temp.device_id group by up.university, temp.difficult_level order by up.university 牛客题目链接\n牛客经验+1\n组合查询 查找山东大学或者性别为男生的信息 以下是修复后的Markdown内容，保持引用格式并优化表格展示：\n题目：现在运营想要分别查看学校为山东大学或者性别为男性的用户的device_id、gender、age和gpa数据，请取出相应结果，结果不去重。\n示例：user_profile\nid device_id gender age university gpa active_days_within_30 question_cnt answer_cnt 1 2138 male 21 北京大学 3.4 7 2 12 根据示例，你的查询应返回以下结果（注意输出的顺序，先输出学校为山东大学再输出性别为男生的信息）：\ndevice_id gender age gpa 5432 male 25 3.8 题目要求 :\n连续查两遍这个表, 第一遍是 山东大学, 第二遍是男性 然后组合输出 思路 :\n一开始以为是 SELECT WHERE OR 的形式,但是发现 2. 第一, 不能查重复数据 ,即山东大学 和 male 如果是一条数据,应该查出两条 3. 第二不好排序,因为UNIVERSITY\n这题纯属新知识点 UNION ALL 不去除重复数据 ,UNOIN 去除重复数据\n1 2 3 4 5 6 7 select device_id, gender,age, gpa from user_profile where university = \u0026#39;山东大学\u0026#39; union all select device_id, gender, age , gpa from user_profile where gender = \u0026#39;male\u0026#39; 牛客题目链接\n留坑 JOIN ,LEFT JOIN, RIGHT JOIN 子查询 ,视图的概念 UNION ALL的性能 ,业务上的使用 ","date":"2025-05-07T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/multi-select/sql_hu_f84849343f8d133e.png","permalink":"https://ddlgitgzzhm.github.io/p/multi-select/","title":"[SQL] 多表查询"},{"content":"新一期的 TGE 目前分数 115 从 4.29 日的 75 分，经过 8 天，就飞升到了 142 分，总共差额 67 ,平均 9.5(除去当天) 简单可以计算出，top 层级的玩家基本上一天会刷个 10 分，资产人均 2分 加上 8分交易量 ，大概是 256u 115 + 11 * x = 142 + 10 * x x = 142 - 115 = 27\n按照这个趋势，我需要吃满 27 天， 不过 x 可以翻倍，从而缩短我的进程 策略更改 目前更改了一下策略，从钱包 飞到了 交易所\n从原先的 BASE 链 回到了 solana 和 bnb chain . Base链的滑点太高了，导致我每次都被吃了很多的费用\n我们可以看见 4. 我做了一笔 solana 的 popcat , 512u 实际到手 511.6u , 加上 0.02 + 0.01 的 gas , 总共一次 就 0.43u 5. 我同样做了一笔 bnb chain 的 b2 , 513u 实际到手 511.79u, 加上 0.43 + 0.43 的 gas , 总共一次 就 2.71u 6. 同样都设置的 滑点 0.5% , 很显然 bnb chain 那个滑点爆炸了 。 不过他的gas费也不低，虽然一次是 double 积分\n综上，目前策略预计改成 solana chain , per 512/trade.\n积分是递增的即 512, 1024,2048, 4096\n按照一次 0.43u 的磨损来看，我2u可以亏损 5 次，即512 * 4 = 2560 ，这样子会把 x 大成为 3，即我9天内就可以赶上. 总共损耗 18u\n如果采用限价单双倍的策略,我很容易吃到 4096 那么我 6.75 天就可以吃满 . 总共损耗 14u\nOther 行情 ： https://www.geckoterminal.com/zh/bsc/pools/0xc1a780989734a0e5df875cebe410748562e1c5e6\n","date":"2025-05-06T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/binance-staking-activity-2/img_hu_932c7e191f998b98.png","permalink":"https://ddlgitgzzhm.github.io/p/binance-staking-activity-2/","title":"[Binance] 送钱活动 3"},{"content":"高级操作符练习 1 题目：现在运营想要找到male且GPA在3.5以上(不包括3.5)的用户进行调研，请你取出相关数据。\n业务场景中, 需要提前加上其他判断 gpa is not null 1 select device_id,gender,age,university,gpa from user_profile where gpa \u0026gt; 3.5 and gender = \u0026#39;male\u0026#39; 牛科练习题目\n高级操作符练习 2 题目：现在运营想要找到学校为北大或GPA在3.7以上(不包括3.7)的用户进行调研，请你取出相关数据（使用OR实现）\n就简单介绍了一下 or 怎么用 顾名思义 1 select device_id, gender, age, university, gpa from user_profile where university = \u0026#39;北京大学\u0026#39; or gpa \u0026gt; 3.7 牛科练习题目\nWhere in 和 Not in 题目：现在运营想要找到学校为北大、复旦和山大的同学进行调研，请你取出相关数据。\n顾名思义 todo 性能分析 @我 1 select device_id, gender,age,university,gpa from user_profile where university in (\u0026#39;北京大学\u0026#39;, \u0026#39;复旦大学\u0026#39;, \u0026#39;山东大学\u0026#39;) 牛科练习题目\n操作符混合运用 题目：现在运营想要找到gpa在3.5以上(不包括3.5)的山东大学用户 或 gpa在3.8以上(不包括3.8)的复旦大学同学进行用户调研，请你取出相应数据,取出的数据按照device_id升序排列\n1 2 3 4 5 6 7 select device_id, gender,age, university,gpa from user_profile where ( gpa \u0026gt; 3.5 and university = \u0026#39;山东大学\u0026#39; ) or (gpa \u0026gt; 3.8 and university = \u0026#39;复旦大学\u0026#39;) order by device_id asc 这里可以改为子查询的方式，时间或许会更短 1 2 3 4 5 6 7 select device_id, gender, age, university, gpa from user_profile where device_id in (select device_id from user_profile where gpa\u0026gt;3.5 and university=\u0026#39;山东大学\u0026#39;) or device_id in (select device_id from user_profile where gpa\u0026gt;3.8 and university=\u0026#39;复旦大学\u0026#39;) 牛客网题目\n查看学校名称中含北京的用户 题目：现在运营想查看所有大学中带有\u0026quot;北京\u0026quot;的用户的信息(device_id,age,university)，请你取出相应数据。\n字符匹配 四种通配符 % 匹配 0 个 或者是 多个字符 _ 匹配任意一个字符 _李 []匹配[]中任意一个字符,如果比较的字符串是连续的，可以用-表达 [^]不匹配[]中任意一个字符 1 select device_id, age, university from user_profile where university like \u0026#39;%北京%\u0026#39; ","date":"2025-05-06T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/senior-select-1/sql_hu_f84849343f8d133e.png","permalink":"https://ddlgitgzzhm.github.io/p/senior-select-1/","title":"[SQL] 高级操作符"},{"content":"计算函数 查找GPA最高值 题目：运营想要知道复旦大学学生gpa最高值是多少，请你取出相应数据\n关键点 max 四舍五入函数 round(value, pos) 1 2 3 4 5 6 7 8 select max(gpa) as gpa from user_profile where university = \u0026#39;复旦大学\u0026#39; limit 1; select round(max(gpa), 1) from user_profile where university=\u0026#39;复旦大学\u0026#39;; SELECT gpa FROM user_profile WHERE university = \u0026#39;复旦大学\u0026#39; ORDER BY gpa DESC LIMIT 1 计算男生人数以及平均GPA 题目：现在运营想要看一下男性用户有多少人以及他们的平均gpa是多少，用以辅助设计相关活动，请你取出相应数据。\n这里可以使用 count(*) 代替 count(gender) 1 select count(gender) as male_num ,avg(gpa) as avg_gpa from user_profile where gender = \u0026#39;male\u0026#39; 分组函数 分组计算练习题 题目：现在运营想要对每个学校不同性别的用户活跃情况和发帖数量进行分析，请分别计算出每个学校每种性别的用户数、30天内平均活跃天数和平均发帖数量。\n用户信息表：user_profile\n30天内活跃天数字段（active_days_within_30）\n发帖数量字段（question_cnt）\n回答数量字段（answer_cnt）\n没什么好说的, group by 多列的时候, 使用 , 进行区分 1 2 3 4 5 6 7 select gender,university, count(university) as user_num, avg(active_days_within_30) as avg_active_day, avg(question_cnt) as avg_question_cnt from user_profile group by gender, university order by gender asc, university asc 分组过滤练习题 题目：现在运营想查看每个学校用户的平均发贴和回帖情况，寻找低活跃度学校进行重点运营，请取出平均发贴数低于5的学校或平均回帖数小于20的学校。\n当使用聚合函数 作为塞选条件的时候，需要使用 where 代替 having 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 SELECT university, avg_question_cnt, avg_answer_cnt FROM ( SELECT university, AVG(question_cnt) AS avg_question_cnt, AVG(answer_cnt) AS avg_answer_cnt FROM user_profile GROUP BY university ) AS temp WHERE avg_question_cnt \u0026lt; 5 OR avg_answer_cnt \u0026lt; 20; 正解应该是 :\n1 2 3 4 5 6 7 8 9 10 11 12 select university, round(avg(question_cnt),3) AS avg_question_cnt, round(AVG(answer_cnt),3) as avg_answer_cnt FROM user_profile GROUP BY university HAVING avg_question_cnt\u0026lt;5 or avg_answer_cnt\u0026lt;20 分组排序练习题 现在运营想要查看不同大学的用户平均发帖情况，并期望结果按照平均发帖情况进行升序排列，请你取出相应数据\n这里和 where不同，竟然不需要子查询和更换其他 关键字。 聚合函数可以直接进行排序 1 2 3 4 5 6 7 select university, avg_question_cnt from ( select university, avg(question_cnt) as avg_question_cnt from user_profile group by university ) as temp order by avg_question_cnt 正解 :\n1 2 3 4 5 6 7 8 9 SELECT university, ROUND(AVG(question_cnt), 4) AS avg_question_cnt FROM user_profile GROUP BY university ORDER BY avg_question_cnt; ","date":"2025-05-06T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/senior-select-2/sql_hu_f84849343f8d133e.png","permalink":"https://ddlgitgzzhm.github.io/p/senior-select-2/","title":"[SQL] 高级查询"},{"content":"资产策略现状 自从第一篇笔记开始，现在每天都有一个 todo list 需要给 binance 刷一下交易量，有点ptsd了 DATE BUY SELL DIFF 04-29 513 509 4U 04-30 513 509 4U 04-30 513 509 4U 05-01 513 509 4U 05-02 513 509 4U 05-03 513 511 2U 05-04 513 510 3U 目前总共磨损 4 *5 + 5 = 25U , 预计磨损 100U 破防。 按照 4 U 一天计算的话，大概还有25天破防 现在积分 85 , 最近一波在 5月7号，大概还有3天, 预计我还可以刷 22 分 ，也就是 118 . 预计是 140 ，大概是赶不上 binance 活动整理 双倍积分 BINANCE 推出积分双倍计算双倍活动 。 用交易所的限价单 和 BINANCE CHAIN 上刷交易量都是以双倍的形式计算 2. 即交易量 512 u 的 会以1024U计算，多个1积分 binance chain 暂时不考虑，手续费实在是太贵了 那么限价单是否有购买的必要呢 ？ 用限价单的话，大概率磨损会多个 1U - 2U ,多花个 1U - 2U 购买1积分 实际上很不划算 . 而且对于限价单, 需要把资产从钱包转移到交易所，很麻烦 。 新链 binance 钱包推出了 Sonic 链的项目。 对于活跃用户会有空投的奖励\n新币打新 OBOL 活动时间 5.7 号，18:00 不确定积分多少，预计要 130 . 大概率达不到 ~ ","date":"2025-05-04T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/binance-staking-activity-1/img_hu_932c7e191f998b98.png","permalink":"https://ddlgitgzzhm.github.io/p/binance-staking-activity-1/","title":"[Binance] 送钱活动 2"},{"content":"引导问题 整数求和 1 2 3 4 5 6 7 8 9 Hey, welcome to HDOJ(Hangzhou Dianzi University Online Judge). In this problem, your task is to calculate SUM(n) = 1 + 2 + 3 + ... + n. Input The input will consist of a series of integers n, one integer per line. Output For each case, output SUM(n) in one line, followed by a blank line. You may assume the result will be in the range of 32-bit signed integer. 常规做法是 for-each 后对 sum 进行累加，然后输出 我们可以根据 等差数列高斯公式求得 (a + b) * n / 2 不过这题需要注意的是 4. 我们计算乘法的时候可能暴 int , 因为对于两个 int32 的数相乘必然会爆 int32 所以我们需要开辟成 int64 题目链接如下, 由于不支持 GO 所以不做\nhttps://vjudge.net/problem/HDU-1001\n例题 最小公倍数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 输入两个整数 a 和 b，请你编写一个函数，int lcm(int a, int b)，计算并输出 a 和 b 的最小公倍数。 输入格式 共一行，包含两个整数 a 和 b。 输出格式 共一行，包含一个整数，表示 a 和 b 的最小公倍数。 数据范围 1≤a,b≤1000 输入样例： 6 8 输出样例： 24 朴素做法\n我们从最大的数开始枚举，如 8,9,10\u0026hellip; 每次都判断是否能整除最小数 简单优化，我们可以直接枚举 最大数的倍数 。 可是最大数的倍数, 我们有很大概率爆int 的风险 正解\nlcm(A,B) = A * B / gcd(A,B) 优化一下 = A / gcd(A,B) * B 问题引导为 如何求最大公约数 Acwing-最小公倍数\n最大公约数 我们现在要求 10, 14 的最大公约数 。 我们假设 X 为这两个数的最大公约数，可以知道，对于 (14%10) 的余数，也应该是 X 的倍数 从而我们可以依次类推 (10,14) , (10,4) , (2,4), (2,0) 则 2 就是最大公约数 todo : 很抓马的一件事，我用 go 写这个程序 TLE 了\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package main import( \u0026#34;fmt\u0026#34; ) func gcd(a,b int) int { if a \u0026gt; b { a,b = b,a } for ; a != 0 ; { a, b = b%a,a } return b } func main() { var n int fmt.Scan(\u0026amp;n) for i := 0 ; i \u0026lt; n ; i ++ { var a,b int fmt.Scan(\u0026amp;a,\u0026amp;b) fmt.Println(gcd(a,b)) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include \u0026lt;iostream\u0026gt; using namespace std ; int gcd(int a,int b) { int temp ; if(a \u0026gt; b) { temp = a; a = b ; b = temp; } for( ; a != 0 ; ) { temp = a; a = b%a; b = temp ; } return b ; } int main() { int a,b ; int n; cin\u0026gt;\u0026gt;n; for(int i = 1; i \u0026lt;= n ; i ++ ) { cin\u0026gt;\u0026gt;a\u0026gt;\u0026gt;b; cout\u0026lt;\u0026lt;gcd(a,b)\u0026lt;\u0026lt;endl; } } 循环节 - N * N 的个位数 Given a positive integer N, you should output the most right digit of N^N.\nInput\nThe input contains several test cases. The first line of the input is a single integer T which is the number of test cases.\nT test cases follow.\nEach test case contains a single positive integer N(1\u0026lt;=N\u0026lt;=1,000,000,000).\nOutput\nFor each test case, you should output the rightmost digit of N^N.\n时间范围 : T * N 必然做法是 T * O(K) 的\n2,4,8,16,32,64,128 ... 3,9,27,81,243 ... 显然，奇数 * 奇数 = 奇数 ， 偶数 * 偶数 = 偶数 。 个位上的偶数 也就 (0,2,4,6,8) , 奇数 (1,3,5,7,9) 所以肯定是一个循环, 因此我们只需要枚举出前5个,找出不重复的循环节即可解出这道题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include \u0026lt;iostream\u0026gt; #include \u0026lt;math.h\u0026gt; using namespace std; int main() { int t;cin\u0026gt;\u0026gt;t; while(t -- ) { int n ; cin \u0026gt;\u0026gt; n; int st[10] = {0}; int last = n % 10 ; int ans[6] = {0} ; int idx = 0 ; for(int i = 0 ; i \u0026lt; 5 ; i++ ) { int k = int(pow(last, i + 1)) % 10; if(st[k]){ break ; } ans[++idx] = k ; st[k] = 1 ; } ans[0] = ans[idx] ; cout\u0026lt;\u0026lt;ans[(n%(idx)) ] \u0026lt;\u0026lt;endl; idx = 0 ; } return 0; } 题目链接\n","date":"2025-05-04T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/hdu-0x01-math/icpc-title_hu_a9d0f649c644dbf8.png","permalink":"https://ddlgitgzzhm.github.io/p/hdu-0x01-math/","title":"[Hdu] 数学基础"},{"content":" 目前积分是 55 分 没想到短短 4 日，积分要求就从 45 飞到 80 ，整整长了 35 分 目前策略是 2 + 5 = 7 , 4 * 7 = 28 怎么都追不上 预计更换策略 2 + 9 = 11 , 4 * 11 = 44 (diff : 16) , ( diff calc per 16 * 4 = 64 ) 预计更换策略 512 一周，吃不到也没办法了 。\n预计每次磨损 8 刀左右，大概就是 8 * 7 = 56 刀 * 7.2 = 403.2 人名币\n积分计算规则 积分策略 最近活动 往期活动 ","date":"2025-04-29T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/binance-staking-activity/img_hu_932c7e191f998b98.png","permalink":"https://ddlgitgzzhm.github.io/p/binance-staking-activity/","title":"[Binance] 送钱活动"},{"content":"题目 给你一个整数数组 nums 和一个整数 k ，请你统计并返回 该数组中和为 k 的子数组的个数 。 子数组是数组中元素的连续非空序列。\n示例 1：\n输入：nums = [1,1,1], k = 2\n输出：2\n示例 2：\n输入：nums = [1,2,3], k = 3\n输出：2\n提示：\n1 \u0026lt;= nums.length \u0026lt;= 2 * 104\n-1000 \u0026lt;= nums[i] \u0026lt;= 1000\n-107 \u0026lt;= k \u0026lt;= 107\n思路 对于 需要找到 X + Y = K 的模型，我们统一想到使用 map[X-K] = Y 的优化，可以将 n^2 -\u0026gt; O(n)\n对于这题我们需要把子数组和看为X, 我们计算前缀和 SUM[i] 表示，以 i 结尾前面所有数值的和. 因为我们知道 K 所以我们并不需要去找到我们的 Y, 我们只需要保证，我们能全量的枚举出X 即 SUM[i]\n因此我们只需要ans = ans + map[sum[i] - K] 即可\ntips :\n当我们 sum[1] - K == 2 并且在 sum[3] - K == 2 , 那么我们是否重复计算了 [0~3] 的某些子数组呢 ? 结论是 : 我们 sum[3]-K == 2 的成立是由于引入了A[2] , 因此不存在重复计算\n1 2 3 4 5 6 7 8 9 10 11 12 func subarraySum(nums []int, k int) int { count := make(map[int]int) count[0] = 1 preSum := 0 ans := 0 for _ , v := range nums { preSum += v ans += count[preSum - k] count[preSum] ++ } return ans } ","date":"2025-04-29T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/leetcode-560/img_hu_a890315985278288.png","permalink":"https://ddlgitgzzhm.github.io/p/leetcode-560/","title":"[LeetCode][hot100] 560. 和为 K 的子数组"},{"content":"基础排序 查询后排序 asc 升序 ascending desc 降序 descending 1 select device_id, age from user_profile order by age asc 牛客网题目链接\n牛客网题目链接 (降序排列)\n查询后多列排序 多列排序使用, 隔开 不指定排序顺序，默认是 asc 1 2 3 4 5 select device_id,gpa,age from user_profile order by gpa asc, age asc SELECT device_id,gpa,age from user_profile order by gpa,age;默认以升序排列 SELECT device_id,gpa,age from user_profile order by gpa,age asc; SELECT device_id,gpa,age from user_profile order by gpa asc,age asc; 牛客网题目链接\n基础操作符 查找学校是北大的学生信息 我们可以通过 where 子句来筛选对应的记录 1 select device_id, university from user_profile where university = \u0026#39;北京大学\u0026#39; 该题背景 : device_id 和 university 为联合索引\n在这个背景下面，我们可以在查询的时候使用联合索引，少去一层回表查询的操作 todo , DBM 不会自己合上id吗\n1 Select device_id,university FROM user_profile where university = \u0026#34;北京大学\u0026#34; and device_id = user_profile.device_id; 牛客网题目链接\n查询年龄大于 24岁的用户信息 引入逻辑运算符 \u0026gt; 以此类推还有 \u0026lt; , = , \u0026gt;= , \u0026lt;= , != , = 1 select device_id, gender, age,university from user_profile where age \u0026gt; 24 [牛客网题目链接](select device_id, gender, age,university from user_profile where age \u0026gt; 24)\n查询某个年龄段的用户信息 使用 between and 和 \u0026gt;= and \u0026lt;= 没什么本质区别，性能和业务使用上无差别 不过相较于between and 逻辑表达式更灵活 1 2 3 4 5 select device_id, gender, age from user_profile where age \u0026gt;= 20 and age \u0026lt;= 23 SELECT device_id, gender, age FROM user_profile WHERE age BETWEEN 20 AND 23; 牛客网题目链接\n查找除复旦大学的用户信息 NOT IN 可以比较多个值, 对于 \u0026lt;\u0026gt; != 如果需要比较多个值需要引入 and \u0026lt;\u0026gt; 和 != 没有本质区别，不过一般都是写 \u0026lt;\u0026gt; 除非团队开发有要求，那么使用 != 尽可能统一组内的代码风格 1 2 3 select device_id,gender,age,university from user_profile where university \u0026lt;\u0026gt; \u0026#39;复旦大学\u0026#39; select device_id,gender,age,university from user_profile where university != \u0026#39;复旦大学\u0026#39; select device_id,gender,age,university from user_profile where university NOT IN (\u0026#34;复旦大学\u0026#34;) 牛客网题目链接\n用 where 过滤空值 可以单独是用 is not null 或者是单独使用 \u0026lt;\u0026gt; \u0026quot;\u0026quot; 但是实际业务最好是两个一起使用 ～ 1 2 3 select device_id,gender,age,university from user_profile where age is not null and age \u0026lt;\u0026gt; \u0026#34;\u0026#34; 牛客网题目链接\n","date":"2025-04-29T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/limit-select/sql_hu_f84849343f8d133e.png","permalink":"https://ddlgitgzzhm.github.io/p/limit-select/","title":"[SQL] 条件查询"},{"content":"查询所有列 两种做法,一种是 *, 另外一种是全量的显示所有的列。 本质上这两个没什么区别\n从业务角度来看\n如果当前的 sql 并不打算查新增的列 则可以使用第一种 如果后续有列被删除了, 第二种也需要同步改动(当然生产环境很少有直接删除的) 从索引角度来看\n如果 剩下的字段建立了二级索引，那么第二种方法可以避免一次回表操作 如果没有二级索引(二级缩影需要覆盖整个查询的列)这两个其实是一样的 1 2 SELECT * FROM user_profile SELECT id,device_id,gender,age,university,province FROM user_profile; 牛客网题目链接\n查询结果去重 select distinct 只用于列的去重 select group by 可以用于一些聚合操作 如 count , avg 1 2 select distinct university from user_profile select unviersity from user_profile group by unviersity 从性能上看\ndistinct 和 group by 没什么区别, 在只需要去重的场景 distinct 性能可能略好于 group by, 效率取决于 DISTINCT 从业务上看\n如果我们想要使用聚合函数, 如计算分组内的平均数 和 总数 那么必须使用 distinct 牛客网题目链接\n","date":"2025-04-24T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/primary-select/sql_hu_f84849343f8d133e.png","permalink":"https://ddlgitgzzhm.github.io/p/primary-select/","title":"[SQL] 基础查询"},{"content":"简要 对于一个温度显示系统，如果我们要显示不同的温度, 简单做法是直接写在一个 class 里面\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type tempV1 struct { temp float64 } func (t *tempV1) ShowCelsius() string { return fmt.Sprintf(\u0026#34;%.1f°C\u0026#34;, t.temp) } func (t *tempV1) ShowFahrenheit() string { return fmt.Sprintf(\u0026#34;%.1f°F\u0026#34;, t.temp*9/5+32) } func (t *tempV1) ShowKelvin() string { return fmt.Sprintf(\u0026#34;%.1fK\u0026#34;, t.temp+273.15) } 如果我们使用 SRP 的做法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type tempV2 struct{ temp float64 } func (t *tempV2) GetTemp() float64 { return t.temp } type TempDisplayed struct{ } func (d *TempDisplayed) Celsius(temp float64) string { return fmt.Sprintf(\u0026#34;%.1f°C\u0026#34;, temp) } func (d *TempDisplayed) Fahrenheit(temp float64) string { return fmt.Sprintf(\u0026#34;%.1f°F\u0026#34;, temp*9/5+32) } 从实际业务开发的角度来看\n第一种方法更适合做快速开发，也就是前期的造轮子阶段 第二种方法适合进行维护和新增以及复用功能 从微服务角度来看\n我们展示温度的功能应该单独的一个组件, 或者说是一个服务, 也就是我们如果新增了新的展示温度功能,我们不应该重启温度服务，只需要重启温度展示服务即可 从内存角度来看\n由于我们新增的 TempDisplayed 并没有实际的 VALUE,属于无状态的 CLASS , 不占用额外内存。从 BenchMark 的结果来看，每次操作内存分配次数都是 4 allocs/op, 每次操作分配字节数都是 32B/op 和预期的一样 即我们并没有花更多的内存代价,就实现了代码服务的隔离 1 2 3 4 5 pkg: GolangLearning/design-pattern cpu: 12th Gen Intel(R) Core(TM) i5-12400 BenchmarkTempV1_Allocs-12 3861781 310.1 ns/op 32 B/op 4 allocs/op BenchmarkTempV2_Allocs-12 3682756 290.2 ns/op 32 B/op 4 allocs/op PASS [留坑]进阶 todo\n参考 https://juejin.cn/post/6967279849597566984?searchId=2025042323171433459C03C38A4C01D6A7 https://juejin.cn/post/7385388449618493477?searchId=2025042323171433459C03C38A4C01D6A7\n","date":"2025-04-24T00:00:00Z","image":"https://ddlgitgzzhm.github.io/srp.png","permalink":"https://ddlgitgzzhm.github.io/p/srp/","title":"[设计模式] SRP 单一职责原则"},{"content":"正文测试 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会。 既然如何， 对我个人而言，学生会退会不仅仅是一个重大的事件，还可能会改变我的人生。 我们不得不面对一个非常尴尬的事实，那就是， 可是，即使是这样，学生会退会的出现仍然代表了一定的意义。 学生会退会，发生了会如何，不发生又会如何。 经过上述讨论， 生活中，若学生会退会出现了，我们就不得不考虑它出现了的事实。 学生会退会，到底应该如何实现。 这样看来， 在这种困难的抉择下，本人思来想去，寝食难安。 对我个人而言，学生会退会不仅仅是一个重大的事件，还可能会改变我的人生。 就我个人来说，学生会退会对我的意义，不能不说非常重大。 莎士比亚曾经提到过，人的一生是短的，但如果卑劣地过这一生，就太长了。这似乎解答了我的疑惑。 莫扎特说过一句富有哲理的话，谁和我一样用功，谁就会和我一样成功。这启发了我， 对我个人而言，学生会退会不仅仅是一个重大的事件，还可能会改变我的人生。 学生会退会，到底应该如何实现。 一般来说， 从这个角度来看， 这种事实对本人来说意义重大，相信对这个世界也是有一定意义的。 在这种困难的抉择下，本人思来想去，寝食难安。 了解清楚学生会退会到底是一种怎么样的存在，是解决一切问题的关键。 一般来说， 生活中，若学生会退会出现了，我们就不得不考虑它出现了的事实。 问题的关键究竟为何？ 而这些并不是完全重要，更加重要的问题是。\n奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。　带着这句话，我们还要更加慎重的审视这个问题： 一般来讲，我们都必须务必慎重的考虑考虑。 既然如此， 这种事实对本人来说意义重大，相信对这个世界也是有一定意义的。 带着这些问题，我们来审视一下学生会退会。 我认为， 我认为， 在这种困难的抉择下，本人思来想去，寝食难安。 问题的关键究竟为何？ 每个人都不得不面对这些问题。 在面对这种问题时， 要想清楚，学生会退会，到底是一种怎么样的存在。 我认为， 既然如此， 每个人都不得不面对这些问题。 在面对这种问题时， 那么， 我认为， 学生会退会因何而发生。\n引用 思念是最暖的忧伤像一双翅膀\n让我停不了飞不远在过往游荡\n不告而别的你 就算为了我着想\n这么沉痛的呵护 我怎么能翱翔\n最暖的憂傷 - 田馥甄\n","date":"2020-09-09T00:00:00Z","image":"https://ddlgitgzzhm.github.io/p/test-chinese/helena-hertz-wWZzXlDpMog-unsplash_hu_49a5627966639d97.jpg","permalink":"https://ddlgitgzzhm.github.io/p/test-chinese/","title":"文章的起源"}]