第01篇:Redis 从入门到上手:核心数据结构与 Java Spring Boot 实战详解

本文速览:本文是 Redis 系列第一篇,系统讲解 Redis 是什么、为什么快、五大核心数据结构的使用场景,并结合 Spring Boot + Lettuce 演示真实代码,最后给出 SaaS 场景下的 Key 设计规范。适合 Redis 入门及有一定 Java 基础的读者。

关键词:Redis教程、Redis数据结构、SpringBoot集成Redis、Redis入门、Redis String Hash List Set ZSet


一、为什么是 Redis?从一个真实的痛点说起

想象这样一个场景:你的 SaaS 系统上线后,用户量从 1000 增长到 10 万,数据库 CPU 飙升到 90%,接口 P99 延迟从 50ms 变成了 2000ms。老板盯着监控大屏,你盯着慢查询日志,一脸茫然。

这个时候,大多数工程师想到的第一个解法就是------引入缓存 。而缓存领域最炙手可热、生产环境使用率最高的方案,就是 Redis

Redis 全称 Remote Dictionary Server,是一个基于内存的高性能键值存储系统。它之所以快,核心原因有以下几点:

Redis 为什么这么快?

  1. 纯内存操作:所有数据存储在内存中,读写不经过磁盘 I/O
  2. 单线程模型(命令执行层):避免了多线程的锁竞争开销(6.0 起引入多线程 I/O,但命令执行仍是单线程)
  3. 高效的 I/O 多路复用:基于 epoll/kqueue 实现,单进程可处理大量并发连接
  4. 精心设计的数据结构:如跳表、压缩列表、哈希表等,在内存布局上做了大量优化

根据官方基准测试,单节点 Redis 每秒可处理 10 万 ~ 100 万次请求,这个数字足以应对绝大多数业务场景。


二、Redis 安装与 Spring Boot 集成

2.1 本地快速安装

在开发阶段,推荐用 Docker 启动 Redis,避免污染本地环境:

bash 复制代码
# 拉取并启动 Redis 7.x(推荐使用最新稳定版)
docker run -d \
  --name redis-dev \
  -p 6379:6379 \
  -v $(pwd)/redis-data:/data \
  redis:7.2 redis-server --appendonly yes

# 验证是否启动成功
docker exec -it redis-dev redis-cli ping
# 返回 PONG 则成功

这里有一个容易被忽略的坑 :不加 --appendonly yes 的话,Redis 默认只做 RDB 快照持久化,重启后可能丢失最近的写入数据。在开发环境无所谓,但如果你直接把这个命令抄到测试环境,就可能踩坑。关于持久化的完整讲解,我们放在第二篇详细展开。

2.2 Spring Boot 项目集成

pom.xml 中引入依赖。Spring Boot 默认使用 Lettuce 作为 Redis 客户端(基于 Netty,支持异步和响应式),如果你的项目有强烈的连接池偏好,也可以切换为 Jedis。

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 如果需要对象序列化,引入 Jackson -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

接下来配置连接信息。很多教程到这里就只写 spring.redis.host=localhost,但在生产 SaaS 项目里,你还需要关注连接池参数

yaml 复制代码
# application.yml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: ""          # 生产环境必须设置密码
      database: 0
      timeout: 3000ms       # 连接超时,建议不超过 3s,防止阻塞业务线程
      lettuce:
        pool:
          max-active: 16    # 最大连接数,根据业务并发量调整
          max-idle: 8       # 最大空闲连接数
          min-idle: 2       # 最小空闲连接数,保持少量热连接
          max-wait: 1000ms  # 获取连接的最大等待时间,超时抛异常而非无限等待

为什么要配置连接池? Lettuce 默认共享一个连接(单连接模式),在高并发场景下会成为瓶颈。配置连接池后,多个业务线程可并发使用不同连接,避免排队。

2.3 RedisTemplate 序列化配置

这是 Spring Boot 集成 Redis 最容易踩的坑之一 。默认的 RedisTemplate<Object, Object> 使用 JDK 序列化,存到 Redis 里的 Key 和 Value 是乱码,用 redis-cli 完全看不懂,给排查问题带来极大困扰。

