Skip to content

阶段六:中间件与运维(2-3周) ​

作为资深前端开发者,你已经具备了工程化部署和性能优化的经验。本阶段将学习Java后端生态中的核心中间件(Redis、Elasticsearch)和运维工具(Docker、K8s、CI/CD、监控),帮助你构建高可用、高性能的后端系统。


6.1 缓存(第1-5天) ​

6.1.1 Redis概述 ​

Redis(Remote Dictionary Server)是一个基于内存的高性能键值存储数据库,广泛用于缓存、消息队列、分布式锁等场景。

与前端概念对比:

前端概念Redis
localStorage / sessionStorageRedis缓存
IndexedDBRedis持久化存储
Vuex / Pinia 状态管理Redis集中式缓存
Service Worker缓存Redis CDN缓存
WebSocket实时通信Redis Pub/Sub消息

6.1.2 Redis安装与基本使用 ​

Windows安装 ​

  1. 下载:Redis Windows版
  2. 解压到指定目录,如 D:\redis
  3. 启动服务:
bash
redis-server.exe redis.conf

Docker安装(推荐) ​

bash
docker run -d --name redis -p 6379:6379 redis:7.0

GUI工具 ​

  • RedisInsight(官方免费,推荐)
  • Another Redis Desktop Manager(免费开源)
  • Redis Manager(IDEA插件)

Redis命令行基本操作 ​

bash
# 连接Redis
redis-cli

# 字符串操作
SET name "张三"
GET name
SET counter 100
INCR counter          # 自增,counter变为101
DECR counter          # 自减,counter变为100
INCRBY counter 10     # 增加10

# 设置过期时间
SET token "abc123" EX 3600     # 3600秒后过期
SET code "1234" PX 60000       # 60000毫秒后过期
EXPISE name 300                 # 给已有key设置过期时间
TTL name                        # 查看剩余生存时间(秒)

# Hash操作(类似JS对象)
HSET user:1 name "张三"
HSET user:1 age 25
HSET user:1 email "zhangsan@example.com"
HGET user:1 name               # 获取单个字段
HGETALL user:1                  # 获取所有字段
HMSET user:1 name "张三" age 25 email "zhangsan@example.com"  # 批量设置

# List操作(类似JS数组)
LPUSH mylist "A" "B" "C"       # 从左边插入
RPUSH mylist "D" "E"           # 从右边插入
LPOP mylist                     # 从左边弹出
RPOP mylist                     # 从右边弹出
LRANGE mylist 0 -1              # 获取所有元素
LLEN mylist                     # 获取长度

# Set操作(类似JS Set)
SADD myset "A" "B" "C"
SADD myset "C" "D"              # C重复,不会添加
SMEMBERS myset                   # 获取所有成员
SISMEMBER myset "A"             # 是否包含
SCARD myset                      # 获取元素个数

# Sorted Set操作(带分数的Set)
ZADD leaderboard 100 "张三"
ZADD leaderboard 200 "李四"
ZADD leaderboard 150 "王五"
ZRANGE leaderboard 0 -1 WITHSCORES      # 按分数升序
ZREVRANGE leaderboard 0 -1 WITHSCORES   # 按分数降序
ZINCRBY leaderboard 50 "张三"           # 增加分数

# 通用命令
KEYS *                          # 查看所有key(生产慎用)
SCAN 0 MATCH user:* COUNT 100  # 分批获取key
DEL name                        # 删除key
EXISTS name                     # 判断key是否存在
TYPE name                       # 查看key的类型
FLUSHDB                         # 清空当前数据库

6.1.3 Redis数据结构与使用场景 ​

五种基本数据类型 ​

数据类型底层结构典型场景前端类比
StringSDS(简单动态字符串)缓存、计数器、分布式锁localStorage
Hash字典(HashTable)对象存储、购物车JS Object
List双向链表 + 压缩列表消息队列、最新列表Array
Set整数集合 + 字典标签、共同关注、去重JS Set
ZSet跳表 + 字典排行榜、延迟队列无直接对应

特殊数据类型 ​

  • HyperLogLog:基数统计(UV统计)
  • Bitmap:位图(签到、在线状态)
  • Stream:消息队列(类似Kafka)
  • GEO:地理位置(附近的人)

6.1.4 Spring Boot集成Redis ​

Maven依赖 ​

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 连接池依赖 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

application.yml配置 ​

yaml
spring:
  redis:
    host: localhost
    port: 6379
    password:                    # 密码,没有则留空
    database: 0                  # 数据库索引(0-15)
    timeout: 5000ms              # 连接超时时间
    lettuce:
      pool:
        max-active: 20           # 最大连接数
        max-idle: 10             # 最大空闲连接
        min-idle: 5              # 最小空闲连接
        max-wait: 3000ms         # 最大等待时间

RedisTemplate配置类 ​

java
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 使用Jackson2JsonRedisSerializer作为值的序列化器
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(
            mapper.getPolymorphicTypeValidator(),
            ObjectMapper.DefaultTyping.NON_FINAL
        );
        jsonSerializer.setObjectMapper(mapper);

        // 使用StringRedisSerializer作为key的序列化器
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // key使用String序列化
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // value使用JSON序列化
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

RedisTemplate操作示例 ​

java
@Service
public class RedisService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // ==================== String操作 ====================

    public void setString(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public void setStringWithExpire(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public Object getString(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }

    public Long increment(String key) {
        return redisTemplate.opsForValue().increment(key);
    }

    public Long incrementBy(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    // ==================== Hash操作 ====================

    public void setHash(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    public void setHashAll(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    public Object getHash(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    public Map<Object, Object> getHashAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    public Long deleteHash(String key, String... hashKeys) {
        return redisTemplate.opsForHash().delete(key, (Object[]) hashKeys);
    }

    // ==================== List操作 ====================

    public void pushLeft(String key, Object value) {
        redisTemplate.opsForList().leftPush(key, value);
    }

    public void pushRight(String key, Object value) {
        redisTemplate.opsForList().rightPush(key, value);
    }

    public Object popLeft(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    public Object popRight(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }

    public List<Object> range(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    public Long listSize(String key) {
        return redisTemplate.opsForList().size(key);
    }

    // ==================== Set操作 ====================

    public void addToSet(String key, Object... values) {
        redisTemplate.opsForSet().add(key, values);
    }

    public Set<Object> getSetMembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    public Boolean isSetMember(String key, Object value) {
        return redisTemplate.opsForSet().isMember(key, value);
    }

    public Long setSize(String key) {
        return redisTemplate.opsForSet().size(key);
    }

    // ==================== ZSet操作 ====================

    public void addToZSet(String key, Object value, double score) {
        redisTemplate.opsForZSet().add(key, value, score);
    }

    public Set<Object> zSetRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

    public Set<Object> zSetReverseRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().reverseRange(key, start, end);
    }

    public Double zSetScore(String key, Object value) {
        return redisTemplate.opsForZSet().score(key, value);
    }

    public Long zSetRank(String key, Object value) {
        return redisTemplate.opsForZSet().rank(key, value);
    }

    // ==================== 通用操作 ====================

    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    public Long getExpire(String key) {
        return redisTemplate.getExpire(key);
    }

    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }
}

实际业务示例:用户缓存 ​

java
@Service
public class UserCacheService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String USER_KEY_PREFIX = "user:";
    private static final long USER_CACHE_TTL = 30; // 分钟

    /**
     * 查询用户(先查缓存,缓存没有再查数据库)
     */
    public User getUserById(Long id) {
        String key = USER_KEY_PREFIX + id;

        // 1. 先查缓存
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }

        // 2. 缓存没有,查数据库
        user = userMapper.selectById(id);
        if (user != null) {
            // 3. 写入缓存,设置过期时间
            redisTemplate.opsForValue().set(key, user, USER_CACHE_TTL, TimeUnit.MINUTES);
        }

        return user;
    }

    /**
     * 更新用户(同时更新缓存)
     */
    public void updateUser(User user) {
        String key = USER_KEY_PREFIX + user.getId();
        userMapper.updateById(user);
        // 更新缓存
        redisTemplate.opsForValue().set(key, user, USER_CACHE_TTL, TimeUnit.MINUTES);
    }

    /**
     * 删除用户(同时删除缓存)
     */
    public void deleteUser(Long id) {
        String key = USER_KEY_PREFIX + id;
        userMapper.deleteById(id);
        redisTemplate.delete(key);
    }
}

使用@Cacheable注解简化缓存 ​

java
// 开启缓存支持
@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    // 查询时自动缓存
    @Cacheable(value = "product", key = "#id")
    public Product getProductById(Long id) {
        return productMapper.selectById(id);
    }

    // 更新时自动清除缓存
    @CacheEvict(value = "product", key = "#product.id")
    public void updateProduct(Product product) {
        productMapper.updateById(product);
    }

    // 更新时自动更新缓存
    @CachePut(value = "product", key = "#product.id")
    public Product updateProductAndReturn(Product product) {
        productMapper.updateById(product);
        return product;
    }

    // 清除所有缓存
    @CacheEvict(value = "product", allEntries = true)
    public void clearAllProductCache() {
        // 清除所有product缓存
    }
}

6.1.5 缓存穿透、击穿、雪崩 ​

缓存穿透(Cache Penetration) ​

问题描述: 查询一个不存在的数据,缓存中没有,每次请求都会打到数据库。就像子弹穿透了缓存一样。

前端类比: 就像前端请求一个不存在的API,缓存和CDN都没有命中,每次都打到后端。

解决方案:

java
@Service
public class CachePenetrationSolution {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private UserMapper userMapper;

    /**
     * 方案1:缓存空值(简单但不够优雅)
     */
    public User getUserById_CacheNull(Long id) {
        String key = "user:" + id;
        Object cached = redisTemplate.opsForValue().get(key);

        // 如果缓存的是空值标记,直接返回null
        if ("NULL".equals(cached)) {
            return null;
        }

        if (cached != null) {
            return (User) cached;
        }

        User user = userMapper.selectById(id);
        if (user == null) {
            // 缓存空值,设置较短的过期时间
            redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
            return null;
        }

        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        return user;
    }

    /**
     * 方案2:布隆过滤器(推荐,高效)
     * 在缓存之前加一层布隆过滤器,快速判断数据是否存在
     */
    @Component
    public class BloomFilterManager {
        // 使用Redis的Bitmap实现布隆过滤器
        // 或者使用Google Guava的BloomFilter
        private BloomFilter<Long> bloomFilter;

        @PostConstruct
        public void init() {
            // 创建布隆过滤器
            // 预期元素数量10000,误判率0.01
            bloomFilter = BloomFilter.create(
                Funnels.longFunnel(), 10000, 0.01);

            // 加载所有用户ID到布隆过滤器
            loadUserIds();
        }

        private void loadUserIds() {
            // 从数据库加载所有用户ID
            // bloomFilter.put(userId);
        }

        public boolean mightContain(Long id) {
            return bloomFilter.mightContain(id);
        }
    }

    public User getUserById_BloomFilter(Long id) {
        // 1. 先查布隆过滤器
        if (!bloomFilterManager.mightContain(id)) {
            return null; // 一定不存在
        }

        // 2. 布隆过滤器说可能存在,再查缓存
        String key = "user:" + id;
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }

        // 3. 查数据库
        user = userMapper.selectById(id);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        }

        return user;
    }
}

