
1) 【一句话结论】:为支撑360安全产品(用户量达亿级,用户ID连续增长)的高并发读写,采用水平分库分表(按用户ID哈希+分段+随机偏移分片)、读写分离(主从复制)、Redis缓存(热点数据预热+LRU淘汰),结合一致性哈希实现分片路由,并通过Golang封装路由逻辑,以分散数据压力、提升系统吞吐。
2) 【原理/概念讲解】:
3) 【对比与适用场景】:
| 方案/概念 | 定义 | 特性 | 使用场景 | 注意点 |
|---|---|---|---|---|
| 垂直分表 | 按业务字段拆分表(如用户表拆为用户基础表、用户行为表) | 单表字段少,查询关联少,但需跨表关联 | 业务字段关联紧密,数据量不大(如用户表字段多,拆为多个表) | 需维护表间关联逻辑,扩展性一般(新增字段需新增表) |
| 水平分表 | 按数据量或ID哈希分表(如用户表按ID取模100分片) | 单表数据量分散,适合海量数据 | 用户数据量巨大(如360安全产品用户量亿级),读/写高并发 | 需处理跨表关联(如关联其他表需合并分片),分片路由复杂(如热点分片) |
| 读写分离 | 主库写,从库读(主从复制) | 提升读吞吐,降低主库压力 | 读请求远多于写请求(如360安全产品用户查询登录状态、数据统计,读/写比例约10:1) | 需保证从库数据一致性(延迟),写操作需同步到从库(如延迟超过500ms时回退主库) |
| 一致性哈希 | 节点列表环状,数据哈希取模节点 | 节点增删时数据迁移量小(约1/n) | 分片节点可能动态增删(如扩容、故障转移) | 需处理哈希冲突(如取模余数相同),节点故障时数据迁移(如故障节点数据迁移到新节点) |
4) 【示例】:
func getShardUserID(userID int64) string {
// 分段:取模100,再取模10(子分片),加随机偏移
baseShard := userID % 100
subShard := (userID % 10) + rand.Intn(10) // 随机偏移0-9
return fmt.Sprintf("user_%d_%d", baseShard+1, subShard+1)
}
func GetUserByID(userID int64) (User, error) {
shard := getShardUserID(userID)
db, err := sql.Open("mysql", fmt.Sprintf("user:pass@tcp:db%d:3306/%s", shard, "user_db"))
if err != nil { return User{}, err }
var user User
err = db.QueryRow("SELECT * FROM users WHERE id = ?", userID).Scan(&user.ID, &user.Name, ...)
return user, err
}
func getShardConsistentHash(key string) string {
hash := fnv1a(key)
nodes := []string{"node1", "node2", "node3"}
idx := (hash % uint32(len(nodes)))
return nodes[idx]
}
SELECT u.*, b.* FROM (
SELECT id FROM user_db.user_1
UNION ALL
SELECT id FROM user_db.user_2
-- ... 所有分片
) u
JOIN user_db.behavior b ON u.id = b.user_id;
5) 【面试口播版答案】:
“针对360安全产品用户数据的高并发读写,核心方案是分库分表(水平分表为主,按用户ID哈希+分段+随机偏移分片)、读写分离(主从复制)、Redis缓存。具体来说,水平分表通过用户ID取模100后,再取模10并加随机偏移,分散海量数据压力,避免热点分片;读写分离用主库写、从库读,提升读吞吐,从库延迟超过500ms时回退主库;Redis缓存缓存热点数据(如登录状态、常用配置),设置过期时间(避免缓存穿透),淘汰策略用LRU,缓存击穿时用布隆过滤器过滤无效请求。分片路由采用一致性哈希,节点增删时数据迁移量小,Golang中通过封装分片函数(如getShardUserID)实现路由,确保请求正确路由到对应分片。这样能支撑亿级用户的高并发读写场景。”
6) 【追问清单】:
7) 【常见坑/雷区】: