SaaS短链接系统-新手从零学习 3

eve2333 发布于 2025-05-22 22 次阅读


海量用户如何分库分表

其实是分为3种的:分库,分表,分库分表;

为什么要分库分表?

  • 数据量庞大。
  • 查询性能缓慢,之前可能是 20ms,后续随着数据量的增长,查询时间呈指数增长。
  • 数据库连接不够。

什么是分库分表?

分库和分表有两种模式,垂直和水平。

分库两种模式:

  • 垂直分库:电商数据库拆分为用户、订单、商品、交易等数据库。

编辑

  • 水平分库:用户数据库,拆分为多个,比如User_DB_0 - x。

编辑

分表两种模式:

  • 垂直分表:将数据库表按照业务维度进行拆分,将不常用的信息放到一个扩展表。

编辑

  • 水平分表:将用户表水平拆分,展现形式就是 User_Table_0 - x。

编辑

什么场景下分库分表?

1. 什么场景下分表?

数据量过大或者数据库表对应的磁盘文件过大。(比如字段过多,text文件占比较大这种类型的数据,总的来说就是数据量较大的情况下)日常业务中需要对磁盘进行每天备份,磁盘文件过大影响备份速度:数据量过大,索引B+树的层级变高,会导致IO增加、数据查找变慢。
磁盘文件过大不利于备份,且遇到损伤时对恢复过程产生影响。

还有一个因素是业务的增长量以及历史数量,你当前有一个4,000万左右的一张表,但是他每个月还按照500万甚至1,000万的这种数据增长量,因为他退早会上亿

Q:多少数据分表?实际上很难回答,你可以说1000w,2000w,但是这涉及到你一个表字段的一个关系,你看假如说我们这里面就这几个字段id,username,password,phone,createtime,updatetime还有flag,5000w都不要分表:

要根据表字段数量、表中数据数量以及是否存在大量text字段来综合考虑,取决于字段的数量。大的字段(例如个人简介这种)不能放到用户主表。还需要考虑到数量的自增,可能要分表,
分表不光跟数据量有关还涉及到表字段数量、字段长度、表中涉及多少text类型字段。一般垂直分表时,主表中不允许有大的text类型字段,要放到扩展表中。

2. 什么情况下分库?

连接不够用。

MySQL Server 假设支持 4000 个数据库连接。一个服务连接池最大 10 个,假设有 40 个节点。已经占用了 400 个数据库连接。

类似于这种服务,有10个,那你这个 MySQL Server 连接就不够了。

这种情况下,不一定直接分库。可以做一下读写分离,在物理上也是把一个库变成了多个库,但是不涉及业务上的拆分,主要做主从同步并配个多数据源即可。
一般情况读写分离就够用了,但是主从同步存在一定的延迟,不能满足要求很高的业务场景。

3. 又分库又分表?

  • 高并发写入或查询场景。查询一般使用缓存,分库分表主要面对高并发写入。
  • 数据量巨大场景。就是随着我们的数据量的一个增加,单一的数据库的存储和查询性能可能会逐渐的下降;我们首先肯定要结合索引,其次按照一定的规则将数据库拆分到多个表中,或者说你就直接去垂直拆分把一个数据库表然后拆成多个这种表,然后再按照我们那个水平的那种方式,然后给它再拆成多个这种水平分片表

数据库分库分表框架 ShardingSphere

Sharding-JDBC。概览 :: ShardingSphere官网

分片键

用于将数据库(表)水平拆分的数据库字段。

分库分表中的分片键(Sharding Key)是一个关键决策,它直接影响了分库分表的性能和可扩展性。以下是一些选择分片键的关键因素:

  1. 访问频率:选择分片键应考虑数据的访问频率。将经常访问的数据放在同一个分片上,可以提高查询性能和降低跨分片查询的开销。
  2. 数据均匀性:分片键应该保证数据的均匀分布在各个分片上,避免出现热点数据集中在某个分片上的情况。(数据均匀:假如分成16张表,插入16条数据,那么这16个数据,均匀分布在各个分片上)
  3. 数据不可变:一旦选择了分片键,它应该是不可变的,不能随着业务的变化而频繁修改。