强烈建议覆盖默认配置,改为 String Key + JSON Value:

java 复制代码
// RedisConfig.java
@Configuration
public class RedisConfig {

    /**
     * 自定义 RedisTemplate,Key 用 String 序列化,Value 用 JSON 序列化。
     * 这样在 redis-cli 里可以直接读懂数据,排查问题效率提升 10 倍。
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // Key 使用 String 序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // Value 使用 Jackson JSON 序列化
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        // 序列化时保留类型信息,反序列化时才能还原为正确的对象类型
        mapper.activateDefaultTyping(
            mapper.getPolymorphicTypeValidator(),
            ObjectMapper.DefaultTyping.NON_FINAL
        );
        jsonSerializer.setObjectMapper(mapper);

        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

注意 activateDefaultTyping 这行。如果不加,存入 User 对象,取出来会变成 LinkedHashMap,你还需要手动转型,非常麻烦。加上之后,JSON 里会带有类型标识,反序列化时自动还原。当然,这也意味着 JSON 里会多一个 @class 字段,存储空间略有增加,这是个权衡。


三、五大核心数据结构精讲

Redis 的核心竞争力之一,是它提供了丰富的语义化数据结构,而不仅仅是 key-value 的字符串存储。不同的数据结构对应不同的业务场景,选错了不仅浪费内存,性能也会大打折扣。

bash 复制代码
Redis 数据结构全景图

┌─────────────────────────────────────────────────────┐
│                    Redis 数据结构                    │
├──────────┬──────────┬──────────┬──────────┬─────────┤
│  String  │   Hash   │   List   │   Set    │  ZSet   │
│  字符串  │  哈希表  │  双端队列│  集合    │ 有序集合│
├──────────┼──────────┼──────────┼──────────┼─────────┤
│ 缓存对象 │ 用户信息 │ 消息队列 │ 标签系统 │排行榜  │
│ 计数器   │ 商品详情 │ 最近访问 │ 共同好友 │延迟队列│
│ 分布式锁 │ 购物车   │ 操作日志 │ 抽奖池   │        │
└──────────┴──────────┴──────────┴──────────┴─────────┘

3.1 String --- 万能的基础类型

String 是 Redis 最基础也最常用的数据类型,最大可存储 512MB 的内容。虽然叫"字符串",但它能存整数(用于计数)、浮点数、甚至二进制数据(如序列化的 Java 对象、图片)。

典型场景一:缓存用户信息

java 复制代码
// UserCacheService.java

@Service
@RequiredArgsConstructor
public class UserCacheService {

    private final RedisTemplate<String, Object> redisTemplate;
    // 统一管理 Key 前缀,避免不同模块 Key 冲突(SaaS 多租户更重要)
    private static final String USER_KEY_PREFIX = "saas:user:";
    private static final Duration USER_CACHE_TTL = Duration.ofMinutes(30);

    /**
     * 获取用户信息:先查缓存,缓存未命中则查数据库并回填。
     * 这是"Cache-Aside"模式,也叫旁路缓存,是最常用的缓存策略。
     */
    public User getUserById(Long userId) {
        String key = USER_KEY_PREFIX + userId;

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

        // 2. 缓存未命中,查数据库
        // 注意:这里有缓存击穿风险!高并发下多个请求同时到达数据库。
        // 解决方案在第四篇《缓存击穿》中详细讲解,这里先实现基础版本。
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));

        // 3. 回填缓存,设置过期时间(必须设置,否则内存会无限增长)
        redisTemplate.opsForValue().set(key, user, USER_CACHE_TTL);

        return user;
    }

    /**
     * 更新用户信息后,删除缓存(Cache-Aside 的删除策略)。
     * 为什么是删除而不是更新缓存?因为更新缓存在并发场景下可能导致数据不一致。
     */
    public void updateUser(User user) {
        userRepository.save(user);
        String key = USER_KEY_PREFIX + user.getId();
        redisTemplate.delete(key);
    }
}

典型场景二:原子计数器

Redis 的 INCR 命令是原子操作,天然适合做计数器,无需担心并发安全问题:

