1. 读书|程序员如何传书到 Kindle

    我有一台 2013 年从日亚海淘的 Kindle Paperwhite,至今仍在服役。除了外观上的磨损,其它一切正常,甚至连续航都依旧给力。 从去年亚马逊宣布,将在今年六月停止中国区 Kindle 电子书店的运营后,我一直想写点什么,来记(ji)录(dian)一下这个陪伴我多年的老伙伴,却一直没有动笔。 一年多以后的今天终于开了个头,计划分几个小主题写一写我是如何使用 Kindle 的,包括传书、屏保图片管理、文件管理等等,作为自己的存档和回忆,也希望能帮到一些「后 Kindle 时代」仍然在继续使用它的人。 虽然被戏称泡面盖,但使用电纸书当然是为了阅读,在官方电子书店停止运营后,如何把自己找到的电子书传输到 Kindle 上就成了一个绕不过去的话题。相信一些比较喜欢折腾的老用户都已经熟知各种传书的方式了,比如邮箱推送、USB 传输、亚马逊公众号等,网上相关介绍也非常多,在此不赘述。 本篇记录一下我传书到 Kindle 的「独特」方式——WiFi 传书插件。 这是我自制的一款插件,可以直接通过 WiFi 传输电子书到 Kindle,不需要使用 USB 线,也不依赖其它服务,只要 Kindle 和手机/电脑在同一个局域网内,就可以通过浏览器直接上传电子书到 Kindle。 运行效果 Kindle 端插件运行效果: 手机端上传页面效果: 电脑端上传页面效果: 原理 这个插件的原理是,在 Kindle 上运行一个 HTTP Server,在 8000 端口提供服务,这样局域网内的电脑、手机等设备访问 http://{Kindle 的局域网 IP}:8000,就可以打开一个能上传电子书到 Kindle 的网页。 安装方法 使用这个插件需要先在 Kindle 上安装 KUAL 和 Python3,请先确保已经正确安装它们。它们的安装方法可以参考 https://bookfere.com/post/311.html。 插件项目地址:https://github.com/mzlogin/kual-wifi-transfer 下载项目代码,可以 git clone https://github.com/mzlogin/kual-wifi-transfer.git,也可以直接到项目的 releases 下载 zip 文件; 将 Kindle 用数据线连接到电脑,把刚刚下载的代码里的 wifi-transfer 文件夹拷贝到 Kindle 的 extensions 目录下(完整路径 /mnt/us/extensions)。 使用方法 在 Kindle 上开启服务器: 在 Kindle 上打开 KUAL,就可以在插件列表里看到「WiFi Transfer」菜单项了,点击「Start Server」,Kindle 上将显示 Starting server at <ip:port>; 在电脑或手机上访问第 1 步显示的 <ip:port>,选择电子书文件并上传; 上传完成后,在 Kindle 上点击「Stop Server」关闭服务器。 小结 以上就是我最喜欢的一种传书方式,它的优点是: 不依赖于 USB 线缆; 不依赖于网络情况——有 WiFi 的时候用 WiFi,没有 WiFi 的时候,手机/电脑开个热点给 Kindle 连上去; 电子书格式不限,Kindle 上能打开的都能直接传输。 另一种我现在比较常用的做法是在 Kindle 的体验版浏览器里打开 r.qq.com,使用微信读书。 Kindle 注定渐行渐远,书籍则继续伴我们同行。

    2023/09/16 Kindle

  2. 一个分布式锁「失效」的案例分析

    小猿最近很苦恼:明明加了分布式锁,为什么并发还是会出问题呢? 故事从接到需求开始说起。 接到需求 小猿前一阵接到一个小任务,里面有一个功能对应的场景如下: 封装一个对账户余额进行加减操作的方法; 所属服务部署了多个实例; 这个方法可能会有并发调用。 注:实际业务场景比较复杂,已做简化。 小猿略作思考,就抓住了关键点:余额操作——要注意事务,多实例——要注意并发。 小猿的原始代码如下: @Override @Lock(key = "#accountNo") @Transactional(rollbackFor = Exception.class) public void updateBalance(String accountNo, AmountOperateParam param) { // do something } 可以看到,这个方法上通过注解方式加了分布式锁和事务,锁的 key 是 accountNo,也就是账户业务主键。 自测和测试也没发现啥问题,就高高兴兴发完版回家了。 出问题了 第二天一早,就接到少量用户反馈,说自己的账户余额不对了。 小猿的第一反应是:我这块自测和测试都没问题,其它功能导致的吧?本地又是一通自测,也没有复现问题。但谨慎起见,还是往代码里加了一些日志,来确认是不是自己的方法引发的。 当又有用户反馈时,小猿根据日志的情况确认了:还真是自己方法的问题,对同一个账户的余额操作,多个并发请求会同时执行到方法体里面。 也就是说……分布式锁没锁住? 冥思苦想了好久,又在本地做了大量的测试,终于让小猿找到了问题所在:AOP 执行顺序问题。 小猿设计的时序: 但实际的时序: 也就是说期望是这样的执行顺序: 但实际的执行顺序: 分布式锁和事务,都是通过 AOP 来实现的,而 AOP 的执行顺序是根据切面的优先级来的,而小猿的分布式锁切面的优先级比事务切面的优先级低,所以就出现了上面的时序问题。 于是通过给分布式锁的切面指定 Order 的方式,让它的优先级高于事务切面(注:Order 值越小,执行优先级越高),验证完没问题后,就又高高兴兴地更新完版本,修复好历史问题数据后回家了。 还有问题 谁知道第二天一早,还是有极少量的用户反馈账户余额不对的问题。 这次小猿就有点懵了,为什么还会出现这种情况呢? 经过一番艰苦卓绝的排查,终于找到了问题所在:事务嵌套。 从前文中的示例代码中可以看到,小猿的方法上加了事务注解 @Transactional(rollbackFor = Exception.class) 里,没有指定事务的传播行为,默认是 Propagation.REQUIRED,也就是说如果当前没有事务,就新建一个事务;如果当前有事务,就加入到当前事务中。 小猿自己写的代码里没有在事务方法里嵌套调用这个方法的情况,但是同事写的代码里有,这样就会导致前文的时序问题再次发生。 找到问题就好办了,小猿将自己的方法上的事务传播行为改成了 Propagation.REQUIRES_NEW,也就是说如果当前没有事务,就新建一个事务;如果当前有事务,就将当前事务挂起,新建一个事务。 这次更新完版本后,小猿就再也没有收到用户反馈了,终于可以安心回家睡觉了。 小结 在日常的开发过程中,如果涉及到并发和事务,一定要多留几个心眼,考虑周全,确认以下要点是否都正确实现: 是否做了必要的并发控制? 事务的传播行为是否符合预期? AOP 的执行顺序是否符合预期? 对并发的场景是否做了充分的测试? 对于比较关键的操作,是否打印了必要的日志?

    2023/09/11 Java

  3. 一个 MySQL 数据库死锁的案例和解决方案

    本文介绍了一个 MySQL 数据库死锁的案例和解决方案。 场景 生产环境出了一个偶现的数据库死锁问题,导致少部分业务处理失败。 分析特征之后,发现是多个线程并发执行同一个方法,更新关联的数据时可能会出现,把场景简化概括一下: 有一个数据表 tb1,主键名 id,有两条 id 分别为 A1 和 A2 的记录,对应的外键 fk_biz_no 相同; 方法 myFunc,整体是一个事务; 方法 myFunc 里的逻辑是先更新 tb1 里的一条记录,执行一些逻辑后,再更新该记录的外键对应的所有记录; 这样 线程1 和 线程2 并发执行 myFunc 方法时,示意如下: 线程1 先更新 A1,此时会对 A1 所在行加写锁,再更新 A1 和 A2,此时会同时给 A1 和 A2 所在行都加上写锁; 线程2 先更新 A2,此时会对 A2 所在行加写锁,再更新 A1 和 A2,此时会同时给 A1 和 A2 所在行都加上写锁。 如此一来,如果出现类似以下的执行时序,则会形成死锁: sequenceDiagram participant 线程1 participant 线程2 线程1->>线程1: 更新 A1 线程2->>线程2: 更新 A2 rect rgba(191, 233, 255, 0.3) note right of 线程1: 线程1 持有 A1 写锁请求 A1A2 写锁,<br>线程2 持有 A2 写锁请求 A1A2 写锁,<br>死锁形成。 线程1->>线程1: 更新 A1 和 A2 线程2->>线程2: 更新 A1 和 A2 end 带着一点伪装的 ABBA 死锁。 解决方案 按照消除死锁条件的思路,一般会想到将两个线程里的加锁顺序改为一致,但是此场景并不完全适用。以下是几种可行的方案: 方案一、对 myFunc 方法加分布式锁,可以用需要更新的记录的 fk_biz_no 作为锁的 key,这样同一个 fk_biz_no 的更新操作就会串行执行; 方案二、在方法/事务的最开始,就提前把 A1A2 的写锁申请到(比如 SELECT ... FOR UPDATE),然后再执行后续逻辑; 方案三、优化 myFunc 方法里的逻辑,先将 A1 和 A2 的数据都处理好了,然后一次性更新 A1A2,即将方法里的两次更新合并成一次更新; 方案一 和 方案二 效果类似,都是使同一 fk_biz_no 的更新操作串行了;而方法三则是消除了 ABBA 的情况(实际场景中有可能需要考虑并发执行下的数据混乱、数据覆盖的问题,那是另外的话题了,在此不展开)。 小结 来一起复习下死锁的四个必要条件: 互斥条件:一个资源每次只能被一个进程使用; 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放; 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺; 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 预防和消除死锁的思路,则无非是消除上述四个条件中的一个或多个。

    2023/08/31 Database

  4. Dubbo 应用切换 ZooKeeper 注册中心实例,流量无损迁移

    首先思考一个问题:如果 Dubbo 应用使用 ZooKeeper 作为注册中心,现在需要切换到新的 ZooKeeper 实例,如何做到流量无损? 本文提供解决这个问题的一种方案。 场景 有两个基于 Dubbo 的微服务应用,一个是服务提供者,简称 Provider,另一个是服务消费者,简称 Consumer; 使用 ZooKeeper 作为注册中心; 现在要将注册中心从旧实例「ZooKeeper(旧)」 切换到新实例「ZooKeeper(新)」; 要求流量无损; 注:实际的场景可能要复杂得多,比如可能涉及很多个应用,有的应用既是服务提供者又是服务消费者等等,但原理一致。 解决方案 主要利用 Dubbo 支持多注册中心的特性来进行设计。 Dubbo 多注册中心的用法参考 多注册中心 - Apache Dubbo。 Step 1 是现状; Step 2 将新实例「ZooKeeper(新)」加入到 Provider 的注册中心列表中,且放在首位,此时 Provider 同时向「ZooKeeper(新)」和「ZooKeeper(旧)」注册,默认为新; Step 3 将 Consumer 的注册中心修改为「ZooKeeper(新)」; Step 4 将「ZooKeeper(旧)」从 Provider 的注册中心列表中移除。 注:有一种特殊情况是一个服务既作为服务提供者又作为其它服务的消费者,这种情况应将其视为 Provider。 至此,我们已经实现了流量无损的迁移。

    2023/08/30 Java

  5. 记一种不错的缓存设计思路

    之前与同事讨论接口性能问题时听他介绍了一种缓存设计思路,觉得不错,做个记录供以后参考。 场景 假设有个以下格式的接口: GET /api?keys={key1,key2,key3,...}&types={1,2,3,...} 其中 keys 是业务主键列表,types 是想要取到的信息的类型。 请求该接口需要返回业务主键列表对应的业务对象列表,对象里需要包含指定类型的信息。 业务主键可能的取值较多,千万量级,type 取值范围为 1-10,可以任意组合,每种 type 对应到数据库是 1-N 张表,示意: 现在设想这个接口遇到了性能瓶颈,打算添加 Redis 缓存来改善响应速度,应该如何设计? 设计思路 方案一: 最简单粗暴的方法是直接使用请求的所有参数作为缓存 key,请求的返回内容为 value。 方案二: 如果稍做一下思考,可能就会想到文首我提到的觉得不错的思路了: 使用 业务主键:表名 作为缓存 key,表名里对应的该业务主键的记录作为 value; 查询时,先根据查询参数 keys,以及 types 对应的表,得到所有 key1:tb_1_1、key1:tb_1_2 这样的组合,使用 Redis 的 mget 命令,批量取到所有缓存中存在的信息,剩下没有命中的,批量到数据库里查询到结果,并放入缓存; 在某个表的数据有更新时,只需刷新 涉及业务主键:该表名 的缓存,或令其失效即可。 小结 在以上两种方案之间做评估和选择,考虑几个方面: 缓存命中率; 缓存数量、占用空间大小; 刷新缓存是否方便; 稍作思考和计算,就会发现此场景下方案二的优势。 另外,就是需要根据实际业务场景,如业务对象复杂度、读写次数比等,来评估合适的缓存数据的粒度和层次,是对应到某一级组合后的业务对象(缓存值对应存储 + 部分逻辑),还是最基本的数据库表/字段(存储的归存储,逻辑的归逻辑)。

    2023/08/27 Java

  6. 发现一种增加在 GitHub 曝光量的方法,已举报

    今天偶然看到一种增加项目和个人在 GitHub 曝光量的方法,但感觉无法赞同这种做法,已经向 GitHub 官方举报。 具体怎么回事呢?我上周在 Vim 插件大佬 tpope 的一个项目提了个 Issue,但一周过去了,大佬也没有回应,我就去他的 GitHub 主页确认他这一周有没有活动记录,看到他最近的提交活动是给 github/copilot.vim 项目——这是 GitHub Copilot 的官方 Vim 插件项目,我也在用,心想这也太巧了吧,于是点进项目主页看了一眼,大佬果然是大佬,竟然是这个插件的主要维护者,不由心生赞叹,同时在 Contributors 列表的上方我还发现了一个以前没太注意到的信息,「Used by」: Figure 1. copilot.vim’s Used by 好奇心驱使,点进去看看大家能依赖一个 Vim 插件构建一些什么项目: Figure 2. 依赖 copilot.vim 的项目 列表里的六个项目点进去基本都是空项目或者仅仅作为个人主页的 README 展示的,只有倒数第二个是有实质内容的项目(但最终发现它也没有实质依赖上面的插件)。 它们的共同点是在项目里有一个巨大的 go.mod 文件(初步判断出自 akirataguchi115 之手),里面列出了大量的依赖,足足有六千多行,但实际上都是没有用到的。里面列举的托管在 GitHub 上的「依赖」项目,我随便扫了一眼,有一些熟悉的名字,比如 HelloGitHub、996.ICU 等都赫然在列,甚至还包括了我的 awesome-adb,随机打开几个链接看了下,都是 Star 数量 5K+ 的热门项目,而且基本上都不是 package 类项目,不可能被作为依赖包。 Figure 3. go.mod 文件内容 至此恍然大悟:这几千个热门项目的浏览量是比较大,然后它们的首页的「Used by」都会显示上面 Figure 1 里的这几个人,点进去都会看到 Figure 2 里的这几个项目……妙啊!引流效果一定不错! 但是,我对这种做法感到恶心。这「巧妙」地利用了 GitHub 的一个功能,但是扰乱了项目间正常的依赖关系的链接和展示,让真正需要的人筛选和寻找正确的信息更加费劲。 如果想要在热门项目的主页里曝光自己,应该通过正常的方式去做,比如提交 PR、提 Issue、参与 Discussions、真正基于它们做一些实质性的项目等,而不是通过这种「巧妙」的方式。 不然,即使获得了流量和曝光量,也只是遭人唾弃的「现眼包」。 在写这篇文章的同时,我已经向 GitHub 官方举报了这个问题,看看官方如何看待吧。

    2023/08/24 GitHub

  7. 不过如此

    当我健康的时候,我常会想,要是更有钱多好,可以选择不同的生活,想躺平可以躺平;当我不得不在病床上「躺平」的时候,我又感觉,健康真好,有健康做前提,那些选项才有意义。 所以这就是我的局限,就和人们常说的屁股决定脑袋一样,很多想法都只是基于当时的处境。 前一段时间比较低迷,和冯提莫消失了一段时间又回来的原因一样。 这真是一段难忘的经历。 从去医院复查,是否要做穿刺检查开始纠结,拿到结果后,又开始纠结要不要手术,最后还得选择是做传统手术还是做腔镜手术。 手术时,在手术台上混杂着寒冷和害怕浑身发抖,失去知觉,醒来时就已经在被运往病房,家人们在不断喊着我让我不要睡着。 躺在病床上的时候,意识到自己在第一次吸氧,插着尿管、导流管,包扎着几处伤口,身上还上着几种监控,当时的感觉是自己像满身新鲜弹孔的短笛。承蒙家人和医护们照顾,等到好不容易敢爬起来了,三天没刮胡子,护士叫我叔叔。 病友们一个比一个生猛,一个跟我同天手术的五十多岁的大叔,第二天就自己拎着导流管瓶子到处溜达;后来的一个七十好几的大爷,第二天就不用别人扶独自翻身下床了。反观我自己,没把握什么动作幅度是安全的,完全不敢活动到手术部位,家人搀着下床走几步还全身僵直,不禁对病友们心生赞叹。 出院在家小心翼翼待了几天,小心翼翼去上班,到现在终于敢逐渐恢复正常点的节奏,就是多说几句话就感觉嗓子累,透着虚。 人生还要继续,苦笑着自嘲一句,好赖也算是与癌做过斗争的人了,按时吃药,继续搬砖,注意伤口不被汗湿,感觉闷的时候,骑上小踏板吹吹风,没有离合,只有悲欢。 生活就是这样吧,总有些沟沟坎坎,当时觉得好难好难,只有等到全都过去了,回过头看时,才有可能云淡风轻地说一声,不过如此。

    2023/08/02 Blog

  8. 解决 Java 打印日志吞异常堆栈的问题

    前几天有同学找我查一个空指针问题,Java 打印日志时,异常堆栈信息被吞了,导致定位不到出问题的地方。 现象 捕获异常打印日志的代码类似这样: try { // ... } catch (Exception e) { log.error("系统异常 customerCode:{},data:{}", customerCode, data, e); // ... } 查到的日志是这样的: 2023-06-26 11:11:11.111 ERROR 1 --- [pool-1-thread-1] c.mazhuang.service.impl.TestServiceImpl : 系统异常 customerCode:123,data:{"name":"mazhuang","age":18} java.lang.NullPointerException: null 异常堆栈丢了。 分析 在之前的一篇文章里已经验证过这种写法是可以正常打印异常和堆栈信息的:AI 自动补全的这句日志能正常打印吗? 再三确认代码写法没问题,纳闷之下只好搜索了一下关键词「Java异常堆栈丢失」,发现了这篇文章:Java异常堆栈丢失的现象及解决方法,这里面提到的问题与我们遇到的一样,而且给出了 Oracle 官方文档里的相关说明: https://www.oracle.com/java/technologies/javase/release-notes-introduction.html The compiler in the server VM now provides correct stack backtraces for all “cold” built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow. 大致意思就是说,为了提高性能,JVM 会针对一些内建异常进行优化,在这些异常被某方法多次抛出时,JVM 可能会重编译该方法,这时候就可能会使用不提供堆栈信息的预分配异常。如果想要完全禁用预分配异常,可以使用 -XX:-OmitStackTraceInFastThrow 参数。 了解到这个信息后,翻了翻从服务上次发版以来的这条日志,果然最早的十几次打印是有异常堆栈的,后面就没有了。 解决方案 回溯历史日志,找到正常打印的堆栈信息,定位和解决问题; 也可以考虑在 JVM 参数里加上 -XX:-OmitStackTraceInFastThrow 参数,禁用优化; 本地复现 在本地写一个简单的程序复现一下: public class StackTraceInFastThrowDemo { public static void main(String[] args) { int count = 0; boolean flag = true; while (flag) { try { count++; npeTest(null); } catch (Exception e) { int stackTraceLength = e.getStackTrace().length; System.out.printf("count: %d, stacktrace length: %d%n", count, stackTraceLength); if (stackTraceLength == 0) { flag = false; } } } } public static void npeTest(Integer i) { System.out.println(i.toString()); } } 不添加 -XX:-OmitStackTraceInFastThrow 作为 JVM 参数时,运行结果如下: ... count: 5783, stacktrace length: 2 count: 5784, stacktrace length: 2 count: 5785, stacktrace length: 0 Process finished with exit code 0 在我本机一般运行五六千次后,会出现异常堆栈丢失的情况。 添加 -XX:-OmitStackTraceInFastThrow 作为 JVM 参数时,运行结果如下: ... count: 3146938, stacktrace length: 2 count: 3146939, stacktrace length: 2 count: 3146940, stacktrace length: Process finished with exit code 137 (interrupted by signal 9: SIGKILL) 运行了几百万次也不会出现异常堆栈丢失的情况,手动终止程序。 完整源码见:https://github.com/mzlogin/java-notes/blob/master/src/org/mazhuang/StackTraceInFastThrowDemo.java 参考 https://www.cnblogs.com/junejs/p/12686906.html https://www.oracle.com/java/technologies/javase/release-notes-introduction.html

    2023/06/26 Java

  9. AI 自动补全的这句日志能正常打印吗?

    最近用上了 GitHub Copilot,它的能力不时让我惊叹,于是越来越多地面向 tab 编程,机械键盘的损耗都小了许多:-p 这天,它给我自动生成了一句像这样的日志打印代码: try { // ... } catch (Exception e) { log.error("Xxx 操作出错,订单号 {},操作人 {}", orderNumber, operatorName, e); } 我盯着这行熟悉又陌生的代码——没错平时我自己也会这么写,但此时竟然产生了一丝不确定,它真的能按期望的效果,先打印出这句话,然后完整打印异常堆栈吗? 既然有疑惑,那就刨根问底一下。 为什么疑惑? 问了自己这个问题之后,我回想了一下,可能是因为以前遇到过这个: 如果最后一个参数不是 Throwable 类型,那 IDEA 会给出警告: More arguments provided (3) than placeholders specified (2) 那为什么最后多出来的那个参数是 Throwable,IDE 就认为正常了呢?这就是本文要探索的问题。 消除疑惑 遇事不决,command + click 一下。可以看到方法的定义是这样的: public void error(String format, Object... arguments); 可惜想看具体实现的时候发现实现类太多,索性写一个测试用例 debug 跟一下,一路 F7 进去(这里用的日志框架是 log4j2): org.apache.logging.slf4j.Log4jLogger#error(java.lang.String, java.lang.Object...) org.apache.logging.log4j.spi.AbstractLogger#logIfEnabled(java.lang.String, org.apache.logging.log4j.Level, org.apache.logging.log4j.Marker, java.lang.String, java.lang.Object...) org.apache.logging.log4j.spi.AbstractLogger#logMessage(java.lang.String, org.apache.logging.log4j.Level, org.apache.logging.log4j.Marker, java.lang.String, java.lang.Object...) org.apache.logging.log4j.message.ParameterizedMessageFactory#newMessage(java.lang.String, java.lang.Object...) org.apache.logging.log4j.message.ParameterizedMessage#ParameterizedMessage(java.lang.String, java.lang.Object...) org.apache.logging.log4j.message.ParameterizedMessage#init 秘密就在这里了: // org.apache.logging.log4j.message.ParameterizedMessage private void init(final String messagePattern) { this.messagePattern = messagePattern; final int len = Math.max(1, messagePattern == null ? 0 : messagePattern.length() >> 1); // divide by 2 this.indices = new int[len]; // LOG4J2-1542 ensure non-zero array length // 计算占位符个数 final int placeholders = ParameterFormatter.countArgumentPlaceholders2(messagePattern, indices); initThrowable(argArray, placeholders); this.usedCount = Math.min(placeholders, argArray == null ? 0 : argArray.length); } private void initThrowable(final Object[] params, final int usedParams) { if (params != null) { final int argCount = params.length; // 如果占位符个数比参数个数少,且最后一个参数是 throwable 类型, // 则将最后一个参数赋值给 Message 的成员 if (usedParams < argCount && this.throwable == null && params[argCount - 1] instanceof Throwable) { this.throwable = (Throwable) params[argCount - 1]; } } } 然后在调用堆栈回溯几步有: // org.apache.logging.log4j.spi.AbstractLogger protected void logMessage(final String fqcn, final Level level, final Marker marker, final String message, final Object... params) { final Message msg = messageFactory.newMessage(message, params); logMessageSafely(fqcn, level, marker, msg, msg.getThrowable()); } 至此基本上清晰了。 结论 经过分析及实际运行验证: AI 生成的代码可以按期望效果打印; 如果有比占位符多的非 Throwable 类型参数,会被忽略掉。

    2023/05/10 Java

  10. 记两个有关线程池的小问题

    最近小伙伴们找我查的问题里,有两个与线程池相关的,最终都是花了一些时间才揪出原因所在,做一下记录,供以后的自己和其它需要的人参考。 一、异步变同步 现象: 有一个方法,被请求后只是向线程池提交一个任务,然后马上返回,但从日志的 traceId 来看,偶现方法与任务在同一线程执行,接口耗时较长的情况。 分析过程: 这个其实就是一个知识点:当线程池里没有空闲线程,且任务队列已满时,会怎么处理新提交的任务? 可以看下 TheadPoolExecutor 类,这个类里面有几种预定义好的策略(implements RejectedExecutionHandler): CallerRunsPolicy AbortPolicy DiscardPolicy DiscardOldestPolicy 结合它们的名字以及注释就可以看到,它们分别对应: 调度线程自己执行任务;(有一种例外情况是线程池被 shutdown 了则丢弃任务) 忽略任务,并抛出异常;(默认值) 丢弃任务,不产生异常; 丢弃队列里最老的未被处理的任务,然后重新尝试调度新任务;(例外情况同一) 除此之外,还可以按需自己定义策略。 在我们的场景里,这个线程池使用的 RejectedExecutionHandler 是 CallerRunsPolicy,所以原因就找到了。 解决方案: 因为场景里主要的诉求是这个接口要快速返回,并且不能丢失任务,那这种情况使用消息队列会更加合适,所以将这里的向线程池提交任务,修改为向消息队列发送消息。 二、消失的任务 现象: 从日志可以看到,向线程池里提交了一个任务,找不到该任务执行的记录。 分析过程: 首先是怀疑这个任务被丢弃或者忽略了,经确认,该线程池的 RejectExecutionHandler 是使用的默认的 AbortPolicy,这样的话如果它被忽略,会有异常抛出,但日志里找不到异常记录。 那就是说,它成功进入了任务队列,但是没有被执行,哪里去了呢? 冥思苦想之后,怀疑是不是应用被杀掉了?查看 K8s 控制台里容器的滚动记录,果然在提交任务的时间点附近,应用发过版——破案。 解决方案: 提供两个思路: 在保证任务执行逻辑幂等的前提下,通过消息队列、数据库记录任务状态+重试机制等方式调度任务; 容器优雅下线,确认正在处理的请求和任务都完成后才能被 kill 掉。

    2023/04/11 Java