API 分页设计:Offset vs Cursor(Keyset)怎么选

后端接口拉列表几乎都要分页。最常见的是 current + pageSize: { "current": 1, "pageSize": 20 }简单直观,但数据量大 + 频繁翻页 + 数据在变化的场景下有致命问题。生产上大厂 API(GitHub、Slack、Stripe)都在推 Cursor 分页。 Offset 分页的两个坑 1. 深度分页越来越慢 SQL 长这样: SELECT * FROM products ORDER BY created_at DESC LIMIT 20 OFFSET 1000000;数据库要扫过前 100 万行才能扔掉、再取 20 行。1000 万条数据翻到第 500 页,实测能到秒级。 原因:MySQL 不知道"跳过 100 万行"能不能走索引——即使能,也要真的扫过去。 2. 数据漂移 分页过程中数据在增删——用户看到重复或者漏掉。 例子:翻第 1 页看到 20 条 → 有人删了第 5 条 → 翻第 2 页时,原本第 21 条变成了第 20 条,被跳过。 对于列表实时更新的场景(订单、动态、评论流),Offset 分页几乎必然会有这种漏项。 Cursor 分页(Keyset) 思路:用"上一页最后一条的位置"作为下一页起点,不用 offset。 -- 第一页 SELECT * FROM products ORDER BY created_at DESC, id DESC LIMIT 20;-- 第二页(用上一页最后一条的 created_at + id 作为起点) SELECT * FROM products WHERE (created_at, id) < ('2026-06-04 13:00:00', 998765) ORDER BY created_at DESC, id DESC LIMIT 20;关键点:有索引的排序字段(created_at、id) 元组比较处理并列时间 走索引 range scan,速度不随深度衰减接口设计 Offset 版本: // 请求 { "current": 3, "pageSize": 20 }// 响应 { "list": [...], "total": 12345, "current": 3, "pageSize": 20 }Cursor 版本: // 请求 { "cursor": "eyJ0IjoxNzE3NDg...", "pageSize": 20 }// 响应 { "list": [...], "nextCursor": "eyJ0IjoxNzE3NDg2...", "hasMore": true }cursor 通常是 Base64 编码的 {time, id} 之类的组合,客户端不需要理解内容,直接透传。 Cursor 具体怎么生成 import base64, jsondef encode_cursor(last_row): payload = {"t": last_row.created_at.isoformat(), "id": last_row.id} return base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()def decode_cursor(cursor): return json.loads(base64.urlsafe_b64decode(cursor))# API 侧 if cursor: c = decode_cursor(cursor) rows = db.execute(""" SELECT * FROM products WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT ? """, [c["t"], c["id"], page_size]) else: rows = db.execute(""" SELECT * FROM products ORDER BY created_at DESC, id DESC LIMIT ? """, [page_size])next_cursor = encode_cursor(rows[-1]) if len(rows) == page_size else None两者对比维度 Offset Cursor简单度 ✅ 直观 需要理解元组比较深度分页性能 ❌ 越来越慢 ✅ 常数时间支持随机跳页 ✅ 可以直接第 N 页 ❌ 只能顺序翻数据变化时稳定 ❌ 漏项 / 重复 ✅ 稳定需要 total 计数 ✅ 好算 ❌ 通常不返回支持排序 任意 排序字段必须唯一 + 有索引什么时候用哪个 用 Offset:后台管理系统的表格(数据不常变、用户会跳页) 数据总量不大(几千到几万条) 客户端要显示"第 5 / 100 页"这种明确导航用 Cursor:时间流列表(订单、聊天、动态、日志) 数据量大(十万+) 客户端做"上拉加载更多"(不需要跳页) 数据实时增删两个都做:GitHub API 大多支持 ?page=N(Offset)和 ?before=cursor(Cursor),场景多的时候都提供。 顺带:total 慢查询 Offset 分页返回 total 时,SELECT COUNT(*) 在千万级表上也会慢。优化:缓存 total(Redis,几分钟一次异步刷新) 估算 total(PostgreSQL pg_class.reltuples、MySQL INFORMATION_SCHEMA.TABLES) 只算前几页的精确 total,之后返回 total: null 或 hasMore: true 就够一句话总结 大数据 + 时间流用 Cursor(速度不衰减、结果稳定)、后台管理小表格用 Offset(简单、支持跳页)。别把 Offset 用在千万级实时数据的 API 上。