java 复制代码
// 统计 API 调用次数(适用于 SaaS 按量计费场景)
public void incrementApiCallCount(String tenantId, String apiName) {
    String key = String.format("saas:api:count:%s:%s:%s",
        tenantId, apiName, LocalDate.now());
    // INCR 是原子操作,即使高并发也不会出现计数丢失
    Long count = redisTemplate.opsForValue().increment(key);
    // 第一次写入时设置过期时间(当天结束后自动清理)
    if (count == 1L) {
        redisTemplate.expire(key, Duration.ofDays(2));
    }
}

踩坑提醒increment 操作和 expire 操作是两条独立命令,不是原子的。极端情况下,increment 成功但 expire 失败,这个 key 就永远不会过期,导致内存泄漏。更严谨的写法是用 Lua 脚本将两个操作打包,或使用 SET key 1 EX 86400 NX 初始化。我们在第五篇分布式锁中会深入讲 Lua 脚本。

3.2 Hash --- 结构化对象的最佳载体

如果说 String 适合缓存整个序列化对象,那 Hash 适合缓存有频繁局部更新需求的结构化对象。Hash 的每个 field 可以单独读写,避免了每次更新都序列化/反序列化整个对象的开销。

bash 复制代码
Hash 内存结构示意

Key: "saas:user:profile:1001"
┌─────────────┬──────────────────────┐
│   Field     │       Value          │
├─────────────┼──────────────────────┤
│ name        │ 张三                  │
│ email       │ zhang@example.com    │
│ vipLevel    │ 3                    │
│ loginCount  │ 128                  │
│ lastLoginAt │ 2024-01-15 10:30:00  │
└─────────────┴──────────────────────┘
java 复制代码
// 用 Hash 存储用户 Profile,支持局部字段更新

@Service
public class UserProfileCacheService {

    private final RedisTemplate<String, Object> redisTemplate;

    private String profileKey(Long userId) {
        return "saas:user:profile:" + userId;
    }

    // 存储完整 profile
    public void saveProfile(Long userId, UserProfile profile) {
        Map<String, Object> fields = new HashMap<>();
        fields.put("name", profile.getName());
        fields.put("email", profile.getEmail());
        fields.put("vipLevel", profile.getVipLevel());
        fields.put("loginCount", profile.getLoginCount());

        redisTemplate.opsForHash().putAll(profileKey(userId), fields);
        redisTemplate.expire(profileKey(userId), Duration.ofHours(2));
    }

    // 只更新 VIP 等级,而无需重新序列化整个对象
    // 这正是 Hash 相比 String 的核心优势:局部更新,节省带宽和 CPU
    public void updateVipLevel(Long userId, int newLevel) {
        redisTemplate.opsForHash().put(profileKey(userId), "vipLevel", newLevel);
    }

    // 读取单个字段
    public Integer getVipLevel(Long userId) {
        return (Integer) redisTemplate.opsForHash().get(profileKey(userId), "vipLevel");
    }

    // 读取完整 profile
    public UserProfile getProfile(Long userId) {
        Map<Object, Object> entries = redisTemplate.opsForHash()
            .entries(profileKey(userId));
        if (entries.isEmpty()) return null;
        // 从 Map 还原对象
        return mapToProfile(entries);
    }
}

何时用 String,何时用 Hash? 经验法则:如果你的业务经常需要"只更新对象的某个字段",用 Hash;如果总是读写整个对象,用 String(序列化后存储反而更省内存,因为 Hash 每个 field 都有元数据开销)。

3.3 List --- 双端队列,消息与日志的家

Redis List 是双端链表(底层在小数据量时会优化为压缩列表),支持从头部或尾部插入/弹出元素,天然适合实现队列、栈、最近记录列表等场景。

bash 复制代码
List 操作示意

LPUSH → [最新] ←─── [元素3] [元素2] [元素1] ───→ RPOP [最旧]
RPUSH → [最旧]   [元素1] [元素2] [元素3] ←─── [最新] ← LPOP
java 复制代码
// 记录用户最近浏览的商品(保留最近 20 条)

@Service
public class BrowseHistoryService {

    private final RedisTemplate<String, Object> redisTemplate;
    private static final int MAX_HISTORY = 20;

    private String historyKey(Long userId) {
        return "saas:browse:history:" + userId;
    }

