为了账号安全,请及时绑定邮箱和手机立即绑定

亿级用户量下验证用户名可用性的可扩展策略

开头

你有没有试过注册一个应用程序,却发现你心仪的用户名已经被别人用了?虽然这可能看起来只是一件小事,但对于那些需要处理大量用户的程序来说,这其实是个挺大的技术问题。确认用户名是否可用有几种不同的方法,每种方法都有它的利弊。在这篇文章里,我们将探讨解决这个问题的三种常见方法是,在使用NestJS连接到任何数据库的应用程序中。

  1. 传统的数据库查询
  2. Redis 缓存机制
  3. Bloom Filter 技术
方法1: 查询数据库的方法

最简单直接的方法来检查用户名是否存在的方法是查询PostgreSQL数据库。然而,随着用户数量的增加,数据库查询速度变慢,导致诸如高延迟和可扩展性问题等性能问题。

    // 用户服务类  
    import { Injectable } from '@nestjs/common';  
    import { InjectRepository } from '@nestjs/typeorm';  
    import { Repository } from 'typeorm';  
    import { User } from './user.entity';  

    @Injectable()  
    export class UserService {  
      constructor(  
        @InjectRepository(User)  
        private readonly userRepository: Repository<User>,  
      ) {}  

      // 检查用户名是否存在  
      async checkUsernameExists(username: string): Promise<boolean> {  
        const user = await this.userRepository.findOne({  
          where: { username },  
        });  
        return !!user;  
      }  
    }

这种方法在较小的数据集上表现良好,但在处理数以百万计的用户时,它的效率会大幅下降,这是因为:

  1. 高延迟问题:
  • 在查询大型数据库时,性能可能会因网络延迟增加而下降。每次查询都会涉及应用程序与数据库服务器之间的网络交互。

2. 数据库负载过重:

  • 常规的 SELECT 查询用于检查用户名的唯一性会占用大量的数据库资源,包括 CPU 和 I/O。这可能导致瓶颈,特别是在高峰期。

3. 扩展性难题:

  • 数据库在处理大量并发连接时存在固有的限制。随着用户注册数量的增长,数据库可能难以跟上注册的增长,导致性能下降的问题。垂直扩展数据库(比如增加更多资源)可能成本高昂,而且还有局限性。
方法二:Redis缓存方案:

更优化的方法是使用Redis作为缓存层来减少直接数据库查询的数量。Redis允许我们将用户名称存放在Redis的哈希映射中,从而实现更快的检查。

它是怎么工作:
  • 当用户注册或登录时将用户名存储在 Redis 中:在查询数据库之前,先检查 Redis 中有没有这个用户名。
  • 更快的验证:因为 Redis 是内存中的操作,所以检查键是否存在比查询数据库要快得多。
    import { Injectable } from '@nestjs/common';  
    import { InjectRepository } from '@nestjs/typeorm';  
    import { Repository } from 'typeorm';  
    import { RedisService } from 'nestjs-redis';  
    import { User } from './user.entity';  

    @Injectable()  
    // 用户服务类
    export class UserService {  
      private redisClient;  

      constructor(  
        @InjectRepository(User)  
        private readonly userRepository: Repository<User>,  
        private readonly redisService: RedisService,  
      ) {  
        this.redisClient = this.redisService.getClient();  
      }  

      async checkUsernameExists(username: string): Promise<boolean> {  
        // 先检查 Redis 缓存  
        const cacheKey = `username:${username}`;  
        const cachedUser = await this.redisClient.get(cacheKey);  

        if (cachedUser) {  
          return true;  
        }  

        // 如果 Redis 中没有该用户,则检查数据库  
        const user = await this.userRepository.findOne({ where: { username } });  

        if (user) {  
          // 将用户名存入 Redis 以供后续查询  
          await this.redisClient.set(cacheKey, 'true');  
          return true;  
        }  

        return false;  
      }  
    }

这种方法中,首先检查 Redis 以避免不必要的数据库查找。如果用户名不在 Redis 中,则执行数据库查找,如果找到该用户名,则将其缓存到 Redis 中以便将来查找。这种方法比每次查询数据库快得多,但可能会受到内存使用的限制。

遇到了一些挑战:
  • 内存使用:在 Redis 中存储 10 亿个用户名会消耗大量内存。每个用户名大约占用 15 字节,因此 10 亿个用户名将需要大约 15GB 的存储空间。
  • 一致性:缓存需要与数据库保持一致,如果更新操作没有正确同步,这可能比较棘手。
