分布式微服务系统架构第166集:华为经验汇总

115 阅读28分钟

加群联系作者vx:xiaoda0423

仓库地址:webvueblog.github.io/JavaPlusDoc…

1024bat.cn/

github.com/webVueBlog/…

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消耗高)
实现方式synchronizedReentrantLock、数据库 SELECT ... FOR UPDATEAtomicInteger、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 的优势

  1. 调用简单:不需要手写 RestTemplate.getForObject(),直接调用接口方法。
  2. 可读性好:调用方看起来像本地方法调用。
  3. 支持负载均衡:配合注册中心自动选择服务实例。
  4. 可扩展:可自定义请求拦截器、编码器、解码器、日志等。

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. 典型微服务调用流程

  1. 服务启动 → 注册到注册中心(Eureka/Nacos)。
  2. 其他服务通过注册中心获取地址列表。
  3. 调用时由 LoadBalancer 选择实例(轮询/随机/权重等)。
  4. 通过 Feign 发起请求,结果返回。
  5. 配置中心可动态推送配置信息到服务。
  6. 链路追踪记录调用路径和耗时。

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.xmlSpring 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,而是结合三种策略:

  1. 定时删除(主动扫描部分 key,减少内存占用)。
  2. 惰性删除(访问时才检查 TTL,过期就删除)。
  3. 淘汰策略(内存满时按策略淘汰)。

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. 进阶优化

  1. 序列化优化

    • 默认 JDK 序列化占用多,可用 JSON(Fastjson / Jackson)或 Kryo。
  2. 批量操作

    • pipeline 一次发送多条命令减少 RTT。
  3. 热点数据预热

    • 系统启动时提前加载常用数据进缓存。
  4. 分布式锁

    • SET key value NX EX 或 Redisson 实现互斥更新。
  5. 监控

    • 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排行榜、延时任务
BitmapSETBIT key offset 1 / GETBIT key offset签到、活跃用户
HyperLogLogPFADD key val / PFCOUNT key去重计数(近似)
GeoGEOADD 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 中挑 LRU
    • allkeys-lru:所有 key 中挑 LRU
    • volatile-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 连接开销)
  • 监控INFOslowlogredis-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 种情况:

  1. 对象年龄达到阈值

    • 对象在 Survivor 区经过多次 Minor GC 仍存活,年龄(Age)增加。
    • 达到 -XX:MaxTenuringThreshold(默认 15)就会晋升到老年代。
  2. 大对象直接进入老年代

    • 如果对象过大(如大数组、长字符串),可能跳过新生代直接放入老年代。
    • 控制参数:-XX:PretenureSizeThreshold(仅对 Serial/ParNew 有效)。
  3. 动态年龄判断

    • Survivor 区对象年龄总大小超过 Survivor 区的一半时,比这个年龄大的对象直接进入老年代。
  4. 新生代空间不足

    • Minor GC 后仍放不下存活对象,多余部分直接晋升老年代。

3. 老年代的 GC 特点

  • 对象存活率高,回收频率低。

  • 采用 标记-整理(Mark-Compact)标记-清除(Mark-Sweep) 算法:

    • 标记-整理:避免碎片化,但会有对象移动成本。
    • 标记-清除:快,但可能导致内存碎片。
  • 触发回收的情况:

    1. 老年代满(分配对象或晋升时触发)。
    2. 调用 System.gc()(可能触发 Full GC)。
    3. 元空间/永久代不足(间接触发 Full GC)。

4. 常见问题

  1. 频繁 Full GC

    • 老年代空间太小。
    • 短命的大对象直接进入老年代。
    • 新生代晋升太快,挤占老年代。
  2. 内存碎片

    • CMS 收集器使用标记-清除,可能产生碎片,导致大对象分配失败 → 提前 Full GC。
  3. 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 调优思路

  1. 确认目标:低延迟还是高吞吐?
  2. 收集数据jstat / GC logs / jmap / jvisualvm
  3. 分析瓶颈:频繁 Full GC?老年代占满?
  4. 调整参数:堆大小、分代比例、GC 类型
  5. 压测验证:确认 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++)。
  • 可能抛 StackOverflowErrorOutOfMemoryError

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

3. 常见内存错误类型

