1. Project Overview & Quickstart (dushixiang/LICENSE)
LICENSE
Open-source repository dushixiang/LICENSE
Repository Details
- Repository: dushixiang/LICENSE
- Primary Language: Code
Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.
2. Official Technical Reference & Guides (dushixiang/dushixiang.github.io)
File: content/posts/air780e-giffgaff.md
title: 30 元成本搞定 GiffGaff 短信转发与自动保活 date: 2025-12-13T21:33:35+08:00 draft: false build: list: never
先说明一点:这个项目并不是重复造轮子。
如果你搜索过「air780e 短信转发」,大概能看到目前主要有两种方案:
方案一:使用 air780e SIM卡自带流量进行转发
实现简单,但问题也很明显——像 GiffGaff 这种卡流量价格非常离谱,长期使用成本太高,直接 Pass。
方案二:air780e + ESP32,通过 WiFi 转发
这种方式在技术上是可行的,但需要自己焊接 ESP32。
奈何我属于那种一拿电烙铁手就开始手抖的人,焊精密元件基本等于劝退 😅。
基于以上原因,我最终选择了一条完全不需要焊接的路线:
串口控制 air780e + Golang 后端
通过串口直接控制 air780e,把短信的收发能力暴露出来,后端使用 Golang 来实现统一的服务层。
目前这个方案的特点是:
即插即用,无需焊接
硬件成本极低,理论上 air780e xx系列的硬件都支持
- 移动 / 联通版本:约 30 元
- 全网通版本:约 50 元
部署简单
- 只需要一台能插 USB 的小型设备
- 比如 N1 盒子、软路由、迷你主机等
功能已经可用
- 短信接收
- 短信发送
- 消息推送
- 自动发送 / 自动回复
插上 air780e,运行我写的程序,完事。
项目地址:https://github.com/dushixiang/uart_sms_forwarder
截图如下:
其他补充说明
合宙 air780 系列型号极多,刚开始搞的时候我也是一脸懵逼,后面看到一张对比图才了解到它们之间的区别。
https://docs.openluat.com/air780epm/
File: content/posts/base58-implementation-in-go.md
title: Base58 编码详解与 Go 实现 date: 2025-08-10T23:36:35+08:00 draft: false
在处理二进制数据时,我们经常需要将其转换成可读的字符串,比如生成地址、密钥、邀请码等。
常见方案有 Base64、Hex 等,但在某些场景下,它们并不是最佳选择。
这时,Base58 就登场了。
什么是 Base58?
Base58 是一种基于 58 个字符的编码方式,它的设计目标是:
- 避免易混淆字符(
0和O,l和I) - 不包含特殊符号(
+/等),适合直接用于 URL、文件名 - 更适合人工抄写和阅读,降低输入错误率
它的字符集是:
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
可以看到,
0、O、l、I等容易混淆的字符被剔除,没有+、/这类特殊符号。
为什么用 Base58?
假设你有一个用户需要手动输入邀请码、钱包地址或交易 ID:
- 如果用 Base64,会出现
+/等特殊字符,在 URL 中需要转义; - 如果用 Hex,数据会变得很长(每个字节要用两个字符表示);
- 如果用 Base58,长度更短,可读性更高,错误率更低。
这也是为什么比特币、IPFS 等项目都选择了 Base58(或其变种 Base58Check)。
Base58 在实际项目中的应用
- 比特币地址:Base58Check 编码,防止字符混淆并带有校验码
- IPFS CID:文件内容 ID 使用 Base58 编码,更短且可直接用在命令行
- 邀请码 / 短链:直接放在 URL 中,不需要额外转义
Go 语言 Base58 编码示例
这里用我开源的 Golang Base58 库来演示:
package main
import (
"fmt"
"log"
"github.com/go-orz/base58"
)
func main() {
data := []byte("Hello, Base58!")
// 编码
encoded := base58.EncodeToString(data)
fmt.Println("Encoded:", encoded)
// 解码
decoded, err := base58.DecodeString(encoded)
if err != nil {
log.Fatal(err)
}
fmt.Println("Decoded:", string(decoded))
}运行结果:
Encoded: TcgsE5e9XJSrakNTEQQ
Decoded: Hello, Base58!Base58 与 Base64 对比
| 特性 | Base64 | Base58 |
|---|---|---|
| 字符集 | 64 个(含 + /) | 58 个(无易混淆字符) |
| URL 友好 | ❌(需转义) | ✅ |
| 人工抄写友好 | ❌ | ✅ |
| 编码长度 | 短 | 稍长 |
| 编解码速度 | 快 | 略慢 |
如果你的数据主要是人类可读/可输入,Base58 更合适;
如果主要是机器传输,Base64 更高效。
项目地址
我开源了一个 Golang 的 Base58 编码库,简单、轻量、无第三方依赖,
欢迎 Star & Fork:
👉 https://github.com/go-orz/base58
💡 总结:
Base58 解决了 Base64 在人工输入、URL 传递上的不便,特别适合区块链、邀请码、短链接等场景。
有了 Go 的实现,你可以在项目中轻松集成这种编码方式。
File: content/posts/clamav-install.md
title: "Linux 杀毒软件 ClamAV 安装" categories: [ "linux" ] tags: [ "杀毒软件", "ClamAV" ] draft: false slug: "clamav-install" date: "2022-03-07T21:52:00+08:00"
Clam AntiVirus(ClamAV)是免费而且开放源代码的杀毒软件,软件与病毒码的更新皆由社群免费发布。Github 地址:https://github.com/Cisco-Talos/clamav
安装 ClamAV
yum install -y clamav*配置 ClamAV
cat > /etc/freshclam.conf <<EOF
# 数据库配置文件夹
DatabaseDirectory /var/lib/clamav
# 更新日志文件夹
UpdateLogFile /var/log/freshclam.log
# 日志大小
LogFileMaxSize 2M
# 日志记录时间
LogTime yes
# 所属用户
DatabaseOwner root
# 同步病毒库的地址
DatabaseMirror database.clamav.net
EOF更新病毒库
大概需要几分钟时间
freshclam进行病毒扫描测试
clamscan -ri常用配置说明
--recursive[=yes/no(*)] -r 递归查找
--infected -I 只打印受影响的文件信息
--remove[=yes/no(*)] 删除受影响的文件。(不建议使用,根据扫描结果进行手动删除,避免误删。)配置邮箱
如果不需要邮件通知的可以忽略此步骤。
安装邮件服务
yum install -y mailx修改配置
修改大写字母为你的邮箱配置
cat > /etc/mail.rc <<EOF
set [email protected]
set smtp=smtps://smtp.exmail.qq.com:465
set smtp-auth=login
set [email protected]
set smtp-auth-password=YOURPASSWORD
set ssl-verify=ignore
set nss-config-dir=/etc/pki/nssdb/
EOF发送邮件进行测试
echo "Your message" | mail -v -s "Message Subject" email@address如果出现 “Error in certificate: Peer's certificate issuer is not recognized” 属于正常。
定期扫描
使用 crontab -e 增加两个定时任务
# 每天凌晨 2 点半进行病毒库更新
30 2 * * * /bin/freshclam
# 每天凌晨 3 点半进行病毒扫描
30 3 * * * /bin/clamscan -ri | mail -s "病毒文件扫描报告" 'youremailaddress'如果不需要发送邮件使用下面的配置
# 每天凌晨 2 点半进行病毒库更新
30 2 * * * /bin/freshclam
# 每天凌晨 3 点半进行病毒扫描
30 3 * * * /bin/clamscan -riFile: content/posts/collect-vm-stats-by-libvirt-java.md
title: "使用libvirt-java采集KVM虚拟机状态信息" categories: [ "虚拟化" ] tags: [ "Java","libvirt" ] draft: false slug: "collect-vm-stats-by-libvirt-java" date: "2021-05-19T20:18:20+08:00"
虚拟化开发相较于普通开发是一个冷门的方向,大多数是使用Python开发,其中使用Java来做虚拟化的少之又少,资料更是少的可怜,为了实现需求我也是踩了不少坑,今天就为大家分享一下如何使用 libvirt-java 来采集KVM虚拟机的资源使用信息。
CPU使用率
libvirt并没有直接提供获取虚拟机CPU使用率的接口,需要我们自己来计算,网上分享的代码或者公式五花八门,大部分都是错误的,经过我的测试,找到了一个相对准确的计算公式。
cpu_usage = (cpu_time_now - cpu_time_t_second_ago) * 100 / (t * vCpus * 10^9)Java代码如下
// t秒前的CPU时间
long c1 = domain.getInfo().cpuTime;
Thread.sleep(1000);
// 当前CPU时间
long c2 = domain.getInfo().cpuTime;
// 虚拟CPU数量
int vCpus = domain.getMaxVcpus();
// t 为1秒
Double cpuUsage = 100 * (c2 - c1) / (1 * vCpus * Math.pow(10, 9));
log.debug("虚拟机[{}]CPU使用率为: {}", uuid, cpuUsage);内存使用率
不要使用domain.getInfo()返回的 memory字段,虽然它注释写的是the memory in KBytes used by the domain,但它的意思真的不是虚拟机内部进程已使用的内存大小,而是从宿主机器的角度来看分配给这个虚拟机的内存它使用了多少,如果没有特殊配置,它会和maxMem字段的值是相同的。
正确做法是使用domain.memoryStats(10)来获取,那为什么参数要输入一个10呢?这是因为10代表的是要返回的信息数量,经过我手动执行virsh dommemstat uuid 测试发现有10个参数返回,所以需要填入10。另外命令返回的unused 字段值与数组中tag=8的数据一致,最终我们获取到了未使用的内存大小,计算内存使用率更是轻轻松松。
Java代码如下
MemoryStatistic[] memoryStatistics = domain.memoryStats(10);
Optional<MemoryStatistic> first = Arrays.stream(memoryStatistics).filter(x -> x.getTag() == 8).findFirst();
if (first.isPresent()) {
MemoryStatistic memoryStatistic = first.get();
long unusedMemory = memoryStatistic.getValue();
long maxMemory = domain.getMaxMemory();
double memoryUsage = (maxMemory - unusedMemory) * 100.0 / maxMemory;
log.debug("虚拟机[{}]内存使用率为: {}", uuid, memoryUsage);
}网卡数据包信息
同样libvirt并没有提供获取虚拟机网卡的接口,因此需要获取虚拟机的xml文件来查询。
此处没有什么坑,解析xml是使用了html解析库Jsoup,xml算是html的亲戚吧,比html书写严格很多,解析数据更为方便。
获取到网卡名称之后再获取统计信息,可以获取的数据有:
| 字段 | 含义 |
|---|---|
| rx_bytes | 接收数据包大小 |
| rx_packets | 接收数据包数量 |
| rx_errs | 接收错误数据包数量 |
| rx_drop | 接收丢弃数据包数量 |
| tx_bytes | 发送数据包大小 |
| tx_packets | 发送数据包数量 |
| tx_errs | 发送错误数据包数量 |
| tx_drop | 发送丢弃数据包数量 |
Java代码如下
String xmlDesc = domain.getXMLDesc(0);
Document document = Jsoup.parse(xmlDesc);
// 网卡
Elements interfaces = document.getElementsByTag("devices").get(0).getElementsByTag("interface");
for (Element inter : interfaces) {
String interName = inter.getElementsByTag("target").get(0).attr("dev");
DomainInterfaceStats domainInterfaceStats = domain.interfaceStats(interName);
log.debug("dev {} stats {}", interName, Json.toJsonString(domainInterfaceStats));
}磁盘IO信息
和网卡同样,libvirt也没有提供获取虚拟机磁盘的接口,还是需要获取虚拟机的xml文件来查询,获取到磁盘名称之后再获取统计信息,可以获取的数据有:
| 字段 | 含义 |
|---|---|
| rd_req | 读取请求总数 |
| rd_bytes | 读取的数据大小 |
| wr_req | 写入请求总数 |
| wr_bytes | 写入的数据大小 |
| errs | 失败次数 |
Java代码如下
String xmlDesc = domain.getXMLDesc(0);
Document document = Jsoup.parse(xmlDesc);
// 磁盘
Elements disks = document.getElementsByTag("devices").get(0).getElementsByTag("disk");
for (Element disk : disks) {
String dev = disk.getElementsByTag("target").get(0).attr("dev");
DomainBlockStats domainBlockStats = domain.blockStats(dev);
log.debug("dev {} stats {}", dev, Json.toJsonString(domainBlockStats));
}至此采集虚拟机状态信息算是告一段落,学的越多才发现不会的越多...
File: content/posts/cyberpoc.md
+++
date = '2025-09-06T00:00:00+08:00'
draft = false
title = '我为什么开源了三年前写的漏洞验证平台?'
+++
大概三年前,我看到有人用代码片段分享漏洞原理,觉得很有意思,我也模仿设计了一些 Golang 的漏洞。
可惜想要验证,得自己搭建环境,门槛不低。
于是我又做了一个平台,把漏洞环境封装成 Docker 镜像。用户只需点一下,就能在几秒内启动环境,直接验证。
第一个版本很快上线,我陆续封装了十几个漏洞环境。但后来越做越复杂,加了虚拟机管理和分布式网络,结果部署维护麻烦到连我自己都不想搞了,再加上我了解到的 Golang 漏洞就那么多,平台没人用,项目也就搁置了。
最近我做了一个违背祖宗的决定:去掉多余的东西,只保留容器化的漏洞环境,并把平台和漏洞环境全部开源。
项目和漏洞环境都是用 Golang 写的,配置要求很低,开箱即用。
如果你觉得有用,欢迎点个 star:
- 漏洞环境:https://github.com/dushixiang/vulnerable-code
- 验证平台:https://github.com/dushixiang/cyberpoc
- 测试地址:https://cyberpoc.typesafe.cn
附上几张截图
File: content/posts/golang-binary-linking-types.md
+++
date = '2025-01-07T00:00:00+08:00'
draft = false
title = 'Golang 二进制文件的静态和动态链接'
+++
概述
Golang 最大的优势之一就是支持跨平台编译,真正的实现了Java的梦想:一次编译,到处运行。
不过,在某些特定情况下,也会存在一些特殊之处。
静态链接与动态链接
静态链接是是在编译时将程序与所依赖的所有库(通常是 .a 或 .lib 文件)一起打包,生成一个独立的可执行文件。也就是说,所有需要的函数和库代码在编译过程中就已经嵌入到可执行文件中。
因为这样编译出来的二进制更便携,它不需要在其运行的主机系统上存在库。因此,你的二进制文件可以在任何系统上运行,无论哪个发行版/版本,并且不依赖于任何系统库。
动态链接是在程序运行时(即执行时)将程序与共享库(如 .dll、.so 或 .dylib 文件)链接。程序本身包含对这些库的引用,而不直接包含库的代码。程序启动时,操作系统会加载这些库文件并链接到程序。
这种方式的优点是比较灵活,如果依赖的库发生了更新,只要接口没变,程序不需要编译就可以使用最新版本的库,而且因为二进制包含对库的引用,不包含库代码,因此文件较小。
静态链接程序
我们先来编写一个简单的 “Hello World” 程序,,经过打包后,它将成为一个静态链接的二进制文件。
package main
func main() {
println("hello world!")
}接着编译这个 Go 文件:
go build -o main1 main.go然后使用 file 命令来检查文件类型:
$ file main1 | tr , '\n'
hello_linux: ELF 64-bit LSB executable
x86-64
version 1 (SYSV)
statically linked
Go BuildID=...
with debug_info
not stripped从上述命令的输出结果可知,main1 文件属于 64 位静态链接的 ELF 格式 可执行文件,适用于 x86-64 架构,包含了 调试信息,并且没有进行 剥离(即保留了调试符号)。
另外,还有 Linux 程序 ldd 能够帮助我们判断二进制文件是静态链接还是动态链接,执行以下命令:
$ ldd main1
not a dynamic executable动态链接程序
在 Golang 中,有一种名为 CGO 的机制,它可以用来调用 C 代码,就连 Go 的标准库也在使用这一机制,例如 net 包(https://pkg.go.dev/net),其就借助了 C 库来处理 DNS。以下是相关示例代码:
package main
import (
"fmt"
"log"
"net"
)
func main() {
ns, err := net.LookupNS("typesafe.cn")
if err != nil {
log.Fatal(err)
}
for _, n := range ns {
fmt.Printf("%+v\n", n)
}
}同样需要编译这个 Go 文件:
go build -o main2 main2.go之后,我们再次利用 file 和 ldd 程序来检查第二个二进制文件,操作及结果如下:
$ file main2 | tr , '\n'
main2: ELF 64-bit LSB executable
x86-64
version 1 (SYSV)
dynamically linked
interpreter /lib64/ld-linux-x86-64.so.2
Go BuildID=...
with debug_info
not stripped$ ldd main2
linux-vdso.so.1 (0x00007ffc09ec8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3651936000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3651b21000)由此可以看出,main2 是一个 64 位动态链接的 ELF 可执行文件,其运行依赖于 glibc(libc.so.6)以及 动态链接器(ld-linux-x86-64.so.2),并且包含 调试信息,未进行剥离操作。在运行时,会通过 动态链接器 来加载这些共享库。
将动态链接转为静态链接
有意思的是,Go 的 net 包提供了纯 Go 实现,这使得我们在编译时可以禁用 cgo。具体而言,可以通过指定构建标签或者设置 CGO_ENABLED=0 来彻底禁用 cgo。示例如下:
$ CGO_ENABLED=0 go build -o main2 main2.go
$ ldd main2
not a dynamic executable从上述输出结果能够证实,通过这两种方式操作后,最终都能得到一个静态二进制文件。
交叉编译
如文章开头所提到的,交叉编译是 Golang 的一大显著优势,它能够让代码在各种不同的平台与架构上运行。以下是一些常见的交叉编译命令示例:
# 编译为 macOS 上的 amd64 架构
GOOS=darwin GOARCH=amd64 go build -o main2_darwin_amd64 main2.go
# 编译为 macOS 上的 arm64 架构
GOOS=darwin GOARCH=arm64 go build -o main2_darwin_arm64 main2.go
# 编译为 Linux 上的 amd64 架构
GOOS=linux GOARCH=amd64 go build -o main2_linux_amd64 main2.go
# 编译为 Linux 上的 arm64 架构
GOOS=linux GOARCH=arm64 go build -o main2_linux_arm64 main2.go
# 编译为 Linux 上的 386 架构 (32-bit)
GOOS=linux GOARCH=386 go build -o main2_linux_386 main2.g
...需要注意的是,前提是没有使用 CGO,一旦使用了 CGO,那么运行平台基本就会和开发平台绑定起来了。
减少二进制大小
或许你已经留意到,在上述 file 命令输出结果中出现了 “with debug_info not stripped” 这样的提示,这意味着二进制文件包含了调试信息。通常情况下,我们并不需要这些调试信息,将其删除能够有效减小二进制文件的大小,示例操作及结果如下:
$ CGO_ENABLED=0 go build -o main2 main.go
$ ll -h main2
-rwxr-xr-x 1 root root 2.9M 1月 5日 15:32 main2
$ file main2 | tr , '\n'
main2: ELF 64-bit LSB executable
x86-64
version 1 (SYSV)
statically linked
Go BuildID=...
with debug_info
not stripped
$ CGO_ENABLED=0 go build -ldflags "-s -w" -o main2 main.go
$ ll -h main2
-rwxr-xr-x 1 root root 2.0M 1月 5日 15:33 main2
$ file main2 | tr , '\n'
main2: ELF 64-bit LSB executable
x86-64
version 1 (SYSV)
statically linked
Go BuildID=...
stripped结语
静态链接和动态链接各有千秋,根据自身的业务选择合适的方案即可。
File: content/posts/golang-unmarshal-vuln.md
+++
date = '2025-06-23T22:00:00+08:00'
draft = true
title = 'Golang Json 反序列化漏洞'
+++
众所周知,Java 的 f***json 反序列化漏洞养活了国内大半的安全从业人员。
作为一个前 Java 现 Go选手,Go 中容易写出来的安全漏洞的代码我也搞过几十个案例,最近我又在网络上发现了一个 Go 中 json 反序列化的非预期行为可以绕过身份验证、规避授权控制的案例。
举个例子。
假设 用户 结构体如下
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"-,omitempty"`
}我们希望在用户注册的接口,反序列化的时候忽略 IsAdmin 这个字段,将会默认设置为 false。
不懂 Go 语言的也没关系,让AI来解释一下。
让我们来写一段代码来模拟注册用户的逻辑。
package main
import (
"encoding/json"
"fmt" "log"
)
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"-,omitempty"`
}
func register() {
// 模拟用户提交的 JSON 字符串
var input = `{
"username": "dushixiang",
"password": "password",
"-": true
}`
// 解析JSON字符串到User结构体
var user User
err := json.Unmarshal([]byte(input), &user)
if err != nil {
log.Fatal(err)
}
// 模拟保存用户到数据库
// ...
fmt.Printf("%+v", user)
}
func main() {
register()
}您猜怎么着?这段代码将会输出
{Username:dushixiang Password:password IsAdmin:true}你问我这怎么了?
用户直接注册成你系统的管理员了,你还问我怎么了。
你问我这是什么问题导致的?
背后的原因让人暖心,AI 说对了一半。
- 和 omitempty 确实会冲突,但是结果是 omitempty 会让 - 作为 IsAdmin 字段的 json key 参与反序列化,最终导致了 IsAdmin 被用户提交的内容赋值了。
解决方案也很简单,把 ,omitempty 移除就好了。
经过其他大佬的指点,实际上是 json: "-" 和 json: "-," 的行为不一致,加上逗号就是把 - 当作 json key。
解决方案也很简单,把 , 移除就好了。
File: content/posts/good-system-design-summary.md
title: 《关于优秀系统设计我所知道的一切》的总结 date: 2025-08-22T13:15:30+08:00 draft: false
今天看到 GitHub 高级工程师写的一篇文章《Everything I know about good system design》,写的很好。
链接地址:
https://www.seangoedecke.com/good-system-design/
总结一下。
真正优秀的系统设计是“不起眼的”(underwhelming)和简单的。它追求的是长期稳定运行,而不是使用各种时髦、复杂的技术。一个看起来令人印象深刻的复杂系统,往往掩盖了糟糕的底层决策。
核心设计哲学
- 好的设计是无趣的:当你感觉某个系统“比预想的简单”或“从来不用操心”时,这恰恰是好设计的标志。
- 从简单开始:一个能正常工作的复杂系统,总是从一个能正常工作的简单系统演化而来的。直接从零开始构建复杂系统是个坏主意。
状态管理 (State)
- 状态是万恶之源:软件设计中最难的部分就是管理状态(State)。无状态(Stateless)的服务更容易维护,因为出问题时可以直接重启恢复。
- 集中管理状态:尽量减少有状态的组件。最佳实践是让一个专门的服务与数据库交互来管理状态,而其他服务通过 API 请求或事件与该服务通信,而不是让多个服务都去读写同一个数据库表。
核心组件与模式
数据库 (Database)
- 数据库访问通常是性能瓶颈。
- Schema 设计要在灵活性和可读性之间找到平衡。
- 为常用查询创建索引 (Index),并将高基数(high-cardinality)的字段放在前面。
- 尽可能让数据库完成工作(如使用
JOIN),而不是在应用内存中处理数据。 - 将读请求分发到只读副本(read-replicas)以减轻主库压力。
处理慢操作:后台作业 (Background Jobs)
- 面向用户的操作必须快速响应(几百毫秒内)。
- 耗时长的任务(如转换一个大文件)应该放入后台作业异步处理。这通常通过一个队列(Queue)和作业执行器(Job Runner)实现。
缓存 (Caching)
- 缓存是解决昂贵重复计算的经典方案,但它本质上也是一种状态,会引入复杂性(如缓存失效、数据不一致)。
- 高级工程师倾向于尽可能少地使用缓存。在使用缓存前,应首先尝试从根本上优化性能(例如,为慢查询添加索引)。
事件驱动 (Events)
- 事件中心(如 Kafka)用于实现服务解耦。一个服务发布“某事发生”的事件,多个下游服务可以消费该事件并各自处理。
- 适用于发送方不关心消费者行为的场景。不要过度使用,很多时候直接的 API 调用更简单、更易于追踪。
系统运维与容错
关注关键路径 (Hot Paths)
- 设计时应集中精力处理系统中最重要和流量最大的部分。这些路径的设计方案选择更少,且一旦出错影响巨大。
日志与监控 (Logging & Metrics)
- 在异常路径(unhappy path)中积极记录日志,以便于排查问题。
- 除了平均响应时间,还应监控 p95 和 p99 分位值,因为这能反映你最大、最重要客户的真实体验。
容错设计 (Failing Gracefully)
- 重试 (Retries):不能盲目重试,可能会压垮下游服务。写操作需要使用“幂等键 (idempotency key)”来防止重复执行。
- 故障开放/关闭 (Fail Open/Closed):必须明确组件故障时的行为。例如,限流系统应“故障开放”(允许请求通过),而认证系统则必须“故障关闭”(拒绝访问)。
结论
好的系统设计不是关于使用各种“聪明技巧”,而是关于知道如何将那些无聊、经过充分测试的组件用在正确的地方。就像好的管道工一样,如果你在工作中寻求刺激,最后很可能会搞得一团糟。
File: content/posts/how-does-java-make-methods-execute-only-once.md
title: "在 Java 里如何让方法只执行一次?" categories: [ "Java" ] tags: ["Java","并发"] draft: false slug: "how-does-java-make-methods-execute-only-once" date: "2023-04-02T13:18:00+08:00"
最近一年时间一直在写 Golang ,也算是对 Golang 有了初步的掌握,再次写 Java 的时候发现有点生疏了,写代码的时候也不自觉代入了写 Golang 的思维。
正如我想要在 Java 里面想让某一个方法只执行一次的时候,我第一时间想到了 Golang 里面的 Once 功能。
sync.Once是 Golang 的一个并发原语,它提供了一种安全地在多个 goroutine 中执行某个函数(或代码块)一次的机制。
sync.Once类型有一个Do方法,该方法接收一个函数作为参数,并确保这个函数只会被执行一次,无论有多少个 goroutine 同时调用它。具体来说,第一个调用 Do 方法的 goroutine 会执行这个函数,而其他 goroutine 则会等待它完成,然后返回相同的结果。
sync.Once可以用于一些需要全局初始化的场景,比如初始化配置信息、数据库连接等。使用 sync.Once 可以确保这些初始化只会被执行一次,并且可以安全地被多个 goroutine 共享使用。-- 来自 ChatGPT
其实和单例模式差不多,但我想要的是让方法只执行一次,我魔改了一下,直接上代码吧。
package cn.typesafe.sync;
import lombok.SneakyThrows;
import java.util.concurrent.Callable;
public class Once<T> {
private volatile T t = null;
@SneakyThrows
public T doOnce(Callable<T> action) {
if (t == null) {
synchronized (this) {
if (t == null) {
t = action.call();
}
}
}
return t;
}
}单元测试
package cn.typesafe.sync;
import cn.typesafe.sync.Once;
import org.junit.jupiter.api.Test;
import org.wildfly.common.Assert;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class OnceTest {
@Test
public void onceTest() {
Once once = new Once<>();
AtomicInteger count = new AtomicInteger(0);
Integer value1 = once.doOnce(count::incrementAndGet);
Assert.assertTrue(value1.equals(1));
Integer value2 = once.doOnce(count::incrementAndGet);
Assert.assertTrue(value2.equals(1));
}
@Test
public void onceConcurrentTest() throws InterruptedException {
Once once = new Once<>();
AtomicInteger count = new AtomicInteger(0);
int threadCount = Runtime.getRuntime().availableProcessors() * 2;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.execute(() -> {
Integer value1 = once.doOnce(count::incrementAndGet);
Assert.assertTrue(value1.equals(1));
countDownLatch.countDown();
});
}
countDownLatch.await();
}
}