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. 删除堆顶(下沉)取出堆顶(最大...
二叉树与二叉搜索树:递归遍历三板斧
线性结构(数组、链表)的局限在于:它们只有”一维”的先后关系。 但很多现实问题天然是”层次”的——目录结构、组织架构、表达式、决策过程。这时候就需要树。 而二叉树,是所有树结构里最基础、也最重要的一种。 什么是二叉树二叉树:每个节点最多有两个子节点(左子节点和右子节点)。 几个必须记住的术语: 根节点:最顶层的节点 叶子节点:没有子节点的节点 深度:从根到该节点的边数 高度:从该节点到最远叶子的边数 递归遍历三板斧处理树的几乎所有问题,都离不开遍历。二叉树有三种经典的深度优先遍历,区别只在于访问根节点的时机: 1234567891011121314151617181920212223// 前序遍历:根 → 左 → 右void preorder(TreeNode node) { if (node == null) return; visit(node); // 先访问根 preorder(node.left); preorder(node.right);}// 中序遍历:左 → 根 → 右void inorder(T...
哈希表:为什么它能做到 O(1)
哈希表(Hash Table)可能是日常写代码里用得最多的数据结构:Java 的 HashMap、Go 的 map、Python 的 dict,本质上都是它。 它的核心卖点是:增删查平均都是 O(1)。 但”为什么是 O(1)”,恰恰是最多人答不清楚的地方。今天把它彻底搞懂。 哈希表的本质:数组 + 哈希函数哈希表底层是一块数组。数组按下标访问是 O(1),这是它快的根本原因。 问题在于:现实中的 key 通常不是整数下标(比如”用户 ID 是字符串”),怎么把一个任意的 key 映射成数组下标? 答案是哈希函数: 1hash(key) → 数组下标 理想情况下,不同的 key 映射到不同的下标,那增删查就都是 O(1)。 但理想情况几乎不可能——因为 key 的取值空间通常远大于数组的大小,所以必然出现哈希冲突:两个不同的 key,算出了同一个下标。 冲突怎么解决解决哈希冲突有两种主流思路: 1. 链地址法(拉链法)同一个下标上挂一条链表,冲突的 key 都放在这条链表里。 查找时:先算哈希找到下标,再在链表里逐个比对 key。 Java 的 HashMap 用的就是这种方法...
数组、链表、栈、队列:最基础也最容易被问倒
很多人在面试里栽跟头,不是栽在什么高深的算法上,而是栽在最基础的线性结构上。 比如被问一句”数组和链表的区别是什么”,能说出”数组连续、链表不连续”的人很多,但能讲清楚”为什么数组按下标访问是 O(1)、链表在中间插入是 O(1)”的人,就少多了。 这篇文章把这四种最基础的线性结构,从底层特性到复杂度一次讲透。 数组:连续内存,随机访问数组的本质是一块连续的内存。 正因为连续,所以当你知道首地址和元素下标时,可以直接算出目标元素的地址: 1目标地址 = 首地址 + 下标 × 元素大小 这个计算是常量时间的,所以数组按下标访问是 O(1),这就是”随机访问”。 但也正因为连续,数组有两个代价: 插入和删除要搬移数据:在中间插入一个元素,后面的元素都要往后挪一位,平均 O(n) 扩容成本高:数组创建时要指定大小,满了就得申请更大的新数组、整体拷贝 数组的适用场景:读多写少、大小相对固定。 链表:离散内存,靠指针连接链表的本质是每个节点存数据和下一个节点的指针,节点之间靠指针串联,不需要连续内存。 优点:插入删除快(只需改指针,O(1)),天然支持动态扩容 缺点:不能随机访问,...
策略模式与观察者模式:从 if-else 地狱里爬出来
如果说创建型模式解决的是”怎么 new 对象”,那行为型模式解决的就是”对象之间怎么协作”。 策略模式和观察者模式,是行为型模式里最常用、也最能立刻改善你代码质量的两个。 策略模式:消灭 if-else 地狱先看一段真实的坏代码。假设你在写电商的优惠券系统,有满减、折扣、立减三种优惠: 1234567891011public double calcPrice(String type, double price) { if ("fullReduction".equals(type)) { return price - 100; } else if ("discount".equals(type)) { return price * 0.8; } else if ("directReduction".equals(type)) { return price - 30; } else ...
