From 67a8b954c98508b12870705d443063f0ac959f75 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Tue, 10 Nov 2026 03:56:29 +0800 Subject: [PATCH 1/5] Update Spring.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充事务超时的实现方式(新增附录 A19)、ORDER BY 中 #{} 与 ${} 的区别和白名单写法、MyBatis 一级缓存返回同一对象与二级缓存的查询顺序,以及参数名匹配需要 -parameters。 --- notes/Spring.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/notes/Spring.md b/notes/Spring.md index 307548d01..431367067 100644 --- a/notes/Spring.md +++ b/notes/Spring.md @@ -44,6 +44,7 @@ * [A16 Bean 的生命周期回调](#a16-bean-的生命周期回调) * [A17 事件监听方法抛出异常](#a17-事件监听方法抛出异常) * [A18 只写在 spring.factories 中的自动配置类](#a18-只写在-springfactories-中的自动配置类) + * [A19 事务超时](#a19-事务超时) * [参考资料](#参考资料) @@ -82,7 +83,7 @@ Spring 的容器用两个接口表示:BeanFactory 是根接口,负责创建 @Autowired 和 @Resource 都能注入依赖,区别在于查找的顺序: -- @Autowired 是 Spring 的注解,先按类型查找,找到多个时依次看 @Primary、字段名或参数名、@Priority(Spring 6.1 及以前 @Priority 先于名称),仍然无法确定就报错,可以用 @Qualifier 指定 Bean 的名称; +- @Autowired 是 Spring 的注解,先按类型查找,找到多个时依次看 @Primary、字段名或参数名、@Priority(Spring 6.1 及以前 @Priority 先于名称),仍然无法确定就报错,可以用 @Qualifier 指定 Bean 的名称。Spring 6.1 起,按构造器或方法的参数名匹配要求编译时开启 -parameters,Spring Boot 的 Maven parent 和 Gradle 插件默认配置了这个选项;没有开启、又不能按其它规则确定唯一的候选时,注入失败,按字段名匹配不受影响(Spring 6.2 实测); - @Resource 是 Jakarta EE(原 JSR-250)的注解,指定了 name 时按名称查找,否则先按字段名查找,找不到再按类型查找。它只能用在字段和 Setter 上,不能用在构造器上。 Bean 的名称默认是 @Bean 的方法名,或者 @Component 类的类名首字母小写(类名前两个字母都大写时保持原样,例如 URLService),也可以在注解中指定。例如容器中有两个 PayService 类型的 Bean,名称分别是 alipay 和 wechat: @@ -420,6 +421,12 @@ REQUIRED 的内层方法加入了外层的事务,用的是同一个连接。RE 使用 HikariCP 时,连接池耗尽后获取连接最多等待 connectionTimeout(默认 30 秒),期间有连接归还就返回,超时仍未取得才抛出 SQLTransientConnectionException。排查连接一直不归还的代码时,可以设置 leakDetectionThreshold,连接借出超过这个时间就在日志中打印借出处的调用栈,它只报告、不回收连接。 +事务属性中的超时(@Transactional(timeout = 2))不是到点就中断方法的定时器。开启事务时记下截止时间,之后执行 SQL 时检查:截止时间已过就抛出 TransactionTimedOutException 并回滚;还没到就把剩余的秒数设为这条语句的查询超时(JDBC 的 setQueryTimeout),MyBatis 配置了更短的语句超时时取较小的一个。Java 代码和调用外部接口同样消耗事务的剩余时间,但不会被中断,外部调用仍要单独设置超时。 + +例如超时为 2 秒:先在 Java 中耗时 3 秒再执行 SQL,事务回滚;先执行 SQL 再在 Java 中耗时 3 秒、之后没有 SQL,事务正常提交;执行 SELECT SLEEP(5) 时,语句约 2 秒被驱动取消(Spring 6.2 实测,见[附录 A19](#a19-事务超时))。 + +需要注意的是,语句被取消抛出的 QueryTimeoutException 本身不会把事务标记为只能回滚:附录的例子在事务方法中直接捕获它、之后不再执行 SQL,之前的修改照常提交。超时也只对新开启的事务生效,加入外层事务的方法上设置的超时不起作用。 + ### 传播行为 传播行为指的是一个事务方法调用另一个事务方法时,事务如何传递。常用的有以下三种: @@ -585,6 +592,16 @@ List byUserUnsafe(@Param("userId") String userId); 第一条语句的结构没有变,整个字符串只是一个参数值,仍然只返回用户 1024 的订单:user_id 是整数列,MySQL 把字符串和数字作为浮点数比较,'1024 OR 1=1' 被截断成 1024 并产生一个警告;第二条语句的条件恒为真,返回了其他用户的订单(见[附录 A10](#a10-mybatis-的参数与拦截器的回调))。${} 只用于表名、列名、ORDER BY 的字段这类不能使用占位符的地方,并且要用白名单校验参数。 +例如按用户选择的列排序时,写成 ORDER BY #{sort} 会变成 ORDER BY ?,传入的 amount 只是一个字符串常量,语句不报错,结果也没有排序;改成 ORDER BY ${sort} 能排序,但参数可以是任意表达式,例如传入 (CASE WHEN (SELECT LEFT(amount,1) FROM orders WHERE user_id=1024)='9' THEN amount ELSE id END),按结果的顺序就能判断用户 1024 的订单金额是否以 9 开头,逐位猜出数据。白名单可以直接写在 SQL 中,只输出固定的列名(见[附录 A10](#a10-mybatis-的参数与拦截器的回调)): + +```xml +ORDER BY + + amount + id + +``` + ### Mapper 接口为什么不需要实现类 MyBatis 在启动时为每个 Mapper 接口注册一个代理工厂 MapperProxyFactory,获取 Mapper 时由它创建 JDK 动态代理;Spring 中注入的就是这个代理,它怎样放进容器见本节最后一段。例如上面的 OrderMapper 注入的对象类型是 jdk.proxy2.$Proxy63,调用处理器是 MapperProxy。调用接口方法时,MapperProxy 按“接口全名 + 方法名”找到对应的 SQL,绑定参数后交给 SqlSession(MyBatis 中执行 SQL、提交和回滚所用的会话对象)执行,再把结果映射成返回类型。 @@ -636,9 +653,11 @@ List byStatuses(@Param("userId") Integer userId, @Param("statuses") List< ### 缓存 -- 一级缓存:默认开启,作用范围是一个 SqlSession,同一个会话中相同的查询只执行一次,执行增删改或提交后清空。与 Spring 集成时,不在事务中的每次调用都会使用新的 SqlSession,一级缓存基本不起作用; +- 一级缓存:默认开启,作用范围是一个 SqlSession,同一个会话中相同的查询只执行一次,执行增删改或提交后清空。命中时返回的是同一个对象,修改它会影响这个会话之后查到的结果。与 Spring 集成时,不在事务中的每次调用都会使用新的 SqlSession,一级缓存基本不起作用; - 二级缓存:作用范围是一个 Mapper 的命名空间(Mapper XML 中 namespace 的值,即 Mapper 接口的全名),多个会话共享,在 Mapper XML 中加 `` 或在接口上加 @CacheNamespace 开启,结果对象要实现 Serializable。查询结果在会话提交后才写入(只有查询的会话关闭时也会写入;执行过增删改的会话回滚时丢弃),同一命名空间的增删改提交后清空(MyBatis 3.5.14 实测,见[附录 A12](#a12-mybatis-的结果映射重载与二级缓存))。缓存在单个应用的内存中,多个实例之间不一致,实际项目中很少使用,通常用 Redis 等缓存代替。 +对开启了二级缓存的普通查询(没有传入 ResultHandler,也没有设置 useCache=false),先查二级缓存,未命中再查一级缓存和数据库。这是因为负责二级缓存的 CachingExecutor 包在执行 SQL 的 Executor 外面,一级缓存在里面的 Executor 中(MyBatis 3.5 源码)。 + ## 七、Spring 中的设计模式 | 模式 | Spring 中的例子 | 解决的问题 | @@ -1124,6 +1143,18 @@ Spring Boot 3.3.5、MyBatis-Spring 3.0、MySQL 8.0.44、JDK 21,Apple M1 Pro。 <== Row: 3, 3, 3 ``` +按用户选择的列排序(MyBatis 3.5.14,不使用 Spring):orders 表有三行,(id, user_id, amount) 依次为 (1, 1001, 300)、(2, 1024, 950)、(3, 1002, 100),查询 SELECT id FROM orders ORDER BY …,连续运行两次结果相同: + +```text +#{sort} sort=amount -> [1, 2, 3] # 没有排序 +${sort} sort=amount -> [3, 1, 2] +${sort} sort=id -> [1, 2, 3] +${sort} 猜第一位是 9 -> [3, 1, 2] # 按 amount 排序,说明猜对了 +${sort} 猜第一位是 5 -> [1, 2, 3] # 按 id 排序 +choose sort=amount -> [3, 1, 2] +choose 猜第一位是 9 -> [1, 2, 3] # 不认识的值按 id 排序 +``` + 同一类应用中按名称获取 Mapper(Spring Boot 3.3.5、mybatis-spring 3.0.3,代理类的编号随应用变化): ```text @@ -1514,6 +1545,32 @@ exit=0 exit=0 ``` +### A19 事务超时 + +Spring 6.2.19、MyBatis 3.5.14(MyBatis-Spring 3.0.3)、HikariCP 6.3.3、MySQL Connector/J 9.4.0、MySQL 8.0.44、JDK 21,Apple M1 Pro。表 t(id, v) 中 id = 1 的行 v 为 0,事务管理器为 DataSourceTransactionManager,用 TransactionTemplate.setTimeout(2) 执行(@Transactional(timeout = 2) 同样经过事务管理器)。T2 提交后、T7 开始前都在事务外把 v 重置为 0: + +```java +tt.executeWithoutResult(s -> { sleep(3000); jdbc.update("UPDATE t SET v=11 WHERE id=1"); }); // T1 +tt.executeWithoutResult(s -> { jdbc.update("UPDATE t SET v=22 WHERE id=1"); sleep(3000); }); // T2 +tt.executeWithoutResult(s -> { jdbc.update("UPDATE t SET v=33 WHERE id=1"); + jdbc.queryForObject("SELECT SLEEP(5)", Integer.class); }); // T3 +tt.executeWithoutResult(s -> { jdbc.update("UPDATE t SET v=77 WHERE id=1"); + try { jdbc.queryForObject("SELECT SLEEP(5)", Integer.class); } + catch (QueryTimeoutException e) { } }); // T7 +``` + +T4 在事务中用 MyBatis 先把 v 改为 44,再执行 SELECT SLEEP(5);T5、T6 设置 MyBatis 的 defaultStatementTimeout 为 1 秒,T5 在事务外执行 SELECT SLEEP(3),T6 在超时为 2 秒的事务中执行 SELECT SLEEP(5)。连续运行两次结果相同,下面是第一次: + +```text +T1 3027ms TransactionTimedOutException: Transaction timed out: deadline was ... v=0 +T2 3012ms 提交 v=22 +T3 2034ms QueryTimeoutException <- MySQLTimeoutException v=0 +T4 2175ms QueryTimeoutException <- MySQLTimeoutException(MyBatis) v=0 +T5 1005ms QueryTimeoutException(事务外,defaultStatementTimeout=1) +T6 1003ms QueryTimeoutException(事务剩余 2 秒,defaultStatementTimeout=1,取较小者) +T7 2010ms 捕获 QueryTimeoutException,rollbackOnly=false,2015ms 提交 v=77 +``` + ## 参考资料 - [Spring Framework: Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html) From e51793b536bd877da7fab1cb383a1438ce1191f1 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Tue, 10 Nov 2026 03:56:30 +0800 Subject: [PATCH 2/5] Update HTTP.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充签名与“私钥加密、公钥解密”说法的区别、URL 中不放敏感数据的原因,调整 JWT 验证算法的配置要求和 OAuth 隐式模式的表述,参考资料补充 RFC 8725。 --- notes/HTTP.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/notes/HTTP.md b/notes/HTTP.md index d4f7e0288..3c6b2d5af 100644 --- a/notes/HTTP.md +++ b/notes/HTTP.md @@ -532,7 +532,11 @@ JWT 与 Session 的区别: 要求更高时可以采用面向前端的后端(Backend for Frontend, BFF)模式:令牌只保存在后端,浏览器只持有会话 Cookie,由后端代为调用接口。RFC 10017 对业务应用和处理个人数据的应用强烈推荐这种做法。 -需要注意的是,服务器验证时要固定使用的签名算法,不能接受头部中 alg 为 none 的令牌;HMAC 的密钥要随机生成,长度不少于 256 位。签名也不能防止窃听和重放,令牌被截获后,在过期前任何人都能拿它冒充用户,所以必须通过 HTTPS 传输;签名只证明令牌是服务器签发的,服务器仍要检查令牌中的用户是否有权访问所请求的资源。 +需要注意的是,服务器要事先配置允许的签名算法,不能只凭令牌头部的 alg 决定用哪种算法验证,每个密钥只用于一种算法,并检查 alg 与之一致(RFC 8725)。头部由客户端提供:验证端无条件接受 alg 为 none 时,攻击者可以绕过签名验证;验证端还会把 RSA 公钥误当作 HMAC 的密钥时,攻击者可以把 RS256 改成 HS256,用公开的公钥伪造签名。 + +HMAC 的密钥要随机生成,长度不少于 256 位。 + +签名也不能防止窃听和重放,令牌被截获后,在过期前任何人都能拿它冒充用户,所以必须通过 HTTPS 传输;签名只证明令牌是服务器签发的,服务器仍要检查令牌中的用户是否有权访问所请求的资源。 JWT 只是令牌的格式,而 OAuth 2.0 是授权框架,规定第三方应用如何获得访问令牌,这个访问令牌可以用 JWT 表示。 @@ -574,7 +578,7 @@ grant_type=authorization_code&code=8XNe0QukdUBz_2wmZ34JLA - state:笔记网站生成的随机值,回调时检查是否与发出的一致,防止攻击者用自己账号的授权码构造回调地址,让用户登录到攻击者的账号上,这是一种 [CSRF](攻击技术.md#二跨站请求伪造); - access_token:访问令牌,由笔记网站的后端直接向授权服务器换取,不经过浏览器。 -早期还有一种隐式模式,授权服务器直接把访问令牌放在重定向的地址中返回,令牌会出现在浏览器历史记录和日志中。RFC 9700(OAuth 2.0 安全最佳实践)规定不应该再使用隐式模式,没有能力保存密钥的客户端(例如单页应用、手机 App)必须使用 PKCE。 +早期还有一种隐式模式,授权服务器直接把访问令牌放在重定向的地址中返回,令牌会出现在浏览器历史记录和日志中。RFC 9700(OAuth 2.0 安全最佳实践)建议不再使用隐式模式(除非已防止授权响应中的访问令牌被注入,并缓解令牌泄露的风险),没有能力保存密钥的客户端(例如单页应用、手机 App)必须使用 PKCE。 OAuth 2.0 本身只负责授权,访问令牌表示“可以读取昵称”,并不说明当前用户是谁。OpenID Connect 在它的基础上增加了 ID Token,这是一个包含用户 ID 的 JWT,专门用于登录,很多“第三方账号登录”使用的就是它。 @@ -994,6 +998,10 @@ HTTPS 只防网络中的窃听、冒充服务器和篡改。SQL 注入、XSS、C 非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行验证,就能判断这个签名是否正确。 +签名常被说成“私钥加密、公钥解密”,这并不准确。RSA 签名与解密用的是相同的底层私钥运算,但编码和处理流程不同:对 PKCS#1 v1.5 的签名做一次公钥运算,可以从得到的编码块中解析出哈希算法的标识和消息摘要;RSA-PSS 的签名没有这种结构。 + +Ed25519 这类签名算法只能用公钥、消息和签名一起验证,不提供从签名中恢复消息或摘要的操作。 + - 优点:公开密钥可以公开传输,被截获也无法用来解密; - 缺点:运算速度慢。 @@ -1214,7 +1222,7 @@ GET 用于获取资源,而 POST 用于传输实体主体。 ### 参数 -GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。不过 URL 可能被保存在浏览器的历史记录、书签以及服务器和代理的日志中,还可能通过 Referer 首部泄露给其它网站,所以密码这类敏感数据不放在 URL 中(RFC 9110 §17.9);防窃听则要靠 HTTPS。 因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。表单采用 application/x-www-form-urlencoded 编码时(GET 提交的表单和 POST 的默认方式),规则相同,只是空格写成 `+`;POST 请求体的编码由 Content-Type 决定,JSON 则直接用 UTF-8。 @@ -1791,6 +1799,7 @@ AES-128-GCM 6280411.97k - [wikipedia: URL](https://en.wikipedia.org/wiki/URL) - [RFC 9110:4.2.1 http URI Scheme](https://www.rfc-editor.org/rfc/rfc9110#section-4.2.1) - [RFC 9110:9 Methods](https://www.rfc-editor.org/rfc/rfc9110#section-9) +- [RFC 8725: JSON Web Token Best Current Practices](https://www.rfc-editor.org/rfc/rfc8725) - [RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2](https://www.rfc-editor.org/rfc/rfc5246) - [RFC 10015: Deprecating Obsolete Key Exchange Methods in TLS 1.2 and DTLS 1.2](https://www.rfc-editor.org/rfc/rfc10015) - [RFC 7568: Deprecating Secure Sockets Layer Version 3.0](https://www.rfc-editor.org/rfc/rfc7568) From 52bf6fc05b948e22757d6e68bb3945390a997a1b Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Tue, 10 Nov 2026 03:56:30 +0800 Subject: [PATCH 3/5] =?UTF-8?q?Update=20=E7=B3=BB=E7=BB=9F=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E5=9F=BA=E7=A1=80.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整 RateLimiter 两种情况的条件表述,补充压测中的闭环发压与协调遗漏(新增附录 A29)。 --- ...76\350\256\241\345\237\272\347\241\200.md" | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git "a/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md" "b/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md" index 8e5e31aef..3d1a70404 100644 --- "a/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md" +++ "b/notes/\347\263\273\347\273\237\350\256\276\350\256\241\345\237\272\347\241\200.md" @@ -152,7 +152,7 @@ I/O 多路复用用的是第二个原因:一个线程同时等待多个连接

