加群联系作者vx:xiaoda0423
仓库地址:webvueblog.github.io/JavaPlusDoc…
webvueblog.github.io/JavaPlusDoc…
点击勘误issues,哪吒感谢大家的阅读
1. 四种事务隔离级别
| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-repeatable Read) | 幻读 (Phantom Read) | MySQL 默认 |
|---|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✅ 可能发生 | ✅ 可能发生 | ✅ 可能发生 | 否 |
| READ COMMITTED(读已提交) | ❌ 阻止 | ✅ 可能发生 | ✅ 可能发生 | 否(Oracle 默认) |
| REPEATABLE READ(可重复读) | ❌ 阻止 | ❌ 阻止 | ✅*(InnoDB 下几乎阻止) | ✅(MySQL 默认) |
| SERIALIZABLE(可串行化) | ❌ 阻止 | ❌ 阻止 | ❌ 阻止 | 否 |
✅ 表示可能发生,❌ 表示可以避免
- InnoDB 默认通过 间隙锁(Gap Lock) 解决了大部分幻读
2. 关键概念
- 脏读:读取到另一个事务未提交的数据
- 不可重复读:同一事务中两次读同一行数据,结果不一致(因为被其他事务修改)
- 幻读:同一事务中两次相同条件查询,结果行数不同(因为其他事务插入/删除了符合条件的行)
3. 配置事务隔离级别
查询当前隔离级别
SELECT @@transaction_isolation;
设置隔离级别(会话级)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
设置隔离级别(全局)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
4. MySQL InnoDB 特点
- 默认隔离级别:REPEATABLE READ
- 为了避免幻读,InnoDB 在可重复读下使用 Next-Key Lock(行锁 + 间隙锁)
- MVCC(多版本并发控制)在 READ COMMITTED 和 REPEATABLE READ 下生效
5. 选择建议
- READ UNCOMMITTED:几乎不用,数据不安全
- READ COMMITTED:常用于高并发、能容忍不可重复读(Oracle 默认)
- REPEATABLE READ:MySQL 默认,平衡性能和一致性
- SERIALIZABLE:最高安全,但性能差,基本不用在高并发
synchronized 不是乐观锁,它是悲观锁。
我给你分三部分说明:
1. 悲观锁 vs 乐观锁
| 特性 | 悲观锁(synchronized/ReentrantLock) | 乐观锁(CAS、版本号机制) |
|---|---|---|
| 加锁方式 | 操作前直接加锁,假设会发生冲突 | 操作时不加锁,先尝试更新,失败再重试 |
| 开销 | 阻塞线程,需要上下文切换 | 自旋重试,无阻塞(但可能CPU消耗高) |
| 实现方式 | synchronized、ReentrantLock、数据库 SELECT ... FOR UPDATE | AtomicInteger、CAS、自增版本号字段 |
| 适合场景 | 并发高、冲突概率大 | 冲突少、读多写少 |
2. synchronized 是悲观锁
public synchronized void increment() {
count++;
}
- JVM 会在进入
increment()方法时直接加锁(monitorenter 指令),其他线程必须等锁释放才能执行。 - 这是悲观策略:假设每次都会冲突,所以先锁住资源。
3. 乐观锁实现(Java CAS)
Java 里常见的乐观锁实现是 CAS(Compare And Swap) :
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int oldValue, newValue;
do {
oldValue = count.get();
newValue = oldValue + 1;
} while (!count.compareAndSet(oldValue, newValue));
}
}
- compareAndSet:比较当前值是否和预期值相等,如果相等就更新,否则重试。
- 无需加锁,适合高并发读多写少场景。
1. Feign 是什么
- 声明式 HTTP 客户端:只需要定义 Java 接口 + 注解,就能发 HTTP 请求。
- 集成了 Ribbon(负载均衡)和 Hystrix(容错降级,旧版),新版可配合 Spring Cloud LoadBalancer 和 Sentinel。
- 在 Spring Cloud 中用于服务间调用,替代 RestTemplate 手写 URL。
2. Feign 的优势
- 调用简单:不需要手写
RestTemplate.getForObject(),直接调用接口方法。 - 可读性好:调用方看起来像本地方法调用。
- 支持负载均衡:配合注册中心自动选择服务实例。
- 可扩展:可自定义请求拦截器、编码器、解码器、日志等。
3. 快速上手
3.1 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.2 启用 Feign
@SpringBootApplication
@EnableFeignClients
public class MyApp {}
3.3 定义 Feign 接口
@FeignClient(name = "order-service") // name = 服务在注册中心的名字
public interface OrderClient {
@GetMapping("/orders/{id}")
Order getOrder(@PathVariable("id") Long id);
}
3.4 调用接口
@Service
public class UserService {
@Autowired
private OrderClient orderClient;
public void showOrder(Long id) {
Order order = orderClient.getOrder(id);
System.out.println(order);
}
}
注意:
name必须与注册中心(Eureka / Nacos)里的服务名一致,且要启用负载均衡(默认开启)。
4. 常用配置
4.1 开启 Feign 日志
logging:
level:
com.example.client.OrderClient: DEBUG
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // NONE, BASIC, HEADERS, FULL
}
4.2 配置超时
feign:
client:
config:
default: # default 表示所有 Feign 客户端
connectTimeout: 5000
readTimeout: 10000
4.3 请求拦截器(统一加 Token)
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("Authorization", "Bearer xxx");
}
}
4.4 降级处理(Fallback)
@FeignClient(name = "order-service", fallback = OrderClientFallback.class)
public interface OrderClient { ... }
@Component
public class OrderClientFallback implements OrderClient {
@Override
public Order getOrder(Long id) {
return new Order(-1L, "服务不可用");
}
}
5. 使用建议
- 小项目可以直接用 Feign 默认配置,大项目要结合 Sentinel 做限流熔断。
- 对高并发调用,可以开启 Feign + HTTP/2 + 连接池(默认 OkHttp 支持)。
- 返回值最好定义统一响应结构,方便做错误处理。
- 尽量避免在 Feign 接口里写复杂逻辑,保持“纯接口”风格。
1. Spring Cloud 是什么
- 微服务全家桶:基于 Spring Boot,提供一套分布式系统基础设施。
- 目标:简化微服务架构下的服务注册、配置管理、负载均衡、服务调用、网关、安全、链路追踪、容错限流等复杂功能。
- 核心理念:关注业务逻辑,公共能力交给 Spring Cloud 组件。
2. 核心功能
| 功能 | 作用 | 常用组件 |
|---|---|---|
| 服务注册与发现 | 让微服务能自动注册、互相调用 | Eureka、Consul、Nacos |
| 负载均衡 | 多实例服务间流量分配 | Ribbon(已过时)、Spring Cloud LoadBalancer |
| 服务调用 | 微服务间 HTTP 调用封装 | OpenFeign |
| 配置中心 | 动态配置管理 | Spring Cloud Config、Nacos |
| 网关 | 请求路由、鉴权、限流 | Spring Cloud Gateway |
| 容错限流 | 熔断、降级、隔离、限流 | Hystrix(过时)、Sentinel、Resilience4j |
| 消息驱动 | 事件驱动通信 | Spring Cloud Stream(Kafka/RabbitMQ) |
| 链路追踪 | 分布式调用跟踪 | Sleuth + Zipkin |
| 分布式任务 | 定时任务调度 | Spring Cloud Task、xxl-job(第三方) |
3. Spring Cloud 常用架构图
┌──────────┐
│ Gateway │ ← 统一入口(鉴权/限流/路由)
└─────┬────┘
↓
┌────────────────────────┐
│ 服务注册中心 (Eureka) │ ← 服务实例注册/发现
└─────┬──────────────────┘
↓
┌───────────────┐ ┌───────────────┐
│ user-service │ │ order-service │ ← 微服务业务模块
└─────┬──────────┘ └─────┬────────┘
↓ ↓
┌───────────────┐ ┌───────────────┐
│ Config Server │ │ Message Broker│
│ (动态配置) │ │ (Kafka/Rabbit)│
└────────────────┘ └───────────────┘
4. 典型微服务调用流程
- 服务启动 → 注册到注册中心(Eureka/Nacos)。
- 其他服务通过注册中心获取地址列表。
- 调用时由 LoadBalancer 选择实例(轮询/随机/权重等)。
- 通过 Feign 发起请求,结果返回。
- 配置中心可动态推送配置信息到服务。
- 链路追踪记录调用路径和耗时。
5. 常用组件快速上手
5.1 Eureka 服务注册与发现
<!-- 服务端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApp {}
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
5.2 OpenFeign 服务调用
@FeignClient("order-service")
public interface OrderClient {
@GetMapping("/orders/{id}")
Order getOrder(@PathVariable Long id);
}
5.3 Gateway 网关
spring:
cloud:
gateway:
routes:
- id: user_route
uri: lb://user-service
predicates:
- Path=/users/**
6. 版本体系
-
Spring Cloud 依赖 Spring Boot,版本需要匹配:
- Hoxton → Spring Boot 2.2/2.3
- 2021.x → Spring Boot 2.6+
- 2022.x → Spring Boot 3.x(Jakarta EE 迁移)
-
在
pom.xml用 Spring Cloud BOM 管理版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2022.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
1. TTL 是什么
-
定义:Key 在 Redis 中还能存活的时间(秒或毫秒)。
-
到期后:
- Key 会被自动删除(实际是惰性+定期删除结合)。
- 再访问会返回
nil(不存在)。
-
用于缓存、会话、限时任务等场景。
2. 相关命令
| 命令 | 作用 | 返回值 |
|---|---|---|
EXPIRE key seconds | 设置过期时间(秒) | 1=成功,0=key不存在或不能设置 |
PEXPIRE key ms | 设置过期时间(毫秒) | 同上 |
TTL key | 查看剩余过期时间(秒) | -1=永不过期,-2=已过期或不存在 |
PTTL key | 查看剩余过期时间(毫秒) | 同上 |
EXPIREAT key timestamp | 指定时间戳(秒)过期 | 同上 |
PEXPIREAT key ms_timestamp | 毫秒时间戳 | 同上 |
SET key value EX 60 | 创建 key 并设置 TTL(秒) | OK |
SET key value PX 60000 | 创建 key 并设置 TTL(毫秒) | OK |
PERSIST key | 移除 TTL(变为永久) | 1=成功,0=失败 |
3. 使用示例
# 设置过期时间
SET session:123 "Tom" EX 300 # 300秒后过期
EXPIRE product:1001 60 # 单独设置TTL
# 查看TTL
TTL session:123
# 毫秒精度
PEXPIRE tempkey 1500 # 1.5秒后过期
PTTL tempkey
# 移除TTL
PERSIST session:123
4. Redis 过期删除机制
Redis 不会精确到期时立刻删除 key,而是结合三种策略:
- 定时删除(主动扫描部分 key,减少内存占用)。
- 惰性删除(访问时才检查 TTL,过期就删除)。
- 淘汰策略(内存满时按策略淘汰)。
5. 常见应用
- 缓存数据:防止数据长期占内存。
- 会话管理:用户 token 自动过期。
- 分布式锁:
SET key value NX EX 30。 - 限时业务:秒杀、优惠券有效期。
- 防缓存雪崩:给 TTL 加随机值。
6. 注意事项
- 更新 key 会保留 TTL(如
SET覆盖会重置 TTL,HSET修改 hash 字段不会影响 TTL)。 - TTL 不保证精确到毫秒删除,删除时间取决于惰性检查/定期扫描。
- 批量 key 统一过期容易造成缓存雪崩 → 加随机 TTL。
- 持久化时 TTL 会一并保存,重启恢复后继续生效。
1. 为什么用 Redis 做缓存
- 速度快:内存存储,单线程事件驱动,延迟通常 < 1ms。
- 支持丰富数据结构:不仅能存字符串,还能存对象、集合、排序集等。
- 支持过期策略:TTL、LRU/LFU 淘汰,防止内存溢出。
- 分布式可用:可配合主从、哨兵、集群,支持高并发。
2. 缓存使用基本思路(Cache Aside Pattern)
读流程:
1. 先从 Redis 缓存查
2. 如果命中,直接返回
3. 如果未命中,从数据库查 → 回填 Redis(加过期时间)
写流程:
1. 更新数据库
2. 删除或更新 Redis 缓存
✅ 优点:保证数据库为最终权威数据
⚠️ 缺点:并发高时可能出现缓存击穿、雪崩、穿透问题(后面会讲解决方案)
3. Spring Boot 使用 Redis 缓存(示例)
3.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 配置 application.yml
spring:
redis:
host: 127.0.0.1
port: 6379
timeout: 2000
lettuce:
pool:
max-active: 8
max-idle: 4
min-idle: 1
3.3 代码示例
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
String key = "user:" + id;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
return JSON.parseObject(json, User.class);
}
User user = userMapper.selectById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 10, TimeUnit.MINUTES);
}
return user;
}
public void updateUser(User user) {
userMapper.updateById(user);
redisTemplate.delete("user:" + user.getId());
}
}
4. 缓存常见问题与解决方案
| 问题 | 场景 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的 key | 缓存空值(短 TTL)+ 布隆过滤器 |
| 缓存击穿 | 热点 key 过期瞬间大量请求涌入 | 设置热点 key 永不过期 + 后台异步更新;或加互斥锁 |
| 缓存雪崩 | 大量 key 同时过期 | 给 TTL 加随机值,错开过期时间;多层缓存 |
| 数据不一致 | 更新数据库后缓存没同步 | 写操作优先更新数据库,然后删除缓存(避免脏数据) |
5. 进阶优化
-
序列化优化
- 默认 JDK 序列化占用多,可用 JSON(Fastjson / Jackson)或 Kryo。
-
批量操作
- 用
pipeline一次发送多条命令减少 RTT。
- 用
-
热点数据预热
- 系统启动时提前加载常用数据进缓存。
-
分布式锁
- 用
SET key value NX EX或 Redisson 实现互斥更新。
- 用
-
监控
INFO查看命中率、内存占用,结合 Prometheus+Grafana 可视化。
6. 简易缓存流程图
┌───────────┐
│ 请求数据 │
└─────┬─────┘
↓
┌─────────────┐
│ 查 Redis 缓存 │───命中──▶ 返回数据
└─────┬───────┘
↓未命中
┌─────────────┐
│ 查数据库 │
└─────┬───────┘
↓
回填 Redis 缓存(TTL)
↓
返回数据
1. Redis 是什么
-
Remote Dictionary Server:基于内存的高性能 Key-Value 数据库。
-
主要特性:
- 内存存储(可持久化到磁盘)
- 数据结构丰富
- 单线程事件驱动(命令原子执行)
- 支持主从、哨兵、高可用、集群
- 常用于缓存、分布式锁、消息队列、排行榜等
2. 常用数据类型
| 类型 | 命令示例 | 使用场景 |
|---|---|---|
| String(字符串/二进制) | SET key val / GET key | 缓存对象、计数器 |
| List(双端链表) | LPUSH key val / RPOP key | 消息队列、时间线 |
| Set(无序去重集合) | SADD key val / SISMEMBER key val | 标签、去重 |
| Hash(哈希表) | HSET user:1 name Tom / HGET user:1 name | 对象存储 |
| ZSet(有序集合) | ZADD rank 100 Tom / ZRANGE rank 0 10 | 排行榜、延时任务 |
| Bitmap | SETBIT key offset 1 / GETBIT key offset | 签到、活跃用户 |
| HyperLogLog | PFADD key val / PFCOUNT key | 去重计数(近似) |
| Geo | GEOADD city 116.4 39.9 Beijing | 地理位置搜索 |
3. 持久化机制
RDB(快照)
-
定期将内存数据写入磁盘(
.rdb文件)。 -
优点:体积小,恢复快。
-
缺点:可能丢失最近数据。
-
配置:
save 900 1 # 900秒内有1次写操作就触发快照 save 300 10
AOF(追加文件)
- 每次写操作追加到
.aof文件。 - 优点:数据丢失更少(可配置每秒/每次写入刷盘)。
- 缺点:文件体积大,恢复慢。
- 常用策略:RDB + AOF 混合持久化(Redis 4.0+)。
4. 过期与淘汰策略
-
过期策略:
- 定时删除(定期扫描)
- 惰性删除(访问时检查)
-
淘汰策略(
maxmemory-policy):volatile-lru:从设置过期时间的 key 中挑 LRUallkeys-lru:所有 key 中挑 LRUvolatile-ttl:按 TTL 近过期时间noeviction:内存满直接报错(默认)
5. 主从复制与高可用
-
主从复制(Master-Slave)
- 从节点实时复制主节点数据
- 读写分离
-
哨兵(Sentinel)
- 监控主从节点
- 主节点故障自动切换
-
集群(Cluster)
- 分片存储(哈希槽 0~16383)
- 自动故障转移
6. 常用实战场景
- 缓存(页面缓存、热点数据缓存)
- 分布式锁(
SET key val NX EX) - 限流(
INCR+ 过期时间) - 延时队列(ZSet + 时间戳)
- 排行榜(ZSet)
- 会话管理(String/Hash + TTL)
7. 性能与调优
- 减少大 key(避免一次性取超大数据)
- 批量命令(
MGET/MSET/ pipeline) - 合理 TTL(防止内存爆炸)
- 使用连接池(减少频繁建立 TCP 连接开销)
- 监控(
INFO、slowlog、redis-cli monitor)
8. 常用命令速查
SET key value EX 60 NX # 设置过期时间 & 仅在不存在时设置
GET key
DEL key
EXPIRE key 60
INCR key
HSET user:1 name Tom age 20
LPUSH queue a b c
RPOP queue
ZADD rank 100 Tom
ZRANGE rank 0 -1 WITHSCORES
1. 老年代是什么
- 属于 Java 堆(Heap) 的一部分,线程共享。
- 存放 生命周期较长 或 体积较大 的对象。
- 与 新生代(Young Generation) 配合使用,形成 JVM 分代内存结构。
- 在 Minor GC 时通常不参与回收,但在 Major GC / Full GC 时会被回收。
2. 对象进入老年代的条件
常见有 4 种情况:
-
对象年龄达到阈值
- 对象在 Survivor 区经过多次 Minor GC 仍存活,年龄(Age)增加。
- 达到
-XX:MaxTenuringThreshold(默认 15)就会晋升到老年代。
-
大对象直接进入老年代
- 如果对象过大(如大数组、长字符串),可能跳过新生代直接放入老年代。
- 控制参数:
-XX:PretenureSizeThreshold(仅对 Serial/ParNew 有效)。
-
动态年龄判断
- Survivor 区对象年龄总大小超过 Survivor 区的一半时,比这个年龄大的对象直接进入老年代。
-
新生代空间不足
- Minor GC 后仍放不下存活对象,多余部分直接晋升老年代。
3. 老年代的 GC 特点
-
对象存活率高,回收频率低。
-
采用 标记-整理(Mark-Compact) 或 标记-清除(Mark-Sweep) 算法:
- 标记-整理:避免碎片化,但会有对象移动成本。
- 标记-清除:快,但可能导致内存碎片。
-
触发回收的情况:
- 老年代满(分配对象或晋升时触发)。
- 调用
System.gc()(可能触发 Full GC)。 - 元空间/永久代不足(间接触发 Full GC)。
4. 常见问题
-
频繁 Full GC
- 老年代空间太小。
- 短命的大对象直接进入老年代。
- 新生代晋升太快,挤占老年代。
-
内存碎片
- CMS 收集器使用标记-清除,可能产生碎片,导致大对象分配失败 → 提前 Full GC。
-
GC 停顿长
- 老年代对象多,回收时要扫描 & 移动大量对象。
5. 调优建议
-
增大老年代
-Xms/-Xmx配合-XX:NewRatio调整新生代与老年代比例。 -
合理设置晋升阈值
-XX:MaxTenuringThreshold,延缓晋升,减轻老年代压力。 -
控制大对象创建
避免一次性分配大数组/长字符串,必要时分片处理。 -
减少对象在 Survivor 区的溢出
调整 Survivor 区大小(-XX:SurvivorRatio)。 -
收集器选择
- 低延迟:G1、ZGC、Shenandoah。
- 吞吐量:Parallel GC。
6. GC 日志中的老年代
示例(G1 GC 日志):
[GC pause (G1 Evacuation Pause) (mixed), 0.0456789 secs]
[Eden: 512M(512M)->0B(512M) Survivors: 32M->32M Heap: 2G(4G)->1.6G(4G)]
Heap中的减少量主要来自老年代 & 新生代混合回收。
✅ 总结口诀
新生代放不下 → 晋升老年代
老年代满 → Full GC
晋升条件:年龄阈值 / 大对象 / 动态年龄 / Survivor 溢出
1. 垃圾回收的目标
- 释放内存:自动回收不再被引用的对象占用的堆空间。
- 保证可达对象可用:只清理不可达的对象。
- 减少停顿:平衡吞吐量与延迟。
2. GC 主要管理的区域
-
堆(Heap) :GC 核心关注区
-
新生代(Young Generation)
- Eden 区(对象初始分配)
- Survivor 0 / Survivor 1 区(对象晋升前的中转)
-
老年代(Old Generation)
- 存活时间长的大对象、晋升对象
-
-
方法区(Method Area / 元空间 Metaspace) :类元数据、常量等(部分 GC 会回收)。
3. 判断对象是否可回收
3.1 引用计数法(Reference Counting)
- 每个对象维护引用计数,计数为 0 时可回收。
- 缺点:循环引用无法回收 → Java 不采用。
3.2 可达性分析(Reachability Analysis)✅
- 从 GC Roots(栈、本地方法栈引用、静态变量等)出发,沿引用链可达的对象为存活对象,其余为垃圾。
4. 常用 GC 算法
| 算法 | 适用区域 | 原理 | 优点 | 缺点 |
|---|---|---|---|---|
| 标记-清除 | 老年代 | 标记存活对象 → 清除未标记对象 | 实现简单 | 碎片多 |
| 复制算法 | 新生代 | 复制存活对象到另一块区域 → 清空原区 | 无碎片 | 需要额外空间 |
| 标记-整理 | 老年代 | 标记存活对象 → 向一端移动 | 无碎片 | 移动对象成本高 |
| 分代收集 | 全堆 | 新生代复制算法 + 老年代标记整理 | 各取所长 | 需分代管理 |
5. GC 类型
-
Minor GC
- 回收新生代(Eden + Survivor)
- 速度快、停顿短
-
Major GC / Full GC
- 回收老年代 + 新生代(Full GC 还可能回收方法区)
- 停顿时间长
- 触发原因:老年代满、
System.gc()、元空间不足等
6. 常见垃圾收集器
6.1 串行收集器(Serial)
- 单线程 GC,适合单核/小内存
- 参数:
-XX:+UseSerialGC
6.2 并行收集器(Parallel / 吞吐量优先)
- 多线程回收,适合批处理
- 参数:
-XX:+UseParallelGC
6.3 CMS(Concurrent Mark Sweep)
- 并发标记-清除,减少停顿时间
- 缺点:内存碎片、浮动垃圾
- 参数:
-XX:+UseConcMarkSweepGC(JDK9 之后被标记废弃)
6.4 G1(Garbage First)✅
- 面向服务端,分区回收,低延迟
- 参数:
-XX:+UseG1GC
6.5 ZGC / Shenandoah(JDK11+ / JDK12+)
- 超低延迟(毫秒级停顿)
- 大堆支持(TB 级)
7. 常用调优参数
-Xms1g -Xmx1g # 堆初始/最大
-XX:NewRatio=2 # 新生代:老年代=1:2
-XX:SurvivorRatio=8 # Eden:Survivor=8:1:1
-XX:+UseG1GC # 使用G1收集器
-XX:MaxGCPauseMillis=200 # 目标最大停顿
-XX:+PrintGCDetails # 输出GC详情
-XX:+PrintGCDateStamps # 输出时间戳
8. GC 调优思路
- 确认目标:低延迟还是高吞吐?
- 收集数据:
jstat/GC logs/jmap/jvisualvm - 分析瓶颈:频繁 Full GC?老年代占满?
- 调整参数:堆大小、分代比例、GC 类型
- 压测验证:确认 GC 次数与停顿符合预期
9. 总结口诀
新生代复制,老年代整理;
GC Roots 做基准,可达即存活;
选对收集器,目标先明确。
1. JVM 内存结构总览
Java 虚拟机在运行时会把内存分成几个区域,主要有:
┌───────────────────────────────┐
│ 线程私有(Thread-private) │
│ - 程序计数器(PC Register) │
│ - Java 虚拟机栈(JVM Stack) │
│ - 本地方法栈(Native Stack) │
├───────────────────────────────┤
│ 线程共享(Thread-shared) │
│ - 堆(Heap) │
│ - 方法区(Method Area,JDK8+ 元空间 Metaspace)│
└───────────────────────────────┘
2. 各区域详细说明
2.1 程序计数器(Program Counter Register)
- 线程私有
- 保存当前线程执行的字节码行号指示器(下一条要执行的指令地址)。
- 如果执行的是本地方法(Native),计数器值为 undefined。
- 不会发生
OutOfMemoryError。
2.2 Java 虚拟机栈(JVM Stack)
-
线程私有
-
每个方法执行时都会创建一个 栈帧(Stack Frame) :
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口
-
深度递归或方法嵌套过多会 StackOverflowError。
-
如果 JVM 栈可动态扩展,扩展失败会 OutOfMemoryError。
2.3 本地方法栈(Native Method Stack)
- 与 JVM 栈类似,但为 Native 方法服务(JNI 调用 C/C++)。
- 可能抛
StackOverflowError或OutOfMemoryError。
2.4 堆(Heap)
-
线程共享
-
存放所有对象实例和数组,是 垃圾收集(GC) 管理的主要区域。
-
逻辑上分为:
新生代(Young Generation) - Eden - Survivor 0 / Survivor 1 老年代(Old Generation) -
特点:
- 新生代对象存活时间短,频繁 Minor GC。
- 老年代对象存活时间长,Major/Full GC。
-
堆不够会抛
OutOfMemoryError: Java heap space。
2.5 方法区(Method Area)
-
线程共享
-
存储已加载的类信息、常量、静态变量、即时编译后的代码等。
-
JDK8 之前:在堆的永久代(PermGen)
-
JDK8 之后:用 元空间(Metaspace) 存储在本地内存中。
-
常见 OOM:
- JDK7 及以前:
OutOfMemoryError: PermGen space - JDK8+:
OutOfMemoryError: Metaspace
- JDK7 及以前:
3. 常见内存错误类型
| 错误 | 原因 |
|---|---|
| StackOverflowError | 递归过深 / 方法调用层级过多(栈空间耗尽) |
| OutOfMemoryError: Java heap space | 堆空间不足 |
| OutOfMemoryError: GC overhead limit exceeded | GC 占用时间过长但回收效果差 |
| OutOfMemoryError: Metaspace | 类元信息占用过多(JDK8+) |
| OutOfMemoryError: Direct buffer memory | NIO 直接内存不足 |
| OutOfMemoryError: unable to create new native thread | 系统线程数达到上限 |
4. JVM 调优常用参数
# 堆设置
-Xms512m # 初始堆
-Xmx1024m # 最大堆
# 新生代
-Xmn256m # 新生代大小
# 元空间(JDK8+)
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
# 栈大小
-Xss1m # 每个线程的栈大小
# GC 日志(JDK9-)
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
5. 总结口诀
三私有:PC 寄存器、JVM 栈、本地方法栈
两共享:堆、方法区(元空间)
GC 主要盯堆,类信息在方法区,递归爆栈找 JVM 栈
一、运行期注解 + AOP(Spring 场景最常用)
1) 定义注解
import java.lang.annotation.*;
@Documented
@Target({ElementType.METHOD}) // 作用在方法(也可 CLASS / FIELD / PARAMETER 等)
@Retention(RetentionPolicy.RUNTIME) // 运行期可见:AOP/反射才能拿到
public @interface AuditLog {
String value() default "";
boolean saveArgs() default true;
}
2) 在业务方法上使用
@Service
public class OrderService {
@AuditLog(value = "创建订单", saveArgs = true)
public String createOrder(String userId, int amount) {
// ... 业务逻辑
return "OK";
}
}
3) 用 AOP 拦截并处理
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Slf4j
@Aspect
@Component
public class AuditLogAspect {
// 按“注解”为切点:标了 @AuditLog 的方法都会被拦截
@Around("@annotation(AuditLog)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
AuditLog anno = method.getAnnotation(AuditLog.class);
Object ret;
Throwable err = null;
try {
ret = pjp.proceed(); // 执行目标方法
return ret;
} catch (Throwable e) {
err = e;
throw e;
} finally {
long cost = System.currentTimeMillis() - start;
String argsStr = anno.saveArgs() ? Arrays.toString(pjp.getArgs()) : "<hidden>";
log.info("AUDIT | {} | method={} | args={} | cost={}ms | ok={}",
anno.value(), method.getName(), argsStr, cost, err == null);
// 这里也可以:落库、发MQ、埋点、告警……
}
}
}
4) 开启 AOP(Spring Boot 通常已开启)
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 可选
public class App {}
到这里,你已经完成了自定义注解 + AOP 织入:谁打了
@AuditLog,谁就触发你的横切逻辑(日志、鉴权、限流、事务补偿等都可这样做)。
二、无 Spring 时的运行期反射处理(轻量示例)
public class Runner {
public static void main(String[] args) throws Exception {
Method m = OrderService.class.getMethod("createOrder", String.class, int.class);
AuditLog anno = m.getAnnotation(AuditLog.class);
if (anno != null) {
System.out.println("Will audit: " + anno.value());
}
// 直接反射调用
OrderService s = new OrderService();
m.invoke(s, "u1", 100);
}
}
适合简单场景;若要“全局拦截”,仍建议用 AOP 或代理。
三、进阶:常见注解型需求模板
- 权限校验
@RequireRole("ADMIN")
在 AOP 里先拿到当前用户角色,不满足则抛AccessDeniedException。 - 分布式锁
@DistributedLock(key="#orderId")
AOP 中解析 SpEL → Redis/Tair 加锁 →proceed()→ finally 解锁。 - 限流
@RateLimit(qps=100)
AOP 里用令牌桶/滑动窗口,超限直接拒绝或降级。 - 缓存
@Cacheable(key="#id", ttl=300)
AOP:先查缓存 → 命中直接返回;未命中执行方法并写缓存。
以上都只是把“横切关注点”抽象成注解 + AOP 逻辑。
四、编译期注解处理(JSR 269,代码生成/校验)
如果你需要在编译期处理(如生成代码、做静态校验),用注解处理器:
@SupportedAnnotationTypes("com.example.AutoDto")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class AutoDtoProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annos, RoundEnvironment env) {
for (Element e : env.getElementsAnnotatedWith(AutoDto.class)) {
// 读取元素信息,生成源文件/校验
// processingEnv.getFiler().createSourceFile("...");
}
return true;
}
}
注册到 META-INF/services/javax.annotation.processing.Processor,编译时自动执行(类似 Lombok、MapStruct 的机制)。
五、易踩坑 & 建议
- @Retention 必须是 RUNTIME(运行期要读到注解)。
- 代理限制:Spring AOP 基于代理,final 类/方法、private 方法不会被代理;需要可以用 CGLIB(默认对无接口类用 CGLIB),或换 AspectJ LTW。
- 自调用失效:同类内部方法互调不会过代理 → 用
AopContext.currentProxy()或拆分到另一个 Bean。 - 作用范围:
@Target选对(方法、类、字段、参数)。 - 性能:高频 AOP 里少用重度反射,缓存
Method/Annotation元数据。 - 顺序:多个切面需要可控顺序 →
@Order。
在 Spring Bean 生命周期 里,AOP 不是单独的一个阶段,而是通过 BeanPostProcessor(特别是 AbstractAutoProxyCreator 及其子类) 在 初始化阶段之后 把代理逻辑织入的。
我给你按顺序标一下 AOP 发生的位置:
1. Bean 生命周期回顾(关键位置标记)
1. 实例化 Bean
2. 属性注入
3. Aware 接口回调
4. BeanPostProcessor.postProcessBeforeInitialization()
5. 初始化方法(afterPropertiesSet / init-method)
6. BeanPostProcessor.postProcessAfterInitialization() ← ★ AOP 在这里织入
7. Bean 就绪可用(可能是代理对象)
8. 销毁阶段
2. 为什么 AOP 在 AfterInitialization
- Spring AOP 本质是 JDK 动态代理 / CGLIB 代理。
- 代理对象必须在 Bean 完成初始化后 才能替换原对象(确保依赖注入和初始化逻辑已经执行)。
postProcessAfterInitialization()返回的不是原始 Bean,而是一个 代理对象,里面包了目标 Bean 和切面逻辑。
3. 工作流程简述
-
容器启动时加载所有 Bean 定义。
-
创建 Bean → 执行依赖注入。
-
调用
BeanPostProcessor:- 如果是
AbstractAutoProxyCreator及子类,会检查该 Bean 是否匹配切面(Advisor)。 - 如果匹配,就用 JDK Proxy(接口)或 CGLIB Proxy(类)生成代理。
- 如果是
-
代理对象放回容器,后续你拿到的就是这个代理对象。
-
调用方法时,代理会先执行切面逻辑,再调用原方法。
4. 代码证明
@Component
public class DemoService {
public void hello() {
System.out.println("Hello method executed");
}
}
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.DemoService.*(..))")
public void before() {
System.out.println("Before advice");
}
}
启动后:
@Autowired
DemoService demoService;
@PostConstruct
public void test() {
System.out.println(demoService.getClass());
// class com.example.DemoService$$EnhancerBySpringCGLIB$$...
}
说明 Spring 返回的是代理类,而不是原始类。
✅ 结论
Spring AOP 是通过 BeanPostProcessor 在初始化后的后置处理阶段 创建代理对象实现的。
所以 AOP 织入点 在生命周期的 postProcessAfterInitialization() 。
1. 生命周期总流程(简化版)
-
实例化(Instantiation)
- Spring 通过反射(构造函数/工厂方法)创建 Bean 实例。
-
属性赋值(Populate Properties)
- 给 Bean 的属性注入依赖(依赖注入阶段)。
-
BeanNameAware / BeanFactoryAware / ApplicationContextAware 回调
- 如果 Bean 实现了这些接口,会被注入对应的上下文信息。
-
BeanPostProcessor(前置处理)
- 所有
BeanPostProcessor.postProcessBeforeInitialization()调用。
- 所有
-
初始化(Initialization)
- 如果实现了
InitializingBean.afterPropertiesSet(),会调用。 - 如果配置了
init-method,也会执行。
- 如果实现了
-
BeanPostProcessor(后置处理)
- 调用
BeanPostProcessor.postProcessAfterInitialization(),可替换或增强 Bean(如 AOP)。
- 调用
-
使用阶段
- Bean 被应用调用。
-
销毁(Destruction)
-
容器关闭时:
- 如果实现了
DisposableBean.destroy(),会调用。 - 如果配置了
destroy-method,也会执行。
- 如果实现了
-
2. 生命周期详细阶段(Spring IOC 内部执行顺序)
[1] Bean实例化(Constructor / Factory)
[2] 属性注入(Dependency Injection)
[3] Aware接口回调(BeanNameAware / BeanFactoryAware / ApplicationContextAware)
[4] BeanPostProcessor 前置处理(postProcessBeforeInitialization)
[5] 初始化:
├── afterPropertiesSet() (InitializingBean接口)
└── 自定义 init-method
[6] BeanPostProcessor 后置处理(postProcessAfterInitialization)
[7] Bean 可用(被应用调用)
[8] 容器关闭:
├── destroy() (DisposableBean接口)
└── 自定义 destroy-method
3. 常用扩展点
| 扩展点 | 时机 | 作用 |
|---|---|---|
| BeanNameAware | 属性注入后 | 获取 Bean 名称 |
| BeanFactoryAware | 属性注入后 | 获取 BeanFactory |
| ApplicationContextAware | 属性注入后 | 获取 ApplicationContext |
| BeanPostProcessor | 初始化前/后 | 修改 Bean 或包装代理对象 |
| InitializingBean | 初始化阶段 | 自定义初始化逻辑 |
| DisposableBean | 销毁阶段 | 自定义销毁逻辑 |
4. 代码示例
@Component
public class MyBean implements BeanNameAware, InitializingBean, DisposableBean {
@Override
public void setBeanName(String name) {
System.out.println("BeanNameAware: " + name);
}
@Override
public void afterPropertiesSet() {
System.out.println("InitializingBean: 初始化逻辑");
}
@Override
public void destroy() {
System.out.println("DisposableBean: 销毁逻辑");
}
}
BeanPostProcessor 示例:
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("前置处理: " + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("后置处理: " + beanName);
return bean;
}
}
5. 总结口诀
实例化 → 注入属性 → Aware回调 → 前置处理 → 初始化 → 后置处理 → 使用 → 销毁
Spring 在 Java 领域里指的是 Spring Framework,它是一个开源的轻量级 Java 企业级开发框架,最初是为了解决 企业级开发中过度复杂的配置与依赖管理 问题,现在已经发展成一个完整的生态体系(Spring Boot、Spring Cloud、Spring Data 等)。
我帮你分成 核心概念 → 核心模块 → 工作原理 → 常用子项目 → 学习建议 来讲。
1. 核心概念
-
IoC(控制反转)
- 把对象的创建与依赖管理交给 Spring 容器,而不是自己
new。 - 常用方式:XML 配置、Java 注解(
@Component、@Autowired)、Java Config。
- 把对象的创建与依赖管理交给 Spring 容器,而不是自己
-
DI(依赖注入)
- IoC 的实现方式,通过构造器注入、Setter 注入或字段注入,把依赖自动注入到类中。
-
AOP(面向切面编程)
- 把通用功能(如日志、事务、安全、缓存)从业务逻辑中分离,通过切面统一织入。
-
声明式事务
- 通过
@Transactional注解实现事务管理,不必手写begin/commit/rollback。
- 通过
-
模块化
- 核心容器、数据访问、Web、AOP、消息、测试等模块可以按需引入。
2. 核心模块
| 模块 | 功能 |
|---|---|
| spring-core | 核心容器与 IoC/DI 实现 |
| spring-beans | Bean 定义与管理 |
| spring-context | IoC 容器高级特性(事件发布、国际化等) |
| spring-aop | 面向切面编程实现 |
| spring-tx | 事务管理 |
| spring-jdbc | 简化 JDBC 数据访问 |
| spring-web | 支持 Web 应用开发(Servlet API 集成) |
| spring-webmvc | MVC 框架(Spring MVC) |
3. 工作原理(简化流程)
-
容器启动
- 加载配置(XML/注解/JavaConfig)。
- 扫描包路径下的组件类(
@Component、@Service、@Controller)。
-
Bean 创建
- 按定义实例化 Bean(构造函数、工厂方法)。
- 执行依赖注入(DI)。
-
AOP 织入
- 匹配切面定义,把代理对象注入到容器。
-
应用运行
- 控制器接收请求(Spring MVC)。
- 调用业务层与数据访问层。
-
销毁阶段
- 容器关闭,调用 Bean 的销毁方法。
4. 常用子项目
- Spring Boot
简化 Spring 应用开发,内置 Tomcat/Jetty,无需繁琐 XML 配置。 - Spring Cloud
微服务框架,提供注册中心、配置中心、网关、负载均衡等。 - Spring Data
统一数据访问方式,支持 JPA、MongoDB、Elasticsearch 等。 - Spring Security
权限与认证框架。 - Spring Batch
批处理框架,适合处理大批量数据任务。
5. 学习建议
- 先学 Spring Core + Spring MVC,掌握 IoC、DI、AOP、MVC 流程。
- 过渡到 Spring Boot,理解自动配置与约定优于配置。
- 了解 Spring Data / Spring Security 等常用模块。
- 最后学习 Spring Cloud,理解分布式与微服务。
1. 原型(prototype)是什么
在 JavaScript 里:
- 每个函数(除了箭头函数)在创建时,都会自动获得一个
prototype属性(是一个对象)。 - 通过
new调用函数创建的实例,会有一个 隐藏属性[[Prototype]](大多数浏览器可用__proto__访问),它指向这个构造函数的prototype对象。 - 这个
prototype对象可以挂载方法或属性,供所有实例共享。
示例:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const p1 = new Person('Alice');
const p2 = new Person('Bob');
p1.sayHi(); // Hi, I'm Alice
p2.sayHi(); // Hi, I'm Bob
console.log(p1.sayHi === p2.sayHi); // true(共享方法)
2. 原型链(prototype chain)是什么
-
当你访问一个对象的属性时,JS 会先在对象自身查找,如果找不到,就沿着
[[Prototype]]继续查找。 -
这种对象通过原型连接起来形成的链式结构,就是 原型链。
-
查找顺序:
对象自身 → 对象的原型 → 原型的原型 → … → Object.prototype → null
示例:
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
3. 原型链查找过程
p1.toString();
查找过程:
- 在
p1自身找toString→ 找不到 - 在
Person.prototype找 → 找不到 - 在
Object.prototype找到 → 调用这个方法 - 找不到时返回
undefined
4. __proto__ vs prototype
| 属性 | 属于谁 | 作用 |
|---|---|---|
prototype | 构造函数 | 定义实例共享的属性/方法 |
__proto__ | 实例对象 | 指向创建它的构造函数的 prototype |
5. 使用场景
- 方法共享:节省内存,避免每个实例都生成一份相同方法。
- 继承:通过修改
prototype或__proto__实现继承链。 - 多层继承:子类原型指向父类实例或父类原型,形成多层原型链。
继承示例:
function Animal(name) {
this.name = name;
}
Animal.prototype.run = function() {
console.log(`${this.name} is running`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype); // 继承
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} barks`);
};
const d = new Dog('Tommy', 'Bulldog');
d.run(); // 来自 Animal
d.bark(); // 自己的
6. 总结口诀
- 原型:对象的公共属性/方法存储地(构造函数的
prototype)。 - 原型链:对象属性查找的路径(沿
__proto__向上找)。 - constructor:原型对象上默认有个
constructor指回构造函数。
1. 核心区别对比
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域(function scope) | 块作用域(block scope) | 块作用域(block scope) |
| 变量提升 | 会提升到作用域顶部,值为 undefined | 会提升,但处于 暂时性死区(TDZ) ,未初始化前不能访问 | 同 let |
| 重复声明 | 允许在同一作用域重复声明 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许(引用类型可修改内部值) |
| 初始化 | 可选 | 可选 | 必须初始化 |
2. 示例
2.1 作用域
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // ✅ 1(var 是函数作用域)
console.log(b); // ❌ ReferenceError
console.log(c); // ❌ ReferenceError
2.2 变量提升
console.log(x); // undefined(声明提升)
var x = 5;
console.log(y); // ❌ ReferenceError(暂时性死区)
let y = 6;
2.3 const 引用类型
const arr = [1, 2, 3];
arr.push(4); // ✅ 可以修改内容
// arr = [5, 6]; // ❌ 不能重新赋值引用
3. 使用建议
-
优先用
const:默认不变的值保持不可变,防止误改。 -
需要重新赋值时用
let:如循环变量、累加器等。 -
避免使用
var:容易因变量提升和作用域问题导致 bug。 -
代码风格(ESLint 推荐):
- 默认用
const - 仅在需要重新赋值时改用
let - 不使用
var
- 默认用
4. 总结口诀
const 定常量,let 定变量,var 尽量别用
const / let 都是块级作用域,var 是函数作用域
var 会提升,let / const 有“暂时性死区”保护
1. px(像素,绝对单位)
-
定义:屏幕上的一个物理像素点(或逻辑像素)。
-
特点:
- 固定大小,不会随父元素或页面字体大小变化。
- 在不同分辨率下可能显示效果差异(高分屏下 1px 实际会更细)。
-
示例:
div { font-size: 16px; /* 始终是 16 像素 */ } -
适用场景:需要绝对精确尺寸的元素(比如边框、图标)。
2. em(相对单位,参考父元素字体大小)
-
定义:相对父元素
font-size的倍数。 -
计算方式:
1em = 父元素 font-size 的大小 -
特点:
- 会继承并叠加(嵌套时容易出现意外放大)。
- 对排版和可读性有更好的适配性。
-
示例:
.parent { font-size: 16px; } .child { font-size: 1.5em; /* 1.5 × 16px = 24px */ } -
适用场景:希望跟随父级字体比例变化的情况(比如按钮内边距)。
3. rem(相对单位,参考根元素字体大小)
-
定义:相对 HTML 根元素
<html>的font-size。 -
计算方式:
1rem = html 元素的 font-size 大小 -
特点:
- 不会受父元素影响(比 em 更稳定)。
- 常用在响应式布局,配合媒体查询动态调整根字体大小。
-
示例:
html { font-size: 16px; } div { font-size: 2rem; /* 2 × 16px = 32px */ } -
适用场景:响应式字体、全局统一尺寸控制。
4. 对比总结
| 单位 | 参考对象 | 是否继承叠加 | 常用场景 |
|---|---|---|---|
| px | 屏幕像素 | 否 | 精确尺寸(边框、图片) |
| em | 父元素 font-size | 是 | 随父元素缩放(按钮、段落) |
| rem | 根元素 font-size | 否 | 响应式全局尺寸 |
5. 实战技巧
-
响应式布局推荐用
rem:只改<html>字体大小即可全局缩放。 -
需要跟随父元素变化,用
em。 -
绝对固定的像素,直接用
px。 -
结合 JS 动态设置根字体大小,可以实现页面随屏幕宽度缩放:
document.documentElement.style.fontSize = window.innerWidth / 10 + 'px';
方法 1:Flex 布局(推荐)
<div class="row">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
.row {
display: flex; /* 开启 Flex 布局 */
}
.item {
padding: 10px;
background: lightblue;
margin: 5px;
}
✅ 优点:简单、响应式好、支持等分、对齐控制方便。
❌ 缺点:IE9 以下不支持。
方法 2:display: inline-block
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
.item {
display: inline-block; /* 改为行内块元素 */
padding: 10px;
background: lightgreen;
margin: 5px;
}
✅ 优点:兼容性好(IE8+)。
❌ 缺点:HTML 代码的空格会造成间距,可用 font-size: 0 消除。
方法 3:浮动(float)
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
<div style="clear: both;"></div> <!-- 清除浮动 -->
.item {
float: left;
padding: 10px;
background: pink;
margin: 5px;
}
✅ 优点:老浏览器通吃。
❌ 缺点:需要清除浮动,布局灵活性差。
方法 4:CSS Grid
<div class="row">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
.row {
display: grid;
grid-auto-flow: column; /* 按列排列 */
}
.item {
padding: 10px;
background: lightcoral;
}
✅ 优点:适合复杂布局。
❌ 缺点:老旧浏览器不支持。
如果只是简单让 div 一行显示,现在主流是 Flex 或 inline-block。
我建议你用 Flex,以后加对齐、等分、换行都方便。
1. 父容器属性(决定子元素排列方式)
| 属性 | 常用值 | 作用 |
|---|---|---|
| display | flex / inline-flex | 开启 Flex 布局(inline-flex 不会独占一行) |
| flex-direction | row / row-reverse / column / column-reverse | 主轴方向(水平/垂直/反向) |
| flex-wrap | nowrap / wrap / wrap-reverse | 是否换行及方向 |
| flex-flow | <flex-direction> <flex-wrap> | 上面两个属性的简写 |
| justify-content | flex-start / flex-end / center / space-between / space-around / space-evenly | 沿主轴对齐方式 |
| align-items | stretch / flex-start / flex-end / center / baseline | 交叉轴对齐方式(子项在单行中如何对齐) |
| align-content | 同上 | 多行时交叉轴的行与行之间的对齐(对多行布局才生效) |
2. 子元素属性(决定自身在 Flex 容器里的表现)
| 属性 | 作用 |
|---|---|
| order | 控制子项在容器中的排列顺序(数字越小越靠前) |
| flex-grow | 剩余空间的放大比例(默认 0,不放大) |
| flex-shrink | 空间不足时的缩小比例(默认 1,可缩小) |
| flex-basis | 子项在分配空间前占据的主轴尺寸(默认 auto) |
| flex | flex-grow flex-shrink flex-basis 的简写 |
| align-self | 覆盖父容器 align-items 对当前子项的设置 |
3. 常见组合示例
3.1 水平垂直居中
.parent {
display: flex;
justify-content: center; /* 主轴居中 */
align-items: center; /* 交叉轴居中 */
}
3.2 等宽布局
.item {
flex: 1; /* 等分剩余空间 */
}
3.3 固定 + 自适应
.left {
flex: 0 0 200px; /* 不放大、不缩小、固定 200px */
}
.right {
flex: 1; /* 占剩余空间 */
}
4. 记忆小技巧
- 主轴方向 → flex-direction
- 主轴对齐 → justify-content
- 交叉轴对齐 → align-items / align-content
- 子项伸缩 → flex-grow / flex-shrink
- 初始大小 → flex-basis
- 覆盖父设置 → align-self
如果你愿意,我可以帮你画一张 Flex 布局属性速查图,把主轴、交叉轴和这些属性的作用范围都标出来,一眼就能找到该用哪个属性。
要我帮你画吗?这样记忆会更直观。
display: block 是 CSS 布局的基础属性之一,用来让一个元素以 块级元素(block-level element) 的方式渲染。
1. 作用
当一个元素是 display: block 时,它会:
- 独占一行(不与其他元素并排显示,默认会换行)。
- 宽度默认占满父容器(可以通过
width改变)。 - 可以设置宽高(
width/height有效)。 - 上下可有
margin,而inline元素的上下margin通常不生效。
2. 常见的 block 元素
即使不写 display: block,以下标签默认就是块级:
<div>, <p>, <h1>~<h6>, <section>, <article>, <header>, <footer>, <ul>, <ol>, <li>, <nav>, <form>, <table>
3. 示例
<style>
.box {
display: block;
width: 200px;
height: 80px;
background: lightblue;
}
</style>
<span class="box">我是块级元素</span>
即使原本
<span>是行内元素(inline),加了display: block后,也会变成块级,能设置宽高、独占一行。
4. 常见用途
-
把 行内元素(如
<a>、<span>、<img>)转换为块级,用来:- 让它支持宽高
- 让它独占一行
- 方便做盒子模型布局
-
在响应式布局中切换显示方式(例如按钮在大屏并排,小屏用
display: block堆叠)
5. 对比
| 值 | 特点 |
|---|---|
block | 独占一行,可设置宽高 |
inline | 不换行,宽高由内容决定,width/height 不生效 |
inline-block | 不换行,但可设置宽高 |
flex / grid | 容器布局模式 |
响应式布局(Responsive Layout)就是让页面在不同设备、不同屏幕宽度下都能良好显示和交互,核心是布局会自动适配,而不是固定死的像素值。
我给你分成 核心思路 → 常用技术 → 示例代码 → 实战建议 来讲。
1. 核心思路
- 流式布局:用百分比、
auto、min/max-width等代替固定像素。 - 媒体查询(Media Queries) :针对不同屏幕宽度、分辨率,切换样式。
- 弹性/网格布局:
flex和grid可以在不同空间下自动调整元素位置和大小。 - 响应式图片:不同分辨率加载不同尺寸的图片。
- 断点(Breakpoints)设计:设定几个关键屏宽(如 576px、768px、1024px、1200px)来调整布局。
2. 常用技术
| 技术 | 作用 |
|---|---|
| 百分比宽度 | 容器和元素随屏幕大小变化 |
max-width / min-width | 限制元素在不同设备下的最大/最小尺寸 |
vh / vw | 按视口宽高比例设置尺寸 |
| 媒体查询(@media) | 针对不同屏宽、设备应用不同样式 |
| Flexbox | 让子元素在空间变化时自动伸缩、换行 |
| CSS Grid | 更复杂的二维布局,自适应调整行列 |
| 响应式单位(rem / em) | 字体和间距随根字体大小缩放 |
3. 示例代码
3.1 基于媒体查询的响应式布局
.container {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 1 1 100%; /* 默认占满一行 */
padding: 10px;
background: lightblue;
}
/* 平板以上屏幕 */
@media (min-width: 768px) {
.item {
flex: 1 1 50%; /* 两列 */
}
}
/* 桌面端 */
@media (min-width: 1200px) {
.item {
flex: 1 1 25%; /* 四列 */
}
}
3.2 百分比 + max-width
img {
max-width: 100%;
height: auto;
}
3.3 使用 vw / vh
.banner {
height: 50vh; /* 高度占视口的一半 */
}
4. 实战建议
-
移动优先(Mobile First)
- 默认写小屏样式 → 用
@media (min-width)逐渐增强适配大屏。
- 默认写小屏样式 → 用
-
内容优先(Content First)
- 断点设计以内容布局变化为依据,不要只看设备型号。
-
组件化
- 每个组件自己处理响应式,减少全局样式冲突。
-
配合框架
- Bootstrap、Tailwind CSS、Ant Design 都有现成的响应式工具类。
-
测试
- Chrome DevTools → Device Toolbar → 模拟不同屏幕查看效果。
那就是局部容器用 display: flex,它的子元素才会受 flex 规则影响,其它区域还是普通布局。
1. 局部 Flex 布局的核心
.container {
display: flex; /* 只让这个容器内的子元素参与 flex 布局 */
}
- Flex 的作用域:只影响设置了
display: flex的元素的 直接子元素。 - 外部布局可以是普通的
block、inline-block、grid等。 - 内部可以自由用
flex-direction、justify-content、align-items等属性控制排列。
2. 示例:局部区域水平垂直居中
<div class="header">普通头部</div>
<div class="flex-box">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
</div>
<div class="footer">普通底部</div>
.flex-box {
display: flex; /* 开启局部 flex */
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
height: 150px;
background: #eee;
}
.item {
padding: 10px 20px;
background: lightblue;
margin: 5px;
}
✅ 只有 .flex-box 内的 A、B、C 受 flex 布局控制,header 和 footer 完全不受影响。
3. 常见局部 Flex 用法
(1) 局部水平排列
.row {
display: flex;
gap: 10px;
}
(2) 局部垂直排列
.column {
display: flex;
flex-direction: column;
gap: 10px;
}
(3) 局部等宽布局
.equal {
display: flex;
}
.equal > div {
flex: 1; /* 平分宽度 */
}
4. 小技巧
- 嵌套布局:Flex 容器里还可以再嵌套 flex 容器,实现更复杂的布局。
- 响应式:结合
flex-wrap: wrap可以让子元素在小屏自动换行。 - 局部与全局混搭:上层可以是 Grid,某些区域再用 flex 局部控制排列。
1. XXL-JOB 是什么
-
一个 分布式任务调度平台(类似轻量级定时任务中心)。
-
支持任务可视化管理、动态修改 Cron、失败重试、分片广播等功能。
-
核心由两部分组成:
- 调度中心(xxl-job-admin) :管理任务、分配执行器。
- 执行器(xxl-job-executor) :你的业务服务,接收调度中心的调度并执行任务。
2. 部署调度中心(xxl-job-admin)
-
下载源码
git clone https://github.com/xuxueli/xxl-job.git -
导入 SQL
- 找到
xxl-job/doc/db/tables_xxl_job.sql - 在 MySQL 中执行,初始化调度中心数据库表。
- 找到
-
修改配置
-
编辑
xxl-job-admin/src/main/resources/application.propertiesspring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456 xxl.job.login.username=admin xxl.job.login.password=123456
-
-
启动调度中心
cd xxl-job-admin mvn spring-boot:run访问
http://localhost:8080/xxl-job-admin(默认 admin / 123456 登录)。
3. 集成执行器(在你的 Spring Boot 项目中)
-
引入依赖
<dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>2.4.0</version> </dependency> -
配置 application.yml
xxl: job: admin: addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心地址 executor: appname: my-job-executor address: ip: port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30 accessToken: -
注册执行器配置类
@Configuration public class XxlJobConfig { @Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor = new XxlJobSpringExecutor(); executor.setAdminAddresses("http://127.0.0.1:8080/xxl-job-admin"); executor.setAppname("my-job-executor"); executor.setPort(9999); executor.setLogPath("/data/applogs/xxl-job/jobhandler"); executor.setLogRetentionDays(30); return executor; } } -
编写任务 Handler
import com.xxl.job.core.handler.annotation.XxlJob; import org.springframework.stereotype.Component; @Component public class DemoJobHandler { @XxlJob("demoJobHandler") public void execute() throws Exception { System.out.println("XXL-JOB 任务执行:" + System.currentTimeMillis()); } }
4. 在调度中心创建任务
- 登录调度中心 → 任务管理 → 新增任务
- 选择执行器
my-job-executor - 填写 JobHandler:
demoJobHandler - 配置 Cron 表达式(如
0/10 * * * * ?表示每 10 秒执行一次) - 保存 → 启动任务
5. 常用功能
- 分片广播:适合分布式批量处理任务(执行器数 = 分片数)。
- 失败重试:任务失败后按配置次数重试。
- GLUE 模式:支持在线编写 Java 代码任务(无需部署)。
- 路由策略:如 FIRST、RANDOM、ROUND、CONSISTENT_HASH 等。
- 日志查看:执行日志可在页面直接查看。
6. XXL-JOB 优势
- Web 界面管理任务,支持动态修改。
- 集群调度,支持多执行器部署。
- 支持分布式任务分片与路由策略。
- 任务失败报警(邮件/钉钉/企业微信)。
一、核心思路(最简流程)
- 采集文档并分配
docID - 规范化文本(小写、Unicode 归一、去标点/HTML、同义与词干可选)
- 分词(英文可按词边界;中文需用分词器)
- 遍历每个
(term, docID, position),写入字典:term -> postings - 对每个 term 的 Posting List 按
docID升序,合并重复,计算tf,可选保存positions - 持久化:写出 词典(term→文件偏移、DF 等)与 倒排表文件(docID 列表、tf、positions),并进行压缩(gap+可变长编码)
- 额外表:
docID -> {length, stored fields}用于排名与展示
二、极简可跑的 Python 示例(含位置与 BM25 所需统计)
from collections import defaultdict
import re
import math
def tokenize(text):
# 英文示例:小写+简单切词;中文请接入 jieba/HanLP 等
text = re.sub(r"<[^>]+>", " ", text) # 去HTML
text = re.sub(r"[^a-zA-Z0-9]+", " ", text) # 非字母数字
return [w for w in text.lower().split() if w]
def build_inverted_index(docs):
"""
docs: dict[int, str] e.g. {1: "Text...", 2: "More..."}
return:
index: dict[term] -> list of {"doc":id, "tf":n, "pos":[...]}
df: dict[term] -> doc_freq
doclen: dict[docID] -> length_in_tokens
"""
postings = defaultdict(lambda: defaultdict(list)) # term -> docID -> positions
doclen = {}
for doc_id, text in docs.items():
tokens = tokenize(text)
doclen[doc_id] = len(tokens)
for i, tok in enumerate(tokens):
postings[tok][doc_id].append(i)
index, df = {}, {}
for term, doc_pos in postings.items():
plist = []
for d, pos_list in sorted(doc_pos.items()): # 按 docID 排序
plist.append({"doc": d, "tf": len(pos_list), "pos": pos_list})
index[term] = plist
df[term] = len(plist)
return index, df, doclen
# 示例与查询(布尔与BM25打分)
docs = {
1: "Elasticsearch builds on Lucene. Lucene uses inverted index.",
2: "Inverted index enables fast full-text search.",
3: "Lucene segments are immutable; merging reduces small files."
}
index, df, doclen = build_inverted_index(docs)
N = len(docs); avgdl = sum(doclen.values())/N
def bm25_score(query_terms, k1=1.2, b=0.75):
# 简化版 BM25:不做查询词频统计
scores = defaultdict(float)
for term in query_terms:
if term not in index:
continue
ni = df[term]
idf = math.log((N - ni + 0.5) / (ni + 0.5) + 1)
for item in index[term]:
d = item["doc"]; tf = item["tf"]; dl = doclen[d]
denom = tf + k1*(1 - b + b*dl/avgdl)
scores[d] += idf * (tf*(k1+1) / denom)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
print(bm25_score(tokenize("lucene inverted index")))
要点:
index[term]的 posting 保存docID/tf/positions,满足布尔/短语/临近搜索与排序。- 线上应将
positions可选化(节省空间)。
三、工程化与性能优化清单
1) 词典与倒排存储
- 词典(term→{df, cf, offset, length}})放内存或 mmap;term 查找可用 FST/前缀树。
- 倒排:
[docID_gap编码][tf][pos_gap...];docID/pos 用 gap(差分)+ 可变长(VarInt/VarByte/VByte)或 PForDelta、QMX 等压缩。 - 按块(block)切分并加 跳跃表/skip pointers,加快并集/交集与跳跃。
2) 写入路径
- 采用 段(segment)不可变 策略:内存缓冲 → 定期 flush 生成小段 → 后台 merge 合段(减少小文件)。
- 更新/删除:写新段与 删除标记(tombstone) ,在合并时清理;避免就地改写。
3) 分词与规范化
- 英文:lowercase、去停用词、词干化(Porter/Lemmatize,可按需)。
- 中文:接入
jieba/HanLP/ICTCLAS/THULAC等;保留位置以支持短语匹配。 - 多语言:Unicode NFKC 归一、同义词扩展(离线字典或查询时扩展)。
4) 查询执行
- 先用倒排做候选集过滤,再用正排/DocValues 做精确筛选(数值/范围/日期)。
- 排序:BM25/PL2 + 学习排序(LTR);需要
doclen/avgdl等统计。 - 并行:多核并行处理多个 term 或多个段;大规模用分片(shards)并行,协调节点合并 TopK。
5) 大规模离线构建(MapReduce/Spark 思路)
- Map:对每个文档输出
(term, (docID, pos)) - Shuffle:按 term 分组并排序
- Reduce:合并为有序 posting list,写出段文件,并记录词典偏移
- 后续再做段级合并与压缩/索引块化
(这正是搜索引擎和 ES/Lucene 的离线/在线混合套路)
6) 评测与质量
- 功能:AND/OR、短语、临近(slop)、高亮
- 质量:MRR、NDCG@K、Recall/Precision
- 性能:QPS、P99 延迟;索引大小、构建时长;合并对查询抖动的影响
四、常见坑
- 中文不分词 → 命中率极差;必须接入分词。
- 未按 docID 排序与 gap 压缩 → 空间/速度都很差。
- positions 全保留 → 指数级膨胀;仅在需要短语/临近检索时开启。
- 频繁原地更新 → 复杂且慢;改用段不可变 + 合并。
- 长文本字段 → 建议只索引必要字段;展示内容走存储字段或外部存储。
五、如果你要“像 ES/Lucene 那样”
- 段式不可变结构 + 后台合并(tiered merge)
- 词典用 FST;posting 分块 + skip list
- mmap 文件;
doc values做排序/聚合 - 刷新(refresh)让新段可见,flush 写磁盘,translog 保持崩溃恢复
1. ES 底层架构
Elasticsearch 是基于 Lucene 的分布式搜索引擎,底层核心结构:
Client
↓
Coordinator Node(协调节点)
↓
Primary Shard(主分片)
↓
Replica Shard(副本分片)
↓
Lucene Segment(倒排索引存储单元)
↓
文件系统(底层存储)
- Node:ES 集群中的单个实例。
- Index:类似数据库的库。
- Shard:索引分成的多个分片(Primary 主分片 + Replica 副本分片)。
- Segment:Lucene 的最小搜索单元,底层不可变文件。
- Cluster State:集群元数据(节点信息、索引信息等)。
2. 存储原理(基于 Lucene)
Elasticsearch 并不是直接存数据到数据库,而是用 倒排索引 来加速全文检索。
倒排索引结构:
term(关键词) → [doc1, doc3, doc7 ...]
- 正排索引:doc → field 值(用于精确获取)。
- 倒排索引:field 值(term) → doc 列表(用于搜索匹配)。
写入过程(Indexing Flow):
- 文档 JSON 通过 Analyzer(分词器) 分解成 Token。
- Token 按字段构建倒排表,写入 Segment(不可变)。
- Segment 写入文件系统(默认基于 Lucene + mmap)。
- 写入前先进入 内存缓冲区(Index Buffer)→ translog(事务日志)保证宕机恢复。
- 定期 flush:生成新的 Segment 并清空缓冲区。
- 定期 merge:将多个小 Segment 合并成大 Segment(减少文件数,提高搜索效率)。
3. 搜索原理
搜索是 分布式 + 并行 的过程。
查询流程(Search Flow):
- 协调节点(Coordinator Node)接收查询请求。
- 将请求转发给所有分片(Primary 或 Replica 都可)。
- 每个分片在自己的 倒排索引(Segment) 中执行查询。
- 分片返回 Top N 结果(docID + score)。
- 协调节点合并排序,返回最终结果。
4. 底层优化机制
4.1 存储优化
- 不可变 Segment:写入快、搜索快,但会产生大量小文件 → 需要 merge。
- 内存映射(mmap) :Lucene 使用操作系统内存映射文件,提高 IO 性能。
- 压缩存储:倒排表 + doc 值采用压缩(减少磁盘 & 提高缓存命中率)。
4.2 查询优化
- 跳表 / FST(Finite State Transducer) :优化 term 查找。
- Bitset / Roaring Bitmap:快速标记匹配的 doc。
- 倒排 + 正排结合:先倒排快速筛选,再正排精确过滤。
4.3 分布式一致性
- 写入:先写主分片,再写副本分片,保证最终一致性。
- 刷新(refresh) :默认 1 秒刷新一次,使新文档可被搜索到(near real-time)。
- 副本分片:既能提高可用性,又能分担读取压力。
5. 关键点总结
-
ES 底层是 Lucene,核心是 倒排索引。
-
Segment 不可变 → 写快、并发安全,但需要合并。
-
搜索过程是 分布式并行,依赖 协调节点合并结果。
-
高性能来自:
- 内存映射文件
- 压缩索引
- 跳表 / FST
- 多分片并行
WebSocket 是一种 全双工、长连接 的网络通信协议,常用于实时通信场景(比如聊天、推送、股票行情、游戏等)。
1. 特点
| 特性 | 说明 |
|---|---|
| 双向通信 | 客户端和服务器都能主动发消息。 |
| 长连接 | 一次握手,持续保持连接,不必频繁建立 HTTP 请求。 |
| 低延迟 | 适合需要实时推送的场景。 |
| 基于 TCP | 在 TCP 基础上定义了自己的帧格式,通常运行在 80/443 端口。 |
2. 通信过程
- 客户端发起 HTTP 请求(带有
Upgrade: websocket头),请求升级为 WebSocket。 - 服务器响应 101 状态码,表示协议切换成功。
- 握手完成后,双方可以通过 TCP 直接交换数据帧。
- 保持长连接,直到一方主动关闭。
3. 浏览器端示例
// 创建连接
const socket = new WebSocket("ws://localhost:8080");
// 连接成功
socket.onopen = () => {
console.log("WebSocket connected");
socket.send("Hello Server");
};
// 接收消息
socket.onmessage = (event) => {
console.log("Message from server:", event.data);
};
// 连接关闭
socket.onclose = () => {
console.log("WebSocket closed");
};
// 错误处理
socket.onerror = (error) => {
console.error("WebSocket error:", error);
};
4. Node.js 服务器端示例
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log('Received:', message);
ws.send(`Echo: ${message}`);
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
5. 常见应用场景
- 即时聊天(WhatsApp、微信网页版)
- 实时推送(股票、体育比分)
- 多人在线游戏
- 协同编辑(Google Docs 实时同步)
Docker 是一个容器化平台,可以让你把应用和它的依赖打包在一个轻量级、可移植的容器里运行,避免“在我电脑上可以跑”的问题。
1. 核心概念
| 概念 | 说明 |
|---|---|
| 镜像(Image) | 应用运行所需的只读模板,比如一个包含 Ubuntu 系统 + Python 环境的打包文件。 |
| 容器(Container) | 镜像的运行实例,可以启动、停止、删除。容器里的环境互相隔离。 |
| Docker Hub | 官方镜像仓库,你可以直接拉取别人做好的镜像,或者上传自己的镜像。 |
| Dockerfile | 用来定义镜像构建步骤的文件。 |
2. 常用命令
# 查看 Docker 版本
docker --version
# 拉取镜像
docker pull nginx:latest
# 运行容器(映射端口,后台运行)
docker run -d -p 8080:80 nginx:latest
# 查看正在运行的容器
docker ps
# 查看所有容器(包括已停止的)
docker ps -a
# 停止容器
docker stop <容器ID或名字>
# 删除容器
docker rm <容器ID或名字>
# 删除镜像
docker rmi <镜像ID>
# 构建镜像(在 Dockerfile 所在目录执行)
docker build -t myapp:1.0 .
# 进入容器终端
docker exec -it <容器ID> /bin/bash
3. Dockerfile 示例
# 基础镜像
FROM python:3.11
# 设置工作目录
WORKDIR /app
# 复制项目文件到容器
COPY . .
# 安装依赖
RUN pip install -r requirements.txt
# 启动应用
CMD ["python", "app.py"]
构建并运行:
docker build -t myapp .
docker run -d -p 5000:5000 myapp
4. Docker 使用优势
- 跨平台一致性:一次构建,到处运行。
- 轻量快速:比虚拟机启动快,资源占用少。
- 易于分发:直接推送到镜像仓库即可分享。
- 隔离性好:每个容器互不影响。