Cloudflare 525 SSL handshake failed 排查全流程

某天访问站点,Cloudflare 直接抛了个 525: { "title": "Error 525: SSL handshake failed", "status": 525, "detail": "The SSL/TLS handshake between Cloudflare and the origin server failed." }意思就是——Cloudflare 收到了请求,但它和你的源站之间的 TLS 握手没建起来。不是 Cloudflare 挂了,是源站 SSL 有问题。 常见 8 种原因 1. 源站证书失效 在源站本机测: echo | openssl s_client -servername example.com -connect example.com:443看输出里有没有: Verify return code: 0 (ok)如果出现 certificate has expired,证书过期了,续签或换证书。 2. Nginx 证书和私钥不匹配 分别算 MD5,两边必须一致: openssl x509 -noout -modulus -in cert.pem | md5sum openssl rsa -noout -modulus -in key.pem | md5sum不一致就是配错了。 3. TLS 版本太老 Cloudflare 现在只接受 TLS 1.2 / 1.3。Nginx 配置里明确写清: ssl_protocols TLSv1.2 TLSv1.3;4. Cipher Suite 不兼容 某些老配置写得太激进(HIGH:!aNULL:!MD5 只留少数几个),Cloudflare 找不到共同 cipher。用 nmap 探一下源站支持哪些: nmap --script ssl-enum-ciphers -p 443 <origin-ip>5. Cloudflare 用了 Full (strict) 模式 Dashboard → SSL/TLS → Overview 里检查模式:Flexible:Cloudflare 到源站走 HTTP Full:源站要有证书(不校验) Full (strict):源站必须有效证书、链完整、域名匹配如果是 strict,用 Cloudflare Origin CA 签的证书最省事。 6. 证书链不完整(最常见的坑) 如果 openssl s_client 里出现: verify error:num=20:unable to get local issuer certificate多半是 Nginx 只配了 cert.pem 而不是 fullchain.pem。 正确写法: ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;7. 源站 443 没监听 直接测: curl -vk https://<origin-ip>如果 Connection refused,Nginx 没在这个 IP 上监听 443。防火墙、云安全组同样要放行。 8. IPv6 配置错误 Cloudflare 可能优先走 IPv6,如果 Nginx 没监听或者防火墙漏放,就 525。 dig AAAA example.com出现 AAAA 记录时,Nginx 记得写: listen 443 ssl; listen [::]:443 ssl;快速定位思路 先在源站本机执行: openssl s_client -connect localhost:443 -servername example.com本机就 handshake failure:问题在 Nginx 或证书 本机 OK 但外网 525:问题在 IP 路由 / 防火墙 / IPv6 / Cloudflare 连的不是这台机器再看 Cloudflare 实际解析到哪个 IP: dig A example.com dig AAAA example.com用 --resolve 强制走特定 IP 复现: curl -I https://example.com --resolve example.com:443:<origin-ip>一句话总结 525 = Cloudflare 和源站的 TLS 握手失败。八成落在 证书链不完整 或 Full (strict) 模式下证书不合规,用 openssl s_client 从源站本机开始逐层排查。

Cloudflare 525 SSL Handshake Failed:原因排查与修复方法