缓存击穿(Cache Breakdown) ​

问题描述: 一个热点key在过期的瞬间,大量并发请求同时打到数据库。

前端类比: 就像前端某个热门数据缓存失效,瞬间大量请求同时发起,导致后端压力骤增。

解决方案:

java
@Service
public class CacheBreakdownSolution {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ProductMapper productMapper;

    /**
     * 方案1:互斥锁(分布式锁)
     * 只允许一个线程查询数据库,其他线程等待
     */
    public Product getProductById_Mutex(Long id) {
        String key = "product:" + id;
        Product product = (Product) redisTemplate.opsForValue().get(key);

        if (product != null) {
            return product;
        }

        // 尝试获取分布式锁
        String lockKey = "lock:product:" + id;
        String lockValue = UUID.randomUUID().toString();

        try {
            // 加锁,设置过期时间防止死锁
            Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);

            if (Boolean.TRUE.equals(locked)) {
                // 获取锁成功,查询数据库
                product = productMapper.selectById(id);
                if (product != null) {
                    redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
                }
            } else {
                // 获取锁失败,休眠后重试
                Thread.sleep(50);
                return getProductById_Mutex(id); // 递归重试
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 释放锁(只释放自己的锁)
            Object currentLockValue = redisTemplate.opsForValue().get(lockKey);
            if (lockValue.equals(currentLockValue)) {
                redisTemplate.delete(lockKey);
            }
        }

        return product;
    }

    /**
     * 方案2:逻辑过期(推荐)
     * 不设置Redis过期时间,在value中存储逻辑过期时间
     */
    public Product getProductById_LogicalExpire(Long id) {
        String key = "product:" + id;
        Map<String, Object> cached = (Map<String, Object>) redisTemplate.opsForValue().get(key);

        if (cached == null) {
            // 缓存不存在,直接查数据库并重建缓存
            return rebuildCache(id);
        }

        Product product = (Product) cached.get("product");
        Long expireTime = (Long) cached.get("expireTime");

        if (System.currentTimeMillis() < expireTime) {
            // 未过期,直接返回
            return product;
        }

        // 已过期,开启异步线程重建缓存
        String lockKey = "lock:product:" + id;
        String lockValue = UUID.randomUUID().toString();
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);

        if (Boolean.TRUE.equals(locked)) {
            // 获取锁成功,异步重建缓存
            CompletableFuture.runAsync(() -> {
                try {
                    rebuildCache(id);
                } finally {
                    redisTemplate.delete(lockKey);
                }
            });
        }

        // 返回旧数据(保证可用性)
        return product;
    }

    private Product rebuildCache(Long id) {
        Product product = productMapper.selectById(id);
        if (product != null) {
            String key = "product:" + id;
            Map<String, Object> cacheMap = new HashMap<>();
            cacheMap.put("product", product);
            cacheMap.put("expireTime", System.currentTimeMillis() + 30 * 60 * 1000L);
            redisTemplate.opsForValue().set(key, cacheMap);
        }
        return product;
    }
}

缓存雪崩(Cache Avalanche) ​

问题描述: 大量缓存key在同一时间过期,导致大量请求同时打到数据库。

前端类比: 就像前端多个页面的缓存同时失效,瞬间所有请求都打到后端。

解决方案:

java
@Service
public class CacheAvalancheSolution {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 方案1:过期时间加随机值
     * 避免大量key同时过期
     */
    public void setWithRandomExpire(String key, Object value) {
        // 基础过期时间30分钟 + 随机0-5分钟
        long baseTime = 30;
        long randomTime = ThreadLocalRandom.current().nextLong(0, 5);
        redisTemplate.opsForValue().set(key, value, baseTime + randomTime, TimeUnit.MINUTES);
    }

    /**
     * 方案2:多级缓存
     * 本地缓存 + Redis缓存 + 数据库
     */
    @Component
    public class MultiLevelCache {

        // 本地缓存(Caffeine)
        private final Cache<String, Object> localCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();

        @Autowired
        private RedisTemplate<String, Object> redisTemplate;

        public Object get(String key) {
            // 1. 查本地缓存
            Object value = localCache.getIfPresent(key);
            if (value != null) {
                return value;
            }

            // 2. 查Redis缓存
            value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                localCache.put(key, value); // 回填本地缓存
                return value;
            }

            // 3. 查数据库(由调用方处理)
            return null;
        }

        public void put(String key, Object value) {
            localCache.put(key, value);
            redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
        }
    }

    /**
     * 方案3:服务降级与限流
     */
    @Component
    public class CacheFallbackHandler {

        // 当缓存和数据库都不可用时,返回默认值
        public <T> T getOrDefault(String key, T defaultValue, Supplier<T> dbQuery) {
            try {
                T result = dbQuery.get();
                return result != null ? result : defaultValue;
            } catch (Exception e) {
                // 数据库查询失败,返回默认值
                return defaultValue;
            }
        }
    }
}

6.1.6 Redis集群与高可用 ​

Redis主从复制 ​

Master(主节点)
├── Slave1(从节点1,只读)
├── Slave2(从节点2,只读)
└── Slave3(从节点3,只读)

配置主从复制:

bash
# 从节点配置(redis-slave.conf)
replicaof 192.168.1.100 6379    # 主节点IP和端口
masterauth your_master_password  # 主节点密码

Redis哨兵模式(Sentinel) ​

Sentinel集群(3个哨兵)
├── Sentinel-1(监控Master和Slave)
├── Sentinel-2
└── Sentinel-3
    │
    ├── Master(主节点)
    ├── Slave1(从节点1)
    └── Slave2(从节点2)

哨兵配置(sentinel.conf):

bash
# 监控主节点,名称mymaster,最少2个哨兵同意
sentinel monitor mymaster 192.168.1.100 6379 2

