首页 > 数据库 >使用Redisson和分库分表技术实现海量请求注册功能

使用Redisson和分库分表技术实现海量请求注册功能

时间:2024-08-02 13:23:58浏览次数:19  
标签:分库 数据库 布隆 过滤器 Redisson 线程 user 注册 分表

文章目录

1. 海量注册的常见问题和解决方案概述

Q: 什么是海量注册?

A: 在短时间内,系统或平台接收到的大量新用户注册行为。

Q: 海量注册要应对什么样的问题?

A:

高并发请求处理:当大量用户同时尝试注册时,系统需要能够快速响应并处理这些请求。如果系统处理能力不足,用户可能会遇到注册失败、页面加载缓慢或超时等问题。

数据重复与唯一性校验: 在注册过程中,需要确保用户提交的信息(如用户名、邮箱、手机号等)是唯一的。在高并发情况下,系统需要高效地进行唯一性校验,避免数据重复。

安全性: 海量注册可能会吸引恶意用户进行自动化注册(如机器人注册、恶意刷号等),因此系统需要采取有效的安全措施,如验证码验证、IP限制、行为分析等,来防止滥用和保障用户数据安全。

用户体验问题:系统需要设计合理的用户体验策略,如提供清晰的注册进度反馈、友好的错误提示和快速的响应速度等。

本文的对上述问题的应对策略:

  1. 布隆过滤器判断用户唯一性
  2. 通过分布式锁和快速失败对同一时间的某一个账号进行锁定
  3. 通过创建数据库唯一索引进行兜底, 捕获DuplicateKeyException
  4. 通过分表技术 水平分库+水平分表 将一个数据库拆分成多个库, 将一张表拆分成多个表
  5. 通过自定义线程池CompletableFuture 异步初始化用户其他信息或其他操作(发送邮箱/短信/记录注册日志), 提升响应速度

2. 布隆过滤器判断用户唯一性

布隆过滤器是一种数据结构,用于快速判断一个元素是否存在于一个集合中。它以牺牲一定的准确性为代价,换取了存储空间的极大节省和查询速度的显著提升。详细: 布隆(Bloom Filter)过滤器——全面讲解

Q: 为什么要使用布隆过滤器

A:

海量用户如果说查询的用户名存在或不存在,全部请求数据库,会将数据库直接打满。

并且它空间效率高可以显著减少内存或存储空间的消耗, 查询速度快查询操作是常数时间复杂度O(k)

容忍误判, 布隆过滤器说存在可能数据库中不存在, 而说布布隆过滤器说不存在就肯定不存在

在注册的业务中, 对布隆过滤器的误判是能接受的, 如果用户的账号是123系统返回账号已存在那么也无所谓, 就换个1235也是没问题的

这里使用Redisson提供布隆过滤器来继承来 SpringBoot 项目中

Redisson官文: Redisson: Easy Redis Java client and Real-Time Data Platform

引入Redisson依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>

在配置文件application.yml|yaml|properties中配置redis相关参数

# yml
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: # 有就写
# properties
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.password= # 有就写

配置布隆过滤器

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 布隆过滤器配置
 * 错误率越低,位数组越长,布隆过滤器的内存占用越大
 * 错误率越低,散列 Hash 函数越多,计算耗时较长
 */
@Configuration(value = "rBloomFilterConfiguration")
public class RBloomFilterConfiguration {

    /**
     * 防止用户注册查询数据库的布隆过滤器
     */
    @Bean
    public RBloomFilter<String> userRegisterBloomFilter(RedissonClient redissonClient) {
        RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("userRegisterCachePenetrationBloomFilter");
        // expectedInsertions:预估布隆过滤器存储的元素长度 = 1亿:能存1亿条数据
		// falseProbability:运行的误判率 = 设置为千分之一的误判率
        cachePenetrationBloomFilter.tryInit(100000000L, 0.001);
        return cachePenetrationBloomFilter;
    }
}

