SpringBoot + Elasticsearch + Redis:八年 Java 开发手把手教你做 “不崩、不卡、不冲突” 的大学排课系统

SpringBoot + Elasticsearch + Redis:八年 Java 开发手把手教你做 "不崩、不卡、不冲突" 的大学排课系统

做 Java 开发八年,接过不少教育类系统的活,但大学排课系统绝对是 "看着简单,做着崩溃" 的典型 ------ 早期用纯 SpringBoot+MySQL 做,排一门课要关联教师、教室、时间三张表查冲突,慢到管理员拍桌子;选课高峰期学生查课表,数据库 CPU 直接飙到 99%,页面转圈半分钟;期末统计 "计算机学院本学期课程分布",一条 Group By 语句跑了 12 秒,领导催得急,我手心全是汗。

直到后来用SpringBoot+Elasticsearch(ES)+Redis+MySQL 重构,才彻底解决这些痛点。今天就从八年开发的视角,带大家拆解这套 "不崩、不卡、不冲突" 的排课系统,从痛点分析到代码实现,再到踩过的坑,全是实战干货,拒绝空谈理论。

一、先聊排课系统的 "痛点暴击":为什么不能只靠 SpringBoot+MySQL?

很多新手会觉得 "排课不就是 CRUD + 时间匹配吗?用 SpringBoot+MySQL 足够了"------ 这话在学生数少于 1 万的小学校可能成立,但一旦到中大型高校(3 万 + 学生、上千教师、几百间教室),分分钟给你上一课。

先列几个我踩过的 "致命痛点",看看纯 MySQL 方案为什么扛不住:

痛点 纯 MySQL 方案的坑 后果
三重冲突检测(教师 / 教室 / 时间) 需关联teacher_course(教师课表)、classroom_course(教室课表)、time_slot(时间段)三张表,用多表 JOIN+WHERE 判断,数据量大时查询耗时超 3 秒 管理员排课要等半天,还容易漏判冲突(比如跨院系的教师重复排课)
选课高峰期查课表 学生查个人课表 / 班级课表,全靠 MySQL 单表查或关联查,1 万学生同时查,数据库连接池直接满了 页面转圈、超时,学生投诉量暴涨,教务处电话被打爆
多维度统计分析 比如 "统计某院系本学期各专业课程数""某教师近 3 个月授课时长",需用复杂 Group By + 子查询,数据量超 10 万条时,查询耗时超 10 秒 领导要数据时拿不出来,影响教学决策
动态调课同步 临时调课后,学生端课表要实时更新,纯 MySQL 需每次查最新数据,没缓存的话,旧课表会残留 学生按旧课表去上课,发现教室被占,场面混乱

SpringBoot+ES+Redis+MySQL 的组合,正好精准解决这些痛点:

  • SpringBoot:作为基础框架,快速整合其他组件,减少 boilerplate 代码(比如用 SpringBoot Starter 整合 ES、Redis);
  • MySQL:存基础数据(学生 / 教师 / 教室 / 课程的核心信息),保证数据持久化和事务性;
  • Elasticsearch:负责多维度检索(比如冲突检测、复杂课表查询)和聚合统计(比如院系课程分布),比 MySQL 快 5-10 倍;
  • Redis:缓存热门数据(学生个人课表、班级课表、公共选修课表),应对高并发查询,减轻 MySQL 压力。

可能有人会问:"为什么不用 Redis 做冲突检测?"------Redis 的 Sorted Set 或 Hash 适合单维度查询(比如查某教师的所有课),但冲突检测是 "教师 + 时间 + 教室" 三重维度,ES 的 bool 查询天生适合这种多条件组合检索,效率比 Redis 高得多。

二、整体架构设计:从 "排课" 到 "查课" 的全链路

markdown 复制代码
用户端(学生/教师/管理员)
       ↓(1. 操作请求)
SpringBoot应用(排课系统核心)
       ↓(2. 身份校验)
Spring Security + JWT(权限控制)
       ↓(3. 校验通过)
SpringBoot应用(排课系统核心)
       ↓(4. 排课/调课)
