1dda7e80b9
- 新增 BloomFilterManager:Guava 布隆过滤器(100万容量,1%误判率) - 新增 RedisLockUtil:基于 Redis SETNX + Lua 脚本的分布式锁 - 改造 CacheManager:添加空值短缓存(30秒TTL)+ 互斥锁防击穿(double-check) - 改造 ThumbServiceImpl:使用分布式锁替代 synchronized,集成布隆过滤器 - 新增 docs/技术调研文档.md:完整技术调研文档 - 更新 README.md:添加技术文档索引
106 lines
3.5 KiB
Markdown
106 lines
3.5 KiB
Markdown
# thumb-up
|
|
|
|
🧐 高并发点赞系统
|
|
|
|
> **📖 技术文档**
|
|
> - [技术调研文档](docs/技术调研文档.md) — 缓存穿透/击穿防护、二级缓存、Lua 原子性、HeavyKeeper Top-K、分布式锁
|
|
|
|
> **🔗 参考资料**
|
|
> - [B站千亿级点赞系统服务架构设计 - 哔哩哔哩技术团队](https://www.bilibili.com/opus/758312609901445367)
|
|
|
|
## 模块
|
|
|
|
### 用户模块
|
|
|
|
1. 登录
|
|
- 使用 Session 简单登录
|
|
2. 用户信息
|
|
- 不是本项目讨论的重点,不提供修改
|
|
|
|
### 点赞模块
|
|
|
|
1. 校验
|
|
- 参数合法性
|
|
- 登录态
|
|
- 是否已点赞
|
|
2. 锁
|
|
- 多次点赞统一博客场景
|
|
3. 事务
|
|
- 修改两张表,需要保证数据一致性
|
|
|
|
### 博客模块
|
|
|
|
1. 获取博客
|
|
2. 获取博客列表
|
|
|
|
## QA
|
|
|
|
> Q1. _联合唯一索引_ | 如何从数据库层面杜绝重复数据出现?
|
|
|
|
对于 `thumb` 表,使用联合唯一索引
|
|
```mysql
|
|
CREATE UNIQUE INDEX idx_userId_blogId ON thumb (userId, blogId);
|
|
```
|
|
|
|
---
|
|
|
|
> Q2. _循环依赖_ | 为什么要在 `BlogServiceImpl` 导入 `ThumbService` 上加 `@Lazy`
|
|
|
|
- 博客服务中需要使用点赞服务查询博客对应的点赞情况列表以返回对应视图。
|
|
- 点赞服务中需要使用博客服务更新单条博客的点赞计数器。
|
|
- 两个服务之间产生了**循环依赖**,Spring Boot 2.6 以后的版本默认不允许循环依赖,这里可以使用懒加载的方式避免这一问题。
|
|
|
|
---
|
|
|
|
> Q3. _锁_ | 点赞接口加锁对象是什么?对用户加锁还是对帖子加锁?
|
|
|
|
- 加锁目的是为了避免用户并发请求造成数据不一致。
|
|
- 不能对帖子加锁,因为帖子是全局的,加锁会导致其他用户无法点赞。
|
|
- 因此需要对用户加锁,故而实际上是将每个用户的 id 当作锁。
|
|
|
|
|
|
---
|
|
|
|
|
|
> Q4. _字符串对象锁_ | 点赞接口的锁具体如何处理?
|
|
|
|
- `synchronized (("LOCK-USERID-" + loginUser.getId().toString()).intern()) `
|
|
- 每个用户持有一个唯一 id,因此考虑把 id 对应的 String 常量池对象加锁。
|
|
- 为了避免对程序中其他需要使用该字符串常量的地方产生影响,这里对字符串作出处理 `"LOCK-USERID-" + loginUser.getId().toString()`
|
|
- 并且我们需要将字符串放到常量池中,调用 `intern()` 方法。
|
|
|
|
---
|
|
|
|
> Q5. _序列化_ | 对于 Redis 数据的存储使用序列化是怎么样的?
|
|
|
|
- `key` 和 `hash filed` 使用 String 类型,`value` 使用 Object 类型。
|
|
- `value` 为 `Long` 或者 `Integer` 类型时,优先进行以下操作——
|
|
- 存储时转化为 `String` 类型
|
|
- 取出时转化为 `Long` 或者 `Integer` 类型
|
|
- (注意转化前需要先判空,避免空指针)
|
|
|
|
---
|
|
|
|
> Q6. _类型转换_ | `Object` 向 `Long` 的转化优化方式
|
|
|
|
- 方法 A
|
|
- `Long.valueOf(object.toString())`
|
|
- 注意:`object` 为非数字字符串时会抛出 `NumberFormatException``
|
|
- 方法 B
|
|
- `((Number) object).longValue() `
|
|
- 注意:`object` 为非数字字符串时会抛出 `ClassCastException``
|
|
- 比较
|
|
- 方法 B 优于方法 A
|
|
- 方法 A 需要调用 `toString()` 方法,有额外开销
|
|
- 方法 B 直接调用 `longValue()` 方法,`JVM` 优化
|
|
- 方法 B 抛出的异常容易定位问题。
|
|
|
|
---
|
|
|
|
> Q7. _常量_ | 定义常量的最佳方式?
|
|
|
|
- > "Do not use interfaces for constants. Interfaces are for defining types and behaviors, not for storing data."
|
|
>
|
|
> — _Effective Java Item 18_
|
|
- 不建议选择 `Interface` 定义常量
|
|
- 而应该选用 `Class - public static final String` 定义常量 |