    /**
     * 记录用户浏览了某商品。
     * 注意这里用 LPUSH + LTRIM 组合,而不是先 LLEN 判断再删除。
     * LPUSH + LTRIM 是更 Redis-native 的写法,且 LTRIM 是 O(N),
     * 但 N 很小(20条),实际性能完全可以接受。
     */
    public void recordBrowse(Long userId, Long productId) {
        String key = historyKey(userId);
        ListOperations<String, Object> listOps = redisTemplate.opsForList();

        // 从头部插入(最新的在最前面)
        listOps.leftPush(key, productId.toString());

        // 裁剪,只保留最近 MAX_HISTORY 条
        // LTRIM key 0 19 表示只保留索引 0 到 19 的元素,其余删除
        listOps.trim(key, 0, MAX_HISTORY - 1);

        // 刷新过期时间(用户活跃时浏览记录持续有效)
        redisTemplate.expire(key, Duration.ofDays(7));
    }

    // 获取全部浏览历史
    public List<Long> getBrowseHistory(Long userId) {
        List<Object> raw = redisTemplate.opsForList()
            .range(historyKey(userId), 0, -1);  // 0 到 -1 表示全部
        if (raw == null) return Collections.emptyList();
        return raw.stream()
            .map(o -> Long.parseLong(o.toString()))
            .collect(Collectors.toList());
    }
}

List 作消息队列的局限 :用 LPUSH/BRPOP 可以实现简单的消息队列,但它不支持消费者组、不能回溯历史消息、也没有 ACK 机制。如果你的业务对消息可靠性有要求,第六篇会介绍 Redis Stream,它解决了这些问题。

3.4 Set --- 去重集合与关系运算

Set 存储不重复的字符串元素,最大价值在于支持集合运算:交集(SINTER)、并集(SUNION)、差集(SDIFF),这让很多业务逻辑可以直接下推到 Redis 层完成,不必在 Java 代码里处理。

java 复制代码
// 基于 Set 实现标签系统与共同关注功能

@Service
public class TagService {

    private final RedisTemplate<String, Object> redisTemplate;

    private String userTagKey(Long userId) {
        return "saas:user:tags:" + userId;
    }

    // 给用户打标签
    public void addTag(Long userId, String... tags) {
        redisTemplate.opsForSet().add(userTagKey(userId), (Object[]) tags);
    }

    /**
     * 计算两个用户的共同标签(交集)。
     * 这个操作如果在数据库层面做,可能需要复杂的 JOIN + GROUP BY。
     * 交给 Redis Set 的 SINTER 命令,一行搞定,性能极佳。
     */
    public Set<Object> getCommonTags(Long userId1, Long userId2) {
        return redisTemplate.opsForSet()
            .intersect(userTagKey(userId1), userTagKey(userId2));
    }

    // 推荐给 userId1 但 userId2 没有的标签(差集)
    public Set<Object> getRecommendedTags(Long userId1, Long userId2) {
        return redisTemplate.opsForSet()
            .difference(userTagKey(userId2), userTagKey(userId1));
    }
}

Set 还有一个常见用法是去重统计 ,比如统计每天的 UV(独立访客数)。不过当数据量很大时(百万级),Set 的内存开销会比较高,此时应该用第六篇介绍的 HyperLogLog,用极少内存实现近似去重统计。

3.5 ZSet(Sorted Set)--- 带分值的有序集合

ZSet 是 Redis 最具特色的数据结构之一。每个成员都对应一个 score(分值),成员按 score 排序。底层由**跳表(Skip List)**实现,支持 O(log N) 的插入、删除和范围查询。

ZSet 的典型场景:排行榜、延迟队列、优先级队列。

bash 复制代码
ZSet 内存结构示意(积分排行榜)

Key: "saas:leaderboard:game:2024-01"
┌────────────┬────────┐
│  Member    │ Score  │
├────────────┼────────┤
│ user:10086 │ 9850.0 │  ← 排名第一
│ user:10001 │ 8820.0 │  ← 排名第二
│ user:10099 │ 7730.0 │  ← 排名第三
│ ...        │ ...    │
└────────────┴────────┘
java 复制代码
// 实时积分排行榜