业务模块{
  冲突检测模块
  课表查询模块
  统计分析模块
  调课同步模块
}
├─ 冲突检测模块 →(5. 多维度冲突检索)→ Elasticsearch(课表索引)
│                →(6. 最终排课结果存储)→ MySQL(基础数据表)
│                →(7. 同步排课数据到ES)→ Elasticsearch(课表索引)
├─ 课表查询模块 →(8. 优先查缓存)→ Redis(课表缓存)
│                →(9. 缓存未命中)→ Elasticsearch(课表索引)
│                →(10. 查后回填缓存)→ Redis(课表缓存)
├─ 统计分析模块 →(11. 多维度统计)→ Elasticsearch(课表索引)
│                →(12. 统计结果返回)→ 用户端
└─ 调课同步模块 →(13. 调课更新)→ MySQL(基础数据表)
                 →(14. 删除旧缓存)→ Redis(课表缓存)
                 →(15. 同步更新ES)→ Elasticsearch(课表索引)

整个链路的核心逻辑是 "数据分层处理":

  1. 写链路(排课 / 调课) :先通过 ES 做冲突检测→检测通过后写 MySQL 存最终结果→同步数据到 ES(供查询和统计)→删除 Redis 旧缓存(避免脏数据);
  2. 读链路(查课表) :先查 Redis→未命中查 ES→查后回填 Redis;
  3. 统计链路:直接查 ES(用聚合功能),避免查 MySQL 拖慢性能。

这种设计的好处很明显:写操作保证 "不冲突",读操作保证 "不卡顿",统计操作保证 "不耗时"。

三、核心功能实现:代码 + 八年经验技巧

接下来分模块讲核心功能的实现,每个模块都贴关键代码,并穿插我总结的避坑技巧。

3.1 基础准备:组件整合(SpringBoot+ES+Redis+MySQL)

先搞定组件依赖和配置,这是后续开发的基础。

3.1.1 引入依赖(pom.xml)
xml 复制代码
<!-- SpringBoot核心 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- MySQL + MyBatis-Plus(简化CRUD) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

<!-- Elasticsearch(SpringBoot Starter) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

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

<!-- 工具类(JSON/日期等) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
3.1.2 核心配置(application.yml)
yaml 复制代码
spring:
  # 数据源配置(MySQL)
  datasource:
    url: jdbc:mysql://localhost:3306/college_timetable?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

  # Redis配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 20  # 最大连接数(选课高峰期调大)
        max-idle: 10
        min-idle: 5

  # Elasticsearch配置
  elasticsearch:
    uris: http://localhost:9200
    username: elastic
    password: 123456
    rest:
      connection-timeout: 3000ms

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.college.timetable.entity
  configuration:
    map-underscore-to-camel-case: true  # 下划线转驼峰

# 日志配置(方便调试ES和MySQL)
logging:
  level:
    com.college.timetable.mapper: debug  # MySQL SQL日志
    org.springframework.data.elasticsearch: debug  # ES操作日志

八年经验技巧

  • Redis 的max-active要根据选课高峰期的并发量调整(比如 3 万学生,按 10% 并发查课表,设 20-30 足够);
  • ES 的connection-timeout别设太短(3 秒足够),避免网络波动导致连接失败;
  • 开启 MyBatis-Plus 和 ES 的 debug 日志,排错时能快速定位问题(比如 SQL 写错、ES 查询语句不对)。

3.2 核心功能 1:三重冲突检测(教师 / 教室 / 时间)

这是排课系统的 "生命线"------ 一旦出现冲突,比如 "教师 A 周一 1-2 节同时在两个教室上课",后果很严重。用 ES 做冲突检测,比 MySQL 多表联查快太多。

3.2.1 ES 索引设计(课表索引)

先在 ES 中创建 "课表索引",字段要覆盖冲突检测的所有维度:

kotlin 复制代码
/**
 * ES课表索引实体(对应MySQL的timetable表)
 */