接收到注册请求后,系统会首先通过布隆过滤器判断该账号是否已存在。若布隆过滤器判定账号已存在,系统将立即反馈账号已被使用的信息,从而提高注册流程的效率和用户体验。

大致流程如下

在这里插入图片描述

具体代码实现

@Autowired
private RBloomFilter<String> userRegisterBloomFilter;

// 判断用户名是可用
public Boolean availableUsername(String username) {
    return !userRegisterBloomFilter.contains(username);
}

public void register(RequestParam RequestParam){
    if (!availableUsername(requestParam.getUsername())) {
		throw new ServiceException("用户名已被占用 ( つ•̀ω•́)つ");
    }
}

当然当用户成功完成注册流程后,将其信息纳入布隆过滤器之中,以此作为高效判断用户是否已存在的机制。减轻了数据库的负担,确保了系统在处理用户存在性验证时能够更为流畅与迅速。

public void register(RequestParam RequestParam){
    // 注册业务完成...
    // 添加到布隆过滤器中
    userRegisterBloomFilter.add(requestParam.getUsername());
}

3. 通过分布式锁和快速失败策略对同一时间的某一个账号进行锁定

在处理高并发环境下的账号操作时,尤其需要精细地管理并发问题,以防止数据不一致和潜在的服务冲突。特别是当多个用户尝试同时以同一用户名进行操作时,若缺乏有效的控制机制,可能会引发严重的并发问题。

如果这个正在注册的username被上锁了, 不管已占用锁的用户是否注册成功则直接返回账号已被使用, 这样可以不占用主线程走剩下的逻辑, 也不需要阻塞等待锁释放, 具体流程和实现代码如下

在这里插入图片描述

private final RedissonClient redissonClient;

public void register(UserRegisterReqDTO requestParam) {
    RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + requestParam.getUsername());
    if (!lock.tryLock()) {
        // 获取不到锁, 快速失败, 不占用线程阻塞等待
        throw new ClientException(USER_NAME_EXIST);
    }
    // 执行注册逻辑...
    // 最后释放锁
    lock.unlock();
}

4. 数据库唯一索引兜底

在集群同步过程中, 由于网络延迟或节点故障等原因,可能会出现数据短暂不一致的情况。这种不一致性若未妥善处理,可能导致本应被布隆过滤器拦截的无效请求直接穿透至MySQL数据库层,进而增加了数据库的负担并可能引入数据错误, 所以还需对username字段添加一个唯一索引并在代码层面捕获这个DuplicateKeyException异常。

public void register(UserRegisterReqDTO requestParam) {
    try {
        int inserted = userMapper.insert(BeanUtil.toBean(requestParam, UserEntity.class));
        if (inserted < 1) {
            throw new ClientException(USER_SAVE_ERROR);
        }
    } catch (DuplicateKeyException ex) {
        throw new ClientException(USER_EXIST);
    } finally {
        lock.unlock();
    }
    userRegisterCachePenetrationBloomFilter.add(requestParam.getUsername());
}

5. 通过水平分库水平分表

分库分表是数据库架构优化的重要手段,旨在应对高并发访问海量数据存储的挑战。通过分散存储数据到多个数据库或表中,可以显著提高系统的扩展性、减少单库负载、优化查询性能,并有效支持更大规模的数据存储与处理需求。

现有实现技术:

ShardingSphere-JDBC: 基于AOP原理, 在应用程序中对本地执行的SQL进行拦截, 解析, 改写, 路由处理, 需要自行编码配置实现, 只支持Java语言, 性能较高, 官文: ShardingSphere-JDBC

MyCat: 数据库分库分表中间件, 不用调整代码, 支持多种语言, 性能不及前者, 且社区不活跃, 更新不频繁

官文: MyCat2

老规矩了, SpringBoot 集成某个技术基本就三个步骤

  1. 引入依赖
  2. 编写配置application.yml|yaml|properties
  3. 跟着官方文档来使用

第一步: pom.xml

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.3.2</version>
</dependency>