Cloudflare 525 错误发生在 Cloudflare 与源站之间,不是用户与 Cloudflare 之间。含义:Cloudflare 已经接收到请求,但无法与源站完成 TLS 握手。 快速检测源站 TLS openssl s_client -connect 源站IP:443 -servername 你的域名正常握手会看到: New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384如果出现 handshake failure,问题在 Nginx 或证书配置。 原因一:Nginx 未使用 fullchain 证书(最常见) Let's Encrypt 证书必须用 fullchain.pem,不能只用叶证书: # 正确 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;检查: nginx -T | grep ssl_certificate如果配置的是 cert.pem 而非 fullchain.pem,Cloudflare 会因为证书链不完整而报 525。 openssl 验证时看到这行也是同一问题: verify error:num=20:unable to get local issuer certificate原因二:TLS 版本过低 Cloudflare 要求至少 TLS 1.2。Nginx 配置: ssl_protocols TLSv1.2 TLSv1.3;原因三:Cipher Suite 过于严格 ssl_ciphers HIGH:!aNULL:!MD5;检查源站支持的 cipher: nmap --script ssl-enum-ciphers -p 443 源站IP原因四:开启了客户端证书验证 ssl_verify_client on;Cloudflare 无法提供匹配的客户端证书,直接导致 525。 检查: nginx -T | grep verify_client原因五:443 端口未监听 telnet 源站IP 443或: curl -vk https://源站IP如果 Connection refused 说明 Nginx 没有监听 443。 原因六:IPv6 配置问题 Cloudflare 可能优先走 IPv6: dig AAAA 你的域名如果有 AAAA 记录,但 Nginx 的 IPv6 监听或防火墙有问题: openssl s_client -connect [IPv6地址]:443 -servername 你的域名测试是否成功。 Cloudflare SSL 模式 Cloudflare Dashboard → SSL/TLS → Overview 查看当前模式:模式 要求Flexible 源站无需 HTTPSFull 源站需要 HTTPS,证书可自签Full (strict) 源站需要有效证书,证书链完整使用 Full (strict) 时,源站必须配置 fullchain.pem。 完整排查步骤 # 1. 检查证书配置 nginx -T | grep ssl_certificate# 2. 测试握手(替换为实际源站 IP) openssl s_client -connect 源站IP:443 -servername 你的域名# 3. 检查 IPv6 dig AAAA 你的域名# 4. 检查客户端验证 nginx -T | grep verify_client# 5. 直接用 curl 测试 curl -Iv https://你的域名

Clash Verge Rev 规则覆写:prepend/append/delete 与 PROCESS-PATH-REGEX 详解

Clash Verge Rev 的规则覆写配置片段: prepend: - 'PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/UURemote.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT' append: [] delete: []prepend / append / delete 的含义字段 作用prepend 在规则列表最前面插入,优先匹配append 在规则列表最后面追加,最后匹配delete 从现有规则中删除匹配项prepend 里的规则优先级最高,命中后直接执行,不再往下匹配。 PROCESS-PATH-REGEX 语法 PROCESS-PATH-REGEX,<正则表达式>,<策略>macOS 示例: PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT匹配进程路径: /Applications/WeChat.app/Contents/MacOS/WeChatWindows 示例: PROCESS-PATH-REGEX,.*\\WeChat\\WeChat.exe,DIRECT注意 Windows 路径需要用 \\ 转义反斜杠。 为什么远程控制软件要设为 DIRECT ToDesk、TeamViewer、向日葵、AnyDesk 等远程桌面软件通过自有中转服务器建立连接,走代理后常见问题:连接黑屏 延迟显著增加 无法建立 P2P 通道 连接频繁中断把这类软件设置为 DIRECT 可以绕过代理,走最短路径连接中转服务器。 常用直连配置 prepend: # 远程桌面 - 'PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/UURemote.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/SunloginClient.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/TeamViewer.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/AnyDesk.app.*,DIRECT' # 国内即时通讯 - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/DingTalk.app.*,DIRECT'查找 macOS 进程真实路径 # 通过进程名查找 ps aux | grep ToDesk# 通过 PID 查看文件 lsof -p <PID> | head -5输出示例: /Applications/ToDesk.app/Contents/MacOS/ToDesk然后写规则: PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT让应用走代理 如果需要某个应用强制走代理(和默认行为相反),改策略组名称: prepend: - 'PROCESS-PATH-REGEX,/Applications/SomeApp.app.*,节点选择'前提是策略组 节点选择 在你的 proxy-groups 中存在。

Clash 用 PROCESS-PATH-REGEX 让特定 App 直连