@Data
@Document(indexName = "college_timetable", shards = 3, replicas = 1)  // 3分片1副本(根据数据量调整)
public class TimetableEsEntity {
    @Id  // ES文档ID(与MySQL的timetable_id一致)
    private Long timetableId;

    @Field(type = FieldType.Long, index = true)  // 教师ID(索引,用于冲突检测)
    private Long teacherId;

    @Field(type = FieldType.Long, index = true)  // 教室ID(索引,用于冲突检测)
    private Long classroomId;

    @Field(type = FieldType.Integer, index = true)  // 周次(比如第1-16周,索引)
    private Integer week;

    @Field(type = FieldType.Integer, index = true)  // 星期(1=周一,7=周日,索引)
    private Integer weekday;

    @Field(type = FieldType.Integer, index = true)  // 节次(1=1-2节,2=3-4节,索引)
    private Integer section;

    @Field(type = FieldType.Long, index = true)  // 课程ID(索引,用于查课表)
    private Long courseId;

    @Field(type = FieldType.Long, index = true)  // 院系ID(索引,用于统计)
    private Long deptId;

    @Field(type = FieldType.Date, format = DateFormat.basic_date_time)  // 创建时间(非索引)
    private Date createTime;
}

索引设计技巧

  • 分片数(shards):按院系分,比如 3 个分片对应 3 个院系,查询时指定分片,减少扫描范围;
  • 只有用于查询和过滤的字段才设index = true(比如 createTime 不用查,设index = false),减少索引体积;
  • 周次、星期、节次用 Integer 类型(比如 "周一 1-2 节" 存为 weekday=1,section=1),比字符串("周一 1-2 节")查询快。
3.2.2 冲突检测实现(用 ES bool 查询)
typescript 复制代码
/**
 * 排课服务(核心:冲突检测)
 */
@Service
public class TimetableService {
    @Autowired
    private ElasticsearchRestTemplate esTemplate;  // ES操作模板
    @Autowired
    private TimetableMapper timetableMapper;  // MySQL Mapper
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;  // Redis操作

    // Redis缓存Key前缀(学生个人课表)
    private static final String STUDENT_TIMETABLE_KEY = "timetable:student:%s";

    /**
     * 排课前检测冲突
     * @param dto 排课DTO(含教师ID、教室ID、周次、星期、节次)
     * @return true=无冲突,false=有冲突
     */
    public boolean checkConflict(TimetableDTO dto) {
        // 构建ES bool查询:同时匹配"教师+时间"或"教室+时间"→有匹配则冲突
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.boolQuery()
                        // 时间条件(周次、星期、节次必须完全匹配)
                        .must(QueryBuilders.termQuery("week", dto.getWeek()))
                        .must(QueryBuilders.termQuery("weekday", dto.getWeekday()))
                        .must(QueryBuilders.termQuery("section", dto.getSection()))
                        // 冲突条件:要么教师冲突,要么教室冲突
                        .should(QueryBuilders.termQuery("teacherId", dto.getTeacherId()))
                        .should(QueryBuilders.termQuery("classroomId", dto.getClassroomId()))
                        .minimumShouldMatch(1)  // 至少满足一个should条件
                )
                .withSize(1)  // 只需要判断是否有数据,不用查全量
                .build();

        // 执行ES查询
        SearchHits<TimetableEsEntity> searchHits = esTemplate.search(
                searchQuery, TimetableEsEntity.class
        );