# 主节点下线判定时间(毫秒)
sentinel down-after-milliseconds mymaster 5000

# 故障转移超时时间
sentinel failover-timeout mymaster 60000

# 同步时从节点数量
sentinel parallel-syncs mymaster 1

Spring Boot配置哨兵:

yaml
spring:
  redis:
    sentinel:
      master: mymaster
      nodes: 192.168.1.100:26379,192.168.1.101:26379,192.168.1.102:26379
    password: your_password

Redis Cluster集群 ​

Redis Cluster(6节点,3主3从)
├── Master-1(端口7001)── Slave-1(端口7004)
├── Master-2(端口7002)── Slave-2(端口7005)
└── Master-3(端口7003)── Slave-3(端口7006)

数据分片:16384个slot
Master-1: 0-5460
Master-2: 5461-10922
Master-3: 10923-16383

Cluster配置(redis-cluster.conf):

bash
port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes

Spring Boot配置Cluster:

yaml
spring:
  redis:
    cluster:
      nodes: 192.168.1.100:7001,192.168.1.100:7002,192.168.1.100:7003,
             192.168.1.100:7004,192.168.1.100:7005,192.168.1.100:7006
      max-redirects: 3
    password: your_password

6.1.7 分布式锁(Redisson) ​

为什么需要分布式锁 ​

前端类比: 就像前端使用乐观锁(版本号)来处理并发编辑冲突,后端在分布式环境下也需要分布式锁来保证数据一致性。

Redisson简介 ​

Redisson是一个基于Redis的Java驻内存数据网格(In-Memory Data Grid),提供了分布式锁、分布式集合、分布式对象等功能。

Maven依赖 ​

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>

配置 ​

yaml
spring:
  redis:
    redisson:
      config: |
        singleServerConfig:
          address: "redis://127.0.0.1:6379"
          password: null
          database: 0
          connectionPoolSize: 64
          connectionMinimumIdleSize: 10

分布式锁使用示例 ​

java
@Service
public class DistributedLockService {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 基本分布式锁
     */
    public void basicLock() {
        RLock lock = redissonClient.getLock("myLock");
        try {
            // 加锁(阻塞等待)
            lock.lock();

            // 执行业务逻辑
            doBusiness();

        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 带超时的分布式锁
     */
    public void lockWithTimeout() {
        RLock lock = redissonClient.getLock("myLock");
        try {
            // 尝试加锁,等待3秒,锁定后10秒自动释放
            boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (acquired) {
                doBusiness();
            } else {
                // 获取锁失败
                System.out.println("获取锁失败");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 公平锁(按请求顺序获取锁)
     */
    public void fairLock() {
        RLock fairLock = redissonClient.getFairLock("myFairLock");
        try {
            fairLock.lock();
            doBusiness();
        } finally {
            if (fairLock.isHeldByCurrentThread()) {
                fairLock.unlock();
            }
        }
    }

    /**
     * 读写锁(读读不互斥,读写互斥)
     */
    public void readWriteLock() {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock("myRWLock");

        // 读锁
        RLock readLock = rwLock.readLock();
        readLock.lock();
        try {
            // 读操作
            readData();
        } finally {
            readLock.unlock();
        }

        // 写锁
        RLock writeLock = rwLock.writeLock();
        writeLock.lock();
        try {
            // 写操作
            writeData();
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 联锁(同时锁定多个锁)
     */
    public void multiLock() {
        RLock lock1 = redissonClient.getLock("lock1");
        RLock lock2 = redissonClient.getLock("lock2");
        RLock lock3 = redissonClient.getLock("lock3");

        RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3);
        try {
            multiLock.lock();
            doBusiness();
        } finally {
            multiLock.unlock();
        }
    }

    /**
     * 红锁(RedLock,跨多个Redis实例)
     */
    public void redLock() {
        RLock lock1 = redissonClient.getLock("lock1");
        RLock lock2 = redissonClient.getLock("lock2");
        RLock lock3 = redissonClient.getLock("lock3");

        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        try {
            // 只要有一个实例加锁成功即可
            redLock.lock();
            doBusiness();
        } finally {
            redLock.unlock();
        }
    }

    /**
     * 实际业务场景:库存扣减
     */
    public boolean deductStock(Long productId, int quantity) {
        String lockKey = "stock:lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);

        try {
            boolean acquired = lock.tryLock(3, TimeUnit.SECONDS);
            if (!acquired) {
                return false;
            }

            // 查询库存
            String stockKey = "stock:" + productId;
            int stock = (int) redissonClient.getBucket(stockKey).get();

            if (stock >= quantity) {
                // 扣减库存
                redissonClient.getBucket(stockKey).set(stock - quantity);
                return true;
            }
            return false;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private void doBusiness() {
        // 业务逻辑
    }

    private void readData() {
        // 读操作
    }

    private void writeData() {
        // 写操作
    }
}

使用注解实现分布式锁 ​

java
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key();
    long waitTime() default 3;
    long leaseTime() default 10;
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

// AOP切面
@Aspect
@Component
public class DistributedLockAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(distributedLock)")
    public Object around(ProceedingJoinPoint joinPoint,
                         DistributedLock distributedLock) throws Throwable {
        String lockKey = "lock:" + distributedLock.key();
        RLock lock = redissonClient.getLock(lockKey);

        try {
            boolean acquired = lock.tryLock(
                distributedLock.waitTime(),
                distributedLock.leaseTime(),
                distributedLock.timeUnit()
            );

            if (!acquired) {
                throw new RuntimeException("获取分布式锁失败: " + lockKey);
            }

            return joinPoint.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

// 使用注解
@Service
public class OrderService {

    @DistributedLock(key = "'order:' + #orderId", waitTime = 5)
    public void processOrder(Long orderId) {
        // 处理订单
    }
}

6.2 搜索引擎(第6-9天) ​

6.2.1 Elasticsearch概述 ​

Elasticsearch(ES)是一个基于Lucene的分布式全文搜索引擎,提供近实时的搜索和分析功能。

与前端概念对比:

前端概念Elasticsearch
浏览器Ctrl+F搜索ES全文检索
前端本地搜索(如表格筛选)ES服务端搜索
前端分页ES分页查询
前端排序ES排序与聚合
前端模糊匹配ES模糊查询

Elasticsearch vs MySQL:

对比项MySQLElasticsearch
数据存储行存储倒排索引
查询方式SQL精确查询全文检索 + 精确查询
事务支持支持ACID不支持事务
实时性实时写入近实时(1秒延迟)
适用场景核心业务数据搜索、日志分析
数据一致性强一致最终一致

6.2.2 倒排索引原理 ​

正排索引(Forward Index) ​

文档ID → 文档内容
doc1 → "Java编程入门"
doc2 → "Python数据分析"
doc3 → "Java与Python对比"

倒排索引(Inverted Index) ​

词条 → 包含该词条的文档ID列表
Java    → [doc1, doc3]
Python  → [doc2, doc3]
编程    → [doc1]
入门    → [doc1]
数据分析 → [doc2]
对比    → [doc3]

倒排索引结构:

Term(词条)
├── Term Dictionary(词条字典,有序)
├── Term Index(词条索引,FST结构,加速查找)
└── Posting List(倒排列表,文档ID + 词频 + 位置)

6.2.3 Elasticsearch安装与基本操作 ​

Docker安装 ​

bash
# 单节点安装
docker run -d --name elasticsearch \
  -p 9200:9200 \
  -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  elasticsearch:8.9.0

# 安装Kibana(可视化管理)
docker run -d --name kibana \
  -p 5601:5601 \
  -e "ELASTICSEARCH_HOSTS=http://elasticsearch:9200" \
  --link elasticsearch \
  kibana:8.9.0

基本REST API ​

bash
# 查看集群健康状态
GET /_cluster/health

# 查看集群节点
GET /_cat/nodes?v

# 查看索引列表
GET /_cat/indices?v

# 创建索引
PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "id": { "type": "long" },
      "name": { "type": "text", "analyzer": "ik_max_word" },
      "description": { "type": "text", "analyzer": "ik_smart" },
      "price": { "type": "double" },
      "category": { "type": "keyword" },
      "brand": { "type": "keyword" },
      "status": { "type": "integer" },
      "createdAt": { "type": "date" }
    }
  }
}

# 添加文档
POST /products/_doc/1
{
  "name": "iPhone 15 Pro Max",
  "description": "苹果公司最新旗舰手机,搭载A17 Pro芯片",
  "price": 9999.00,
  "category": "手机",
  "brand": "Apple",
  "status": 1,
  "createdAt": "2024-01-01T00:00:00"
}

# 批量添加文档
POST /products/_bulk
{"index": {"_id": "2"}}
{"name": "华为Mate 60 Pro", "description": "华为最新旗舰,搭载麒麟9000S芯片", "price": 6999.00, "category": "手机", "brand": "华为", "status": 1, "createdAt": "2024-01-02T00:00:00"}
{"index": {"_id": "3"}}
{"name": "小米14 Ultra", "description": "小米旗舰手机,徕卡影像系统", "price": 5999.00, "category": "手机", "brand": "小米", "status": 1, "createdAt": "2024-01-03T00:00:00"}

# 查询文档
GET /products/_doc/1

# 搜索文档
GET /products/_search
{
  "query": {
    "match": {
      "name": "手机"
    }
  }
}

# 更新文档
PUT /products/_doc/1
{
  "name": "iPhone 15 Pro Max 256GB",
  "price": 10999.00
}

# 局部更新
POST /products/_update/1
{
  "doc": {
    "price": 9499.00
  }
}

# 删除文档
DELETE /products/_doc/1

# 删除索引
DELETE /products

6.2.4 查询DSL详解 ​

全文检索查询 ​

bash
# match查询(分词后匹配)
GET /products/_search
{
  "query": {
    "match": {
      "name": "苹果 手机"
    }
  }
}

# match_phrase查询(短语匹配,要求顺序和位置)
GET /products/_search
{
  "query": {
    "match_phrase": {
      "description": "旗舰手机"
    }
  }
}

# multi_match查询(多字段搜索)
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "华为",
      "fields": ["name^3", "description^2", "brand"]
    }
  }
}

精确查询 ​

bash
# term查询(不分词,精确匹配)
GET /products/_search
{
  "query": {
    "term": {
      "category": "手机"
    }
  }
}

# terms查询(多值匹配)
GET /products/_search
{
  "query": {
    "terms": {
      "brand": ["Apple", "华为", "小米"]
    }
  }
}

# range查询(范围查询)
GET /products/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 5000,
        "lte": 10000
      }
    }
  }
}

