51mee - AI智能招聘平台Logo
模拟面试题目大全招聘中心会员专区

在好未来教育App中,用户学习进度需要多端(iOS、Android、网页)实时同步,请设计数据库方案,并说明如何保证数据一致性和实时性,以及处理并发写入的场景。

好未来IOS难度:中等

答案

1) 【一句话结论】:采用**分布式数据库(MySQL)+ 分布式缓存(Redis)+ WebSocket + 消息队列(如Kafka)**的最终一致性方案,通过乐观锁(版本号字段)和本地缓存容错机制,实现多端学习进度实时同步且一致性可控(允许短暂不一致,符合业务容忍度)。

2) 【原理/概念讲解】:老师口吻,先讲核心挑战:多端实时同步的核心矛盾是“并发写入冲突”与“网络延迟下的数据不一致”。传统强一致性(如分布式事务)在分布式场景下成本高、性能低,因此选择最终一致性(CAP定理中CA或CP,根据业务对一致性的容忍度调整)。具体架构设计如下:

  • 数据存储层:使用MySQL主从复制,主库负责写操作(保证数据最终一致性),从库用于读(提升读性能);
  • 缓存层:Redis作为分布式缓存,存储用户学习进度(如user_progress:{user_id}),作为读源或同步源,减少数据库压力;
  • 实时同步层:通过WebSocket实现客户端与服务端的实时通信,用户端更新进度时,先通过WebSocket向服务端发送变更请求;
  • 异步同步层:服务端接收请求后,先更新MySQL主库(主键为user_id+course_id+chapter_id),再更新Redis缓存(写时更新),同时将变更消息推送到消息队列(如Kafka),其他端通过订阅消息队列或Redis Pub/Sub获取最新数据。
  • 并发冲突处理:在MySQL的user_progress表中添加版本号字段(version,自增整数),采用乐观锁机制:当更新进度时,先检查当前版本号是否等于预期版本号,若匹配则更新并递增版本号,否则回滚并提示用户重试,避免脏数据。
    类比:就像多人编辑Word文档时用“版本历史”防止覆盖,这里用数据库版本号解决并发冲突,确保数据不丢失。

3) 【对比与适用场景】:

方案类型定义特性使用场景注意点
强一致性(数据库级同步)通过分布式事务(如两阶段提交)确保多端写入原子性数据立即同步,无延迟对数据一致性要求极高(如金融交易、订单系统),但性能低分布式事务成本高,网络故障易导致阻塞,不适合高并发场景
最终一致性(应用级消息队列)用户端写入数据库,服务端通过MQ异步同步其他端延迟在秒级,允许短暂不一致对实时性要求高,可容忍短暂不一致(如学习进度、社交动态)需处理消息丢失、重试逻辑,保证幂等性,避免数据重复

4) 【示例】:
数据模型设计(学习进度表):