-令牌桶不需要定时放入令牌,只要记下剩余的令牌数和上次更新的时间,请求到来时补上这段时间应放入的令牌,再判断够不够。Guava 的 RateLimiter 和 Spring Cloud Gateway 的 Redis 限流器都是这样实现的。RateLimiter 把当前请求欠下的令牌记到后面的请求上:没有尚未偿还的等待时间时,即使令牌不足也立即通过;前面的请求预支过时,acquire() 阻塞调用线程直到偿还完,无参的 tryAcquire() 则立即返回 false(Guava 33.4 实测,见[附录 A14](#a14-guava-的-ratelimiter))。 +令牌桶不需要定时放入令牌,只要记下剩余的令牌数和上次更新的时间,请求到来时补上这段时间应放入的令牌,再判断够不够。Guava 的 RateLimiter 和 Spring Cloud Gateway 的 Redis 限流器都是这样实现的。RateLimiter 把当前请求欠下的令牌记到后面的请求上:没有尚未偿还的等待时间时,即使令牌不足也立即通过;还有尚未偿还的等待时间时,acquire() 阻塞调用线程直到偿还完,无参的 tryAcquire() 则立即返回 false(Guava 33.4 实测,见[附录 A14](#a14-guava-的-ratelimiter))。 四种算法的对比: @@ -796,7 +796,19 @@ WHERE sku_id = 1 AND slot = 3 AND stock > 0; - 压测客户端:客户端的 CPU、连接数和带宽要有余量,不能先于服务端成为瓶颈; - 持续时间:每个并发级别至少运行几分钟,时间太短看不到连接池耗尽、GC 和缓存过期这类随时间出现的问题。 -JMeter、wrk、k6 等工具负责按设定的并发或速率发出请求,并统计延迟分布;同时要看服务端各层的监控。压测的结论应该是“哪一层先到达拐点、原因是什么”,而不只是一个 QPS 数字。 +JMeter、wrk、k6 等工具负责按设定的并发或速率发出请求,并统计延迟分布;同时要看服务端各层的监控。 + +需要注意的是,固定线程数、每个线程收到响应才发下一个请求的 JMeter 测试,以及默认的 wrk,都是闭环(closed-loop)发压:服务端变慢时,发出的请求也随之减少。 + +如果要模拟的是不管服务端快慢都会到来的用户请求,闭环会漏掉服务端变慢期间本该到达的请求和它们的排队时间,测出的延迟分位数偏乐观,这称为协调遗漏(Coordinated Omission, CO)。 + +例如服务端每个请求 4 ms、中途停顿 1 秒,1 个闭环用户测得 P99 为 4.0 ms;每秒固定发 200 个请求的开环(open-loop)发压下,停顿期间到达的请求都要排队,P99 为 885 ms(模型计算,见[附录 A29](#a29-闭环与开环发压))。 + +要按固定的到达速率施压,可以用 k6 的 constant-arrival-rate 执行器:它按固定速率启动迭代,要配置足够的虚拟用户(Virtual User, VU),每次迭代只发一个请求时,迭代的启动速率就是请求的速率。 + +也可以用 wrk2 的 -R 设置总的目标请求率,它从计划发送的时刻开始计算延迟,把推迟发送造成的等待也算进去。 + +压测的结论应该是“哪一层先到达拐点、原因是什么”,而不只是一个 QPS 数字。 大促前通常还要在生产环境做全链路压测,因为测试环境很难复现生产的规模和依赖关系。压测请求带上专门的标记(例如一个请求首部),每个服务识别这个标记并传给下游,传递方式与[链路追踪](微服务.md#八链路追踪)中的 traceId 相同,消息和异步任务中也要带上;带标记的请求写入影子库或影子表,不污染真实的订单和库存。压测安排在业务低峰期,并准备好随时停止压测的开关。 @@ -2762,6 +2774,29 @@ print(f"A P99 {pa:.1f} B P99 {pb:.1f} 平均 {(pa + pb) / 2:.1f} 合并后 P9 A P99 29.9 B P99 866.8 平均 448.3 合并后 P99 51.5 ``` +### A29 闭环与开环发压 + +用 Python 计算一个简化的模型(不是实测):服务端每个请求固定处理 4 ms,同时只处理一个;压测 60 秒,第 30~31 秒服务端停顿,期间到达的请求排队。闭环为 1 个用户,收到响应后立即发下一个;开环每 5 ms 发一个请求(每秒 200 个),不管之前的是否完成: + +```python +MS = 1000 # 时间用整数微秒表示,避免浮点误差 +def serve(arrive, free): # 返回完成时刻,free 为服务端空闲的时刻 + start = max(arrive, free) + if 30_000 * MS <= start < 31_000 * MS: # 停顿期间不处理 + start = 31_000 * MS + end = start + 4 * MS + if start < 30_000 * MS < end: # 处理途中遇到停顿 + end += 1_000 * MS + return end +``` + +```text +闭环 1 个用户:请求 14750 个,平均 4.1 ms,P99 4.0 ms,最大 1004 ms +开环 200 次/秒:请求 12000 个,平均 45.7 ms,P99 885.0 ms,最大 1004 ms +``` + +闭环只有一个请求经历了停顿,开环在停顿的 1 秒中到达了 200 个请求,之后的请求还要等前面的排完,所以 P99 相差两个数量级。 + ## 参考资料 - [Prometheus: Histograms and summaries](https://prometheus.io/docs/practices/histograms/) From 53ef7ff58c7f7836d9489f4d94d03701cbf7bfa7 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Tue, 10 Nov 2026 03:56:30 +0800 Subject: [PATCH 4/5] Update Redis.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充脑裂时旧主恢复后停止写入的时间,以及事务中入队时就会被拒绝的错误类型。 --- notes/Redis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/Redis.md b/notes/Redis.md index cf4fafcc6..86e8a212e 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -598,7 +598,7 @@ QUEUED 3) OK ``` -命令入队时就能发现的错误(例如命令不存在),EXEC 会放弃整个事务,返回 EXECABORT。 +命令入队时就能发现的错误(例如命令不存在、参数个数不对,或者内存超过 maxmemory、无法通过淘汰释放足够的空间时,SET 这类可能增加内存的命令被拒绝),EXEC 会放弃整个事务,返回 EXECABORT(Redis 7.4 实测)。 WATCH 可以实现乐观锁:从 WATCH 到 EXEC 之间被 WATCH 的键被修改,EXEC 返回 nil,事务中的命令都不执行,由客户端决定是否重试。这里的修改不只是其它客户端的写入,写入相同的值、本客户端在 MULTI 之前的写入、键过期也算(Redis 7.4 实测);EXEC 之后无论成败,WATCH 都会取消。 @@ -706,7 +706,7 @@ Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入 down-after-milliseconds 为 5 秒时,从主服务器宕机到客户端查到新地址约 6 秒,其中 5 秒是超时检测。新的主服务器保留了旧的复制 ID,另一个从服务器转向它时只需要部分重同步,只发送了 156 字节的积压数据;重启的旧主服务器复制 ID 不匹配,只能全量复制(Redis 7.4 实测,见[附录 A5](#a5-sentinel-故障转移))。 -需要注意的是,主从之间是异步复制,切换时还没有复制到从服务器的写入会丢失。网络分区时,旧的主服务器还可能继续接受写入(脑裂),恢复后它变为从服务器,这段时间的写入也会丢失。分布式锁同样会因此丢失,见 [Redis 的 SET NX 指令](分布式.md#redis-的-set-nx-指令)。 +需要注意的是,主从之间是异步复制,切换时还没有复制到从服务器的写入会丢失。网络分区时,没有配置下面的 min-replicas 写入限制的旧主服务器仍可继续接受写入(脑裂),分区恢复后也要等哨兵把它改为从服务器才停止(Redis 7.4 实测恢复后约 10 秒),其中没有复制到新主服务器的写入会丢失。分布式锁同样会因此丢失,见 [Redis 的 SET NX 指令](分布式.md#redis-的-set-nx-指令)。 可以配置 min-replicas-to-write 1 和 min-replicas-max-lag 10:延迟不超过 10 秒的从服务器少于 1 个时,主服务器拒绝写入并返回 NOREPLICAS,旧的主服务器与所有从服务器断开后最多再接受约 10 秒的写入;它不保证每次写入都已复制,分区后还有从服务器和旧主在同一侧时也挡不住写入。代价是即使主服务器本身正常,只要没有满足延迟要求的从服务器,它也会拒绝写入。 From 92803a5d422dfdaf61944952aea55f2dae33a87d Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Tue, 10 Nov 2026 03:56:30 +0800 Subject: [PATCH 5/5] =?UTF-8?q?Update=20=E5=AD=A6=E4=B9=A0=E8=B7=AF?= =?UTF-8?q?=E7=BA=BF=20-=20Java=20=E5=90=8E=E7=AB=AF.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 性能与可用性一天的动手说明和预期与 RateLimiter 的说法同步。 --- ...50\267\257\347\272\277 - Java \345\220\216\347\253\257.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md" "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md" index d44137de5..f8cb0710a 100644 --- "a/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md" +++ "b/notes/\345\255\246\344\271\240\350\267\257\347\272\277 - Java \345\220\216\347\253\257.md" @@ -2053,9 +2053,9 @@ RPC 一节对比了它和 HTTP 的取舍。雪花算法要注意时钟回拨; -动手:运行[附录 A14](系统设计基础.md#a14-guava-的-ratelimiter)的代码,比较 acquire() 和 tryAcquire() 在令牌不足时的行为。 +动手:运行[附录 A14](系统设计基础.md#a14-guava-的-ratelimiter)的代码,比较 acquire() 和 tryAcquire() 什么时候等待、什么时候拒绝。 -预期:新建后连续 acquire() 第 1 次不等待、之后每次等待约 0.2 秒,空闲 2 秒后前 6 次都不等待、第 7 次等待约 0.2 秒,令牌不足时 tryAcquire() 立即返回 false。 +预期:新建后连续 acquire() 第 1 次不等待、之后每次等待约 0.2 秒,空闲 2 秒后前 6 次都不等待、第 7 次等待约 0.2 秒,第 7 次 acquire() 预支的等待还没过去,紧接着的 tryAcquire() 立即返回 false。 算法(动态规划,中等):[62. 不同路径](https://leetcode.cn/problems/unique-paths/),题解见[矩阵的总路径数](Leetcode%20题解%20-%20动态规划.md#2-矩阵的总路径数)。