异步任务执行期间状态变了的几个坑
一个数据量比较大的画布应用,后台异步加载、重算数据,算完再渲染到 Canvas。这种结构踩了好几个坑,根子其实都出在同一件事上:异步任务跑的过程中,它依赖的外部状态已经被别的地方改了,任务回来还按旧状态干活,就出错。
下面这几个坑表现完全不一样,但捋到底都是这个原因。
整块空白后台读完数据回来,往像素缓冲区里写的时候,要按”上一段的位置”算偏移。问题出在那个”上一段”——遍历的时候有些段因为被别的段完全包含而跳过了,结果拿”上一个没跳过的段”当基准,基准就错了,偏移算成负数,写缓冲越界,抛 IndexOutOfBoundsException。这个异常在渲染流程里被 catch 住了,程序不崩,但这一块就没写进去,画面上就是一片空白,啥报错都没有。
更隐蔽的是另一个根因:待渲染的段列表是个会被原地 clear() + 重建的 ArrayList。后台线程正迭代它的时候,主线程正好在 clear + add 重建,后台读到的就是个半新半旧的列表,照样算出错误的段。
第一个修法是基准只用”真正产生了段的项”,被跳过的不当参照。第二个是段列表改成不可变快照发布:
private volatil ...
做 licence 防破解踩的几个坑
给一个 Java 工具做了套 licence,要解决两个现实问题:一份 licence 不被全公司共用、不被改系统时间无限续期。先说清楚一点,客户端 licence 本质是抬高破解成本,不是绝对防得住——私钥不泄露的前提下,能做到”普通用户懒得搞”的程度,真遇上专业逆向一样会破。
licence 本体是一张 JWT,RSA 私钥签发,客户端内嵌公钥验签。下面几个是做的时候要注意的点。
时间回拨JWT 的过期判断(exp)依赖系统时钟,所以最简单的破解就是把系统时间往回调,过期就判不出来了。
办法是本地存一个单调不降的时间记录:每次校验通过,把当前时间写进 ~/.myapp/.lastseen;下次校验前先比一下,now 比 lastseen 早超过一个容差就拒掉:
long lastSeen = Long.parseLong(readLastSeen());long now = System.currentTimeMillis();// 留 5 分钟容差吸收 NTP 微调、时区抖动return now >= lastSeen - CLOCK_ROLLBACK_TOLERANCE_ ...
Dewow:把偏到一边的秤归零
原始 A-scan 应该围绕零线上下振动,实际却经常整体偏到一边,像秤没归零。Dewow 就是先把秤拨回零。
为什么会偏波形整体偏移主要来自:
发射天线直接窜到接收天线的耦合信号;
接收机电路的直流偏置和缓慢漂移;
地面耦合条件随推车过程缓慢变化;
低频电磁干扰。
这些成分变化很慢,频率集中在零频和极低频,把真实信号的基线顶歪了。
用滑动平均估计漂移有用反射信号围绕天线中心频率快速振动,正半周和负半周大致抵消。取一个足够长的窗口做滑动平均:
快速振动部分一正一负,平均下来接近 0;
缓慢漂移在窗口里几乎是常数,平均后保留下来。
这个滑动平均就是漂移的估计,把它减掉,漂移就去掉了。
公式:
$$\hat d[n]=\frac{1}{W}\sum_{k=-W/2}^{W/2}x[n+k], \qquad \hat s[n]=x[n]-\hat d[n]$$
$x$ 是原信号,$\hat d$ 是估计漂移,$\hat s$ 是输出,$W$ 是窗长,取奇数。
写成 FIR 滤波器,所有抽头之和为 0,意味着直流增益正好是 0——这就是去 ...
微信支付接入踩的几个坑
前言微信支付的文档其实挺全,但接入的时候总有些”文档写了、你却未必留意”的坑。下面是我自己踩过或者帮人排查过的几条,不绑具体项目,只说通用经验。
平台公钥模式 vs 证书模式,怎么选微信支付回调验签现在主要有两种配置:
证书模式(RSAAutoCertificateConfig):SDK 自动下载并轮换平台证书。
平台公钥模式(RSAPublicKeyNotificationConfig):用一份固定的平台公钥验签。
证书模式省事,但在平台证书切换的那段时间,有可能 SDK 还没更新到新证书,验签就会失败。公钥模式稳定些,缺点是真到了换公钥的时候,得自己手动换文件。
我的选择是:商户号稳定、回调量不大的,用公钥模式,自己可控;商户号活跃、证书换得勤的,用证书模式,但切换那几天盯着点失败率。
回调 body 必须取原始字符串这是最容易踩的坑。微信支付回调的签名是对原始请求体算的,所以接回调时 body 得原封不动拿到,不能让框架先帮你反序列化:
@PostMapping("/notify")public ResponseEntity<String> n ...
支付状态同步-异步回调与主动查单兜底
不能只靠回调支付平台一般是异步回调通知商户:用户付完钱,平台往你的 /notify 地址 POST 一个请求,告诉你”这笔订单付了”。
但回调这东西不能全信。网络抖动、服务器重启、notify-url 改了、DNS 出问题、甚至平台那边延迟,都可能让回调丢掉或者晚到。只靠回调,迟早会遇到”用户钱都付了,你这边还不知道”这种情况。
所以支付状态同步一般做成:异步回调(推)+ 主动查单(拉),两条腿走路。
推和拉各管什么支付平台 商户服务 │ │ │ ① 用户完成支付 │ │ │ │──── POST /notify ────▶│ 推:被动接收支付结果 │ │ │◀── 返回 SUCCESS ──────│ │ │ │ │ │◀── GET /queryOrder ───│ 拉:主动查询 ...
支付回调的幂等设计
为什么回调必须幂等支付平台为了让你一定收到通知,回调是会重试的。同一笔订单支付成功,你这边可能收到一次、两次,甚至更多次 /notify。
如果每次回调都老老实实走一遍”更新订单 + 发货 + 开通权益”,轻则重复发货,重则重复扣库存、给用户重复加钱。所以回调这步必须做成幂等——同一笔通知来多少次,结果都一样。这块平时不起眼,但出了事基本都是它。
思路这三样我习惯叠在一起用,单独靠哪一个都不够稳。
状态机:PAID 是个单向终点订单状态尽量简单:
PENDING → PAID ↓EXPIRED / CANCELLED
PAID 只能从 PENDING 转过来。已经是 PAID 的订单再收到回调,直接返回成功,什么都不做——这是挡重复通知的第一步。
if (order.status == PAID) { return success; // 处理过了,直接 Ack}if (order.status != PENDING) { throw illegalState(); // 过期或已取消的订单,不再接受支付通知}
数据库唯一键 ...
GPR 数据到底长什么样:A-scan、B-scan 与采样定理
探地雷达往地下打一束高频电磁脉冲,遇到材料变了的地方,一部分反射回来,剩下的继续往下走。仪器只记录两个数:什么时候回来,回来有多强。所有 GPR 数据说到底就是这个。
反射和深度反射强弱看界面两边材料差多少:
$$R = \frac{\sqrt{\varepsilon_1}-\sqrt{\varepsilon_2}}{\sqrt{\varepsilon_1}+\sqrt{\varepsilon_2}}$$
混凝土介电常数大概 6~9,空气接近 1。衬砌背后出现空洞时,两边差得大,反射就强。空洞、脱空能被探到,原因就在这里。
深度从回波时间换算:
$$d = \frac{v \cdot t}{2}, \qquad v = \frac{c}{\sqrt{\varepsilon_r}}$$
$t$ 是电磁波下去再上来的双程时间,所以除以 2。光速 $c=0.3,\text{m/ns}$,混凝土里波速大概 $0.10\sim0.12,\text{m/ns}$。回波时间对应深度,回波强弱对应界面差异——这两句话是后面所有算法的基础。
A ...
零线刷新幂等去重 缓存数组逐元素比较
现象零线对齐初始化那阵,发现磁盘被频繁读、初始化很慢。profile 了一下,看到 reloadBuffer 被反复触发了很多次。
原因初始化时有好几个事件源(不同组件的状态变更)都会触发 reloadBuffer——也就是重新从磁盘读雷达数据再刷新。这些事件短时间里密集到达,虽然最后零线的状态其实没变,但每次都老老实实重读一遍磁盘,做了大量重复 IO。
去重要让刷新幂等,说白了就是相同的输入只处理一次。把上一次的 zeroPositions 数组缓存下来,新事件来了先比一比新旧:
fun onZeroLineUpdate(newPositions: DoubleArray) { if (newPositions contentEquals lastPositions) return // 没变,跳过 lastPositions = newPositions.copyOf() reloadBuffer()}
为什么不算哈希第一反应可能是算个哈希比一下,快。但哈希会碰撞:两个内容不同的数组哈希恰好一样,就会被当成”没变”漏掉刷新,雷达图就错位了 ...
雷达采样直方图从 O(N×M) 优化到 O(N) 单遍桶计数
背景雷达图渲染前要统计采样值的分布——算百分位、做增益和对比度映射,这步一般叫”采样直方图”。打开一个多文件的测线时,每打开一个文件都得算一遍。
原来的 O(N×M)最早的做法是”对每个待统计的输出桶,把所有采样点扫一遍,数有多少落进这个桶”。N 个采样点 × M 个桶,复杂度 O(N×M)。单文件几百万采样点的时候,光这一步就把打开速度拖慢一大截,多文件叠在一起更明显。
改成 O(N) 单遍桶计数其实直方图没必要每个桶都扫一遍数据。一趟遍历,把每个采样点直接扔进它对应的桶就行了:
fun histogram(samples: IntArray, min: Int, max: Int, bucketCount: Int): IntArray { val buckets = IntArray(bucketCount) val range = (max - min).coerceAtLeast(1) for (v in samples) { val idx = ((v - min).toLong() * bucketCount / ran ...
Kotlin 协程生命周期 SupervisorJob 与 Mutex 超时保护
SupervisorJob默认情况下,协程作用域用的是 Job(),父子关系就像一根绳上的蚂蚱——任意一个子协程抛异常,整个作用域(包括其他正常跑着的兄弟协程)都会被取消。对于”加载多条测线、其中一条失败不该拖累其他”的场景,这就太脆了。
换成 SupervisorJob(),子协程的失败不会往上传播,其他兄弟照常跑:
class RadarViewModel { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)}
@PreDestroy协程要是不主动取消,它会一直挂着,引用着 ViewModel / UI,造成内存泄漏,甚至组件都销毁了还跑回来刷新一个已经失效的 UI。把 scope 的取消绑到生命周期上:
@PreDestroyfun onDestroy() { scope.cancel() // 取消所有子协程}
Mutex 加超时共有 / 复合病害的计算可能短时间里被触发好几次(连续编辑),不加锁的话,两个并 ...