@Service
public class LeaderboardService {

    private final RedisTemplate<String, Object> redisTemplate;

    private String leaderboardKey(String gameId, String period) {
        return String.format("saas:leaderboard:%s:%s", gameId, period);
    }

    // 用户得分后更新排行榜
    // ZINCRBY 是原子操作,高并发下不会丢分
    public void addScore(String gameId, Long userId, double score) {
        String key = leaderboardKey(gameId, getCurrentPeriod());
        redisTemplate.opsForZSet().incrementScore(key, "user:" + userId, score);
    }

    /**
     * 获取 Top N 排行榜。
     * ZREVRANGEBYSCORE / ZREVRANGE 按 score 从高到低返回。
     * withScores = true 同时返回分值,方便展示。
     */
    public List<LeaderboardEntry> getTopN(String gameId, int n) {
        String key = leaderboardKey(gameId, getCurrentPeriod());

        // reverseRangeWithScores 返回 Set<ZSetOperations.TypedTuple>,按 score 降序
        Set<ZSetOperations.TypedTuple<Object>> tuples =
            redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, n - 1);

        if (tuples == null) return Collections.emptyList();

        List<LeaderboardEntry> result = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
            result.add(new LeaderboardEntry(
                tuple.getValue().toString(),
                tuple.getScore(),
                rank++
            ));
        }
        return result;
    }

    // 查询某用户的当前排名(ZREVRANK,从 0 开始,需要 +1)
    public Long getUserRank(String gameId, Long userId) {
        String key = leaderboardKey(gameId, getCurrentPeriod());
        Long rank = redisTemplate.opsForZSet()
            .reverseRank(key, "user:" + userId);
        return rank == null ? -1L : rank + 1;
    }

    private String getCurrentPeriod() {
        return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
    }
}

ZSet 实现延迟队列 :把任务的"执行时间戳"作为 score,任务内容作为 member。用一个定时线程轮询 ZRANGEBYSCORE key 0 当前时间戳,取出所有到期任务执行。这是一个轻量级延迟队列的经典实现,无需引入 RabbitMQ 等重型 MQ,适合任务量不大的 SaaS 场景。


四、SaaS 场景下的 Key 设计规范

Key 的设计看似简单,实则是 Redis 使用中最容易被忽视、后期最难修改的部分。特别是 SaaS 多租户场景,Key 设计不合理会导致租户数据互相干扰、无法按租户统计内存用量、批量清理困难等一系列问题。

推荐的 Key 命名规范:

bash 复制代码
{应用名}:{租户ID}:{业务模块}:{数据类型}:{唯一标识}

示例:
saas:tenant001:user:info:10086      # 租户001的用户10086信息
saas:tenant001:order:count:2024-01  # 租户001的1月订单计数
saas:tenant002:product:list:cat:3   # 租户002的分类3商品列表

关键原则:

  1. 必须包含租户标识:多租户系统中,不带租户 ID 的 Key 是定时炸弹
  2. 使用冒号分隔层级:redis-cli、Redis Insight 等工具会自动识别冒号并以树形结构展示,极大提升可维护性
  3. Key 长度适中:Key 本身也占内存,过长的 Key 在大规模场景下会有可观的内存开销,建议不超过 100 字符
  4. 必须设置 TTL:没有 TTL 的 Key 是内存泄漏的根源,永远为 Key 设置合理的过期时间
java 复制代码
// 推荐:使用常量类统一管理 Key 模板,避免散落在业务代码各处

public final class RedisKeyConstants {
    private RedisKeyConstants() {}

    // Key 模板,使用 String.format 填充参数
    public static final String USER_INFO = "saas:%s:user:info:%d";
    public static final String USER_SESSION = "saas:%s:user:session:%s";
    public static final String ORDER_COUNT = "saas:%s:order:count:%s";
    public static final String LEADERBOARD = "saas:%s:leaderboard:%s:%s";

    // TTL 常量
    public static final Duration USER_INFO_TTL = Duration.ofMinutes(30);
    public static final Duration SESSION_TTL = Duration.ofHours(2);
    public static final Duration ORDER_COUNT_TTL = Duration.ofDays(32);

