LICENSE Documentation

Official TypeScript developer resources and documentation for dushixiang/LICENSE. Verified technical context for AI.

0 stars TypeScript TypeScript 1 file · ~64,239 tokens
RAW Doc

1. Project Overview & Quickstart (dushixiang/LICENSE)

LICENSE

Open-source repository dushixiang/LICENSE

Repository Details

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 库来演示:

go
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))
}

运行结果:

shell
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

bash
yum install -y clamav*

配置 ClamAV

bash
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

更新病毒库

大概需要几分钟时间

bash
freshclam

进行病毒扫描测试

bash
clamscan -ri

常用配置说明

text
--recursive[=yes/no(*)]     -r      递归查找
--infected                  -I      只打印受影响的文件信息
--remove[=yes/no(*)]                删除受影响的文件。(不建议使用,根据扫描结果进行手动删除,避免误删。)

配置邮箱

如果不需要邮件通知的可以忽略此步骤。

安装邮件服务

bash
yum install -y mailx

修改配置

修改大写字母为你的邮箱配置

bash
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

发送邮件进行测试

bash
echo "Your message" | mail -v -s "Message Subject" email@address

如果出现 “Error in certificate: Peer's certificate issuer is not recognized” 属于正常。

定期扫描

使用 crontab -e 增加两个定时任务

bash
# 每天凌晨 2 点半进行病毒库更新
30 2 * * * /bin/freshclam
# 每天凌晨 3 点半进行病毒扫描
30 3 * * * /bin/clamscan -ri  | mail -s "病毒文件扫描报告" 'youremailaddress'

如果不需要发送邮件使用下面的配置

bash
# 每天凌晨 2 点半进行病毒库更新
30 2 * * * /bin/freshclam
# 每天凌晨 3 点半进行病毒扫描
30 3 * * * /bin/clamscan -ri

File: 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使用率的接口,需要我们自己来计算,网上分享的代码或者公式五花八门,大部分都是错误的,经过我的测试,找到了一个相对准确的计算公式。

latex
cpu_usage = (cpu_time_now - cpu_time_t_second_ago) * 100 / (t * vCpus * 10^9)

Java代码如下

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代码如下

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代码如下

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代码如下

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:

附上几张截图


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” 程序,,经过打包后,它将成为一个静态链接的二进制文件。

go
package main  
  
func main() {  
    println("hello world!")  
}

接着编译这个 Go 文件:

bash
go build -o main1 main.go

然后使用 file 命令来检查文件类型:

bash
$ 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 能够帮助我们判断二进制文件是静态链接还是动态链接,执行以下命令:

bash
$ ldd main1
not a dynamic executable

动态链接程序

在 Golang 中,有一种名为 CGO 的机制,它可以用来调用 C 代码,就连 Go 的标准库也在使用这一机制,例如 net 包(https://pkg.go.dev/net),其就借助了 C 库来处理 DNS。以下是相关示例代码:

go
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 文件:

bash
go build -o main2 main2.go

之后,我们再次利用 file 和 ldd 程序来检查第二个二进制文件,操作及结果如下:

bash
$ 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
bash
$ 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。示例如下:

bash
$ CGO_ENABLED=0 go build -o main2 main2.go
$ ldd main2
not a dynamic executable

从上述输出结果能够证实,通过这两种方式操作后,最终都能得到一个静态二进制文件。

交叉编译

如文章开头所提到的,交叉编译是 Golang 的一大显著优势,它能够让代码在各种不同的平台与架构上运行。以下是一些常见的交叉编译命令示例:

bash
# 编译为 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” 这样的提示,这意味着二进制文件包含了调试信息。通常情况下,我们并不需要这些调试信息,将其删除能够有效减小二进制文件的大小,示例操作及结果如下:

bash
$ 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 反序列化的非预期行为可以绕过身份验证、规避授权控制的案例。

举个例子。

假设 用户 结构体如下

go
type User struct {  
    Username string `json:"username,omitempty"`  
    Password string `json:"password,omitempty"`  
    IsAdmin  bool   `json:"-,omitempty"`  
}

我们希望在用户注册的接口,反序列化的时候忽略 IsAdmin 这个字段,将会默认设置为 false。

不懂 Go 语言的也没关系,让AI来解释一下。

让我们来写一段代码来模拟注册用户的逻辑。

go
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()  
}

您猜怎么着?这段代码将会输出

shell
{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

其实和单例模式差不多,但我想要的是让方法只执行一次,我魔改了一下,直接上代码吧。

java
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;
	}
}

单元测试

java
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();
	}
}