        // 有命中结果→冲突;无命中→无冲突
        return searchHits.getTotalHits() == 0;
    }

    /**
     * 排课(冲突检测通过后执行)
     */
    public void addTimetable(TimetableDTO dto) {
        // 1. 先检测冲突
        boolean noConflict = checkConflict(dto);
        if (!noConflict) {
            throw new BusinessException("排课冲突!请检查教师、教室或时间");
        }

        // 2. 保存到MySQL(用MyBatis-Plus)
        TimetableEntity entity = new TimetableEntity();
        BeanUtils.copyProperties(dto, entity);
        entity.setCreateTime(new Date());
        timetableMapper.insert(entity);

        // 3. 同步数据到ES(供后续查询和统计)
        TimetableEsEntity esEntity = new TimetableEsEntity();
        BeanUtils.copyProperties(entity, esEntity);
        esTemplate.save(esEntity);

        // 4. (可选)删除相关缓存(比如教师课表缓存,避免旧数据)
        deleteRelatedCache(dto.getTeacherId(), null);

        log.info("排课成功!课表ID:{}", entity.getTimetableId());
    }

    /**
     * 删除相关缓存(调课/删课时用)
     */
    private void deleteRelatedCache(Long teacherId, Long studentId) {
        if (teacherId != null) {
            String teacherKey = "timetable:teacher:" + teacherId;
            redisTemplate.delete(teacherKey);
        }
        if (studentId != null) {
            String studentKey = STUDENT_TIMETABLE_KEY + studentId;
            redisTemplate.delete(studentKey);
        }
    }
}

八年经验技巧

  • ES 查询时用withSize(1):只要查到 1 条数据就说明冲突,不用查全量,效率提升 50%;
  • 冲突检测的minimumShouldMatch(1):必须满足 "教师冲突" 或 "教室冲突" 中的一个,逻辑别写错;
  • 排课后同步 ES 时,用esTemplate.save()而不是update():确保 ES 数据与 MySQL 一致。

3.3 核心功能 2:高并发课表查询(学生 / 教师端)

选课高峰期,上千学生同时查课表,靠 Redis 缓存扛住压力,避免 MySQL 崩溃。

3.3.1 学生个人课表查询(Redis+ES)
java 复制代码
/**
 * 课表查询服务
 */
@Service
public class TimetableQueryService {
    @Autowired
    private ElasticsearchRestTemplate esTemplate;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private StudentCourseMapper studentCourseMapper;  // 学生-课程关联表(查学生选的课程)

    // Redis缓存Key:学生个人课表(过期时间2小时,课表不会频繁变)
    private static final String STUDENT_TIMETABLE_KEY = "timetable:student:%s";
    private static final long CACHE_EXPIRE = 2 * 60 * 60;  // 2小时

    /**
     * 学生查询个人课表
     * @param studentId 学生ID
     * @param week 当前周次
     * @return 个人课表列表
     */
    public List<TimetableVO> getStudentTimetable(Long studentId, Integer week) {
        String cacheKey = STUDENT_TIMETABLE_KEY + studentId;

        // 1. 先查Redis缓存
        Object cacheObj = redisTemplate.opsForValue().get(cacheKey);
        if (cacheObj != null) {
            log.info("学生{}查课表,命中Redis缓存", studentId);
            return JSON.parseObject(cacheObj.toString(), new TypeReference<List<TimetableVO>>() {});
        }

        // 2. 缓存未命中,查ES(先查学生选的课程ID,再查课表)
        // 2.1 查学生选的课程ID(从MySQL查,数据量小)
        List<Long> courseIds = studentCourseMapper.selectCourseIdByStudentId(studentId);
        if (CollectionUtils.isEmpty(courseIds)) {
            return Collections.emptyList();
        }

        // 2.2 查ES:匹配课程ID+周次
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.boolQuery()
                        .must(QueryBuilders.termsQuery("courseId", courseIds))  // 学生选的课程
                        .must(QueryBuilders.termQuery("week", week))  // 当前周次
                )
                .withSort(SortBuilders.fieldSort("weekday").order(SortOrder.ASC))  // 按星期升序
                .withSort(SortBuilders.fieldSort("section").order(SortOrder.ASC))   // 按节次升序
                .build();

        SearchHits<TimetableEsEntity> searchHits = esTemplate.search(
                searchQuery, TimetableEsEntity.class
        );

        // 3. 转换为VO(给前端返回)
        List<TimetableVO> result = searchHits.stream()
                .map(hit -> {
                    TimetableEsEntity esEntity = hit.getContent();
                    TimetableVO vo = new TimetableVO();
                    BeanUtils.copyProperties(esEntity, vo);
                    // 补充课程名称、教师名称(从缓存或MySQL查,这里省略)
                    return vo;
                })
                .collect(Collectors.toList());