    // 工具方法
    public static String userInfo(String tenantId, long userId) {
        return String.format(USER_INFO, tenantId, userId);
    }
}

五、与 AI 大模型结合:Redis 在 LLM 应用中的入门用法

现在 AI 大模型应用越来越普遍,Redis 在这个领域也有天然的用武之地。最简单的场景是语义缓存(Semantic Cache):同一个问题(或语义相近的问题),不必每次都调用昂贵的 LLM API,可以先查 Redis 缓存。

java 复制代码
// 简单的 LLM 响应缓存(基于精确 Key 匹配)
// 进阶的语义相似度匹配需要向量数据库,我们在第七篇详细讲解

@Service
public class LlmCacheService {

    private final RedisTemplate<String, Object> redisTemplate;
    private static final Duration LLM_CACHE_TTL = Duration.ofHours(24);

    /**
     * 缓存 LLM 响应。Key 是问题内容的 MD5,Value 是响应文本。
     * 这种方式只能命中完全相同的问题,无法处理语义相近的变体。
     * 真正的语义缓存需要将问题转为向量,存入 Redis Vector Store,
     * 查询时通过相似度搜索而非精确匹配------第七篇见!
     */
    public Optional<String> getFromCache(String question) {
        String key = "saas:llm:cache:" + DigestUtils.md5DigestAsHex(
            question.getBytes(StandardCharsets.UTF_8));
        Object cached = redisTemplate.opsForValue().get(key);
        return Optional.ofNullable(cached != null ? cached.toString() : null);
    }

    public void saveToCache(String question, String response) {
        String key = "saas:llm:cache:" + DigestUtils.md5DigestAsHex(
            question.getBytes(StandardCharsets.UTF_8));
        redisTemplate.opsForValue().set(key, response, LLM_CACHE_TTL);
    }
}

这只是 Redis 与 AI 结合的冰山一角。Redis 还支持存储向量嵌入(通过 RedisSearch 模块),可以作为 RAG(检索增强生成)架构中的向量数据库,实现基于语义相似度的知识检索。这部分内容我们在第七篇详细展开。


六、本篇总结与系列预告

本文覆盖了 Redis 的核心基础:

知识点 关键结论
为什么快 内存 + 单线程命令执行 + I/O 多路复用
Spring Boot 集成 务必覆盖默认序列化配置,改为 String Key + JSON Value
String 缓存对象、原子计数,配合 TTL 使用
Hash 结构化对象的局部更新场景
List 最近记录、轻量消息队列
Set 去重、集合运算(交差并)
ZSet 排行榜、延迟队列
Key 设计 分层命名 + 租户隔离 + 必须设 TTL

下一篇预告:数据写到 Redis 里,进程重启后还在吗?磁盘上到底存了什么?RDB 快照和 AOF 日志各有什么优缺点?混合持久化是怎么工作的?------《Redis 持久化深度解析》,我们下篇见。


📌 觉得有帮助?点个赞支持一下,系列持续更新中!

💬 有问题欢迎评论区交流,踩过的坑越多,帮助越大。

相关推荐
星马梦缘1 小时前
数据库 第十三章 未完结版本
java·网络·数据库
程序猿乐锅1 小时前
【JAVASE | 第十六篇】多线程
java·开发语言
影寂ldy1 小时前
C# 多接口、同名冲突、显式实现、接口继承 完整笔记
java·笔记·c#
JAVA面经实录9171 小时前
Spring Cloud Alibaba 微服务企业实战完整文档(架构+规范+调优+故障+源码)
java·运维·spring cloud·微服务
布局呆星1 小时前
Spring Boot + JWT + Spring Security 认证授权实战:双角色、双 Token、方法级权限,一次讲透
java·开发语言
大G的笔记本1 小时前
生产级 Spring Boot 网关完整实现方案
java·笔记·gateway
LucianaiB1 小时前
Swarm管理面板的多项目配置策略与模型别名机制的效率分析
java·服务器·前端
qq_2518364571 小时前
基于Spring Boot的数据标注与质检系统设计与实现
java·spring boot·后端
總鑽風1 小时前
Spring AI实战:快速集成阿里通义千问
java·后端·spring·ai编程