黑马点评项目知识分析

黑马点评项目知识分析
mengnankkzhou缓存
1.你在项目中是怎么设计缓存系统的?如何解决缓存与数据库之间的一致性问题?
1.缓存的读取流程:本地缓存->redis->数据库
2.缓存写入策略,为了保持数据库和redis和缓存的一致性,使用了延迟双删策略。
- 写入数据时,先更新数据库;
- 然后立即删除 Redis 缓存;
- 最后通过定时任务或延迟队列再次删除缓存,避免并发读取导致旧缓存重新写入。
3.解决缓存的三大问题
缓存穿透:布隆过滤器,接口限流,空值检验。
缓存击穿:互斥锁,逻辑过期,延迟双写,热点key处理。
缓存雪崩:空对象,随机因子,限流,缓存预热,互斥锁,热点key不过期
4.服务降级&熔断
令牌限流,服务降级,只允许部分接口访问
秒杀
1.假设您要设计一个秒杀系统,需要支持10万QPS的并发请求,商品库存只有1000件。
整体架构设计(包括限流、缓存、数据库等)
如何防止超卖问题?
如何处理热点数据问题?
用户请求 → CDN → 网关限流 → 应用服务器 → Redis集群 → 异步写入MySQL
限流:AOP + IP/用户ID限流,网关 + 应用层双重限流,系统过载时快速失败
缓存设计:三层缓存,线程池异步写入数据库,预热机制,定时加载到Redis.
核心:redis+lua 用户资格验证 + 库存检查 + 原子扣减
2.你使用Set存储参与秒杀的用户,如果参与的用户过多,Set会不会形成大Key? 如果会,请说明理由以及如何避免。
使用 Set 的目的是为了去重,防止用户重复参与秒杀,并且意识到了如果参与用户过多,Set 可能会变成大Key, 从而影响Redis性能, 以及对大key进行了拆分。
- 按时间窗口拆分: 可以按照秒杀活动的时间窗口,将Key进行拆分。例如,每分钟创建一个新的Set,用于存储当分钟参与秒杀的用户ID。这样可以避免单个Key过大,并且方便后续的数据清理。
- 按用户ID哈希拆分: 可以根据用户ID的哈希值,将用户分散到不同的Set中。例如,可以使用
userId % shardCount的方式,将用户分配到不同的Set中。其中,shardCount表示Set的数量。
在此之前可以使用布隆过滤器进行过滤出重复的用户,避免写入set
可以设置Set的最大元素数量。当Set中的元素数量达到上限时,拒绝新的用户参与秒杀。
3.如何界定Redis中的大Key? 有哪些判断标准? 从内存占用、性能影响等方面进行考虑。
标准:
- 内存占用: 这是最直观的判断标准。可以使用
redis-cli --bigkeys命令扫描Redis实例,找出占用内存最多的Key。通常,如果一个Key的大小超过了几MB,就可以认为是BigKey。 更精确的判断,可以使用MEMORY USAGE key命令查看key占用的具体字节数。 - 操作耗时: 可以通过Redis的慢查询日志来监控Key的操作耗时。如果某个Key的读写操作经常超过
slowlog-log-slower-than配置的阈值(默认10毫秒),则可以认为是BigKey。 - 网络带宽: 当客户端获取BigKey时,会占用大量的网络带宽,导致其他客户端的请求变慢。可以通过监控Redis的网络流量来判断是否存在BigKey。
影响:阻塞redis,网络拥塞,持久化困难
4.redis集群分片处理
用户ID如何分片?在Lua脚本中如何处理分片查询?Redis集群配置
分片策略:
hash取模,一致性hash,根据用户ID分片,根据时间分片
Lua脚本的分片处理:
- 在Lua脚本中计算分片索引: 将分片算法嵌入到Lua脚本中,根据用户ID计算出对应的分片索引。
- 动态构建Key: 根据分片索引动态构建Key,然后查询对应的分片。
- 传递分片数量 需要在调用Lua脚本时,将分片数量作为参数传递给Lua脚本。
- 然后再进行Lua脚本的正经操作
- 哈希算法一致性: 必须保证Lua脚本中使用的哈希算法与Java代码中使用的哈希算法一致,才能保证计算出的分片索引正确。
- 避免跨分片操作: 尽量避免跨分片操作。如果需要跨分片操作,可以考虑将数据迁移到同一个分片,或者使用分布式事务。 在秒杀场景下,尽量将单个用户的操作限制在单个分片内。
- 简化Hash算法: Lua 中可能没有Java 中一样的hash算法函数,需要根据实际情况简化。 比如上面的
string.byte(user_id, 1)只是取UserID的第一个字符的ASCII码作为简化hash, 实际情况要更复杂。
那我们完成之后怎么清理分片的数据
- 设置过期时间,设置一个较短的过期时间,可能存在一定的延迟,因为Redis的过期删除机制是惰性删除和定期删除相结合。 可能会占用一些额外的内存。然后Lua脚本的成功购买的数据过期时间可以长一点。
- 手动清理,编写一个脚本,遍历所有分片Key,然后使用
DEL命令删除。可以使用使用SCAN命令分批删除Key,避免一次性删除大量Key导致Redis阻塞。 - 异步清理,在秒杀活动结束后,将需要清理的分片Key放入一个队列中,然后由消费者异步清理。
- 重命名+Lazy Free,可以将需要删除的key重命名,然后使用
UNLINK key命令。UNLINK命令是异步删除数据,不会阻塞Redis主线程 。
最好的方案是过期时间+异步清理
5.集群配置
我们使用Redis Cluster
Redis Cluster提供了自动分片、高可用、故障转移等功能,可以满足秒杀场景的需求。
- 哈希槽: Redis Cluster将所有的数据划分为16384个哈希槽。
- 分配策略: 每个主节点负责一部分哈希槽。
- 数据存储: 当一个Key被写入Redis Cluster时,会根据CRC16算法计算出该Key的哈希值,然后将哈希值对16384取模,得到该Key对应的哈希槽。最后,将该Key存储到负责该哈希槽的主节点上。
- 动态调整: Redis Cluster可以动态调整哈希槽的分配,实现节点的扩容和缩容。
在集群的条件下,lua脚本会有限制
Lua脚本只能操作属于同一个哈希槽的Key,否则会报错。 这是Redis Cluster设计上的限制。
- 将相关Key放在同一个哈希槽: 使用
{}将Key的一部分作为哈希槽,例如:{product_123}:user_1和{product_123}:user_2会放在同一个哈希槽。 - 使用Redlock: 如果必须跨哈希槽操作,可以使用Redlock算法实现分布式锁,保证原子性。 但不推荐,因为Redlock性能较低,实现复杂
尽可能的将相关的Key放在一个hash槽里面
6.bitmap记录活跃用户
bitmap在Redis中是如何实现的?如果让你设计一个功能:统计你们”生活商店优选”平台上一个月内每天的活跃用户,用户ID范围是1到1000万,你会如何用bitmap来实现?
如何存储每天的活跃用户数据?
如何快速查询某个用户在某天是否活跃?
如何统计某天的总活跃用户数?
内存占用大概是多少?
每天一个bitmap,active_users:20250101,然后setbit位数为1,活跃就是1.统计活跃总的用户数就是BITCOUNT
1000万用户需要:10,000,000 bits = 1.25MB,30天需要:1.25MB × 30 = 37.5MB
如果要用bitmap来记录RefreshToken的黑名单(已失效的token),你会如何设计?考虑到token是字符串,如何映射到bitmap的位置?
token是一段字符串,如何将这个字符串映射到bitmap的位置?
直接使用redis set来存储token,然后去sismember黑名单的key
登录
1.在你的JWT双Token方案中,如果发现RefreshToken被盗用了(比如攻击者用它疯狂刷新AccessToken发起请求),你会如何设计检测和防护机制?
如何检测异常的Token使用模式? 如何在不影响正常用户的情况下阻止攻击? 需要记录哪些信息来支持安全分析?
检测异常的Token:我们进行分层限流,
- UserID级别: 限制单个UserID在单位时间内刷新Token的次数(例如:5次/分钟,30次/小时)。
- IP地址级别: 限制单个IP地址在单位时间内刷新Token的次数(例如:10次/分钟)。
- 全局级别: 监控整个系统的RefreshToken刷新频率,达到阈值时触发告警。
可以加上地理位置/设备指纹
或者是使用AOP+令牌桶,实现设定一个时间窗口,例如1小时,如果一个RefreshToken 在多个不同的IP地址或者设备指纹上使用,而且使用次数超过设定的阈值,则判定为异常。
可疑行为:我们登出之后先加入黑名单,防止重刷,设置一个相对较短的过期时间
记录行为:
- RefreshToken使用记录:
- 时间戳: RefreshToken的使用时间。
- IP地址: RefreshToken请求的IP地址。
- 地理位置: RefreshToken请求的地理位置。
- 设备指纹: RefreshToken请求的设备指纹。
- UserID: RefreshToken对应的UserID。
- AccessToken: 刷新后的AccessToken。
- 安全事件记录:
- 事件类型: 异常登录、RefreshToken被盗用、恶意攻击等。
- 事件等级: 高危、中危、低危。
- 处理状态: 已处理、未处理、处理中。
- 处理人: 处理安全事件的人员。
- 处理时间: 处理安全事件的时间。
- 备注: 处理安全事件的备注信息。
用户如何












