面试题答案
一键面试数据结构设计
- 用户权限存储:
- 使用Redis的Hash结构,以用户ID作为key,权限信息作为field - value对。例如:
HSET user:123 permission:stock_code "AAPL,GOOG" permission:sector "Technology"
。这样可以方便地获取每个用户对于不同粒度频道的订阅权限。
- 使用Redis的Hash结构,以用户ID作为key,权限信息作为field - value对。例如:
- 用户订阅关系存储:
- 使用Redis的Set结构,以频道名称作为key,用户ID集合作为value。例如,对于股票代码为AAPL的频道,
SADD channel:AAPL 123 456
,其中123和456是订阅该频道的用户ID。这有助于快速获取订阅某个频道的所有用户。 - 同时,为了反向查询用户订阅了哪些频道,可以使用Hash结构,以用户ID为key,频道名称集合作为field - value对。例如:
HSET user_subscriptions:123 channel:AAPL 1 channel:Tech_sector 1
。这里的1表示已订阅。
- 使用Redis的Set结构,以频道名称作为key,用户ID集合作为value。例如,对于股票代码为AAPL的频道,
- 用户活跃度存储:
- 使用Redis的Sorted Set结构,以用户ID作为member,活跃度分数作为score。例如:
ZADD user_activity 100 123
,表示用户123的活跃度分数为100。通过这种结构可以方便地根据活跃度对用户进行排序。
- 使用Redis的Sorted Set结构,以用户ID作为member,活跃度分数作为score。例如:
算法设计
- 订阅算法:
- 当用户订阅频道时,首先检查用户权限。从用户权限Hash结构中获取用户对于该频道粒度的权限。
- 如果有权限,将用户ID添加到频道对应的Set结构中,并在用户订阅Hash结构中添加相应记录。例如,对于用户123订阅股票代码AAPL的频道:
# 检查权限 permission = redis.hget("user:123", "permission:stock_code") if "AAPL" in permission: redis.sadd("channel:AAPL", 123) redis.hset("user_subscriptions:123", "channel:AAPL", 1)
- 退订算法:
- 用户退订频道时,从频道对应的Set结构中移除用户ID,并在用户订阅Hash结构中删除相应记录。例如,对于用户123退订股票代码AAPL的频道:
redis.srem("channel:AAPL", 123) redis.hdel("user_subscriptions:123", "channel:AAPL")
- 用户退订频道时,从频道对应的Set结构中移除用户ID,并在用户订阅Hash结构中删除相应记录。例如,对于用户123退订股票代码AAPL的频道:
- 动态调整算法:
- 定期(例如每分钟)检查用户活跃度Sorted Set。设定一个活跃度阈值,如活跃度分数低于50的用户视为低活跃度用户。
- 对于低活跃度用户,遍历其订阅的频道,根据业务规则判断是否可以退订。例如,如果业务允许低活跃度用户只保留按板块订阅,而退订按股票代码的订阅:
low_activity_users = redis.zrangebyscore("user_activity", 0, 50) for user_id in low_activity_users: subscriptions = redis.hgetall("user_subscriptions:" + user_id) for channel in subscriptions.keys(): if "stock_code" in channel: redis.srem(channel, user_id) redis.hdel("user_subscriptions:" + user_id, channel)
可能面临的挑战及解决方案
- 高并发订阅退订:
- 挑战:在实时金融数据推送系统中,可能会有大量用户同时进行订阅和退订操作,导致Redis的写入压力增大。
- 解决方案:使用Redis的事务(MULTI - EXEC)机制,将多个相关操作打包成一个原子操作,减少并发冲突。例如,将添加用户到频道Set和更新用户订阅Hash结构的操作放在一个事务中执行。同时,可以考虑使用Redis集群,通过数据分片分散写入压力。
- 数据一致性:
- 挑战:在动态调整订阅时,可能会出现部分数据更新成功,部分失败的情况,导致数据不一致。
- 解决方案:除了使用事务保证操作原子性外,还可以引入版本号机制。在每次更新数据时,增加版本号,读取数据时也读取版本号。如果发现版本号不一致,说明数据在读取后被其他操作修改,需要重新读取和操作。
- 大规模数据存储:
- 挑战:随着用户数量和频道数量的增加,Redis的内存占用可能会过高。
- 解决方案:可以采用数据淘汰策略,如使用
volatile - lru
(对设置了过期时间的key使用LRU算法淘汰)或allkeys - lru
(对所有key使用LRU算法淘汰)策略,在内存不足时自动淘汰不常用的数据。另外,可以定期清理无效数据,如长时间未活跃且已退订所有频道的用户记录。