用户名和用户ID选哪个作为分片键?

  • 用户名。用户名可以登录。(分表后,如果sql语句不传分片件,数据库会用union all的形式查所有的表,造成非常大的性能深渊。) 在用户登录操作中,系统频繁地通过用户名来查询用户信息,那么使用用户名作为分片键可以直接路由到包含该用户名数据的特定节点,这样可以减少查询时跨节点的通信,提高查询效率

引入 ShardingSphere-JDBC到项目

1. 引入依赖

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

2. 定义分片规则

spring:
  datasource:
  	# ShardingSphere 对 Driver 自定义,实现分库分表等隐藏逻辑
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    # ShardingSphere 配置文件路径
    url: jdbc:shardingsphere:classpath:shardingsphere-config.yaml

shardingsphere-config.yaml

# 数据源集合
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?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_user_${0..15}
        # 分表策略
        tableStrategy:
          # 用于单分片键的标准分片场景
          standard:
            # 分片键
            shardingColumn: username
            # 分片算法,对应 rules[0].shardingAlgorithms
            shardingAlgorithmName: user_table_hash_mod
    # 分片算法
    shardingAlgorithms:
      # 数据表分片算法
      user_table_hash_mod:
        # 根据分片键 Hash 分片
        type: HASH_MOD
        # 分片数量
        props:
          sharding-count: 16
# 展现逻辑 SQL & 真实 SQL
props:
  sql-show: true

第23行表示哈希取模算法,根据username分片键,算出哈希值,将该哈希值对16(分表的大小)取模。这个值,就是对应存储的数据库下标t_user_i 

ShardingSphere 数据分片核心概念

1. 逻辑表

相同结构的水平拆分数据库(表)的逻辑名称,是 SQL 中表的逻辑标识。逻辑表是t_user,数据库中不是真实存在的。真实表,t_user_0到t_user_15,是数据库真实存在的

2. 真实表

在水平拆分的数据库中真实存在的物理表。

实操分表

分库作为拓展讲,不会在这个我们的主流程里面,因为有非常多的这种前置知识;我们目前主要的就是先搞清楚分表,在test.java中新建com.nageoffer.shortlink.admin.test包,新建如下java文件

package com.nageoffer.shortlink.admin.test;

/**
 *  做一个分片的处理,我们先分十六个片啊
 */
public class UserTableShardingTest {

    public static final String SQL = "CREATE TABLE `t_user_%d` (\n" +
            "  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',\n" +
            "  `username` varchar(256) DEFAULT NULL COMMENT '用户名',\n" +
            "  `password` varchar(512) DEFAULT NULL COMMENT '密码',\n" +
            "  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',\n" +
            "  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',\n" +
            "  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',\n" +
            "  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',\n" +
            "  `create_time` datetime DEFAULT NULL COMMENT '创建时间',\n" +
            "  `update_time` datetime DEFAULT NULL COMMENT '修改时间',\n" +
            "  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',\n" +
            "  PRIMARY KEY (`id`),\n" +
            "  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE\n" +
            ") ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;";

    public static void main(String[] args) {
        for (int i = 0; i < 16; i++) {
            System.out.printf((SQL) + "%n", i);
        }
    }
}

将运行出来的话到navicat中运行(新建这个sql文件,将原来的t_user删除)

