private plot

应无所住 | 而生其心

引言

在业务系统的高并发读写压力下,合理架构数据库系统成为系统稳定性与可扩展性的关键。本文从 OLTP、OLAP、HTAP 三类架构模型出发,逐步探讨主流解决方案的适用场景,最终回归到底层数据结构和事务引擎的设计哲学。


一、OLTP、OLAP、HTAP 的关系与区别

OLTP(Online Transaction Processing)

  • 面向高并发、实时写入与读取。
  • 典型场景:订单系统、支付系统。
  • 特点:小事务、快速响应、数据一致性强。

OLAP(Online Analytical Processing)

  • 面向数据分析与多维查询,重读操作。
  • 典型场景:报表分析、BI 系统。
  • 特点:复杂查询、大数据量聚合、写入延迟可接受。

HTAP(Hybrid Transactional and Analytical Processing)

  • 目标:在同一系统中同时支持 OLTP 与 OLAP。
  • 代表系统:TiDB、SingleStore、Doris。
  • 难点:一套引擎既要保证事务一致性,又要提供分析性能,成本高、复杂度高。

总结:

架构 主要目的 数据延迟容忍 一致性要求 查询复杂度 应用类型
OLTP 快速写入与查询 简单 订单、库存系统
OLAP 多维分析 可容忍 弱/最终一致性 报表、BI分析
HTAP 统一处理混合负载 低至中等 中等至高 通用大数据平台

二、读写分离方案与数据分析架构

读写分离(MySQL 主从 + ShardingSphere-JDBC)

  • 主节点专职写,从节点负责读,通过代理层实现透明路由。
  • 适用场景:业务系统中读请求远大于写请求,提高系统吞吐量。
  • 特点:
    • 快速部署,技术成熟。
    • 只能提升读性能,写性能无法提升。
    • 一致性依赖 binlog 异步复制,存在数据延迟。

OLTP + ETL + OLAP 架构

  • ETL:周期性从 OLTP 拉取数据,清洗加工后写入数据仓库。
  • OLAP:在数据仓库中进行高复杂度查询与分析。
  • 优点:读写解耦,业务与分析隔离,灵活可扩展。
  • 缺点:数据同步延迟(分钟级、小时级),一致性难保证。

三、读写冲突的本质

读写冲突的根源是结构的共享与修改竞争

  • 读操作依赖于当前数据结构(如索引树),要求一致性。
  • 写操作修改数据结构(新增记录、变更索引节点)。
  • 写涉及加锁、日志、索引更新;读为了事务隔离也需要锁或快照。
  • 二者对相同数据结构的访问模式不同,引发冲突。

四、InnoDB 如何减少读写冲突:MVCC 机制

InnoDB 实现了多版本并发控制(MVCC)来减少锁冲突,关键字段:

  • trx_id:表示该版本由哪个事务创建。
  • roll_pointer:指向上一个旧版本的数据。
  • undo log:通过回滚指针形成历史版本链表。
  • 快照读(Snapshot Read):读取满足版本可见性的数据版本。
  • 当前读(Current Read):必须加锁,例如 SELECT ... FOR UPDATE

**可重复读(RR)**通过 MVCC 实现无需加锁的快照读取,兼顾一致性与并发性。


五、索引的结构设计:为什么选择 B+ 树

InnoDB 的索引(聚簇与辅助索引)采用 B+ 树实现,原因如下:

相比 B 树

  • 所有数据仅存在叶子节点,内节点只存储 key,占空间更小,分支因子更高,更适合磁盘存储。
  • 叶子节点之间通过链表连接,天然支持范围查询。

相比红黑树

  • B+ 树为多路平衡树,树高更低,磁盘访问更少
  • 红黑树为二叉树,在大数据量下树高过深,不适合磁盘场景

六、三种结构直观对比

特性 B 树 B+ 树 红黑树
数据存储位置 所有节点都可存 只在叶子节点存储数据 所有节点存储数据
查询效率 高效(叶子节点链表)
范围查询 最优 最差
适用场景 磁盘索引 数据库、文件系统索引 内存数据结构(TreeMap 等)

推荐阅读:


结语

读写冲突、索引设计、事务并发控制,这些数据库内部机制是我们构建健壮系统的基石。掌握这些原理不仅能设计出更高效的系统架构,也有助于我们在性能瓶颈中找到最优解法。

主结构

本文记录一次对 Spring Boot 项目中 Logback 配置的优化过程,围绕如下目标展开:

  • 控制台 + 文件双输出
  • INFO / WARN / ERROR 日志分级输出
  • 用户行为日志单独归档
  • 区分开发与生产环境的日志级别(dev 为 DEBUG,prod 为 WARN)
  • 日志格式统一、结构清晰,方便后期问题排查与行为分析

日志配置结构与关键实现

基础配置

1
2
<property name="log.path" value="/home/kairo/logs"/>
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/>

控制台输出

1
2
3
4
5
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>

分级输出文件:INFO/WARN 与 ERROR 拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<appender name="file_info" class="...RollingFileAppender">
<filter class="...LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>NEUTRAL</onMismatch>
</filter>
<filter class="...LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<appender name="file_error" class="...RollingFileAppender">
<filter class="...LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

用户行为日志单独输出

1
2
3
4
5
6
7
8
<appender name="sys-user" class="...RollingFileAppender">
<file>${log.path}/sys-user.log</file>
...
</appender>