# prefix查询(前缀查询)
GET /products/_search
{
  "query": {
    "prefix": {
      "name": "华为"
    }
  }
}

# wildcard查询(通配符查询)
GET /products/_search
{
  "query": {
    "wildcard": {
      "name": "华*"
    }
  }
}

复合查询 ​

bash
# bool查询(组合查询)
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "手机" } }
      ],
      "filter": [
        { "term": { "status": 1 } },
        { "range": { "price": { "gte": 5000, "lte": 10000 } } }
      ],
      "should": [
        { "match": { "brand": "华为" } }
      ],
      "must_not": [
        { "term": { "category": "配件" } }
      ]
    }
  }
}

# function_score查询(自定义评分)
GET /products/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "name": "手机" } },
      "functions": [
        {
          "field_value_factor": {
            "field": "price",
            "modifier": "log1p",
            "factor": 0.001
          }
        },
        {
          "gauss": {
            "createdAt": {
              "origin": "now",
              "scale": "30d",
              "offset": "7d"
            }
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

高亮显示 ​

bash
GET /products/_search
{
  "query": {
    "match": { "name": "手机" }
  },
  "highlight": {
    "fields": {
      "name": {},
      "description": {}
    },
    "pre_tags": ["<em>"],
    "post_tags": ["</em>"]
  }
}

6.2.5 聚合分析 ​

bash
# 聚合查询(类似SQL的GROUP BY)
GET /products/_search
{
  "size": 0,
  "aggs": {
    "category_agg": {
      "terms": {
        "field": "category",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": { "field": "price" }
        },
        "max_price": {
          "max": { "field": "price" }
        },
        "min_price": {
          "min": { "field": "price" }
        }
      }
    }
  }
}

# 日期直方图聚合
GET /products/_search
{
  "size": 0,
  "aggs": {
    "date_histogram": {
      "date_histogram": {
        "field": "createdAt",
        "calendar_interval": "month"
      }
    }
  }
}

# 范围聚合
GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "0-3000", "to": 3000 },
          { "key": "3000-6000", "from": 3000, "to": 6000 },
          { "key": "6000-10000", "from": 6000, "to": 10000 },
          { "key": "10000+", "from": 10000 }
        ]
      }
    }
  }
}

6.2.6 Spring Data Elasticsearch集成 ​

Maven依赖 ​

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

application.yml配置 ​

yaml
spring:
  elasticsearch:
    uris: http://localhost:9200
    # username: elastic       # 如果开启了安全认证
    # password: your_password
    connection-timeout: 5s
    socket-timeout: 30s

实体类定义 ​

java
@Data
@Document(indexName = "products")
@Setting(shards = 3, replicas = 1)
public class ProductDocument {

    @Id
    private Long id;

    @MultiField(
        mainField = @Field(type = FieldType.Text, analyzer = "ik_max_word"),
        subFields = @Field(type = FieldType.Keyword)
    )
    private String name;

    @Field(type = FieldType.Text, analyzer = "ik_smart")
    private String description;

    @Field(type = FieldType.Double)
    private Double price;

    @Field(type = FieldType.Keyword)
    private String category;

    @Field(type = FieldType.Keyword)
    private String brand;

    @Field(type = FieldType.Integer)
    private Integer status;

    @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second_millis)
    private LocalDateTime createdAt;
}

Repository接口 ​

java
public interface ProductRepository extends ReactiveElasticsearchRepository<ProductDocument, Long> {

    // 方法命名查询
    Flux<ProductDocument> findByNameContaining(String name);

    Flux<ProductDocument> findByCategory(String category);

    Flux<ProductDocument> findByPriceBetween(Double min, Double max);

    Flux<ProductDocument> findByBrandAndStatus(String brand, Integer status);

    // 自定义查询
    @Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
    Flux<ProductDocument> searchByName(String keyword);
}

ElasticsearchRestTemplate操作 ​

java
@Service
public class ProductService {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    /**
     * 全文检索
     */
    public List<ProductDocument> search(String keyword, int page, int size) {
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        // 搜索条件
        queryBuilder.withQuery(QueryBuilders.multiMatchQuery(keyword, "name", "description", "brand"));

        // 分页
        queryBuilder.withPageable(PageRequest.of(page, size));

        // 高亮
        queryBuilder.withHighlightBuilder(
            new HighlightBuilder()
                .field("name")
                .field("description")
                .preTags("<em>")
                .postTags("</em>")
        );

        // 排序
        queryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));

        SearchHits<ProductDocument> searchHits = elasticsearchRestTemplate.search(
            queryBuilder.build(), ProductDocument.class);

        return searchHits.getSearchHits().stream()
            .map(SearchHit::getContent)
            .collect(Collectors.toList());
    }

    /**
     * 复合查询
     */
    public List<ProductDocument> complexSearch(String keyword, String category,
                                                Double minPrice, Double maxPrice) {
        BoolQuery.Builder boolQueryBuilder = QueryBuilders.boolQuery();

        // 关键词搜索
        if (StringUtils.hasText(keyword)) {
            boolQueryBuilder.must(QueryBuilders.multiMatchQuery(keyword, "name", "description"));
        }

        // 分类过滤
        if (StringUtils.hasText(category)) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("category", category));
        }

        // 价格范围
        if (minPrice != null || maxPrice != null) {
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
            if (minPrice != null) rangeQuery.gte(minPrice);
            if (maxPrice != null) rangeQuery.lte(maxPrice);
            boolQueryBuilder.filter(rangeQuery);
        }

        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
            .withQuery(boolQueryBuilder.build())
            .withPageable(PageRequest.of(0, 20));

        SearchHits<ProductDocument> searchHits = elasticsearchRestTemplate.search(
            queryBuilder.build(), ProductDocument.class);

        return searchHits.getSearchHits().stream()
            .map(SearchHit::getContent)
            .collect(Collectors.toList());
    }

    /**
     * 聚合查询
     */
    public Map<String, Long> aggregateByCategory() {
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
        .withAggregations(
            AggregationBuilders.terms("category_agg")
                .field("category")
                .size(10)
        );

    SearchHits<ProductDocument> searchHits = elasticsearchRestTemplate.search(
        queryBuilder.build(), ProductDocument.class);

    // 解析聚合结果
    // ...
    return Collections.emptyMap();
}
}

数据同步(MySQL → Elasticsearch) ​

