Docker 从入门到生产实践
Docker 从入门到生产实践 Docker 的价值不在于”会跑一个容器”,而在于把”环境一致性”从口头承诺变成工程事实。本文聚焦真正影响生产环境的几个关键点。 镜像分层:缓存用对了,构建快 10 倍Dockerfile 的每一行指令都会生成一层。构建缓存能否命中,取决于指令和上下文是否变化。 一个反面例子: 1234# 差:每次代码变动都让 COPY 之后的层全部失效FROM openjdk:17COPY . /appRUN mvn package 依赖下载是构建中最慢的环节,却每次都要重跑。正确做法是先拷贝依赖清单,再拷贝源码: 123456FROM openjdk:17WORKDIR /appCOPY pom.xml .RUN mvn dependency:go-offlineCOPY src ./srcRUN mvn package -DskipTests 依赖层只有在 pom.xml 变化时才重建,日常改动源码时构建秒级完成。 镜像瘦身:从 800MB 到 120MB大镜像的问题不只是占磁盘,更影响部署速度——每台机器都要拉一遍。 瘦身三板斧: 多阶段构建:构建...
依赖注入:为什么你的代码不该自己 new 对象
上一篇讲 SOLID 时,最后一条”依赖倒置原则(DIP)”是最难的。而依赖注入(DI)就是 DIP 的落地实现。 如果你用过 Spring,那你天天都在用 DI,只是可能没想过它背后的道理。 没有 DI 的世界先看一段”正常”的代码: 1234public class OrderService { private OrderRepository repository = new MySQLOrderRepository(); private EmailSender emailSender = new SmtpEmailSender();} 这段代码有什么问题? 强耦合:OrderService 直接依赖了具体实现 MySQLOrderRepository。要换数据库,就得改 OrderService。 难测试:想测试 OrderService,没法单独测,因为它内部硬绑定了真实的数据库和邮件服务。 重复创建:每个用到 OrderRepository 的类都自己 new 一个,浪费资源且状态难统一。 依赖注入的解法DI 的思路是:对象不再自己...
单元测试与 TDD:先写测试再写代码,真的值得吗
写代码十年,我发现一个规律:越资深的工程师,越重视测试。 而新手最常见的误区是:觉得写测试是”额外负担”,是”拖慢进度”的事。 其实恰恰相反——测试是帮你提效的,不是给你添堵的。 为什么要写单元测试1. 让你敢重构没有测试的代码,重构就是赌博。你改了 A 处,可能偷偷破坏了 B 处,而你自己完全不知道。 有了测试,重构完跑一遍测试,全绿就说明行为没变。测试是重构的安全网。 2. 让 bug 更早暴露bug 越晚发现,修复成本越高。单元测试能在开发阶段就抓住大部分逻辑错误,而不是等到线上出问题才返工。 3. 测试本身就是文档一份好的测试用例,比注释更能说明”这段代码到底该怎么用、边界条件是什么”。 测试金字塔一个常见的分层模型: 1234567 /\ / \ E2E 测试(少,慢,贵) /----\ / \ 集成测试(中) /--------\ / \ 单元测试(多,快,便宜)/------------\ 原则:底层多写,顶层少写。 单元测试快、便宜、稳定,应该是主力。E2E 测试慢、贵、脆弱,只...
函数式编程范式:纯函数、不可变数据与高阶函数
如果你长期写面向对象的代码,第一次接触函数式编程(FP)可能会觉得”别扭”。 但理解之后你会发现,它不是要取代面向对象,而是给了你另一种处理复杂问题的思维方式。 函数式编程的核心思想函数式编程有三个核心概念,理解了它们就抓住了 FP 的精髓。 1. 纯函数(Pure Function)纯函数满足两个条件: 同样的输入,永远得到同样的输出(确定性) 不产生任何副作用(不修改外部状态) 12345678910// 纯函数:输入相同,输出必然相同int add(int a, int b) { return a + b;}// 非纯函数:依赖并修改了外部状态int counter = 0;int increment() { return ++counter;} 为什么纯函数好?因为: 容易测试:不需要 mock 一堆状态 容易推理:看输入输出就能理解,不用追踪外部状态 天生线程安全:没有共享可变状态,就没有并发问题 2. 不可变数据(Immutability)函数式编程倾向于使用不可变数据:对象一旦创建,就永不改变。要”修改”...
Spring Cloud 微服务架构演进
Spring Cloud 微服务架构演进 从单体到微服务,是企业级系统架构演进的必经之路。本文梳理 Spring Cloud 微服务的核心组件与实战经验。 演进路径1234567单体应用 ↓垂直拆分(按业务) ↓SOA(服务化) ↓微服务(独立部署、独立伸缩) Spring Cloud 核心组件 组件 职责 Eureka / Nacos 服务注册与发现 Config / Nacos 配置中心 Gateway / Zuul API 网关 OpenFeign / RestTemplate 服务调用 Hystrix / Sentinel 熔断限流 Sleuth / SkyWalking 链路追踪 选型建议 新项目:Spring Cloud Alibaba(Nacos + Sentinel + Seata) 存量项目:Spring Cloud Netflix(Hystrix 已停止维护,注意迁移) 微服务 ≠ 银弹引入微服务带来的复杂度: 分布式事务 链路追踪 服务治理 部署...
SOLID 五大原则:面向对象设计的地基
很多人学了二十多个设计模式,却还是写不好代码。原因往往在于:只学了”招式”(模式),没学”心法”(原则)。 SOLID 就是面向对象设计的”心法”,五个字母对应五条原则: S:单一职责原则 O:开闭原则 L:里氏替换原则 I:接口隔离原则 D:依赖倒置原则 理解它们,你才能真正理解那些设计模式为什么那么设计。 S:单一职责原则(SRP) 一个类只应该有一个引起它变化的原因。 说白了:一个类只干一件事。 反例:一个 UserService 既处理用户注册逻辑,又负责发短信、写日志、发邮件。这个类有四个变化原因,任何一处需求变更都要动它。 正例:拆成 UserService、SmsService、EmailService,各司其职。 好处:类的职责清晰,改一个功能不会波及无关逻辑,也更容易测试和复用。 判断标准:如果描述一个类要做的事,需要用”和”字连接(”它负责 A 和 B 和 C”),那大概率违反了单一职责。 O:开闭原则(OCP) 对扩展开放,对修改关闭。 这是五条里最重要的一条,也是所有设计模式的共同目标:加新功能时,尽量通过”新增代码”实现,而不是”修改已有代码”。...
时间复杂度与空间复杂度:Big-O 到底怎么算
评价一个算法好不好,不能靠”感觉快慢”,得有一个统一的标尺——这就是复杂度分析。 而复杂度分析里最重要的工具,就是 Big-O(大 O 记号)。 为什么需要 Big-O假设你写了一个查找函数,怎么评价它快不快? 有人会说”在测试机器上跑了 10ms”。但换个机器、换个数据量,结果就变了。 Big-O 解决的就是这个问题:它描述的是”随着数据量增大,运行时间怎么增长”的趋势,而不是具体的运行时间。 比如: O(1):数据量翻倍,时间不变 O(n):数据量翻倍,时间也翻倍 O(n²):数据量翻倍,时间变 4 倍 Big-O 到底怎么算算 Big-O,核心就一句话: 忽略常数和低阶项,只保留增长最快的项。 举例: 13n² + 2n + 100 → O(n²) 为什么可以忽略?因为当 n 足够大时,n² 那一项会完全主导,2n 和 100 都微不足道。 所以: 3n → O(n) n² + n → O(n²) 2^n + n² → O(2^n) 常见复杂度量级(从小到大) 量级 名称 例子 O(1) 常数 数组按下标访问、哈希表查找 O(log n) 对...
堆与优先队列:Top K 问题的标准答案
面试里有一类题出现频率极高: 从一个亿级数据里,找出最大的 10 个元素(Top K)。 如果你直接排序再取前 10,那是 O(n log n)。但如果用堆,可以做到 O(n log k)。 堆到底是什么,为什么这么强?今天讲透。 堆是什么堆是一种特殊的完全二叉树,它有一个关键性质: 大顶堆:每个节点的值都 ≥ 其子节点的值(根最大) 小顶堆:每个节点的值都 ≤ 其子节点的值(根最小) 注意,堆只保证”父大于子”,不保证左右子树之间的大小关系,也不像二叉搜索树那样全局有序。 堆还有一个巧妙的地方:它可以用数组来存储。 因为完全二叉树的结构是确定的,所以可以用下标表示父子关系: 下标 i 的左子节点:2i + 1 下标 i 的右子节点:2i + 2 下标 i 的父节点:(i - 1) / 2 用数组存储,省去了指针开销,这也是堆高效的原因之一。 堆的两种核心操作1. 插入元素(上浮)把新元素放到数组末尾,然后不断和父节点比较,如果比父节点大(大顶堆)就交换,直到满足堆的性质。 这个过程叫”上浮”,时间复杂度 O(log n)。 2. 删除堆顶(下沉)取出堆顶(最大...