<logger name="sys-user" level="INFO">
<appender-ref ref="sys-user"/>
</logger>

环境隔离配置(开发用 DEBUG,生产用 WARN)

1
2
3
4
5
6
<springProfile name="dev">
<logger name="com.kairo" level="DEBUG"/>
</springProfile>
<springProfile name="!dev">
<logger name="com.kairo" level="WARN"/>
</springProfile>

关键问题深度剖析

✅ 1. springProfile 不生效的排查逻辑

现象:在 dev 环境下指定 logger.level=DEBUG 无效。

原因与排查:

  • 检查 application.yml 中是否重复配置 logging.level,会覆盖 logback 的 logger 设置
  • **springProfile 标签仅作用于 <logger>,**不作用于 <root>
  • <springProfile> 标签不能嵌套在 <root> 外部,只能出现在 <logger> 外部,只能控制类或包级别日志,不控制 appender 或 root。

建议:使用 <springProfile> 时必须绑定 <logger>,用于区分环境中 logger 的级别控制。


✅ 2. root 与 logger 的职责区分:输出 vs 定位

  • <root> 是日志系统的兜底配置,当 logger 找不到匹配项时,将使用 root 的日志级别及输出配置。
  • <logger> 是针对特定类或包名(如 com.kairo)做更细粒度的日志级别控制。
  • 简单说:logger 控制日志“来源”,root 控制日志“去向”(默认路径和等级)

✅ 3. logger 优先级高于 root,合理组合精细化配置

  • 如果某个类或包使用了 <logger> 配置,则 root 的配置将不再生效
  • 仅当未指定 <logger> 时,root 才会起作用。
  • 合理做法是:关键业务模块用 logger 精细配置,其他模块用 root 兜底。

✅ 4. logger / root / appender + filter 三者关系

元素 控制内容 粒度 备注
logger 控制日志来源(类/包) 可环境隔离配置
root 控制日志兜底来源 不能用 springProfile 隔离
appender + filter 控制日志输出去向(文件/控制台)和级别 输出端 是最终的过滤逻辑

核心结论:最终能不能输出,还要通过 appender 的 LevelFilter 筛一遍,即使 logger 放行了,如果 filter 不放行也无法写出。


✅ 5. 环境隔离要基于 logger 做

  • 不能用 springProfile 控制 root 的输出级别
  • 需要将 logger 包名配置明确,并分别在 dev / prod 环境下赋予不同级别。
  • 建议约定所有业务日志均以统一包名开头,例如 com.kairo,方便统一配置。

附录(可选)

logback.xml配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="/home/kairo/logs"/>
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/>

<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>

<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:继续交由后续 filter 判断是否接收 -->
<onMismatch>NEUTRAL</onMismatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>WARN</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 用户访问日志输出 -->
<appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sys-user.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按天回滚 daily -->
<fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>


<!-- ============================== 输出渠道日志级别控制 ============================== -->

<!-- 根日志输出级别(“兜底”的默认级别控制器,仅对没有单独指定 logger 的类或包生效,配合appender里的filter使用) -->
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="file_info"/>
<appender-ref ref="file_error"/>
</root>
<!--系统用户操作日志(单独控制,记录登录等)-->
<logger name="sys-user" level="info">
<appender-ref ref="sys-user"/>
</logger>


<!-- ============================== 资源日志级别控制(<logger> 的级别优先于 <root>,控制的是包或类级别的日志输出) ============================== -->

<!-- 区分环境的包控制 -->
<!-- 开发环境,日志级别控制 -->
<springProfile name="dev">
<logger name="com.kairo" level="DEBUG"/>
</springProfile>
<!-- 其他环境,日志级别为 warn -->
<springProfile name="!dev">
<logger name="com.kairo" level="WARN"/>
</springProfile>

<!-- 不区分环境的包控制 -->
<!-- framework日志控制 -->
<logger name="org.springframework" level="WARN"/>
<logger name="springfox.documentation.spring.web" level="WARN"/>
<logger name="org.activiti" level="WARN"/>
<logger name="org.quartz" level="WARN"/>
<!-- 特定业务日志精细控制 -->
<logger name="com.kairo.oms" level="INFO"/>
<logger name="com.kairo.stat" level="INFO"/>

</configuration>

日志输出样例

1
2
12:01:23.456 [main] INFO  c.z.s.controller.LoginController - [login,42] - 用户登录成功,userId=123
12:01:25.789 [main] ERROR c.z.s.service.OrderService - [submitOrder,88] - 下单失败:库存不足

推荐日志目录结构

1
2
3
4
/home/kairo/logs/
├── sys-info.2025-05-17.log
├── sys-error.2025-05-17.log
├── sys-user.2025-05-17.log

总结

本次 Logback 优化实现了以下能力:

  • 分级、分模块、分环境的日志输出
  • 控制台与文件并行输出
  • 可扩展性强,支持后续 JSON 格式或 ELK 接入
  • 易于维护与排错,结构清晰

特别注意多个配置项之间的交互逻辑,掌握“logger 控源,filter 控流,root 兜底”的思维模型,有助于写出稳定、灵活的日志配置方案。

背景

在项目接口设计过程中,团队成员对于接口 URL 命名风格存在分歧:是否应采用 camelCase(驼峰命名)?对此展开了讨论。


结论

接口 URL 不应使用驼峰命名(camelCase),推荐统一采用 小写 + 中划线(kebab-case) 风格,或根据项目团队的规范选择使用下划线(snake_case),但应保持一致性。