CREATE TABLE `t_user_0` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_1` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_2` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_3` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_4` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_5` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_6` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_7` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_8` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_9` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_10` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_11` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_12` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_13` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_14` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `t_user_15` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(256) DEFAULT NULL COMMENT '用户名',
  `password` varchar(512) DEFAULT NULL COMMENT '密码',
  `real_name` varchar(256) DEFAULT NULL COMMENT '真实姓名',
  `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
  `mail` varchar(512) DEFAULT NULL COMMENT '邮箱',
  `deletion_time` bigint(20) DEFAULT NULL COMMENT '注销时间戳',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0:未删除 1:已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1715030926162935810 DEFAULT CHARSET=utf8mb4;

就是我们之前你比如说你做任何操作都是在t_user表上,但是现在我们的表分成了0-15一共16个表那这个时候假如说增产改查,怎么把数据就是在正确的数据节点上去执行呢?

分片键

用于将数据库(表)水平拆分的数据库字段。
假如是t_user里面有一个 哈希算法来看属于哪个

分库分表中的分片键(Sharding Key)是一个关键决策,它直接影响了分库分表的性能和可扩展性。以下是一些选择分片键的关键因素:

  1. 访问频率:选择分片键应考虑数据的访问频率。将经常访问的数据放在同一个分片上,可以提高查询性能和降低跨分片查询的开销。
  2. 数据均匀性:分片键应该保证数据的均匀分布在各个分片上,避免出现热点数据集中在某个分片上的情况。(最。理想的情况是插入16条记录,应该0-15每个表里面有一条记录,比如16万的表中有12w在tuser0下面,你那么就是热点问题,违背了分库分表的初衷)
  3. 数据不可变:一旦选择了分片键,它应该是不可变的,不能随着业务的变化而频繁修改。

用户名和用户ID选哪个作为分片键?

  • 用户名。用户名可以登录。

 当你用我不知道用户ID是什么,我只传会给你一个用户名,就不知道这个数据在哪个表里面了吗?如果说你那个你你分表之后,如果这个语包上面不传分片键,他会查你所有的表,性能上有问题

引入 ShardingSphere-JDBC到项目

1. 引入依赖

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

5.几以后实现了他通过driver层面去做了一系列的操作,不再完全的依赖JBDC;不太好的一点 是需要额外的文件存储配置文件,在resources下新建shardingsphere-config.yaml

# 数据源集合
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?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_user_${0..15}
        # 分表策略
        tableStrategy:
          # 用于单分片键的标准分片场景
          standard:
            # 分片键
            shardingColumn: username
            # 分片算法,对应 rules[0].shardingAlgorithms
            shardingAlgorithmName: user_table_hash_mod
    # 分片算法
    shardingAlgorithms:
      # 数据表分片算法
      user_table_hash_mod:
        # 根据分片键 Hash 分片
        type: HASH_MOD
        # 分片数量
        props:
          sharding-count: 16
# 展现逻辑 SQL & 真实 SQL
props:
  sql-show: true

原来的application.yaml如下

server:
  port: 8002

spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:shardingsphere-config.yaml
  data:
    redis:
      host: 192.168.111.130
      password: 123321
      port: 6379

 ShardingSphereDriver,不再是jdbc.Driver,然后这个文件里面就是我们去做分布分表的一系列的核心的配置,首先第一数据源的话就是你的,比如说mysql数据库对吧,我们这里还是用这种hikari.HikariDataSource,下面配一些链接,和rules规则。比如说T_user也可以有T_其他的对不对?然后接下来是真实的数据节点,这个时候我需要跟大家讲一下什么是真实的表和逻辑表。逻辑表是什么意思?逻辑表是sql当中的表标识。分到了T_userXX就是做这种分表处理,但是我们的代码里面就是我们不用改我们的任何一行,代码就相当于抽象了一层逻辑表的概念

1. 逻辑表

相同结构的水平拆分数据库(表)的逻辑名称,是 SQL 中表的逻辑标识。

2. 真实表

在水平拆分的数据库中真实存在的物理表。

 现在t_user就是逻辑表,它相当于是虚拟作业.现在T-user就是逻辑表,它相当于是虚拟存在的,它通过这个逻辑表去跟我们数据库里面的这些真实的物理表做成了一层映射,然后真实表就是我们真实存在数据库中的物理表,我们的 T-user 0~15,看我们的真实节点,这个是我们的数据源名称,ds_0,然后ds_0.t_user_${0..15},这是一个内置的表达式,就代表着012345一直到15,然后分表策略的话这里用的是单分片键,因为分库分表是可以用单分片键和多分片键的,我们这里直接就用username单分片键就好了。然后我们这里面分配算法,对应我们sharding的算法的一个节点,下面的第一个user_table_hash_mod什么意思?

假如我们用户名称分片,比如说一个select *,然后from T-user where username=mading,他会把马丁这个值给拿出来去进行hash获取到他的hashcode,然后根据hashcode的进行取模,就是%16,之后他就会去这个表里面去找,以及把数据新增的表。比如说比如说123,然后%个16,因为我们16它这里面%出来的就是下标,下标就对应我们数据库表里面的后缀,大家明白了吧?很容易理解哈希取模算法是比较好的。然后我们数据库,然后我们sharding里面支持非常多的这种分表算法,这里大家去了解一下就好了。根据自己的实际情况可以去灵活的去使用。再然后指定我们的分片算法的一个type,hashmod相当于是我们的基于看到没有哈希莫的配置的一个算法名称,然后我们配置一下我们的沙丁count。16. 好吧?然后这边的话是相当于他沙丁随便它的底层做了些什么事情,但是它底层 props: sql-show: true

SELECT *FORM t_user where username = 'mading'
SELECT *FORM t_user_0 where username = 'mading'

逻辑sql是在代码层面执行的

如果我们做了多个数据源,要在shardingsphere-config.yaml继续创建, 比如ds-1

# 数据源集合
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?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/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

下面也分库的算法即可

但是这样不是意味着用户名不能修改吗,这在功能上是不是显得不合理? 

因此现在是uid了,在数据库设计之初引入 UID(Unique Identifier)作为分片键 是解决“用户名不可变”问题的合理方案。这种设计将分片键的稳定性 业务字段的灵活性 分离,既能满足分库分表的技术需求,又能支持业务上用户名的修改。

敏感数据加密存储

用现有的框架完成加密,不只是通过随便做了,直接在S的牙套里面去就行了,然后它的规则的话它是分为多种,一种是分配规则,还有加密规则,加密规则的话跟分配规则基本上就是差不多,比如说tables下面有我们的用户表,然后用户表下面有你要加密的字段,比如说我们的phone

如果说再去数据库里面去存储的话,它其实手机号和邮箱其实就是密文的一个状态。然后它这里面有一个字段是否按照密文字段查询。你既然说是密文存储对吧?我们有很多的加密方式,比如说像一些国内的那种算法对吧?还有一些 Aes和那种其他的,我们这里面就有加密算法的话,我们就定义一个公共的加密就行了。然后这里面我们用一个aes的话是一种可逆的这种加密算法类型,什么叫可逆的?就相当于你用一个你把它加密后,你还能再还原成原来的数据,这是我们想要的,因为你想我们把数据加密之后存到数据库里面,我们肯定还是要用的,要用的话肯定得再给它解密对不对?加解密的过程当中是需要一个叫做密钥的东西来处理的,密钥是不能丢的,一旦丢的话,人家把你的库拖走,但是人家知道了你的密钥一定能给你解密出来。

# 数据源集合
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?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_user_${0..15}
        tableStrategy:
          # 用于单分片键的标准分片场景
          standard:
            # 分片键
            shardingColumn: username
            # 分片算法,对应 rules[0].shardingAlgorithms
            shardingAlgorithmName: user_table_hash_mod
    # 分片算法
    shardingAlgorithms:
      # 数据表分片算法
      user_table_hash_mod:
        # 根据分片键 Hash 分片
        type: HASH_MOD
        # 分片数量
        props:
          sharding-count: 16

  - !ENCRYPT
    # 加密表集合(顶层)
    tables:
      # 用户表
      t_user:
        # 用户表中哪些字段需要进行加密
        columns:
          # 手机号字段,逻辑字段,不一定是在数据库中真实存在
          phone:
            # 手机号字段存储的密文字段,这个是数据库中真实存在的字段
            cipherColumn: phone
            encryptorName: common_encryptor
          mail:
            cipherColumn: mail
            encryptorName: common_encryptor
        # 是否按照密文字段查询
        queryWithCipherColumn: true

    # 加密算法定义(顶层)
    encryptors:
      common_encryptor:
        # 加密算法类型
        type: AES
        props:
          # AES 加密密钥
          aes-key-value: d6oadClrrb9A3GWo

props:
  sql-show: true

加密原理如下:

​编辑

OK,我们先以功能为导向来演示一下,看看行不行。首先我们先把这条记录删掉。这个功能是关于加密 分片 的,其实它们的实现原理是类似的。

ShardingSphere 是在底层对你的 SQL 进行了一层解析。如果它判断出你配置了加密规则,并且当前执行的 SQL 涉及到加密表,同时语句中包含需要加密的字段,那么 ShardingSphere 就会对你这条 SQL 进行改写。

那它是怎么改写的呢?比如说你原来的字段叫 a,它可能会映射成另一个字段 b,然后根据你配置的加密算法,把对应的值进行加密处理。比如在插入数据时自动加密,在查询数据时自动解密,最终落到数据库里的就是加密后的数据。

我们来试一下,看能不能读取到数据,以及读出来的数据是否可用。现在提示“用户记录不存在”。我们这里只配置了一个敏感字段,是不是还应该有一个不敏感字段?大家注意,这种情况只有在真实的企业场景中才会出现字段为非敏感的情况。

这是因为在我们当前的测试环境中,数据库表是从零开始构建的,所以可以直接启用加密。但在某些实际业务场景中,可能已经存在了一些数据。这时候如果你想无缝地引入加密机制,就需要做一些额外的处理。

举个例子:假设我们现在有一张用户表,里面有手机号字段。如果我们想在不影响现有系统的情况下逐步上线加密功能,可以这样做:

我们在数据库表里新增一个用于存储加密后手机号的字段,这样数据库中就会同时存在两个手机号字段——一个是明文字段,一个是密文字段。

在初始阶段,我们可以将该字段的加密状态设置为 false,也就是不启用加密,这样系统仍然按照明文字段来操作。之后我们可以通过数据迁移任务,把明文字段的数据清洗到新的密文字段中。

当数据清洗完成后,我们可以把这个字段的加密状态改为 true,这时 ShardingSphere 就会自动去操作密文字段。等系统运行稳定一段时间后,再将明文字段删除即可。

这个流程是在企业级项目中非常常见的一种加密升级方案。而我们这个项目,大家只要理解到这一步就可以了。

另外可以看到,这里的字段也叫做逻辑字段 ,类似于分片键的概念。它只是一个逻辑上的名称,真正执行的时候会被替换为物理字段。

我再举个例子说明一下。比如我们这边配置了一个逻辑值是 123,但实际在数据库中存储的是 -123。我们重启一下服务,再执行查询,发现没有问题。为什么?因为 ShardingSphere 在底层帮你完成了 SQL 的改写。

我们来看一下逻辑表结构,发现查询的是逻辑字段,但实际执行的时候已经变成了 -123,也就是说它在中间做了一层转换。大家明白了吗?​编辑

​编辑

用户个人信息修改功能

在dto下的req新建

package com.nageoffer.shortlink.admin.dto.req;

import lombok.Data;

/**
 * 用户注册请求参数
 * 
 */
@Data
public class UserUpdateReqDTO {
    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 真实姓名
     */
    private String realName;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 邮箱
     */
    private String mail;
}

controller层service层impl层依次新建

@PutMapping("/api/short-link/v1/user")
public Result<Void> update(@RequestBody UserUpdateReqDTO requestParam){
    userService.update(requestParam);
    return Results.success();
}
void update(UserUpdateReqDTO requestParam);
@Override
public void update(UserUpdateReqDTO requestParam){
//TODO 验证当前用户名是否为登录用户
//  我们请求到这里肯定是有登录凭证token的,这里是依赖网关的,因此后面再写
    LambdaQueryWrapper<UserDO> updateWrapper = Wrappers.lambdaQuery(UserDO.class)
            .eq(UserDO::getUsername, requestParam.getUsername());
    baseMapper.update(BeanUtil.toBean(requestParam, UserDO.class), updateWrapper);
}

MyMetaObjectHandler里面对修改日期的修改 

@Override
public void updateFill(MetaObject metaObject) {
    strictInsertFill(metaObject, "updateTime", Date::new, Date.class);
}

用户系统登录功能

在req中新建UserLoginReqDTO

package com.nageoffer.shortlink.admin.dto.req;

import lombok.Data;

@Data
public class UserLoginReqDTO {
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
}

在resq中新建UserLoginRespDTO

package com.nageoffer.shortlink.admin.dto.resq;
import lombok.Data;
@Data
public class UserLoginRespDTO {
    /**
     * 用户token
     */
    private String token;
}

 在pom中引入alibaba的fastjson2

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
</dependency>

在service,cintroller和impl中

/**
 * 用户登录
 * @param requestParam 用户登录请求参数
 * @return 用户登录返回参数
 */
UserLoginRespDTO login(UserLoginReqDTO requestParam);
@PostMapping("/api/short-link/v1/user/login")
public Result<UserLoginRespDTO> login(@RequestBody UserLoginReqDTO requestParam) {
    UserLoginRespDTO result= userService.login(requestParam);
    return Results.success(result);
}
@Override
public UserLoginRespDTO login(UserLoginReqDTO requestParam)
{
    LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
            .eq(UserDO::getUsername, requestParam.getUsername())
            .eq(UserDO::getPassword, requestParam.getPassword())
            .eq(UserDO::getDelFlag, 0);
    UserDO userDO = baseMapper.selectOne(queryWrapper);
    if (userDO == null) {
        throw new ClientException("用户不存在");
    }
    String uuid = UUID.randomUUID().toString();
    stringRedisTemplate.opsForValue().set(uuid, JSON.toJSONString(userDO), 30L, TimeUnit.MINUTES);
    return new UserLoginRespDTO(uuid);
}

 使用对应的token就能获取那个用户的访问,​编辑​编辑

相应的,输入错误就会获得对应的错误码, (你想偷懒就直接有现成的构造,爆一级宏观码,接下来看看是否处于登录状态)

@Override
public Boolean checkLogin(String token) {
    //只要检查一下token是否存在
    return stringRedisTemplate.hasKey(token);
}
    /**
     * 检查用户是否登录
     * @param token 用户登录token
     * @return 用户是否登录标识
     */
    Boolean checkLogin(String token);
/**
 * 检查用户是否登录
 */
@GetMapping("/api/short-link/v1/user/check-login")
public Result<Boolean> checkLogin(@RequestParam("token")String token) {
    return Results.success(userService.checkLogin(token));
}

 ​编辑​编辑

 这里有一个bug,那就是可能会重复登录,你重复在login的发送posting,不停的放回不同的token;因此要对login进行改造

    @Override
    public UserLoginRespDTO login(UserLoginReqDTO requestParam) {
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, requestParam.getUsername())
                .eq(UserDO::getPassword, requestParam.getPassword())
                .eq(UserDO::getDelFlag, 0);
        UserDO userDO = baseMapper.selectOne(queryWrapper);
        if (userDO == null) {
            throw new ClientException("用户不存在");
        }
        Boolean hasLogin = stringRedisTemplate.hasKey("login_" + requestParam.getUsername());
        if (hasLogin != null && hasLogin) {
            throw new ClientException("用户已登录");
        }
        /**
         * Hash
         * Key: login_用户名
         * Value:
         * Key: token标识
         * Val: JSON 字符串(用户信息)
         */
        // HashOperations中没有发现办法设置过期时间
        String uuid = UUID.randomUUID().toString();
        stringRedisTemplate.opsForHash().put("login_" + requestParam.getUsername(), "token", JSON.toJSONString(userDO));
        stringRedisTemplate.expire("login_" + requestParam.getUsername(), 30L, TimeUnit.MINUTES);
        return new UserLoginRespDTO(uuid);
    }

    @Override
    public Boolean checkLogin(String username,String token) {
        //只要检查一下token是否存在
        return stringRedisTemplate.opsForHash().get("token_"+username,token)!=null;
    }

    /**
     * 检查用户是否登录
     * @param username 用户名
     * @param token 用户登录token
     * @return 用户是否登录标识
     */
    Boolean checkLogin(String username,String token);

    /**
     * 检查用户是否登录
     */
    @GetMapping("/api/short-link/v1/user/check-login")
    public Result<Boolean> checkLogin(@RequestParam("username")String username,@RequestParam("token")String token) {
        return Results.success(userService.checkLogin(username,token));
    }

登录后token: df7aeb25-567e-4509-b1b9-daff259b8b01,再次登录,就报错用户已登录​编辑

 还有你像苍穹外卖,黑马点评等的session,jwt令牌等功能,可以随意使用,这里很粗糙啊

用户退出登录功能

/**
 * 用户退出登录
 * @param username 用户名
 * @param token 用户登录的token
 */
@DeleteMapping("/api/short-link/v1/user/logout")
public Result<Void> logout(@RequestParam("username")String username,@RequestParam("token") String token) {
    userService.logout(username,token);
    return Results.success();
}
@Override
public void logout(String username,String token) {
    if(checkLogin(username,token)){
        stringRedisTemplate.delete("login_" + username);
        return;
    }
    throw new ClientException("用户token不存在或用户未登录");
}
void logout(String username,String token);