java
@Service
public class DataSyncService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    /**
     * 全量同步
     */
    public void fullSync() {
        int page = 0;
        int size = 500;

        while (true) {
            Page<Product> productPage = productMapper.selectPage(
                new Page<>(page, size), null);

            List<ProductDocument> documents = productPage.getRecords().stream()
                .map(this::convertToDocument)
                .collect(Collectors.toList());

            if (documents.isEmpty()) break;

            elasticsearchRestTemplate.save(documents);
            page++;

            if (!productPage.hasNext()) break;
        }
    }

    /**
     * 增量同步(基于更新时间)
     */
    public void incrementalSync(LocalDateTime lastSyncTime) {
        List<Product> products = productMapper.selectList(
            new QueryWrapper<Product>()
                .ge("updated_at", lastSyncTime)
        );

        List<ProductDocument> documents = products.stream()
            .map(this::convertToDocument)
            .collect(Collectors.toList());

        elasticsearchRestTemplate.save(documents);
    }

    private ProductDocument convertToDocument(Product product) {
        ProductDocument doc = new ProductDocument();
        doc.setId(product.getId());
        doc.setName(product.getName());
        doc.setDescription(product.getDescription());
        doc.setPrice(product.getPrice());
        doc.setCategory(product.getCategory());
        doc.setBrand(product.getBrand());
        doc.setStatus(product.getStatus());
        doc.setCreatedAt(product.getCreatedAt());
        return doc;
    }
}

6.3 DevOps基础(第10-16天) ​

6.3.1 Docker容器化 ​

Docker与前端概念对比 ​

前端概念Docker
npm initdocker init
package.jsonDockerfile
npm run builddocker build
node_modulesDocker Image
运行项目docker run
docker-compose(前端也用)docker-compose(编排多个服务)
Vercel / Netlify部署Docker部署

Docker安装 ​

  1. 下载:Docker Desktop
  2. 安装后验证:
bash
docker --version
docker-compose --version

Docker基本命令 ​

bash
# 镜像操作
docker pull nginx:latest           # 拉取镜像
docker images                      # 查看本地镜像
docker rmi nginx:latest            # 删除镜像
docker image prune                 # 清理无用镜像

# 容器操作
docker ps                          # 查看运行中的容器
docker ps -a                       # 查看所有容器
docker run -d --name mynginx -p 8080:80 nginx  # 运行容器
docker stop mynginx                # 停止容器
docker start mynginx               # 启动容器
docker restart mynginx             # 重启容器
docker rm mynginx                  # 删除容器
docker logs mynginx                # 查看日志
docker exec -it mynginx /bin/bash  # 进入容器

# 容器与宿主机文件交互
docker cp mynginx:/usr/share/nginx/html/index.html ./index.html  # 容器→宿主机
docker cp ./index.html mynginx:/usr/share/nginx/html/index.html  # 宿主机→容器

6.3.2 Dockerfile编写 ​

Java应用Dockerfile ​

dockerfile
# 多阶段构建(推荐)

# 第一阶段:构建
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B

# 第二阶段:运行
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app

# 设置时区
RUN apk add --no-cache tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