理由分析

1. URL 是大小写敏感的

  • /userInfo/userinfo 被视为不同资源,易引发错误。
  • 驼峰风格加剧了大小写混淆的风险。

2. REST 社区与业界共识

  • 主流 RESTful API 设计(GitHub、Stripe、Twitter 等)均采用 kebab-case。
  • 阿里巴巴《Java 开发手册》、Google API 设计文档均推荐使用中划线分词。

3. 可读性更优

  • user-infouserInfo 更直观显示单词边界。
  • 相比下划线 _,中划线 - 在浏览器中的可视效果更明显、更美观。

4. SEO 更友好

  • 搜索引擎(如 Google)明确推荐使用中划线分隔词,避免使用下划线。
  • kebab-case 能被识别为多个关键词,有利于网页检索。

5. 为什么不推荐使用下划线(snake_case)

虽然下划线在数据库字段命名中很常见,但在 URL 中使用存在如下问题:

  • 不利于 SEO
    Google 明确表示:中划线被视为“词边界”,下划线不会;即 /user-info 会被当作两个词,而 /user_info 会被当作一个词。

  • 可读性差
    下划线在链接中经常被渲染为“不可见”或与底线混淆(尤其在下划线带超链接时),不如中划线易于识别。

  • 不符合社区主流
    主流框架和 API 设计规范文档均推荐使用 kebab-case,而非 snake_case。

6. 阿里巴巴 Java 开发手册的推荐

根据阿里巴巴的 Java 开发手册(《中册》)规定:

  • URL 地址命名风格推荐使用下划线分隔单词,并且全部小写。
  • 示例:/query_user_info/register_device_token

阿里规范与社区主流差异

  • 阿里规范:推荐使用下划线(snake_case)。
  • RESTful API 规范:社区主流(例如 GitHub、Stripe)推荐使用中划线(kebab-case)。

这两者的差异反映了不同技术社区和团队的偏好,而在具体项目中,选择遵循哪种规范应考虑团队需求及一致性。

7. 统一风格提升可维护性

为了避免不同风格的混杂,建议根据项目需求选择一种命名风格,并在整个项目中保持一致。

场景 命名风格
URL 路径 kebab-case / snake_case(根据项目规范选择)
JSON 字段名 camelCase
数据库字段 snake_case
常量/枚举 UPPER_CASE

不推荐的命名风格比较

风格 是否推荐 原因简述
camelCase ❌ 不推荐 易混淆、大小写敏感
snake_case ✅ 推荐(阿里规范) 适用于数据库,但对 URL 不太友好
kebab-case ✅ 推荐(REST 社区主流) SEO 友好、可读性好、符合行业趋势

推荐阅读与参考资料


总结

URL 是接口设计中面向外部的重要部分,命名风格应符合通用标准。为了减少歧义、提升可维护性、增强可读性,推荐统一使用 kebab-case 风格定义接口 URL,或者根据团队的具体要求选择下划线(snake_case)风格,但需保持一致性,避免混合使用。

引言

Jeff Dean 是 Google 历史上最具影响力的工程师之一。他不仅是多个关键基础设施的第一作者或领导者,也长期主导 Google 的技术发展方向。从早年的分布式系统,到后来的机器学习框架与深度模型架构,他的工作体系贯穿了计算范式的两次重大转变。

本文将围绕 Jeff Dean 所参与的几篇关键论文,梳理其背后的技术体系和影响轨迹。


一、大数据时代:分布式计算基础设施的建立

1. The Google File System (2003)

  • 作者:Sanjay Ghemawat, Howard Gobioff, Jeff Dean
  • 贡献:首次系统性构建适用于廉价硬件的大规模分布式文件系统,为 Google 内部所有高吞吐服务提供了存储支撑。
  • 技术亮点:Chunk-based 存储、主控节点简化调度、自动副本管理容错。

2. MapReduce: Simplified Data Processing on Large Clusters (2004)

  • 作者:Jeff Dean, Sanjay Ghemawat
  • 贡献:提出了通用的分布式计算模型,将大规模数据处理抽象为 Map/Reduce 两个阶段。
  • 影响:成为 Hadoop 等系统的灵感来源,极大降低了分布式编程门槛。

3. Bigtable: A Distributed Storage System for Structured Data (2006)

  • 作者:Fay Chang, Jeffrey Dean 等
  • 贡献:构建高可扩展性的结构化存储系统,支撑搜索、地图、广告等核心服务。
  • 技术亮点:SSTable、Chubby、列式存储、自动分区与负载均衡。

Jeff Dean 亲自参与并主导了 Google 构建分布式计算三大支柱(GFS、MapReduce、Bigtable),确立了技术基础框架,影响了后来的云计算生态。


二、AI 时代:深度学习平台与模型架构的兴起

4. TensorFlow: Large-Scale Machine Learning on Heterogeneous Systems (2015)

  • 作者:Martín Abadi, Jeff Dean(主导 Google Brain)
  • 贡献:统一计算图、自动求导、多设备分布式训练,开启深度学习工业化。
  • 亮点:灵活图结构、支持 CPU/GPU/TPU、生态繁荣。

5. Attention is All You Need (2017)

  • 作者:Ashish Vaswani, Noam Shazeer, Jakob Uszkoreit 等(Google Brain/Research)
  • 背景:Transformer 模型的提出催生了 BERT、GPT、T5 等大语言模型。
  • Jeff Dean 的角色:当时领导 Google Brain,指导性推动 Transformer 等架构探索,形成从框架到模型的协同研发文化。