第二步: application.yml|yaml|properties

spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:shardingsphere-config.yaml

第三步: shardingsphere-config.yaml配置数据源和分片规则

了解分库分表的核心概念:

  • 分片键: 用于将数据库(表)水平拆分的数据库字段
  • 逻辑表: 相同结构的水平拆分数据库(表)的逻辑名称,是 SQL 中表的逻辑标识。 例:用户数据拆分为 32 张表,分别是 t_user_0t_user_31,他们的逻辑表名为 t_user
  • 真实表: 在水平拆分的数据库中真实存在的物理表。 即下个示例中的 t_user_0t_user_31

将一个大型数据库中的t_user表根据用户的账号拆分到多个不同的数据库实例中,每个数据库实例中的t_user表结构相同,但存储的数据子集不同,以分散数据负载和提高查询效率。所有数据库实例中的t_user表数据的并集构成了原始t_user表的全量数据。
在这里插入图片描述

配置分片规则

根据图上的配置的规则如下

  • 分片键为 username
  • 两个数据源: ds_0, ds_1
  • 每个数据源中水平拆分成16张表
  • 分片规则为对 username 进行 HAHS_MOD
dataSources:
  # 数据源配置
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/db_0?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/db_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

rules:
  - !SHARDING
    tables:
      t_user: # 逻辑表名称
        actualDataNodes: ds_0.t_link_${0..15},  ds_1.t_link_${16..31} # Inline 语法
        # 分表策略
        tableStrategy:
          standard:
            # 分片键满足 1.访问频率高 2.数据均匀 3.数据不可变
            shardingColumn: username
            shardingAlgorithmName: user_table_hash_mod
        # 分库策略
        defaultDatabaseStrategy:
          standard:
            shardingColumn: username
            shardingAlgorithmName: database_hash_mod
            
    # 分片算法 对应 rules[0].shardingAlgorithms
    shardingAlgorithms:
      # 数据库表分片算法
      user_table_hash_mod: # 分片算法文档: https://shardingsphere.apache.org/document/current/cn/dev-manual/sharding/
        # 根据分片键进行Hash分片
        type: HASH_MOD
        props:
          # 分片数量
          sharding-count: 16
      # 分库算法
      database_hash_mod:
        type: HASH_MOD
        props:
          sharding-count: 2
props:
  # 输出逻辑SQL和真实SQL
  sql-show: true

然后正常使用即可

int inserted = userMapper.insert(BeanUtil.toBean(requestParam, UserEntity.class));

输出的逻辑SQL真实SQL如下

# Logic SQL: 
INSERT INTO t_user  ( id,username)  VALUES  (?,?)
# Actual SQL: ds_0
INSERT INTO t_user_11  ( id,username) VALUES  (?, ?) ::: [1, tiantian] # 使用了 ds_0 数据源和 11 号用户表

6. 通过自定义线程池和异步初始化

配置线程池

三个核心参数

  • corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
  • maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

确定核心线程数:

  • 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换
@Bean(name = "registerExecutorService")
public ThreadPoolExecutor registerExecutorService() {
    // 计算核心线程数
    int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
    // 设置最大线程数
    int maximumPoolSize = corePoolSize * 2;
    // 其容量可以根据实际情况调整
    BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(300);
    
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize, 		// 核心线程数
        maximumPoolSize,    // 最大线程数
        60L,                // 线程空闲时的存活时间
        TimeUnit.SECONDS,   // 存活时间的时间单位
        workQueue           // 工作队列
    );
    return executor;
}

操作异步化

private RegisterExecutorService registerExecutorService;

public void register(RequestParam RequestParam){
    // 注册业务完成...
    userRegisterBloomFilter.add(requestParam.getUsername());
    CompletableFuture.runAsync(() -> {
        // 初始化用户信息/发送邮件/短信/记录注册日志
    }, registerExecutorService);
}

标签:分库,数据库,布隆,过滤器,Redisson,线程,user,注册,分表
From: https://blog.csdn.net/m0_72918997/article/details/140869732