方法三:布隆过滤器技术

当内存效率特别重要时,Bloom Filter 提供了一种优雅的解决方法。Bloom Filter 是一种概率型数据结构,可以用来检查某个元素是否属于一个集合,而使用的内存比实际存储这些数据要少。但是可能存在 假阳性(即,过滤器可能会误报某个用户名存在),但不会有 假阴性(如果它说某个用户名不存在,那这个用户名就肯定不存在)。

这是怎么工作的:
  1. 位数组和哈希函数:布隆过滤器(Bloom Filter)使用位数组和多个哈希函数将用户名映射到数组中的位。
  2. 添加一个用户名:当添加一个用户名时,数组中多个位会被翻转为1。
  3. 检查用户名是否存在:在检查用户名是否存在时,使用相同的哈希函数。如果所有对应的位都是1,那么该用户名可能(假阳性)存在。如果任何一个位都不是1,那么该用户名肯定不存在。
    import { Injectable } from '@nestjs/common';  
    import { InjectRepository } from '@nestjs/typeorm';  
    import { Repository } from 'typeorm';  
    import { User } from './user.entity';  
    import { BloomFilter } from 'bloom-filters';  

    @Injectable()  
    export class UserService {  
      private bloomFilter: BloomFilter;  

      constructor(  
        @InjectRepository(User)  
        private readonly userRepository: Repository<User>,  
      ) {  
        // 初始化布隆过滤器为2000万大小并使用5个哈希函数  
        this.bloomFilter = new BloomFilter(20 * 1000 * 1000, 5);  
      }  

      // 将用户名添加到布隆过滤器  
      async addUsernameToFilter(username: string): Promise<void> {  
        this.bloomFilter.add(username);  
      }  

      // 使用布隆过滤器检查用户名是否存在  
      async checkUsernameExists(username: string): Promise<boolean> {  
        return this.bloomFilter.has(username);  
      }  
    }

结果为:

    用户名 'moise' 存在吗? 是  
   用户名 'moise' 存在吗? 否

布隆过滤器的视觉解释

下面的图表直观地展示了布隆过滤器是如何工作的。

第(a)部分:插入序列

  • 序列“ACCGTAG”:假设我们要检查序列“ACCGTAG”是否在我们的集合里。
  • k-mer:这个序列会被分解成称为“k-mer”的较小部分,像是片段或者碎片。例如,“ACCG”,“CCGT”,“CGTA”和“GTAG”。
  • 哈希k-mer:这些k-mer通过一组哈希函数处理,哈希函数会将它们映射到位数组中的特定位置。
  • 设置位:对于每个k-mer,位数组中对应的位会被设置为1。位数组最初全是0,但在添加k-mer时,特定的位会被打开,也就是设置为1。

部分 (b): 查序列

  • 查询“CGTAT”:现在,假设我们想检查“CGTAT”是否在我们的集合中。
  • k-mer:就像之前一样,这个序列被分解为k-mer,例如“CGTA”和“GTAT”。
  • 检查比特位:这些k-mer经过哈希处理,我们检查比特数组中对应的比特位:
  • 如果所有比特位都为1(例如,“CGTA”),这意味着序列可能在集合里。
  • 如果有一个比特位为0(例如,“GTAT”),这说明序列肯定不在集合里。

总结:

  • 布隆过滤器优势:这种方法在检查某物是否可能存在于集合中时,既节省内存又快速。
  • 误报:有时,它可能会错误地表明某项存在于集合中,而实际上它并不在(这就是所谓的“误报”)。
  • 确定的否定:如果检查结果显示某项不在集合中,那么可以肯定这是正确的。

此图表直观展示了Bloom Filter如何高效检查数据是否存在,而无需查看整个数据集,使其在诸如过滤或加快数据库查询等众多场景中非常有用。

优点:
  • 内存效率高:与缓存所有用户名相比,内存使用量显著减少。
  • 快速查找用户:查找时间是固定的,与用户数量无关。
最后的结论
  • 数据库查询:简单但不适用于大数据集的扩展性。
  • Redis缓存:通过减少数据库查询次数提高了性能,但存在内存限制的问题。
  • Bloom过滤器:为大数据集提供了高度内存高效的解决方案,具有快速查询速度,但引入了误报的可能性。

每种方法都有其优缺点,合适的解决方案取决于您的具体需求,包括用户基数大小、可用资源以及性能、内存和准确性之间的权衡。

栈区 🎓

感谢你一直读到最后。在你离开之前,还有件事想告诉你:

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消