三、Jeff Dean 的工作体系特征

1. 系统思维主导的工程设计

  • 将“抽象”与“工程实现”深度结合,先后建立了 Google 的存储系统、计算框架、机器学习平台。
  • 每个系统都强调:扩展性、容错性、易用性

2. 注重论文产出与开源结合

  • 其工作不只是内部系统设计,还特别注重通过论文、白皮书、开源推动学术与工业的互动。
  • TensorFlow 即是科研与工程结合的代表产物。

3. 技术演进中的桥梁角色

  • 从 C++ 系统级工程,到 Go/TPU/AI 平台,Jeff Dean 始终是 Google 技术变迁中承上启下的枢纽人物。
  • 他倡导工程师参与研究、研究者深入工程,打造跨界团队。

四、小结:一个技术体系的缩影

从 GFS 到 TensorFlow,从 MapReduce 到 Transformer,Jeff Dean 的工作体系不仅见证,也构建了 Google 技术体系的脊梁。这些工作推动了两个时代的技术浪潮:

  • 分布式计算 → 云平台生态
  • 神经网络训练 → 通用 AI 架构

Jeff Dean 本人始终强调**“将抽象转化为生产力”**,这也是 Google 技术创新的一以贯之之道。


附录:涉及论文与链接一览

序号 论文标题 链接 发表年份
1 The Google File System 链接 2003
2 MapReduce: Simplified Data Processing on Large Clusters 链接 2004
3 Bigtable: A Distributed Storage System for Structured Data 链接 2006
4 TensorFlow: Large-Scale Machine Learning on Heterogeneous Systems 链接 2015
5 Attention is All You Need 链接 2017

在日常 Java 后台开发中,良好的命名规范能显著提升代码的可读性、可维护性与协作效率。本文基于实践经验,总结出一套命名规范模板,适用于典型的 Web 后端开发场景。

🌟 命名原则

  • 语义清晰:命名要能准确表达其所代表的实体或行为。
  • 统一风格:统一使用驼峰命名(CamelCase),类名首字母大写,变量/方法名首字母小写。
  • 简洁优雅:避免冗长,拒绝无意义缩写。
  • 领域驱动:以业务语言命名,贴合领域模型。

📦 包名规范

1
com.company.project.module

示例:

1
com.acme.warehouse.outbound // 出库模块

🔠 类名规范

类型 命名后缀 示例
实体类 Entity OrderEntity, UserEntity
DTO类 DTO UserDTO, CreateOrderDTO
VO类 VO UserVO, TabVO
参数类 Param, Request LoginRequest, QueryOrderParam
返参类 Response, DO QueryResultResponse, OrderDO
接口类 以功能命名 OrderService
实现类 Impl OrderServiceImpl
Mapper接口 Mapper UserMapper
枚举类 Enum OrderStatusEnum

🧩 方法命名规范

增删改查

功能 方法名前缀 示例
新增 create / add createOrder()
查询 get / list / query getOrderById() / listUsers()
更新 update / refresh / modify updateUserProfile()
删除 delete / remove deleteOrderById()

业务行为

  • 尽量使用动宾结构,如 shipOrder()cancelOrder()
  • 特殊行为用动词短语,如 rollbackInventory()refreshStatusAfterShipping()

🔧 变量命名规范

类型 命名风格 示例
基本类型 简短语义 count, totalAmount
对象实例 实体名小写 order, userDTO
集合 复数形式 orders, userList
布尔值 is, has, can 前缀 isValid, hasPermission

📄 控制器规范

  • 控制器类名:xxxController
  • 路由方法名建议语义化,如:
1
2
@PostMapping("/create")
public AjaxResult createOutboundOrder(@RequestBody OutboundOrderDTO dto)

🧱 DAO 层命名与参数返回规范(重点)

DAO 层是数据持久化访问层,命名与参数结构需统一规范,避免 VO/DTO 混用、职责模糊等问题。

🔹 入参规范

场景 入参类型 示例
基础查询 MyBatis-Plus 自动封装 selectById(id)
复杂查询 QueryParam / QueryRequest QueryOrderParam

不再使用 PO 作为术语或命名后缀,基础持久化逻辑由 MyBatis-Plus 提供,Entity 即可满足结构映射需求。

🔹 返回值规范

场景 返回类型 示例 说明
基础查询 Entity UserEntity, OrderEntity 由 MyBatis-Plus 提供,直接返回数据库字段映射结构
复杂查询 DO(Data Object) OrderDetailDO, WarehouseDO 用于多表联查、聚合字段等,结构不直接绑定数据库表
复杂查询 QueryResponse OrderQueryResponse, UserQueryResponse 表达请求响应对称性,结构更贴近业务语义

✅ DO 全称解释

  • DOData Object
  • 意指:数据库查询返回的只读结构体,通常用于多表结果、聚合字段等
  • 与 Entity 的职责分离,不与数据库直接绑定

❌ 不推荐

  • ❌ 使用 VO 作为 DAO 返回结构:职责在于视图展示,不应参与数据访问层逻辑
  • ❌ 使用 DTO 作为 DAO 返回结构:DTO 是服务间传输结构,非查询结果结构

