短链接核心1:用户注册

短链接核心1:用户注册
penjc前提:数据库用户表分表
用户名全局唯一
分片键为用户名
登录时需要通过用户名查询用户信息,所以需要将用户表分片,以用户名为分片键,将用户名的hash值对分片数取模。
而如果用用户ID作为分片键,那么在通过用户名登录时查询没有带上分片键,数据库会用union all的形式查所有的表,造成非常大的性能深渊。
分片键的关键因素:
访问频率:选择分片键应考虑数据访问频率。将经常访问的数据放在一个分片上,可以提高查询性能和降低跨分片查询的开销。
数据均匀性:分片键应该保证数据的均匀分布在各个分片上,避免出现热点数据集中在某个分片上的情况。
数据不可变:一旦选择了分片键,它应该是不可变的,不能随着业务的变化而频繁修改。
业务流程
完整代码
1 |
|
检查用户名是否已存在
如果有海量用户查询的用户名存在或不存在,全部请求数据库,会将数据库直接打满。
需要将用户名存储在缓存中
本项目通过布隆过滤器检查用户名是否已存在
为什么使用布隆过滤器?
- 布隆过滤器是一个很长的二进制向量和一系列随机映射函数。
- 布隆过滤器可以用于检索一个元素是否在一个集合中。
- 布隆过滤器的优点是空间效率和查询时间都远远超过一般的算法。
- 布隆过滤器的缺点是有一定的误识别率和删除困难。
- 布隆过滤器的应用场景:缓存穿透、用户注册、垃圾邮件过滤、爬虫URL去重等。
- 布隆过滤器的原理:将元素通过多个 Hash 函数映射到位数组中,当查询元素时,通过多个 Hash 函数映射到位数组中,只有当所有位都为 1 时,才表示元素存在。
- 布隆过滤器的误判率:当位数组的长度和 Hash 函数的个数确定时,误判率是固定的。
如果不采用布隆过滤器而直接用缓存,需要考虑缓存过期时间,如果永不过期,占用 Redis 内存太高。
- 配置布隆过滤器
1 |
|
tryInit 有两个核心参数:
- expectedInsertions:预估布隆过滤器存储的元素长度。
- falseProbability:运行的误判率。
错误率越低,位数组越长,布隆过滤器的内存占用越大。
错误率越低,散列 Hash 函数越多,计算耗时较长。
- 使用布隆过滤器
1 | private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter; |
- 检查用户名是否已存在
1 |
|
用户注册
如何防止恶意请求毫秒级触发大量请求去一个未注册的用户名?
因为用户名没注册,所以布隆过滤器不存在,代表着可以触发注册流程插入数据库。但是如果恶意请求短时间海量请求,这些请求都会落到数据库,造成数据库访问压力。
这里通过分布式锁,锁定用户名进行串行执行,防止恶意请求利用未注册用户名将请求打到数据库。
1 | RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + requestParam.getUsername()); |
tryLock() 和 lock() 区别
RLock
(RedissonLock
)是 Redisson 提供的分布式锁,它基于 Redis 实现。lock()
和 tryLock()
都可以用于加锁,但它们的工作方式有所不同。
1. lock()
方法
1 | RLock lock = redissonClient.getLock("myLock"); |
特点
- 阻塞式:如果锁已被其他线程或进程持有,
lock()
会一直等待,直到获取到锁。 - 默认支持 WatchDog 机制(避免死锁):Redisson 默认会给锁设置 30 秒的过期时间,并且如果业务逻辑没有执行完,Redisson 会每隔 10 秒自动续期,直到
unlock()
释放锁。 - 适用于长时间的任务,或者必须保证成功获取锁的场景。
2. tryLock()
方法
1 | RLock lock = redissonClient.getLock("myLock"); |
特点
- 非阻塞:如果锁已经被占用,它不会等待,而是立即返回
false
。 - 可以设置超时时间:
1
boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
5
秒是等待时间(如果锁已被占用,它会最多等待 5 秒)。10
秒是锁的持有时间(如果获取到锁,它最多会持有 10 秒,之后自动释放)。
- 适用于尝试获取锁但不想长时间等待的场景,如限时任务或高并发场景。
3. lock()
vs tryLock()
区别总结
lock() | tryLock() | |
---|---|---|
是否阻塞 | 阻塞(一直等待获取锁) | 非阻塞(立即返回) |
等待锁的方式 | 获取不到锁会一直等待 | 立即返回 false ,可设定最大等待时间 |
是否可以设置超时 | 支持(默认 30 秒 WatchDog 续期) | 支持(可手动指定锁的持有时间) |
适用场景 | 必须确保拿到锁的任务 | 不能等待太久的任务,如限时任务 |
4. 什么时候用 lock()
,什么时候用 tryLock()
?
✅ 用 lock()
的情况:
- 需要一定要成功获取锁才能执行任务,例如分布式事务。
- 任务执行时间不确定,依赖Redisson WatchDog 续期机制。
✅ 用 tryLock()
的情况:
- 不想等待太久,如果锁不可用,可以立即做其他事情(如返回错误或稍后重试)。
- 需要设置最大等待时间和锁的持有时间,如秒杀、限时抢购等高并发场景。
示例:
订单支付并发处理(
lock()
):1
lock.lock();
- 确保订单不会被多个进程同时处理。
限时秒杀抢购(
tryLock(3, 5, TimeUnit.SECONDS)
):1
if (lock.tryLock(3, 5, TimeUnit.SECONDS)) {
- 如果 3 秒内没拿到锁,直接返回 “抢购失败”。
🚀 结论:
- 如果需要一定要获取锁再执行任务,用
lock()
。 - 如果获取不到锁就直接返回或限时等待,用
tryLock()
。