错误原因
StackOverflowError递归过深 / 方法调用层级过多(栈空间耗尽)
OutOfMemoryError: Java heap space堆空间不足
OutOfMemoryError: GC overhead limit exceededGC 占用时间过长但回收效果差
OutOfMemoryError: Metaspace类元信息占用过多(JDK8+)
OutOfMemoryError: Direct buffer memoryNIO 直接内存不足
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 或代理。


三、进阶:常见注解型需求模板

  1. 权限校验 @RequireRole("ADMIN")
    在 AOP 里先拿到当前用户角色,不满足则抛 AccessDeniedException
  2. 分布式锁 @DistributedLock(key="#orderId")
    AOP 中解析 SpEL → Redis/Tair 加锁 → proceed() → finally 解锁。
  3. 限流 @RateLimit(qps=100)
    AOP 里用令牌桶/滑动窗口,超限直接拒绝或降级。
  4. 缓存 @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. 工作流程简述

  1. 容器启动时加载所有 Bean 定义。

  2. 创建 Bean → 执行依赖注入。

  3. 调用 BeanPostProcessor

    • 如果是 AbstractAutoProxyCreator 及子类,会检查该 Bean 是否匹配切面(Advisor)。
    • 如果匹配,就用 JDK Proxy(接口)或 CGLIB Proxy(类)生成代理。
  4. 代理对象放回容器,后续你拿到的就是这个代理对象。

  5. 调用方法时,代理会先执行切面逻辑,再调用原方法。


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. 生命周期总流程(简化版)

  1. 实例化(Instantiation)

    • Spring 通过反射(构造函数/工厂方法)创建 Bean 实例。
  2. 属性赋值(Populate Properties)

    • 给 Bean 的属性注入依赖(依赖注入阶段)。
  3. BeanNameAware / BeanFactoryAware / ApplicationContextAware 回调

    • 如果 Bean 实现了这些接口,会被注入对应的上下文信息。
  4. BeanPostProcessor(前置处理)

    • 所有 BeanPostProcessor.postProcessBeforeInitialization() 调用。
  5. 初始化(Initialization)

    • 如果实现了 InitializingBean.afterPropertiesSet(),会调用。
    • 如果配置了 init-method,也会执行。
  6. BeanPostProcessor(后置处理)

    • 调用 BeanPostProcessor.postProcessAfterInitialization(),可替换或增强 Bean(如 AOP)。
  7. 使用阶段

    • Bean 被应用调用。
  8. 销毁(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。
  • DI(依赖注入)

    • IoC 的实现方式,通过构造器注入、Setter 注入或字段注入,把依赖自动注入到类中。
  • AOP(面向切面编程)

    • 把通用功能(如日志、事务、安全、缓存)从业务逻辑中分离,通过切面统一织入。
  • 声明式事务

    • 通过 @Transactional 注解实现事务管理,不必手写 begin/commit/rollback
  • 模块化

    • 核心容器、数据访问、Web、AOP、消息、测试等模块可以按需引入。

2. 核心模块

模块功能
spring-core核心容器与 IoC/DI 实现
spring-beansBean 定义与管理
spring-contextIoC 容器高级特性(事件发布、国际化等)
spring-aop面向切面编程实现
spring-tx事务管理
spring-jdbc简化 JDBC 数据访问
spring-web支持 Web 应用开发(Servlet API 集成)
spring-webmvcMVC 框架(Spring MVC)

3. 工作原理(简化流程)

  1. 容器启动

    • 加载配置(XML/注解/JavaConfig)。
    • 扫描包路径下的组件类(@Component@Service@Controller)。
  2. Bean 创建

    • 按定义实例化 Bean(构造函数、工厂方法)。
    • 执行依赖注入(DI)。
  3. AOP 织入

    • 匹配切面定义,把代理对象注入到容器。
  4. 应用运行

    • 控制器接收请求(Spring MVC)。
    • 调用业务层与数据访问层。
  5. 销毁阶段

    • 容器关闭,调用 Bean 的销毁方法。

4. 常用子项目

  • Spring Boot
    简化 Spring 应用开发,内置 Tomcat/Jetty,无需繁琐 XML 配置。
  • Spring Cloud
    微服务框架,提供注册中心、配置中心、网关、负载均衡等。
  • Spring Data
    统一数据访问方式,支持 JPA、MongoDB、Elasticsearch 等。
  • Spring Security
    权限与认证框架。
  • Spring Batch
    批处理框架,适合处理大批量数据任务。

5. 学习建议

  1. 先学 Spring Core + Spring MVC,掌握 IoC、DI、AOP、MVC 流程。
  2. 过渡到 Spring Boot,理解自动配置与约定优于配置。
  3. 了解 Spring Data / Spring Security 等常用模块。
  4. 最后学习 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.prototypenull
    

示例:

console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

3. 原型链查找过程

p1.toString();

查找过程:

  1. p1 自身找 toString → 找不到
  2. Person.prototype 找 → 找不到
  3. Object.prototype 找到 → 调用这个方法
  4. 找不到时返回 undefined

4. __proto__ vs prototype

属性属于谁作用
prototype构造函数定义实例共享的属性/方法
__proto__实例对象指向创建它的构造函数的 prototype

5. 使用场景

  1. 方法共享:节省内存,避免每个实例都生成一份相同方法。
  2. 继承:通过修改 prototype__proto__ 实现继承链。
  3. 多层继承:子类原型指向父类实例或父类原型,形成多层原型链。

继承示例:

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. 核心区别对比

特性varletconst
作用域函数作用域(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. 使用建议

  1. 优先用 const:默认不变的值保持不可变,防止误改。

  2. 需要重新赋值时用 let:如循环变量、累加器等。

  3. 避免使用 var:容易因变量提升和作用域问题导致 bug。

  4. 代码风格(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 一行显示,现在主流是 Flexinline-block
我建议你用 Flex,以后加对齐、等分、换行都方便。

1. 父容器属性(决定子元素排列方式)

属性常用值作用
displayflex / inline-flex开启 Flex 布局(inline-flex 不会独占一行)
flex-directionrow / row-reverse / column / column-reverse主轴方向(水平/垂直/反向)
flex-wrapnowrap / wrap / wrap-reverse是否换行及方向
flex-flow<flex-direction> <flex-wrap>上面两个属性的简写
justify-contentflex-start / flex-end / center / space-between / space-around / space-evenly沿主轴对齐方式
align-itemsstretch / flex-start / flex-end / center / baseline交叉轴对齐方式(子项在单行中如何对齐)
align-content同上多行时交叉轴的行与行之间的对齐(对多行布局才生效)

2. 子元素属性(决定自身在 Flex 容器里的表现)

属性作用
order控制子项在容器中的排列顺序(数字越小越靠前)
flex-grow剩余空间的放大比例(默认 0,不放大)
flex-shrink空间不足时的缩小比例(默认 1,可缩小)
flex-basis子项在分配空间前占据的主轴尺寸(默认 auto
flexflex-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: blockCSS 布局的基础属性之一,用来让一个元素以 块级元素(block-level element) 的方式渲染。


1. 作用

当一个元素是 display: block 时,它会:

  1. 独占一行(不与其他元素并排显示,默认会换行)。
  2. 宽度默认占满父容器(可以通过 width 改变)。
  3. 可以设置宽高width / height 有效)。
  4. 上下可有 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. 核心思路

  1. 流式布局:用百分比、automin/max-width 等代替固定像素。
  2. 媒体查询(Media Queries) :针对不同屏幕宽度、分辨率,切换样式。
  3. 弹性/网格布局flexgrid 可以在不同空间下自动调整元素位置和大小。
  4. 响应式图片:不同分辨率加载不同尺寸的图片。
  5. 断点(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. 实战建议

  1. 移动优先(Mobile First)

    • 默认写小屏样式 → 用 @media (min-width) 逐渐增强适配大屏。
  2. 内容优先(Content First)

    • 断点设计以内容布局变化为依据,不要只看设备型号。
  3. 组件化

    • 每个组件自己处理响应式,减少全局样式冲突。
  4. 配合框架

    • Bootstrap、Tailwind CSS、Ant Design 都有现成的响应式工具类。
  5. 测试

    • Chrome DevTools → Device Toolbar → 模拟不同屏幕查看效果。

那就是局部容器用 display: flex,它的子元素才会受 flex 规则影响,其它区域还是普通布局。


1. 局部 Flex 布局的核心

.container {
  display: flex; /* 只让这个容器内的子元素参与 flex 布局 */
}
  • Flex 的作用域:只影响设置了 display: flex 的元素的 直接子元素
  • 外部布局可以是普通的 blockinline-blockgrid 等。
  • 内部可以自由用 flex-directionjustify-contentalign-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、失败重试、分片广播等功能。

  • 核心由两部分组成:

    1. 调度中心(xxl-job-admin) :管理任务、分配执行器。
    2. 执行器(xxl-job-executor) :你的业务服务,接收调度中心的调度并执行任务。

2. 部署调度中心(xxl-job-admin)

  1. 下载源码

    git clone https://github.com/xuxueli/xxl-job.git
    
  2. 导入 SQL

    • 找到 xxl-job/doc/db/tables_xxl_job.sql
    • 在 MySQL 中执行,初始化调度中心数据库表。
  3. 修改配置

    • 编辑 xxl-job-admin/src/main/resources/application.properties

      spring.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
      
  4. 启动调度中心

    cd xxl-job-admin
    mvn spring-boot:run
    

    访问 http://localhost:8080/xxl-job-admin(默认 admin / 123456 登录)。


3. 集成执行器(在你的 Spring Boot 项目中)

  1. 引入依赖

    <dependency>
        <groupId>com.xuxueli</groupId>
        <artifactId>xxl-job-core</artifactId>
        <version>2.4.0</version>
    </dependency>
    
  2. 配置 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:
    
  3. 注册执行器配置类

    @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;
        }
    }
    
  4. 编写任务 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. 在调度中心创建任务

  1. 登录调度中心 → 任务管理 → 新增任务
  2. 选择执行器 my-job-executor
  3. 填写 JobHandlerdemoJobHandler
  4. 配置 Cron 表达式(如 0/10 * * * * ? 表示每 10 秒执行一次)
  5. 保存 → 启动任务

5. 常用功能

  • 分片广播:适合分布式批量处理任务(执行器数 = 分片数)。
  • 失败重试:任务失败后按配置次数重试。
  • GLUE 模式:支持在线编写 Java 代码任务(无需部署)。
  • 路由策略:如 FIRST、RANDOM、ROUND、CONSISTENT_HASH 等。
  • 日志查看:执行日志可在页面直接查看。

6. XXL-JOB 优势

  • Web 界面管理任务,支持动态修改。
  • 集群调度,支持多执行器部署。
  • 支持分布式任务分片与路由策略。
  • 任务失败报警(邮件/钉钉/企业微信)。

一、核心思路(最简流程)

  1. 采集文档并分配 docID
  2. 规范化文本(小写、Unicode 归一、去标点/HTML、同义与词干可选)
  3. 分词(英文可按词边界;中文需用分词器)
  4. 遍历每个 (term, docID, position),写入字典:term -> postings
  5. 对每个 term 的 Posting List 按 docID 升序,合并重复,计算 tf,可选保存 positions
  6. 持久化:写出 词典(term→文件偏移、DF 等)与 倒排表文件(docID 列表、tf、positions),并进行压缩(gap+可变长编码)
  7. 额外表: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):

  1. 文档 JSON 通过 Analyzer(分词器) 分解成 Token。
  2. Token 按字段构建倒排表,写入 Segment(不可变)。
  3. Segment 写入文件系统(默认基于 Lucene + mmap)。
  4. 写入前先进入 内存缓冲区(Index Buffer)→ translog(事务日志)保证宕机恢复。
  5. 定期 flush:生成新的 Segment 并清空缓冲区。
  6. 定期 merge:将多个小 Segment 合并成大 Segment(减少文件数,提高搜索效率)。

3. 搜索原理

搜索是 分布式 + 并行 的过程。

查询流程(Search Flow):

  1. 协调节点(Coordinator Node)接收查询请求。
  2. 将请求转发给所有分片(Primary 或 Replica 都可)。
  3. 每个分片在自己的 倒排索引(Segment) 中执行查询。
  4. 分片返回 Top N 结果(docID + score)。
  5. 协调节点合并排序,返回最终结果。

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. 通信过程

  1. 客户端发起 HTTP 请求(带有 Upgrade: websocket 头),请求升级为 WebSocket。
  2. 服务器响应 101 状态码,表示协议切换成功。
  3. 握手完成后,双方可以通过 TCP 直接交换数据帧。
  4. 保持长连接,直到一方主动关闭。

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 使用优势

  • 跨平台一致性:一次构建,到处运行。
  • 轻量快速:比虚拟机启动快,资源占用少。
  • 易于分发:直接推送到镜像仓库即可分享。
  • 隔离性好:每个容器互不影响。