CREATE TABLE user_progress (
    user_id INT PRIMARY KEY,
    course_id INT NOT NULL,
    chapter_id INT NOT NULL,
    progress INT NOT NULL DEFAULT 0,  -- 当前学习进度(如章节完成百分比)
    version INT NOT NULL AUTO_INCREMENT,  -- 乐观锁版本号
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

伪代码(iOS客户端更新进度):

func updateStudyProgress(progress: Int, completion: @escaping (Result<Void, Error>) -> Void) {
    // 1. 检查网络连接,若断网则缓存到本地(如Core Data)
    if !isNetworkReachable {
        localDB.updateProgress(userId: userId, courseId: courseId, chapterId: chapterId, progress: progress)
        completion(.success)
        return
    }
    
    // 2. 发送WebSocket请求到服务端
    let request = ["action": "updateProgress", "userId": userId, "courseId": courseId, "chapterId": chapterId, "progress": progress]
    socket.send(request) { result in
        switch result {
        case .success(let response):
            // 3. 服务端处理:更新数据库(主库)并刷新Redis
            let sql = "UPDATE user_progress SET progress = ?, version = version + 1 WHERE user_id = ? AND course_id = ? AND chapter_id = ? AND version = ?"
            db.execute(sql, [progress, userId, courseId, chapterId, currentVersion]) { success, err in
                if success {
                    redis.set("user_progress:\(userId):\(courseId):\(chapterId)", value: progress)
                    completion(.success)
                } else {
                    completion(.failure(DatabaseConflictError()))
                }
            }
        case .failure(let err):
            // 网络错误:重试逻辑(指数退避)
            retryUpdate(progress: progress, completion: completion, attempt: 1)
        }
    }
}

func retryUpdate(progress: Int, completion: @escaping (Result<Void, Error>) -> Void, attempt: Int) {
    let delay = 2 << (attempt - 1)  // 指数退避(2^attempt秒)
    DispatchQueue.global().after(deadline: .now() + delay) {
        updateStudyProgress(progress: progress, completion: completion)
    }
}

服务端处理WebSocket请求(Go语言伪代码):

wsHandler.HandleFunc("/progress", func(c *websocket.Conn) {
    var msg map[string]interface{}
    if err := json.NewDecoder(c).Decode(&msg); err != nil {
        return
    }
    userId := msg["userId"].(string)
    courseId := msg["courseId"].(string)
    chapterId := msg["chapterId"].(string)
    progress := msg["progress"].(int)
    
    db.Exec("UPDATE user_progress SET progress = ?, version = version + 1 WHERE user_id = ? AND course_id = ? AND chapter_id = ? AND version = ?", progress, userId, courseId, chapterId, currentVersion)
    
    redis.Set("user_progress:" + userId + ":" + courseId + ":" + chapterId, progress)
    
    kafkaProducer.Produce("progress_updates", []byte(userId + ":" + courseId + ":" + chapterId + ":" + strconv.Itoa(progress)))
})

5) 【面试口播版答案】:
“面试官您好,针对多端学习进度实时同步,我设计的方案是采用**分布式数据库(MySQL)+ 分布式缓存(Redis)+ WebSocket + 消息队列(如Kafka)**的最终一致性方案,通过乐观锁和本地缓存容错机制,实现多端数据同步。具体来说,用户在任意端更新进度时,先通过WebSocket向服务端发送请求;服务端先更新MySQL主库(带版本号字段),再刷新Redis缓存,同时将变更推送到消息队列;其他端通过WebSocket或消息队列实时获取最新数据。对于并发冲突,使用数据库的乐观锁(版本号),当更新失败时回滚并提示用户重试,避免数据冲突。网络不稳定时,客户端会缓存到本地,恢复后自动同步,保证数据不丢失。”

6) 【追问清单】:

  • 问题1:网络不稳定时如何保证数据同步?
    回答要点:通过WebSocket心跳检测(3秒一次)保持连接,消息重试(指数退避算法,最多3次),客户端本地缓存(如Core Data),恢复后自动同步。
  • 问题2:如何处理缓存雪崩或击穿?
    回答要点:Redis设置TTL(如5分钟),采用分布式锁(如Redis SETNX)避免雪崩;热点数据缓存预热,使用布隆过滤器减少击穿。
  • 问题3:消息队列的延迟或丢失如何处理?
    回答要点:消息队列(如Kafka)采用幂等消费(根据消息唯一标识检查是否已处理),设置消息保留时间(避免旧消息堆积),服务端记录消费状态(如数据库表记录已同步的进度)。

7) 【常见坑/雷区】:

  • 坑1:只考虑强一致性,忽略性能
    雷区:使用分布式事务(如两阶段提交)导致服务端阻塞,影响并发性能,适合对一致性要求极高的场景(如金融),但教育App对实时性要求高,强一致性成本过高。
  • 坑2:并发处理不当(如悲观锁)
    雷区:使用数据库悲观锁(如SELECT FOR UPDATE)锁定行,导致其他端写入阻塞,降低系统吞吐量;应采用乐观锁(版本号),减少锁竞争。
  • 坑3:缓存未考虑过期策略或失效机制
    雷区:Redis缓存未设置TTL,导致数据过期后不一致;或未实现缓存失效(如更新数据库后未删除Redis缓存,导致读数据旧),应结合写时更新、读时更新策略。
51mee.com致力于为招聘者提供最新、最全的招聘信息。AI智能解析岗位要求,聚合全网优质机会。
产品招聘中心面经会员专区简历解析Resume API
联系我们南京浅度求索科技有限公司admin@51mee.com
联系客服
51mee客服微信二维码 - 扫码添加客服获取帮助
© 2025 南京浅度求索科技有限公司. All rights reserved.
公安备案图标苏公网安备32010602012192号苏ICP备2025178433号-1