        // 4. 回填Redis缓存
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(result), CACHE_EXPIRE, TimeUnit.SECONDS);
        log.info("学生{}查课表,缓存未命中,查ES后回填缓存", studentId);

        return result;
    }
}

八年经验技巧

  • 缓存过期时间设 2 小时:课表不会频繁变,2 小时足够,太长会导致调课后缓存脏读;
  • 查 ES 前先从 MySQL 查 "学生选的课程 ID":因为 "学生 - 课程" 关联数据量小(每个学生选 5-10 门课),MySQL 查更快,避免在 ES 中查大量数据;
  • 课表排序:按 "星期→节次" 升序,符合学生看课表的习惯(比如周一 1-2 节、周一 3-4 节......)。

3.4 核心功能 3:多维度统计分析(院系 / 教师)

领导经常需要 "统计计算机学院本学期各专业课程数""统计教师 A 近 3 个月授课时长",用 ES 的聚合功能比 MySQL 的 Group By 快 10 倍以上。

3.4.1 院系课程分布统计(ES 聚合)
scss 复制代码
/**
 * 统计分析服务
 */
@Service
public class TimetableStatService {
    @Autowired
    private ElasticsearchRestTemplate esTemplate;

    /**
     * 统计某院系本学期各专业的课程数量
     * @param deptId 院系ID
     * @param semester 学期(比如2024-2025-1)
     * @return 统计结果(专业ID→课程数)
     */
    public Map<Long, Integer> statCourseByMajor(Long deptId, String semester) {
        // 1. 构建ES聚合查询:按"专业ID"分组,统计每组的课程数
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.boolQuery()
                        .must(QueryBuilders.termQuery("deptId", deptId))  // 指定院系
                        .must(QueryBuilders.termQuery("semester", semester))  // 指定学期
                )
                // 聚合:按majorId分组(专业ID),统计count
                .addAggregation(AggregationBuilders.terms("major_course_count")
                        .field("majorId")  // 按专业ID分组
                        .size(100)  // 最多返回100个专业(足够)
                )
                .withSize(0)  // 只需要聚合结果,不需要原始数据
                .build();

        // 2. 执行查询
        SearchHits<TimetableEsEntity> searchHits = esTemplate.search(
                searchQuery, TimetableEsEntity.class
        );

        // 3. 解析聚合结果
        Aggregations aggregations = searchHits.getAggregations();
        Terms termsAgg = aggregations.get("major_course_count");

        Map<Long, Integer> result = new HashMap<>();
        for (Terms.Bucket bucket : termsAgg.getBuckets()) {
            Long majorId = Long.parseLong(bucket.getKeyAsString());  // 专业ID
            long count = bucket.getDocCount();  // 课程数
            result.put(majorId, (int) count);
        }

        return result;
    }
}

八年经验技巧

  • 聚合查询时用withSize(0):不需要返回原始课表数据,只需要聚合结果,减少数据传输量;
  • terms聚合的size设足够大(比如 100):避免专业数量超过默认 size(10)导致统计不全;
  • 统计结果可以缓存到 Redis(比如设 1 小时过期):因为统计数据不会实时变,减少 ES 压力。

四、踩坑实录:这些坑我替你踩过了

做排课系统的八年里,踩过不少 "血坑",分享 3 个最典型的,帮你少走弯路:

4.1 坑 1:ES 索引没分片,数据量大了查询慢

问题:早期把 ES 索引设为 1 个分片,当课表数据超过 10 万条后,冲突检测查询从 50ms 涨到 500ms,排课时明显卡顿。

原因:1 个分片只能用 1 个 ES 节点的 CPU,数据量大时处理不过来。

解决方案

  1. 重建索引,设 3-5 个分片(根据数据量和节点数调整,比如 3 个节点设 3 个分片);
  2. 按 "院系 ID" 做路由分片:查询时指定分片,比如 "计算机学院(deptId=1)" 的数据只存在分片 1,减少扫描范围。