# 创建非root用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# 从构建阶段复制jar包
COPY --from=builder /app/target/*.jar app.jar

# 暴露端口
EXPOSE 8080

# JVM参数优化
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD wget -q --spider http://localhost:8080/actuator/health || exit 1

# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

前端应用Dockerfile(对比) ​

dockerfile
# 前端构建
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --registry=https://registry.npmmirror.com
COPY . .
RUN npm run build

# Nginx运行
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

完整Spring Boot项目Dockerfile ​

dockerfile
# 基础镜像
FROM eclipse-temurin:17-jdk-alpine AS builder

# 设置工作目录
WORKDIR /app

# 先复制Maven配置,利用Docker缓存
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .

# 下载依赖(利用Docker缓存层)
RUN ./mvnw dependency:go-offline -B

# 复制源代码
COPY src ./src

# 构建
RUN ./mvnw clean package -DskipTests -B

# 运行阶段
FROM eclipse-temurin:17-jre-alpine

# 安装必要工具
RUN apk add --no-cache curl tzdata

# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

WORKDIR /app

# 从构建阶段复制
COPY --from=builder /app/target/*.jar app.jar

# 创建日志目录
RUN mkdir -p /app/logs

# 暴露端口
EXPOSE 8080

# 环境变量
ENV SPRING_PROFILES_ACTIVE=prod
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC"

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

# 启动
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Dspring.profiles.active=$SPRING_PROFILES_ACTIVE -jar app.jar"]

构建与运行 ​

bash
# 构建镜像
docker build -t myapp:1.0 .

# 运行容器
docker run -d \
  --name myapp \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e JAVA_OPTS="-Xms512m -Xmx1024m" \
  -v /app/logs:/app/logs \
  myapp:1.0

6.3.3 Docker Compose编排 ​

完整项目docker-compose.yml ​

yaml
version: '3.8'

services:
  # MySQL数据库
  mysql:
    image: mysql:8.0
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: myapp
      MYSQL_USER: appuser
      MYSQL_PASSWORD: app123
      TZ: Asia/Shanghai
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis缓存
  redis:
    image: redis:7.0-alpine
    container_name: redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Elasticsearch搜索引擎
  elasticsearch:
    image: elasticsearch:8.9.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - TZ=Asia/Shanghai
    ports:
      - "9200:9200"
      - "9300:9300"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

  # Kibana可视化
  kibana:
    image: kibana:8.9.0
    container_name: kibana
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - TZ=Asia/Shanghai
    ports:
      - "5601:5601"
    depends_on:
      elasticsearch:
        condition: service_healthy
    networks:
      - app-network

  # Spring Boot应用
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: myapp
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - JAVA_OPTS=-Xms512m -Xmx1024m
    ports:
      - "8080:8080"
    volumes:
      - app-logs:/app/logs
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      elasticsearch:
        condition: service_healthy
    networks:
      - app-network
    restart: unless-stopped

  # Nginx反向代理
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/ssl:/etc/nginx/ssl
      - ./nginx/html:/usr/share/nginx/html
    depends_on:
      - app
    networks:
      - app-network
    restart: unless-stopped

# 数据卷
volumes:
  mysql-data:
  redis-data:
  es-data:
  app-logs:

# 网络
networks:
  app-network:
    driver: bridge

Docker Compose常用命令 ​

bash
# 启动所有服务
docker-compose up -d

# 查看运行状态
docker-compose ps

# 查看日志
docker-compose logs -f

# 查看特定服务日志
docker-compose logs -f app

# 停止所有服务
docker-compose stop

# 停止并删除容器、网络
docker-compose down

# 停止并删除容器、网络、数据卷(慎用!)
docker-compose down -v

# 重新构建并启动
docker-compose up -d --build

# 进入容器
docker-compose exec app /bin/sh

# 扩容服务
docker-compose up -d --scale app=3

6.3.4 Kubernetes(K8s)基础 ​

K8s核心概念 ​

概念说明前端类比
Pod最小部署单元,包含一个或多个容器单个页面/组件
Deployment管理Pod的副本数和滚动更新CI/CD部署流程
Service暴露Pod的网络访问Nginx反向代理
Ingress外部访问入口前端路由 + Nginx
ConfigMap配置文件.env文件
Secret敏感配置.env.local
Namespace资源隔离项目目录
Node运行Pod的物理/虚拟机器服务器
Cluster一组Node的集合服务器集群

Pod配置 ​

yaml
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
    - name: myapp
      image: myapp:1.0
      ports:
        - containerPort: 8080
      resources:
        requests:
          memory: "256Mi"
          cpu: "250m"
        limits:
          memory: "512Mi"
          cpu: "500m"
      env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
      livenessProbe:
        httpGet:
          path: /actuator/health
          port: 8080
        initialDelaySeconds: 60
        periodSeconds: 10
      readinessProbe:
        httpGet:
          path: /actuator/health
          port: 8080
        initialDelaySeconds: 30
        periodSeconds: 5

Deployment配置 ​

yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  labels:
    app: myapp
spec:
  replicas: 3                    # 副本数
  selector:
    matchLabels:
      app: myapp
  strategy:
    type: RollingUpdate          # 滚动更新策略
    rollingUpdate:
      maxSurge: 1                # 最多多出1个Pod
      maxUnavailable: 0          # 更新时不允许有Pod不可用
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:1.0
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          env:
            - name: SPRING_PROFILES_ACTIVE
              valueFrom:
                configMapKeyRef:
                  name: myapp-config
                  key: spring-profiles-active
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: myapp-secret
                  key: db-password
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 5

Service配置 ​

yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: ClusterIP              # 集群内部访问
  selector:
    app: myapp
  ports:
    - port: 80                  # Service端口
      targetPort: 8080          # 容器端口
      protocol: TCP

---
# NodePort类型(外部可访问)
apiVersion: v1
kind: Service
metadata:
  name: myapp-service-nodeport
spec:
  type: NodePort
  selector:
    app: myapp
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080           # 节点端口(30000-32767)

---
# LoadBalancer类型(云环境)
apiVersion: v1
kind: Service
metadata:
  name: myapp-service-lb
spec:
  type: LoadBalancer
  selector:
    app: myapp
  ports:
    - port: 80
      targetPort: 8080

Ingress配置 ​

yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - myapp.example.com
      secretName: myapp-tls
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: myapp-service
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80

ConfigMap和Secret ​

yaml
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  spring-profiles-active: "prod"
  application.yml: |
    spring:
      datasource:
        url: jdbc:mysql://mysql-service:3306/myapp
      redis:
        host: redis-service
        port: 6379

---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secret
type: Opaque
data:
  # base64编码
  db-password: cm9vdDEyMw==
  redis-password: cmVkaXMxMjM=

K8s常用命令 ​

bash
# 部署
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml

# 查看状态
kubectl get pods
kubectl get deployments
kubectl get services
kubectl get ingress
kubectl get all

# 查看详细信息
kubectl describe pod myapp-pod
kubectl describe deployment myapp-deployment

# 查看日志
kubectl logs -f myapp-pod
kubectl logs -f deployment/myapp-deployment

# 进入容器
kubectl exec -it myapp-pod -- /bin/sh

# 扩缩容
kubectl scale deployment myapp-deployment --replicas=5

# 滚动更新
kubectl set image deployment/myapp-deployment myapp=myapp:2.0
kubectl rollout status deployment/myapp-deployment
kubectl rollout history deployment/myapp-deployment
kubectl rollout undo deployment/myapp-deployment

# 删除
kubectl delete -f deployment.yaml
kubectl delete pod myapp-pod
kubectl delete deployment myapp-deployment

6.3.5 CI/CD ​

GitHub Actions ​

yaml
# .github/workflows/ci-cd.yml
name: Java CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  JAVA_VERSION: '17'
  DOCKER_IMAGE: myapp
  DOCKER_REGISTRY: registry.cn-hangzhou.aliyuncs.com

jobs:
  # 代码质量检查
  lint:
    name: Code Quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: 'maven'

      - name: Run Checkstyle
        run: mvn checkstyle:check

      - name: Run SpotBugs
        run: mvn spotbugs:check

  # 单元测试
  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: 'maven'

      - name: Run Tests
        run: mvn test -B

      - name: Generate Coverage Report
        run: mvn jacoco:report

      - name: Upload Coverage
        uses: codecov/codecov-action@v3
        with:
          file: target/site/jacoco/jacoco.xml

  # 构建与推送Docker镜像
  build:
    name: Build & Push Docker Image
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.DOCKER_REGISTRY }}
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # 部署到测试环境
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to K8s
        run: |
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
          export KUBECONFIG=kubeconfig
          kubectl set image deployment/myapp-deployment \
            myapp=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ github.sha }} \
            -n staging
          kubectl rollout status deployment/myapp-deployment -n staging --timeout=300s

  # 部署到生产环境
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to K8s
        run: |
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
          export KUBECONFIG=kubeconfig
          kubectl set image deployment/myapp-deployment \
            myapp=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ github.sha }} \
            -n production
          kubectl rollout status deployment/myapp-deployment -n production --timeout=300s

GitLab CI/CD ​

yaml
# .gitlab-ci.yml
stages:
  - lint
  - test
  - build
  - deploy-staging
  - deploy-production

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

cache:
  paths:
    - .m2/repository

# 代码检查
lint:
  stage: lint
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn checkstyle:check spotbugs:check
  only:
    - merge_requests
    - main
    - develop

# 单元测试
test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn test -B
    - mvn jacoco:report
  artifacts:
    reports:
      junit: target/surefire-reports/TEST-*.xml
      coverage_report:
        coverage_format: cobertura
        path: target/site/jacoco/jacoco.xml

# 构建Docker镜像
build:
  stage: build
  image: docker:24-dind
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE
  only:
    - main
    - develop

# 部署到测试环境
deploy-staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/myapp-deployment myapp=$DOCKER_IMAGE -n staging
    - kubectl rollout status deployment/myapp-deployment -n staging
  environment:
    name: staging
    url: https://staging.myapp.com
  only:
    - develop

# 部署到生产环境
deploy-production:
  stage: deploy-production
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/myapp-deployment myapp=$DOCKER_IMAGE -n production
    - kubectl rollout status deployment/myapp-deployment -n production
  environment:
    name: production
    url: https://myapp.com
  when: manual                    # 手动触发
  only:
    - main

Jenkins Pipeline ​

groovy
// Jenkinsfile
pipeline {
    agent any

    environment {
        MAVEN_HOME = tool 'Maven-3.9'
        JAVA_HOME = tool 'JDK-17'
        DOCKER_IMAGE = 'myapp'
        DOCKER_REGISTRY = 'registry.cn-hangzhou.aliyuncs.com'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Code Quality') {
            steps {
                sh '${MAVEN_HOME}/bin/mvn checkstyle:check'
            }
        }

        stage('Unit Test') {
            steps {
                sh '${MAVEN_HOME}/bin/mvn test -B'
            }
            post {
                always {
                    junit 'target/surefire-reports/TEST-*.xml'
                    jacoco execPattern: 'target/**/*.exec'
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
                        def image = docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
                        image.push()
                        image.push('latest')
                    }
                }
            }
        }

        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh '''
                    kubectl set image deployment/myapp-deployment \
                        myapp=${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER} \
                        -n staging
                    kubectl rollout status deployment/myapp-deployment -n staging
                '''
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            input {
                message '确认部署到生产环境?'
                ok '确认部署'
            }
            steps {
                sh '''
                    kubectl set image deployment/myapp-deployment \
                        myapp=${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER} \
                        -n production
                    kubectl rollout status deployment/myapp-deployment -n production
                '''
            }
        }
    }

    post {
        success {
            echo 'Pipeline succeeded!'
        }
        failure {
            echo 'Pipeline failed!'
        }
        always {
            cleanWs()
        }
    }
}

6.3.6 Nginx反向代理与负载均衡 ​

Nginx基本配置 ​