日常科学上网走 Clash 系客户端,大多数应用没问题。但有些特殊 App 不适合走代理:远程控制类(ToDesk、向日葵、AnyDesk、TeamViewer):走代理会连不上或黑屏 微信 / QQ:中转导致延迟大或者协议对不上 游戏:延迟敏感、部分反作弊系统会怀疑 IP 一些内网 App:需要局域网直连Clash Meta(含 Verge Rev、ClashX Meta、OpenClash)提供 PROCESS-PATH-REGEX 规则,按进程路径匹配后强制直连。 规则语法 - 'PROCESS-PATH-REGEX,<正则>,<策略>'例: - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT'匹配路径包含 /Applications/WeChat.app 的进程,直连。 常用配置(macOS) prepend: # 远程控制软件 - 'PROCESS-PATH-REGEX,/Applications/ToDesk.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/UURemote.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/SunloginClient.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/TeamViewer.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/AnyDesk.app.*,DIRECT' # 通讯软件 - 'PROCESS-PATH-REGEX,/Applications/WeChat.app.*,DIRECT' - 'PROCESS-PATH-REGEX,/Applications/QQ.app.*,DIRECT' # 开发工具(可选) - 'PROCESS-PATH-REGEX,/Applications/Docker.app.*,DIRECT'append: [] delete: []prepend 表示加到规则最前面(优先级最高,第一个匹配到就返回)。 Windows 上路径 Windows 版路径不同: - 'PROCESS-PATH-REGEX,.*\\WeChat\.exe$,DIRECT' - 'PROCESS-PATH-REGEX,.*\\ToDesk\.exe$,DIRECT' - 'PROCESS-PATH-REGEX,.*\\SunloginClient\.exe$,DIRECT'或者用 PROCESS-NAME(更省心): - 'PROCESS-NAME,WeChat.exe,DIRECT' - 'PROCESS-NAME,ToDesk.exe,DIRECT'PROCESS-NAME 只匹配文件名,不用管路径,跨平台更方便。 放哪儿 Clash Verge Rev: 配置界面 → 配置 → 脚本 / Rules 覆写。规则放在 prepend 数组里。 ClashX / ClashX Meta: ~/.config/clash/config.yaml 的 rules: 数组开头(或用 mixin/override)。 Clash for Windows(已停止更新): Parsers → 编辑 YAML 覆写规则。 想让某 App 走特定节点 改成策略组名字: - 'PROCESS-PATH-REGEX,/Applications/Chrome.app.*,🚀 节点选择'前提是策略组里有这个名字: proxy-groups: - name: 🚀 节点选择 type: select proxies: - 🇺🇸 美国 - 🇯🇵 日本 - 🇭🇰 香港查真实进程路径 不确定 App 的路径,先查一下(macOS): # 按名字找 PID pgrep -x WeChat# 用 PID 查完整路径 lsof -p <PID> | head -5# 或者一句话 ps -ax -o command | grep WeChat | grep -v grepWindows: Get-Process | Where-Object Name -like "*WeChat*" | Select-Object Name, Path规则匹配优先级 Clash 从上往下匹配,第一个命中就返回。所以 prepend 里的规则会覆盖后面的常规规则。 要精细控制某类流量的策略: rules: # 高优先级:某个 App 强制直连 - PROCESS-NAME,WeChat.exe,DIRECT # 中优先级:国内 IP 直连 - GEOIP,CN,DIRECT # 中优先级:广告拦截 - DOMAIN-SUFFIX,doubleclick.net,REJECT # 兜底:所有其它走代理 - MATCH,🚀 节点选择顺序错了效果就变了。 常见问题 1. 规则没生效Clash Meta 才支持 PROCESS-* 系列规则,老版 Clash 不支持。看客户端版本,升级到 Meta 内核。 2. macOS 上要给 Clash 权限系统偏好 → 安全性与隐私 → 隐私 → 允许 Clash 访问其他应用的进程信息。 3. 路径匹配正则要转义. 在正则里是任意字符,严格匹配要写 \.: - 'PROCESS-PATH-REGEX,/Applications/WeChat\.app.*,DIRECT'4. 微信直连了还是登不上微信是走多个域名的,有的不在国内。除了 App 直连,可能还要加域名规则: - DOMAIN-SUFFIX,weixin.qq.com,DIRECT - DOMAIN-SUFFIX,wechat.com,DIRECT一句话总结 远程控制、微信、游戏这类"不适合走代理"的应用,用 PROCESS-PATH-REGEX 或 PROCESS-NAME 走直连。规则要放在 prepend(最高优先级),只有 Clash Meta 内核才支持这类规则。