## 1. Project Overview & Quickstart (dushixiang/README-en)

# README-en

Open-source repository dushixiang/README-en

### Repository Details
- **Repository:** [dushixiang/README-en](https://github.com/dushixiang/README-en)
- **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 库来演示：

```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](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  
```

常用配置说明

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

### 配置邮箱

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

#### 安装邮件服务

```bash
yum install -y mailx
```

#### 修改配置

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

```bash
cat > /etc/mail.rc <<EOF
set from=USERNAME@YOURDOMAIN.COM
set smtp=smtps://smtp.exmail.qq.com:465
set smtp-auth=login
set smtp-auth-user=USERNAME@YOURDOMAIN.COM
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：

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

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