🔁 Service 层转换职责(重点)

  • DTO:用作接口入参、插入/更新结构化输入
  • VO:作为返回结构
  • DTO -> DAO层的QueryParam和QueryResponse的转换
  • DAO层的DO或QueryResponse -> VO的转换
  • ❗ Service 层仅负责业务逻辑组织,不应承载转换职责过多,建议结构拆分清晰。可使用 MapStruct / BeanUtil 等工具进行结构转换

💬 总结建议

  • 明确每层职责,结构命名与使用语义清晰区分
  • 接口层结构:入参DTO、返参VO
  • Service层结构:入参DTO,返参VO
  • DAO层基础持久化结构:Entity(建议不加Entity后缀,与表名一致),基于MyBatis-Plus
  • DAO层查询结构:入参QueryParam/QueryRequest,返参DO/QueryResponse

命名即抽象,抽象即设计。持续迭代你的命名风格,它反映了你的认知深度和系统设计水平。

欢迎在评论区补充你常用的命名模式或反面案例!

引言

随着容器化技术的普及,Docker 成为现代开发中非常流行的工具。它提供了轻量级的虚拟化解决方案,帮助开发者提高应用的可移植性和扩展性。但是,Docker 并不是适合所有场景。在选择是否将应用容器化时,需要仔细考虑应用的特性、部署需求以及管理复杂度。本文将深入探讨 Docker 的使用场景,分析哪些应用适合使用 Docker,哪些应用可能会带来不必要的复杂性。

1. Docker 的基础概念与应用场景

首先,我们简要回顾 Docker 的基础概念:

  • Docker 镜像:一种轻量级、可执行的软件包,包含了运行某个应用所需的所有代码、运行时、库和依赖。
  • Docker 容器:镜像的运行实例,它是隔离的、快速的,并且可以在不同环境中运行。
  • 数据持久化:通过 -v 参数将宿主机的目录挂载到容器中,从而实现数据的持久化存储。

在容器化应用时,我们通常需要考虑以下几个方面:

  • 可移植性:Docker 提供了一致的运行环境,可以确保应用在任何支持 Docker 的平台上都能以相同方式运行。
  • 资源隔离与弹性扩展:容器能够在同一台物理服务器上运行多个应用,并能灵活地调整资源配额。
  • 开发与部署流程:容器化应用可以方便地与 CI/CD 流程结合,自动化部署和测试。

2. 使用 Docker 时的考虑因素

虽然 Docker 在很多场景下都能带来便利,但它并不是所有应用的最佳选择。以下是需要重点考虑的几个因素:

2.1 是否需要持久化数据?

Docker 中的容器默认是临时的,容器停止或删除后,容器内的数据也会丢失。因此,对于需要持久化数据的应用(如数据库应用),我们需要使用 卷(Volume)绑定挂载(Bind Mount) 来持久化数据。

  • 最佳实践
    • 对于需要持久化数据的应用,如数据库、缓存服务等,可以通过 -v 参数将宿主机的目录挂载到容器内,确保数据在容器生命周期之外持久保存。
    • 如果数据量较大,建议使用外部存储解决方案来避免存储在容器内。

2.2 是否需要频繁更新或重建镜像?

对于一些依赖频繁更新的外部资源(如机器学习模型、第三方库等)的应用,每次修改或重建镜像时都可能丧失这些资源,这样就会增加不必要的管理复杂性。

  • 最佳实践
    • 对于需要频繁更新的文件或数据,建议将它们存储在宿主机上,并通过 -v 参数挂载到容器中,这样可以避免每次重新构建镜像时重复下载和复制这些文件。

2.3 管理与运维复杂性

Docker 通过将应用及其依赖封装到容器中,提供了高度的隔离性和一致性。然而,这种隔离性也可能增加管理和运维的复杂性。例如,容器的日志管理、网络配置、数据卷管理等都需要额外的配置和监控。

  • 最佳实践
    • 使用 Docker ComposeKubernetes 来管理和编排多个容器,特别是在涉及多容器服务(如数据库、后端服务和前端服务)时。
    • 通过 PrometheusGrafana 等工具来监控 Docker 容器,确保容器的健康状态和资源使用情况。

3. Docker 使用的常见场景

3.1 应用服务的隔离与弹性扩展

Docker 非常适合用于需要服务隔离并且具有弹性扩展要求的应用。例如,微服务架构中的每个微服务都可以通过独立的容器运行,并在需要时动态扩容。

  • 最佳实践
    • 使用 Docker Compose 来管理多容器应用,确保容器之间的网络、存储和其他配置可以统一管理。
    • 对于需要弹性扩展的服务,使用 Kubernetes 来编排容器,自动化扩展和负载均衡。

3.2 CI/CD 流程中的自动化部署

在持续集成/持续交付(CI/CD)流程中,Docker 镜像可以帮助开发团队以一致的环境进行开发、测试和生产部署。通过容器化,开发者能够轻松地复制和迁移应用。

  • 最佳实践
    • 使用 Docker Hub 或私有镜像仓库来存储构建好的 Docker 镜像,并通过 CI/CD 工具(如 GitLab CIJenkins)进行自动化部署。
    • 结合 Docker Compose,在 CI 流程中自动构建和测试容器化应用。

3.3 不需要持久化数据的应用

如果你的应用不需要长期存储数据或只在容器生命周期内存储临时数据,那么 Docker 是一个理想的选择。例如,用于临时计算的应用或一次性运行的任务。

  • 最佳实践
    • 这类应用可以直接运行在 Docker 容器中,并且可以随时销毁,减少运维成本。
    • 使用容器自动化运行短期任务(如批处理任务、定时任务等)。