scss 复制代码
// 按院系ID路由分片(新增课表时指定)
IndexQuery indexQuery = new IndexQueryBuilder()
        .withObject(esEntity)
        .withRouting(String.valueOf(esEntity.getDeptId()))  // 路由键=院系ID
        .build();
esTemplate.index(indexQuery);

4.2 坑 2:调课后 Redis 缓存没更新,学生看到旧课表

问题:管理员给学生 A 调课后,学生 A 查课表还是旧的,需要刷新页面才显示新的,投诉 "课表不对"。

原因:调课后只更新了 MySQL 和 ES,没删除 Redis 中的旧缓存,导致缓存脏读。

解决方案

  1. 调课时,主动删除对应学生 / 教师的 Redis 缓存;
  2. 用 Redis 的发布订阅机制:调课服务发 "调课消息",缓存服务收到消息后删除对应缓存。
scss 复制代码
// 调课时删除缓存(关键代码)
public void updateTimetable(TimetableDTO dto) {
    // 1. 更新MySQL
    // ...(省略更新逻辑)

    // 2. 更新ES
    // ...(省略更新逻辑)

    // 3. 删除相关缓存(学生+教师)
    Long studentId = dto.getStudentId();
    Long teacherId = dto.getTeacherId();
    deleteRelatedCache(teacherId, studentId);  // 调用之前写的删除缓存方法
}

4.3 坑 3:纯 MySQL 做统计,期末时查询超时

问题 :期末统计 "全校各院系课程总数",用 MySQL 的GROUP BY dept_id查询,数据量 50 万条时,耗时 12 秒,领导催得急,我只能重启数据库临时缓解。

原因:MySQL 的 Group By 需要扫描全表,数据量大时性能极差。

解决方案

  1. 把统计功能迁移到 ES,用聚合查询替代 MySQL 的 Group By;
  2. 定时(比如每天凌晨)预计算统计结果,存到 Redis,领导查的时候直接取缓存,不用实时计算。

五、总结:排课系统的 "设计原则"

做了八年排课系统,总结出 3 个核心设计原则,适用于所有教育类系统:

  1. 数据分层,各司其职:MySQL 存基础数据(保证持久化),ES 做检索和统计(保证快),Redis 做缓存(保证高并发),不要让一个组件干所有活;
  2. 冲突前置,避免补救:排课前必须做冲突检测,不要等排完课再发现冲突(事后补救成本太高,比如要通知学生 / 教师调课);
  3. 高可用兜底:ES 挂了降级查 MySQL,Redis 挂了降级查 ES,避免单点故障导致系统崩溃(比如用 Spring Cloud CircuitBreaker 做熔断)。

最后给同行一个建议:做排课系统不要一开始就堆复杂组件 ------ 如果学校规模小(学生 < 1 万),用 SpringBoot+MySQL 足够;等规模大了再加 ES 和 Redis,循序渐进,避免过度设计。

如果你的项目也在做排课系统,希望这篇文章能帮你少走弯路。有其他问题的话,欢迎在评论区交流 ------ 教育类系统的坑,我们一起踩,一起填!

相关推荐
阿杆3 小时前
国产神级开源 OCR 模型,登顶全球第一!再次起飞!
后端·github·图像识别
CryptoRzz3 小时前
Java 对接印度股票数据源实现 http+ws实时数据
后端
调试人生的显微镜3 小时前
iOS 混淆工具链实战,多工具组合完成 IPA 混淆与加固(iOS混淆|IPA加固|无源码混淆|App 防反编译)
后端
渣哥4 小时前
用错注入方式?你的代码可能早就埋下隐患
javascript·后端·面试
王中阳Go4 小时前
我发现不管是Java还是Golang,懂AI之后,是真吃香!
后端·go·ai编程
亲爱的马哥5 小时前
再见,TDuckX3.0 结束了
前端·后端·github
我是天龙_绍5 小时前
redis 秒杀 分布式 锁
后端
AAA修煤气灶刘哥5 小时前
Spring AI 通关秘籍:从聊天到业务落地,Java 选手再也不用馋 Python 了!
后端·spring·openai
自由的疯5 小时前
Java Jenkins+Docker部署jar包
java·后端·架构