nginx
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;                    # Linux推荐
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time';

    access_log /var/log/nginx/access.log main;

    # 性能优化
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 100m;

    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript
               application/json application/javascript application/xml
               application/rss+xml image/svg+xml;

    # 引入子配置
    include /etc/nginx/conf.d/*.conf;
}

反向代理配置 ​

nginx
# conf.d/myapp.conf

# 上游服务(负载均衡)
upstream backend {
    # 轮询(默认)
    # server 192.168.1.100:8080;
    # server 192.168.1.101:8080;

    # 权重轮询
    server 192.168.1.100:8080 weight=3;
    server 192.168.1.101:8080 weight=2;
    server 192.168.1.102:8080 weight=1;

    # IP哈希(会话保持)
    # ip_hash;

    # 最少连接
    # least_conn;

    # 健康检查
    # server 192.168.1.100:8080 max_fails=3 fail_timeout=30s;

    keepalive 32;
}

# HTTP重定向到HTTPS
server {
    listen 80;
    server_name myapp.com www.myapp.com;
    return 301 https://$server_name$request_uri;
}

# HTTPS配置
server {
    listen 443 ssl http2;
    server_name myapp.com www.myapp.com;

    # SSL证书
    ssl_certificate /etc/nginx/ssl/myapp.crt;
    ssl_certificate_key /etc/nginx/ssl/myapp.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # 安全头
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # 前端静态文件
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;    # SPA路由支持

        # 缓存策略
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 30d;
            add_header Cache-Control "public, immutable";
        }

        location = /index.html {
            expires -1;
            add_header Cache-Control "no-cache, no-store, must-revalidate";
        }
    }

    # API反向代理
    location /api/ {
        proxy_pass http://backend/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时设置
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # 缓冲区
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;

        # WebSocket支持
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # 健康检查端点
    location /nginx-health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }

    # 错误页面
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

前后端分离部署架构 ​

                    ┌─────────────┐
                    │   Nginx     │
                    │  (443/80)   │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
        ┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
        │  前端静态   │ │ API   │ │ WebSocket │
        │  文件服务   │ │ 代理  │ │   代理    │
        └───────────┘ └───┬───┘ └─────┬─────┘
                          │           │
                    ┌─────▼───────────▼─────┐
                    │   后端服务集群          │
                    │  ┌─────┐ ┌─────┐      │
                    │  │App-1│ │App-2│ ...  │
                    │  └─────┘ └─────┘      │
                    └───────────┬───────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                 │
        ┌─────▼─────┐   ┌──────▼──────┐   ┌─────▼─────┐
        │   MySQL   │   │   Redis     │   │    ES     │
        │  (主从)    │   │  (集群)     │   │  (集群)   │
        └───────────┘   └─────────────┘   └───────────┘

6.4 监控与链路追踪(第17-21天) ​

6.4.1 Spring Boot Actuator ​

Maven依赖 ​

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.yml配置 ​

yaml
management:
  endpoints:
    web:
      exposure:
        include: "*"              # 暴露所有端点
      base-path: /actuator
  endpoint:
    health:
      show-details: always        # 显示健康详情
      show-components: always
    info:
      enabled: true
    metrics:
      enabled: true
    prometheus:
      enabled: true
  info:
    env:
      enabled: true
    java:
      enabled: true
    os:
      enabled: true
  metrics:
    tags:
      application: ${spring.application.name}
    export:
      prometheus:
        enabled: true
        step: 15s                 # 采集间隔
  health:
    redis:
      enabled: true
    db:
      enabled: true
    elasticsearch:
      enabled: true

常用端点 ​

bash
# 健康检查
GET /actuator/health
{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "MySQL",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 500107862016,
        "free": 200043548672,
        "threshold": 10485760
      }
    },
    "redis": {
      "status": "UP",
      "details": {
        "version": "7.0.0"
      }
    }
  }
}

# 应用信息
GET /actuator/info
{
  "app": {
    "name": "myapp",
    "version": "1.0.0"
  },
  "java": {
    "version": "17.0.8",
    "vendor": "Eclipse Adoptium"
  }
}

# 所有指标
GET /actuator/metrics

# JVM内存使用
GET /actuator/metrics/jvm.memory.used

# HTTP请求统计
GET /actuator/metrics/http.server.requests

# 自定义指标
GET /actuator/metrics/custom.metric.name

# 环境变量
GET /actuator/env

# 配置属性
GET /actuator/configprops

# 线程dump
GET /actuator/threaddump

# heap dump
GET /actuator/heapdump

自定义指标 ​

java
@Service
public class MetricsService {

    private final MeterRegistry meterRegistry;
    private final Counter orderCounter;
    private final Timer orderTimer;
    private final AtomicInteger activeUsers;

    public MetricsService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;

        // 计数器
        this.orderCounter = Counter.builder("order.created")
            .description("Total number of orders created")
            .tag("type", "normal")
            .register(meterRegistry);

        // 计时器
        this.orderTimer = Timer.builder("order.process.time")
            .description("Order processing time")
            .register(meterRegistry);

        // 仪表盘
        this.activeUsers = meterRegistry.gauge("user.active",
            new AtomicInteger(0));
    }

    public void createOrder(Order order) {
        orderCounter.increment();
        orderTimer.record(() -> {
            // 处理订单
            processOrder(order);
        });
    }

    public void updateActiveUsers(int count) {
        activeUsers.set(count);
    }
}

6.4.2 Prometheus + Grafana ​

Docker部署 ​

yaml
# docker-compose-monitoring.yml
version: '3.8'

services:
  # Prometheus
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
    networks:
      - monitoring

  # Grafana
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    depends_on:
      - prometheus
    networks:
      - monitoring

  # Node Exporter(主机监控)
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    networks:
      - monitoring

  # cAdvisor(容器监控)
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    ports:
      - "8081:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    networks:
      - monitoring

volumes:
  prometheus-data:
  grafana-data:

networks:
  monitoring:
    driver: bridge

Prometheus配置 ​

yaml
# prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  # Prometheus自身
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # Spring Boot应用
  - job_name: 'spring-boot-app'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 10s
    static_configs:
      - targets: ['app:8080']
        labels:
          application: 'myapp'
          environment: 'production'

  # Node Exporter
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  # cAdvisor
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

  # MySQL Exporter
  - job_name: 'mysql'
    static_configs:
      - targets: ['mysql-exporter:9104']

  # Redis Exporter
  - job_name: 'redis'
    static_configs:
      - targets: ['redis-exporter:9121']

常用PromQL查询 ​

bash
# JVM内存使用
jvm_memory_used_bytes{application="myapp"}

# HTTP请求速率
rate(http_server_requests_seconds_count[5m])

# HTTP请求平均耗时
rate(http_server_requests_seconds_sum[5m]) / rate(http_server_requests_seconds_count[5m])

# CPU使用率
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# 内存使用率
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# 磁盘使用率
(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100

# 容器CPU使用率
rate(container_cpu_usage_seconds_total{name="myapp"}[5m])

# 容器内存使用
container_memory_usage_bytes{name="myapp"}

# 错误率
rate(http_server_requests_seconds_count{status=~"5.."}[5m]) / rate(http_server_requests_seconds_count[5m]) * 100

6.4.3 ELK日志收集 ​

ELK架构 ​

应用日志 → Logstash/Filebeat → Elasticsearch → Kibana
                                      ↓
                                  存储与检索
                                      ↓
                                  可视化展示

Docker部署ELK ​

yaml
# docker-compose-elk.yml
version: '3.8'

services:
  elasticsearch:
    image: elasticsearch:8.9.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    networks:
      - elk

  logstash:
    image: logstash:8.9.0
    container_name: logstash
    ports:
      - "5044:5044"
      - "5000:5000/tcp"
      - "5000:5000/udp"
      - "9600:9600"
    volumes:
      - ./logstash/pipeline:/usr/share/logstash/pipeline
      - ./logstash/config:/usr/share/logstash/config
    depends_on:
      - elasticsearch
    networks:
      - elk

  kibana:
    image: kibana:8.9.0
    container_name: kibana
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch
    networks:
      - elk

  filebeat:
    image: elastic/filebeat:8.9.0
    container_name: filebeat
    volumes:
      - ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml
      - /var/log:/var/log:ro
      - app-logs:/app-logs:ro
    depends_on:
      - logstash
    networks:
      - elk

volumes:
  es-data:
  app-logs:

networks:
  elk:
    driver: bridge

Logstash配置 ​

ruby
# logstash/pipeline/logstash.conf
input {
  # 接收Beats(Filebeat)
  beats {
    port => 5044
  }

  # TCP输入
  tcp {
    port => 5000
    codec => json
  }

  # 从文件读取
  # file {
  #   path => "/app-logs/*.log"
  #   start_position => "beginning"
  #   sincedb_path => "/dev/null"
  # }
}

filter {
  # 解析JSON日志
  if [message] =~ /^\{/ {
    json {
      source => "message"
    }
  }

  # 解析日期
  date {
    match => ["timestamp", "ISO8601", "yyyy-MM-dd HH:mm:ss.SSS"]
    target => "@timestamp"
  }

  # 添加标签
  if [level] == "ERROR" {
    mutate {
      add_tag => ["error"]
    }
  }

  # 地理位置解析
  if [client_ip] {
    geoip {
      source => "client_ip"
    }
  }

  # 删除不需要的字段
  mutate {
    remove_field => ["@version", "host", "agent"]
  }
}

output {
  # 输出到Elasticsearch
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    index => "app-logs-%{+YYYY.MM.dd}"
    # 如果需要认证
    # user => "elastic"
    # password => "your_password"
  }

  # 调试输出(开发环境)
  # stdout {
  #   codec => rubydebug
  # }
}

Filebeat配置 ​

yaml
# filebeat/filebeat.yml
filebeat.inputs:
  # 应用日志
  - type: log
    enabled: true
    paths:
      - /app-logs/*.log
    fields:
      app: myapp
      env: production
    fields_under_root: true
    multiline:
      pattern: '^\d{4}-\d{2}-\d{2}'
      negate: true
      match: after

  # Nginx访问日志
  - type: log
    enabled: true
    paths:
      - /var/log/nginx/access.log
    fields:
      app: nginx
      type: access
    fields_under_root: true

  # Nginx错误日志
  - type: log
    enabled: true
    paths:
      - /var/log/nginx/error.log
    fields:
      app: nginx
      type: error
    fields_under_root: true

output.logstash:
  hosts: ["logstash:5044"]

processors:
  - add_host_metadata:
      when.not.contains.tags: forwarded
  - add_cloud_metadata: ~

Spring Boot日志配置 ​

xml
<!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <springProperty scope="context" name="appName" source="spring.application.name"/>
    <springProperty scope="context" name="env" source="spring.profiles.active"/>

    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- JSON格式文件输出(便于Logstash解析) -->
    <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/${appName}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/${appName}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <customFields>{"app":"${appName}","env":"${env}"}</customFields>
        </encoder>
    </appender>

    <!-- TCP直接发送到Logstash -->
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>localhost:5000</destination>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="JSON_FILE"/>
        <appender-ref ref="LOGSTASH"/>
    </root>
</configuration>

Logstash TCP依赖 ​

xml
<!-- pom.xml -->
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.4</version>
</dependency>

6.4.4 SkyWalking链路追踪 ​

SkyWalking概述 ​

SkyWalking是一个开源的应用性能监控(APM)系统,提供分布式追踪、服务拓扑分析、性能指标分析等功能。

与前端概念对比:

前端概念SkyWalking
Chrome DevTools PerformanceSkyWalking性能分析
Lighthouse性能评分SkyWalking服务评分
Sentry错误追踪SkyWalking链路追踪
前端埋点监控SkyWalking指标采集

Docker部署SkyWalking ​

yaml
# docker-compose-skywalking.yml
version: '3.8'

services:
  # SkyWalking OAP Server
  oap:
    image: apache/skywalking-oap-server:9.5.0
    container_name: skywalking-oap
    ports:
      - "11800:11800"    # gRPC
      - "12800:12800"    # REST
    environment:
      SW_STORAGE: elasticsearch
      SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
      SW_CORE_REST_HOST: 0.0.0.0
      SW_CORE_REST_PORT: 12800
      SW_CORE_GRPC_HOST: 0.0.0.0
      SW_CORE_GRPC_PORT: 11800
      JAVA_OPTS: "-Xms512m -Xmx512m"
    depends_on:
      - elasticsearch
    networks:
      - skywalking

  # SkyWalking UI
  ui:
    image: apache/skywalking-ui:9.5.0
    container_name: skywalking-ui
    ports:
      - "8088:8080"
    environment:
      SW_OAP_ADDRESS: http://oap:12800
    depends_on:
      - oap
    networks:
      - skywalking

  elasticsearch:
    image: elasticsearch:8.9.0
    container_name: skywalking-es
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - skywalking-es-data:/usr/share/elasticsearch/data
    networks:
      - skywalking

volumes:
  skywalking-es-data:

networks:
  skywalking:
    driver: bridge

Java Agent接入 ​

bash
# 下载SkyWalking Java Agent
# https://skywalking.apache.org/downloads/

# 启动时添加Agent
java -javaagent:/path/to/skywalking-agent.jar \
     -Dskywalking.agent.service_name=myapp \
     -Dskywalking.collector.backend_service=localhost:11800 \
     -jar app.jar

Docker集成Agent ​

dockerfile
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

# 下载SkyWalking Agent
ADD https://archive.apache.org/dist/skywalking/java-agent/9.0.0/apache-skywalking-java-agent-9.0.0.tgz /tmp/
RUN tar -xzf /tmp/apache-skywalking-java-agent-9.0.0.tgz -C /opt/

COPY target/*.jar app.jar

ENV SW_AGENT_NAME=myapp
ENV SW_BACKEND_SERVICE=localhost:11800

ENTRYPOINT ["sh", "-c", "java -javaagent:/opt/skywalking-agent/skywalking-agent.jar -Dskywalking.agent.service_name=${SW_AGENT_NAME} -Dskywalking.collector.backend_service=${SW_BACKEND_SERVICE} -jar app.jar"]

自定义追踪 ​

java
@Service
public class TracedService {

    /**
     * 使用注解追踪方法
     */
    @Trace
    public void processOrder(Order order) {
        // 方法会被自动追踪
    }

    /**
     * 自定义标签
     */
    @Tag(key = "orderId", value = "arg[0].id")
    @Tag(key = "amount", value = "arg[0].amount")
    public void createOrder(Order order) {
        // 自定义标签会附加到Span上
    }

    /**
     * 自定义Span
     */
    public void complexBusiness() {
        try (Scope scope = ContextManager.createLocalSpan("complexBusiness")) {
            // 业务逻辑
            step1();
            step2();
            step3();
        }
    }

    /**
     * 自定义上下文传播
     */
    public void crossServiceCall() {
        ContextCarrier carrier = new ContextCarrier();
        AbstractSpan span = ContextManager.createExitSpan("remoteCall", carrier, "remote-service:8080");

        try {
            // 传递上下文到远程服务
            // carrier.serialize() 可以放入HTTP Header
            remoteCall();
        } finally {
            ContextManager.stopSpan();
        }
    }
}