4. 为什么不一定所有应用都适合 Docker?

虽然 Docker 具有很多优点,但在一些场景下,它可能会增加不必要的复杂性。例如:

  • 简单的桌面应用或开发环境:这些应用不需要 Docker 的隔离性和资源管理,直接安装和运行即可。
  • 数据库服务:虽然 Docker 可以容器化数据库服务,但对于需要高性能和稳定性的生产数据库,直接在宿主机上安装数据库可能更合适。
  • 频繁更新的文件或数据:如果应用中有频繁变化的文件或数据,Docker 容器可能会带来额外的管理负担。

5. 结论:是否使用 Docker?

在选择是否使用 Docker 时,开发者需要根据应用的特点来权衡 Docker 的优劣:

  • 适合使用 Docker 的场景

    • 微服务架构
    • CI/CD 自动化部署
    • 快速部署和移植
    • 临时性、轻量级的应用
  • 不适合使用 Docker 的场景

    • 对性能要求极高的数据库应用
    • 不需要隔离和快速扩展的单体应用
    • 频繁更新和变化的数据存储需求

6. Docker 的最佳实践与总结

总结下来,Docker 是一个非常强大的工具,但并非所有场景都适合使用它。对于需要高可用、高性能、持久化存储以及频繁更新的应用,Docker 可能并不是最佳选择。而对于需要快速部署、隔离服务、跨平台运行、以及支持弹性扩展的应用,Docker 提供了显著的优势。

在实际开发中,选择 Docker 还是其他解决方案,应该根据具体需求进行评估,避免在不合适的场景下使用 Docker 带来额外的复杂性和管理负担。通过结合 最佳实践开发规范,我们可以更好地利用 Docker,提升开发效率和运维质量。

引言

Ollama 是一个功能强大的本地推理模型运行框架,支持快速部署和高效调用各种大语言模型。我最初在 Windows 本机上直接安装并运行 Ollama,加载了 Gemma 3 1B 模型,整个过程非常顺利。然而,在部署 Open WebUI 时,我注意到其官方脚本中已经集成了 Ollama 的 Docker 启动逻辑,于是产生了一个想法:既然 Docker 应用列表也有 Ollama 镜像,是否可以把它也一并容器化,方便统一管理?

Docker 的隔离性、迁移便利性和配置一致性显然是它的优势。但从本机切换到 Docker 的过程中,我也遇到了一些不小的坑。本文将记录这一过程中的关键问题与解决方案,帮助你更好地理解在 Ollama 这类工具中使用 Docker 的适用性。

1. 切换到 Docker 部署 Ollama

为了更好地管理环境与数据持久化,我尝试将 Ollama 容器化。但过程中暴露了一些潜在问题,尤其是网络访问与模型数据的持久保存。

1.1 问题:容器网络访问失败

初次启动容器后,发现外部无法访问 Ollama 的 HTTP 接口,排查发现是端口映射和网络配置问题。

  • 解决方案:确认端口映射正确,并添加 host.docker.internal 到容器的 hosts 配置。

示例命令如下:

1
2
3
4
bash
docker run -d -p 11434:11434 \
--add-host=host.docker.internal:host-gateway \
--name ollama ghcr.io/ollama/ollama:latest

1.2 问题:容器内模型数据丢失

下载好的模型,在镜像重新构建后,容器内使用 ollama list 命令查看时为空。

  • 解决方案:问题出在模型未被持久化到宿主机。应使用 -v 参数将宿主机目录挂载到容器内,确保模型数据可持久保存。

示例命令:

1
2
3
4
bash
docker run -d -p 11434:11434 \
-v /path/to/models:/root/.ollama \
--name ollama ghcr.io/ollama/ollama:latest

注意:Ollama 默认将模型存储在 /root/.ollama,如果你想保留容器之间的模型状态,挂载路径必须一致。

2. Docker 部署 Ollama 的优势与挑战

2.1 优势:理论大于实践

从理论上讲,Docker 提供的隔离性、环境一致性、易迁移等优点,对需要跨平台部署或团队协作的项目非常有帮助。但 Ollama 本质上更像一种本地基础设施,它不太需要频繁迁移或多端部署。因此,Docker 在此场景下的优势难以显现。

2.2 挑战:数据持久化与运维成本上升

容器本身是无状态的,任何模型数据如果未通过挂载方式保存,将在容器销毁后丢失。同时,每次镜像更新、容器重建等操作也会带来额外的同步和配置成本。这要求用户对 Docker 的挂载机制、生命周期管理有一定掌握。

3. 总结与建议

将 Ollama 从本地迁移到 Docker 是一个“工程洁癖”式的优化实践,但对 Ollama 这种低变更、重本地场景而言,并不划算,甚至可以说是“为了用 Docker 而用 Docker”。

3.1 最佳实践建议

  • 持久化模型数据:使用 -v 参数挂载宿主机目录至容器内 /root/.ollama,确保模型持久保存。
  • 网络配置合理化:务必确认端口映射正确,并根据实际需要配置 host.docker.internal
  • 生命周期管理清晰:建议使用 Docker Compose 或构建持久容器策略,避免模型数据在重建时丢失。

3.2 是否值得使用 Docker?

如果你计划将 Ollama 集成到一整套基于 Docker 的工具链中(如 Open WebUI),并有能力处理数据挂载与容器管理问题,那么容器化是值得的;但如果你只是单机使用、模型少变,直接本地运行反而更简单、高效。