相关文章

  • 数据库系列: 主流分库分表中间件介绍(图文总结)
    相关文章数据库系列:MySQL慢查询分析和性能优化数据库系列:MySQL索引优化总结(综合版)数据库系列:高并发下的数据字段变更数据库系列:覆盖索引和规避回表数据库系列:数据库高可用及无损扩容数据库系列:使用高区分度索引列提升性能数据库系列:前缀索引和索引长度的取舍数据库系列:My......
  • 深入理解MyCAT分库分表机制:架构师的秘密武器
    一、MyCAT分库和分表的概念1.分库(DatabaseSharding)分库是将一个大数据库拆分成多个小数据库,以减小单个数据库的压力并提高系统的扩展性。每个子数据库可以分布在不同的服务器上,从而分散负载并提高性能。示例:假设我们有一个用户信息数据库users_db,其中包含了大量的用......
  • 手搓Lock注解,一举解决redisson分布式锁
    最近闲来时间,写了个redis-lock-stater,在pom中引入即可,直接在所要加锁的函数中使用@lock注解即可1、首先需要了解什么是分布式锁分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作,若不同的系统或是同一个系统的不同主机之间共享了......
  • 面试官:聊聊你对分库分表的理解?
    在MySQL集群架构中有两种主流的集群实现,一种是读写分离,而另外一种则是数据分片。所谓的数据分片其实就是今天要聊的分库分表技术。分库分表技术不但是日常工作中用于解决数据库中的数据量会急剧增长,解决单库单表性能瓶颈的一种方案,更是面试中的高频知识点。在阿里巴巴的《Java......
  • Redisson常用的数据结构及应用场景
    Redisson提供了一系列高级数据结构,这些数据结构封装了Redis的原生数据类型,提供了JavaAPI的便利性和分布式特性。以下是Redisson中一些常用的数据结构,场景还在不断完善中:RBucket:这是一个简单的键值对存储,相当于Redis中的String类型。你可以使用它来存储和检索......
  • [Mysql]分库分表
    分库分表读写分离主要应对的是数据库读并发,没有解决数据库存储问题。试想一下:如果MySQL一张表的数据量过大怎么办?换言之,我们该如何解决MySQL的存储压力呢?答案之一就是分库分表。什么是分库?分库就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。......
  • 分表
    分表设计是数据库优化的一种常见手段,旨在通过将数据分散到多个表中来提高数据库的性能和扩展性。以下是分表设计的一些关键点:1.分表的原因性能提升:单表数据量过大时,查询、更新等操作的性能会下降。分表可以减少单次操作的数据量,提高响应速度。避免热点:将热点数据分散到不同的......
  • 记一次 Redisson 线上问题 → 你怎么能释放别人的锁
    开心一刻今天,我的又一个好哥们脱单了,只剩下我自己单身了我向一个我喜欢的女生吐苦水我:我这辈子是找不到女朋友了她:怎么可能,你很优秀的,会有很多女孩子愿意当你女朋友的我内心窃喜,问道:那你愿意当我女朋友吗她:我都在开导你了,你不要恩将仇报!线上问题生产环境突然告警,告警信......
  • 【SQL】常用的分库策略有哪些
    分库是数据库设计中的一种常见策略,用于解决大规模数据处理和高并发访问的问题。通过将数据分布到多个数据库实例上,可以提高系统的可扩展性、性能和可用性。常用的分库策略主要包括垂直分库、水平分库和混合分库。以下是这些策略的详细介绍:1.垂直分库(VerticalSharding)垂......
  • 【SQL】分库分表带来的问题以及解决方案
    分库分表是解决大规模数据和高并发访问的有效方法,但它也会带来一些问题和挑战。以下是分库分表可能带来的主要问题:1.跨分片查询复杂性在分库分表的架构中,数据分布在多个数据库实例或表中,这导致跨分片的查询变得复杂。问题:需要跨多个数据库实例或表进行数据聚合。查询性......