SkyWalking告警配置 ​

yaml
# alarm-settings.yml
rules:
  # 服务响应时间告警
  service_resp_time_rule:
    metrics-name: service_resp_time
    op: ">"
    threshold: 1000          # 响应时间超过1000ms
    period: 10               # 10分钟窗口
    count: 3                 # 连续3次
    silence-period: 5        # 静默5分钟
    message: "服务 {name} 响应时间超过1秒"

  # 服务成功率告警
  service_sla_rule:
    metrics-name: service_sla
    op: "<"
    threshold: 8000          # 成功率低于80%
    period: 10
    count: 2
    silence-period: 5
    message: "服务 {name} 成功率低于80%"

  # 端点响应时间告警
  endpoint_resp_time_rule:
    metrics-name: endpoint_resp_time
    op: ">"
    threshold: 2000
    period: 10
    count: 3
    silence-period: 5
    message: "端点 {name} 响应时间超过2秒"

# Webhook通知
webhooks:
  - http://localhost:8080/webhook/receive

6.5 综合练习任务 ​

练习1:电商缓存系统 ​

  • 实现商品详情页缓存(Redis)
  • 处理缓存穿透、击穿、雪崩问题
  • 使用Redisson实现分布式锁
  • 实现库存扣减的并发控制

练习2:全文搜索系统 ​

  • 使用Elasticsearch实现商品搜索
  • 实现搜索建议(Suggest)
  • 实现搜索结果高亮
  • 实现聚合分析(分类统计、价格区间)
  • 实现MySQL与Elasticsearch数据同步

练习3:Docker化部署 ​

  • 编写Spring Boot应用的Dockerfile
  • 编写docker-compose.yml编排所有服务
  • 配置Nginx反向代理
  • 实现前后端分离部署

练习4:监控体系搭建 ​

  • 集成Spring Boot Actuator
  • 部署Prometheus + Grafana
  • 配置ELK日志收集
  • 接入SkyWalking链路追踪
  • 配置告警规则

练习5:完整CI/CD流水线 ​

  • 编写GitHub Actions / GitLab CI配置
  • 实现代码质量检查 → 测试 → 构建 → 部署全流程
  • 实现滚动更新和回滚

6.6 学习检查清单 ​

Redis缓存 ​

  • [ ] 掌握Redis五种基本数据类型及使用场景
  • [ ] 能使用Spring Boot集成Redis
  • [ ] 理解缓存穿透、击穿、雪崩的原因和解决方案
  • [ ] 掌握Redis主从、哨兵、集群架构
  • [ ] 能使用Redisson实现分布式锁

Elasticsearch搜索 ​

  • [ ] 理解倒排索引原理
  • [ ] 掌握Elasticsearch基本操作
  • [ ] 能使用Spring Data Elasticsearch集成
  • [ ] 掌握全文检索、复合查询、聚合分析
  • [ ] 理解MySQL与Elasticsearch数据同步方案

Docker与K8s ​

  • [ ] 能编写Java应用的Dockerfile
  • [ ] 掌握Docker Compose编排
  • [ ] 理解K8s核心概念(Pod、Deployment、Service、Ingress)
  • [ ] 能编写K8s部署配置文件
  • [ ] 掌握基本的K8s运维命令

CI/CD ​

  • [ ] 能编写GitHub Actions / GitLab CI配置
  • [ ] 理解CI/CD流水线的各个环节
  • [ ] 掌握自动化测试、构建、部署流程

监控与追踪 ​

  • [ ] 掌握Spring Boot Actuator的使用
  • [ ] 能部署Prometheus + Grafana监控体系
  • [ ] 掌握ELK日志收集架构
  • [ ] 理解SkyWalking链路追踪原理
  • [ ] 能配置告警规则

完成本阶段后,你应该能够构建高可用、高性能的Java后端系统,掌握中间件的使用和运维技能,具备全栈开发能力。

评论

正在加载评论…