结语:不是所有工具都适合上 Docker,关键在于它是否真的解决了你的问题,而不是制造了更多麻烦。

微软 BitNet b1.58 模型和它的 bitnet.cpp 推理引擎最近在社区中引起了关注。作为一个可以运行在纯 CPU 上、显著降低内存和计算需求的模型实现,它让我们重新审视:在追求大模型能力的主流路径之外,还有没有另一种“极简 AI”的可能?

本文将从架构、性能、生态、使用场景几个角度,系统比较 bitnet.cpp 与当前主流模型格式 GGUF(Gemma、Mistral、Phi 等),并结合实际部署和应用需求给出选择建议。


🧠 一、BitNet b1.58 简介

微软在 2024 年发布的 BitNet 系列模型采用了一种创新的 1-bit Transformer 架构:

  • 权重仅为三值(-1、0、+1)
  • 计算核心为三值矩阵乘法(极大压缩计算资源)
  • 最新的 BitNet b1.58 2B4T 模型在 GSM8K(数学推理)任务中达到了 58.38% 的准确率,在小模型中表现突出
  • 官方提供 C++ 实现的推理引擎 bitnet.cpp,可在纯 CPU 环境中运行,内存占用仅 0.4GB

🎯 目标用户:边缘设备 / 低功耗场景 / 资源受限设备


🌐 二、GGUF 模型生态概览

GGUF 是由 llama.cpp 推出的新一代模型格式,现已成为主流开源模型的标准格式,支持:

  • Mistral 7B / Mixtral
  • Gemma 2B / 7B
  • Phi-2 / LLaMA-2 / Qwen 系列
  • 丰富的量化方案(Q4_K_M、Q5_K_M、Q8 等)
  • 完整生态支持:llama.cpp / Ollama / LM Studio / webUI / KoboldCpp

🎯 目标用户:个人 AI 助手、私有部署、离线对话、代码补全、企业接入模型服务


⚖️ 三、核心对比:BitNet b1.58 vs Gemma 2B

维度 BitNet b1.58 2B4T Gemma 2B
参数量 2B(1-bit 权重) 2B(全精度/INT4)
推理引擎 ✅ bitnet.cpp(CPU) ✅ GGUF + llama.cpp / Ollama
GSM8K(数学推理) ✅ 58.4(小模型顶级) ⬇️ 45.6 左右
MMLU(通识能力) ⬇️ 中等偏弱 ✅ 更好
模型输出流畅性 ⬇️ 偏生硬 ✅ 更自然
是否支持 Ollama ❌ 不支持 ✅ 完美支持
是否支持微调 / Embedding ❌ 暂不支持 ✅ 全面支持
部署门槛 ✅ 极低(仅需 CPU) ⬆️ 需 4GB+ RAM,CPU/GPU 皆可
场景适配性 ✅ 边缘设备 ✅ 本地助手、轻量部署、研发测试

🛠 四、实际部署体验

✅ BitNet.cpp 部署体验

  • 拉取源码编译简单,CMake + g++ 即可编译
  • 无需额外依赖,真正的“裸跑”
  • 内存占用极低(启动仅几百 MB)
  • 启动响应快,适合硬件受限场景
  • 不支持多轮上下文、无法接入现有生态(如 Ollama、LangChain)

✅ Gemma 2B (GGUF) 部署体验

  • 可直接用 ollama pull gemma:2b 拉取
  • 一条命令部署 API 接口,支持聊天历史、模版、上下文长度设置
  • 与 llama.cpp 完全兼容,可跑在 Linux、Windows、Mac、甚至树莓派上
  • 可扩展性强,便于与前端、数据库集成

📌 五、使用建议与总结

使用目标 推荐路径
轻量推理 / 边缘设备 / N100 主机 ✅ BitNet b1.58 + bitnet.cpp
私有本地助手 / 开发 AI 应用 / ChatGPT 替代 ✅ GGUF + Ollama(推荐 Gemma / Mistral)
公司或组织内部模型部署 / 接口服务开发 ✅ GGUF + llama.cpp / Ollama + 微服务架构
研究压缩算法 / AI 硬件优化方向 ✅ BitNet 是绝佳研究起点

✍️ 结语

BitNet 的出现证明了:在参数量极小的条件下,也能通过结构设计提升模型能力,是边缘 AI 一种有前景的探索。但就当前生态、部署效率与整体能力而言,GGUF + Ollama 仍是开源社区主流之选

作为开发者,我们可以在享受 GGUF 带来的高效部署体验的同时,关注像 BitNet 这样的新方向,或许正是未来大模型压缩与边缘计算的起点。


如果你对本地部署感兴趣,欢迎阅读我后续关于:

  • Ollama 多模型热切换方案(MCP 适配器)
  • Gemma vs Phi vs Mistral 本地模型横评
  • 如何将本地模型接入 Spring Boot 架构

欢迎交流 🙌


在学习编程语言和自然语言的过程中,我越来越意识到它们之间的深刻联系。最近,我读了王垠的一篇博客,其中提到:

“英语或者任何自然语言,最精髓的部分都很像编程语言。句子是最关键的结构。每个句子都是一个「函数调用」。动词(谓语)是函数名,其它内容(主语,宾语等)都是参数。每个部分又可以有修饰语,就像对象的 property 一样。理解这一点可以帮助你快速分析句子结构,而不是停留在字面上。”

这段话引起了我的深刻思考。它不仅启发了我对英语语法的理解,也让我联想到编程语言的设计理念。随着思考的深入,我发现,编程语言与自然语言之间的相似性远不止如此,从语法结构到抽象语法树(AST),再到编译器的解析过程,它们之间的共通性令人惊讶。


从函数到语法树:语言的结构化

首先,让我们回到王垠所提到的“句子是一个函数调用”的观点。在编程语言中,函数是最基本的操作单元,它接收输入(参数),然后执行操作,返回输出。而在自然语言中,句子也是一种“函数”,动词(谓语)类似于函数名,主语、宾语等则相当于函数的参数。修饰语则像是对象的属性,提供了更多关于这些参数的信息。

理解了这一点,我们就能更好地分析句子的结构,类似于我们分析编程语言中的代码结构。例如,句子“Tom kicked the ball”就可以被理解为:

  • 动词 kicked 是函数名
  • 主语 Tom 是参数
  • 宾语 ball 是另一个参数

这就像是编程语言中的一个函数调用:

1
2
3
python

kick(Tom, ball)

这种思维方式不仅能帮助我们更清晰地理解自然语言句子的结构,还能让我们发现编程语言和自然语言在结构上的相似性。

抽象语法树(AST):语言的内在表示

在编程语言的领域,编译器需要对源代码进行语法分析,生成抽象语法树(AST)。AST 是编程语言的结构化表示,反映了代码的语法结构和各个部分之间的关系。

同样,在自然语言处理中,我们也可以通过类似的方式构建语法树。每个句子可以被解析为一个树形结构,其中:

  • 每个节点表示句子的一个成分(如动词、主语、宾语等)
  • 边表示各成分之间的依赖关系

例如,句子 “The cat chased the mouse” 的语法树可能是这样的:

1
2
3
   chased
/ \
cat mouse

在这个例子中,“chased” 是谓语动词,连接了主语 “cat” 和宾语 “mouse”。通过这样的树形结构,我们能清楚地看到句子中各个部分的关系。

编译器的 Parser:从源代码到语法树

编译器在解析编程语言时,会将源代码转换为语法树。在这个过程中,编译器的解析器(Parser)会将源代码的字符流转化为一系列的语法单位(Token),然后根据语言的语法规则构建出抽象语法树。

这个过程与我们分析自然语言句子的结构非常相似。在编译器中,Parser 负责将代码分解为不同的语法成分,并确定它们之间的关系。这和我们通过词法分析和语法分析理解句子结构的过程非常相似。比如,动词、主语、宾语的识别,以及它们之间的依赖关系,正是这种分析的核心。

自然语言和编程语言的区别

虽然自然语言和编程语言有很多相似之处,但它们也有显著的区别。编程语言是形式化的,具有严格的语法和规则,而自然语言则充满了模糊性、歧义性和不确定性。例如:

  • 自然语言的多义性:同一个单词或句子可能在不同的上下文中有不同的含义。例如,“bank” 可以指银行,也可以指河岸。
  • 语法的灵活性:自然语言的语法更为灵活,允许一些不完全或不规则的结构存在。比如,英语中的 “She’s gone to the store” 和 “Gone to the store, she has” 都是有效的句子,尽管它们的结构不同。
  • 上下文依赖性:自然语言句子的理解往往依赖于上下文,而编程语言则更倾向于局部性,依赖于明确的语法规则。

这些区别使得自然语言的处理比编程语言更具挑战性,尤其是在自动化分析时。编程语言的分析通常依赖于固定的语法规则,而自然语言的分析则需要应对模糊性和多样化的表达方式。

大语言模型与单词标记:自然语言处理的现代解决方案

现代计算机科学通过各种方法来解决自然语言处理中的这些挑战。近年来,基于深度学习的大语言模型(如 GPT-3 和 BERT)已经在处理自然语言方面取得了显著的进展。

在传统的自然语言处理方法中,文本会首先经过“词法分析”和“句法分析”两个步骤。在词法分析中,系统将输入的文本分解为一系列的单词或标记(Tokens)。这些标记是理解句子结构的基本单位。

然而,单词本身并不总是足够的。语境和句法结构的理解同样至关重要。传统的语法树分析需要考虑不同单词之间的关系,但现代的大语言模型通过预训练和上下文理解,可以在更深层次上解决这些问题。

例如,大语言模型通过“自注意力机制”能够捕捉句子中单词之间的长期依赖关系,而不仅仅是基于局部的语法规则。这使得它们能够在处理复杂和不规则的句子时,提供比传统方法更高的准确度。

结语:通过语法树深化对语言的理解

这个思考过程让我意识到,不仅仅是编程语言,任何形式的语言都能通过构建语法树来加深对其结构的理解。从自然语言到编程语言,从语法树到抽象语法树,我们通过这种层次化、结构化的方式,可以更清晰地理解语言的内在逻辑。

对于编程语言,我们通过构建抽象语法树(AST)来表示其结构和语法规则。而在自然语言中,类似的语法树可以帮助我们理解句子的含义、结构以及各部分之间的关系。

在未来,我希望通过我的项目 lang-ast-analyze,将这种思维模式付诸实践,不仅帮助自己深入学习编译原理,也能通过分析自然语言的语法树,提升自己的英语学习效率。更重要的是,这个过程将加深我对函数式编程的理解,帮助我在编程语言中更好地运用这种思维。

0%