海量数据下的分库分表
# 海量数据下的分库分表
# 流量包业务模型梳理和需求
流量包业务模型梳理


数据量预估(尽量⼀次性扩容预估好,数据迁移成本⼤)
- 未来 2 年,短链平台累计 5 百万⽤户
- ⼀个⽤户 10 条记录 / 年,总量就是 5 千万条
- 单表不超过 1 千万数据,需要分 5 张表
- 进⼀步延伸,进⾏⽔平分表,⽐如 2 张表、4 张表、8 张表、16 张表
因为业务逻辑复杂,我们就不分那么多表,分 2 张表操作即可
问题
- 分库分表怎么分?有哪些形式呢?还会带来哪些问题?
- ⽐如 2 张表、4 张表、8 张表、16 张表 这样的思路?
- ... 更多问题
# 业务增⻓ - 数据库性能优化思路
⾯试官:这边有个数据库 - 单表 1 千万数据,未来 1 年还会增⻓多 500 万,性能⽐较慢,说下你的优化思路
思路
- 千万不要⼀上来就说分库分表,这个是最忌讳的事项
- ⼀定要根据实际情况分析,两个⻆度思考
不分库分表
软优化
- 数据库参数调优
- 分析慢查询 SQL 语句,分析执⾏计划,进⾏ sql 改写和程序改写
- 优化数据库索引结构
- 优化数据表结构优化
- 引⼊ NOSQL 和程序架构调整
硬优化
- 提升系统硬件(更快的 IO、更多的内存):带宽、CPU、硬盘
分库分表
根据业务情况⽽定,选择合适的分库分表策略(没有通⽤的策略)
- 外卖、物流、电商领域
先看只分表是否满⾜业务的需求和未来增⻓
- 数据库分表能够解决单表数据量很⼤的时,数据查询的效率问题,
- ⽆法给数据库的并发操作带来效率上的提⾼,分表的实质还是在⼀个数据库上进⾏的操作,受数据库 IO 性能的限制
如果单分表满⾜不了需求,再分库分表⼀起
结论
- 在数据量及访问压⼒不是特别⼤的情况,⾸先考虑缓存、读写分离、索引技术等⽅案
- 如果数据量极⼤,且业务持续增⻓快,再考虑分库分表⽅案
# 分库分表后带来的优点和缺点
# 分库分表解决的现状问题
解决数据库本身瓶颈
- 连接数: 连接数过多时,就会出现‘too manyconnections’的错误,访问量太⼤或者数据库设置的最⼤连接数太⼩的原因
- Mysql 默认的最⼤连接数为 100. 可以修改,⽽ mysql 服务允许的最⼤连接数为 16384
- 数据库分表可以解决单表海量数据的查询性能问题
- 数据库分库可以解决单台数据库的并发访问压⼒问题

解决系统本身 IO、CPU 瓶颈
- 磁盘读写 IO 瓶颈,热点数据太多,尽管使⽤了数据库本身缓存,但是依旧有⼤量 IO, 导致 sql 执⾏速度慢
- ⽹络 IO 瓶颈,请求的数据太多,数据传输⼤,⽹络带宽不够,链路响应时间变⻓
- CPU 瓶颈,尤其在基础数据量⼤单机复杂 SQL 计算,SQL 语句执⾏占⽤ CPU 使⽤率⾼,也有扫描⾏数⼤、锁冲突、锁等待等原因
# 带来新的问题
问题⼀:跨节点数据库 Join 关联查询和多维度查询
- 数据库切分前,多表关联查询,可以通过 sql join 进⾏实现
- 分库分表后,数据可能分布在不同的节点上,sql join 带来的问题就⽐较麻烦
- 不同维度查看数据,利⽤的 partitionKey 是不⼀样的
- 如:订单表 的 partionKey 是 user_id,⽤户查看⾃⼰的订单列表⽅便
但商家查看⾃⼰店铺的订单列表就麻烦,分布在不同数据节点

- 如:订单表 的 partionKey 是 user_id,⽤户查看⾃⼰的订单列表⽅便
但商家查看⾃⼰店铺的订单列表就麻烦,分布在不同数据节点
问题⼆:分库操作带来的分布式事务问题
- 操作内容同时分布在不同库中,不可避免会带来跨库事务问题,即分布式事务
问题三:执⾏的 SQL 排序、翻⻚、函数计算问题
- 分库后,数据分布再不同的节点上, 跨节点多库进⾏查询时,会出现 limit 分⻚、order by 排序等问题
- ⽽且当排序字段⾮分⽚字段时,更加复杂了,要在不同的分⽚节点中将数据进⾏排序并返回,然后将不同分⽚返回的结果集进⾏汇总和再次排序(也会带来更多的 CPU/IO 资源损耗)
问题四:数据库全局主键重复问题
- 常规表的 id 是使⽤⾃增 id 进⾏实现,分库分表后,由于表中数据同时存在不同数据库中,如果⽤⾃增 id,则会出现冲突问题
问题五:容量规划,分库分表后⼆次扩容问题
- 业务发展快,初次分库分表后,满⾜不了数据存储,导致需要多次扩容
问题六:分库分表技术选型问题
- 市场分库分表中间件相对较多,框架各有各的优势与短板,应该如何选择
# 垂直分表 - 垂直分库
# 垂直分表
需求:商品表字段太多,每个字段访问频次不⼀样,浪费了 IO 资源,需要进⾏优化
垂直分表介绍
- 也就是 “⼤表拆⼩表”,基于列字段进⾏的
- 拆分原则⼀般是表中的字段较多,将不常⽤的或者数据较⼤,
- ⻓度较⻓的拆分到 “扩展表 如 text 类型字段
- 访问频次低、字段⼤的商品描述信息单独存放在⼀张表中;
- 访问频次较⾼的商品基本信息单独放在⼀张表中
垂直拆分原则
- 把不常⽤的字段单独放在⼀张表;
- 把 text,blob 等⼤字段拆分出来放在附表中;
- 业务经常组合查询的列放在⼀张表中
例⼦:商品详情⼀般是拆分主表和附表
-- 拆分前
CREATE TABLE `product` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(524) DEFAULT NULL COMMENT '视频标题',
`cover_img` varchar(524) DEFAULT NULL COMMENT '封⾯图',
`price` int(11) DEFAULT NULL COMMENT '价格,分',
`total` int(10) DEFAULT '0' COMMENT '总库存',
`left_num` int(10) DEFAULT '0' COMMENT '剩余',
`learn_base` text COMMENT '课前须知,学习基础',
`learn_result` text COMMENT '达到⽔平',
`summary` varchar(1026) DEFAULT NULL COMMENT '概述',
`detail` text COMMENT '视频商品详情',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT
CHARSET=utf8;
-- 拆分后
CREATE TABLE `product` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(524) DEFAULT NULL COMMENT '视频标题',
`cover_img` varchar(524) DEFAULT NULL COMMENT '封面图',
`price` int(11) DEFAULT NULL COMMENT '价格,分',
`total` int(10) DEFAULT '0' COMMENT '总库存',
`left_num` int(10) DEFAULT '0' COMMENT '剩余',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='产品表';
CREATE TABLE `product_detail` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`product_id` int(11) DEFAULT NULL COMMENT '产品主键',
`learn_base` text COMMENT '课前须知,学习基础',
`learn_result` text COMMENT '达到水平',
`summary` varchar(1026) DEFAULT NULL COMMENT '概述',
`detail` text COMMENT '视频商品详情',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='产品详情表';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 垂直分库
需求:C 端项⽬⾥⾯,单个数据库的 CPU、内存⻓期处于 90%+ 的利⽤率,数据库连接经常不够,需要进⾏优化
- 垂直分库针对的是⼀个系统中的不同业务进⾏拆分, 数据库的连接资源⽐较宝贵且单机处理能⼒也有限
- 没拆分之前全部都是落到单⼀的库上的,单库处理能⼒成为瓶颈,还有磁盘空间,内存,tps 等限制
- 拆分之后,避免不同库竞争同⼀个物理机的 CPU、内存、⽹络 IO、磁盘,所以在⾼并发场景下,垂直分库⼀定程度上能够突破 IO、连接数及单机硬件资源的瓶颈
- 垂直分库可以更好解决业务层⾯的耦合,业务清晰,且⽅便管理和维护
- ⼀般从单体项⽬升级改造为微服务项⽬,就是垂直分库

问题:垂直分库分表可以提⾼并发,但是依然没有解决单表数据量过⼤的问题
# ⽔平分表 - ⽔平分库
需求:当⼀张表的数据达到⼏千万时,查询⼀次所花的时间⻓,需要进⾏优化,缩短查询时间
都是⼤表拆⼩表
- 垂直分表:表结构拆分
- ⽔平分表:数据拆分
# ⽔平分表
- 把⼀个表的数据分到⼀个数据库的多张表中,每个表只有这个表的部分数据
- 核⼼是把⼀个⼤表,分割 N 个⼩表,每个表的结构是⼀样的,数据不⼀样,全部表的数据合起来就是全部数据
- 针对数据量巨⼤的单张表(⽐如订单表),按照某种规则(RANGE,HASH 取模等),切分到多张表⾥⾯去
- 但是这些表还是在同⼀个库中,所以单数据库操作还是有 IO 瓶颈,主要是解决单表数据量过⼤的问题
- 减少锁表时间,没分表前,如果是 DDL (create/alter/add 等) 语句,当需要添加⼀列的时候 mysql 会锁表,期间所有的读写操作只能等待

# ⽔平分库
需求:⾼并发的项⽬中,⽔平分表后依旧在单个库上⾯,1 个数据库资源瓶颈 CPU / 内存 / 带宽等限制导致响应慢,需要进⾏优化
- 把同个表的数据按照⼀定规则分到不同的数据库中,数据库在不同的服务器上
- ⽔平分库是把不同表拆到不同数据库中,它是对数据⾏的拆分,不影响表结构
- 每个库的结构都⼀样,但每个库的数据都不⼀样,没有交集,所有库的并集就是全量数据
- ⽔平分库的粒度,⽐⽔平分表更⼤

# 分库分表策略
# range
⽔平分库分表,根据什么规则进⾏?怎么划分?
⽅案⼀:⾃增 id,根据 ID 范围进⾏分表(左闭右开)
规则案例
- 1~1,000,000 是 table_1
- 1,000,000 ~2,000,000 是 table_2
- 2,000,000~3,000,000 是 table_3
- ... 更多
优点
- id 是⾃增⻓,可以⽆限增⻓
- 扩容不⽤迁移数据,容易理解和维护
缺点
- ⼤部分读和写都访会问新的数据,有 IO 瓶颈,整体资源利⽤率低
- 数据倾斜严重,热点数据过于集中,部分节点有瓶颈

# range 延伸
Range 范围分库分表,有热点问题,所以这个没⽤?
关于怎么选择分库分表策略问题,如果业务适合就⾏,没有万能策略!!!!
范围⻆度思考问题 (范围的话更多是⽔平分表)
- 数字
- ⾃增 id 范围
- 时间
- 年、⽉、⽇范围
- ⽐如按照⽉份⽣成 库或表 pay_log_2022_01、
- pay_log_2022_02
- 空间
- 地理位置:省份、区域(华东、华北、华南)
- ⽐如按照 省份 ⽣成 库或表
基于 Range 范围分库分表业务场景
微博发送记录、微信消息记录、⽇志记录,id 增⻓ / 时间分区都⾏
- ⽔平分表为主,⽔平分库则容易造成资源的浪费
⽹站签到等活动流⽔数据时间分区最好
- ⽔平分表为主,⽔平分库则容易造成资源的浪费
⼤区划分(⼀⼆线城市和五六线城市活跃度不⼀样,如果能避免热点问题,即可选择)
- saas 业务⽔平分库(华东、华南、华北等)

- saas 业务⽔平分库(华东、华南、华北等)
# Hash 取模
⽅案⼆:hash 取模(Hash 分库分表是最普遍的⽅案)
为啥不直接取模,如果取模的字段不是整数型要先 hash,统⼀规则就⾏

案例规则
- ⽤户 ID 是整数型的,要分 2 库,每个库表数量 4 表,⼀共 8 张表
- ⽤户 ID 取模后,值是 0 到 7 的要平均分配到每张表
库 ID = userId % 库数量 2 表 ID = userId / 库数量 2 % 表数量 4
例⼦
| userId | id%2 (库 - 取余) | id/2%4 (表) |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 0 | 1 |
| 3 | 1 | 1 |
| 4 | 0 | 2 |
| 5 | 1 | 2 |
| 6 | 0 | 3 |
| 7 | 1 | 3 |
| 8 | 0 | 0 |
| 9 | 1 | 0 |
- 优点:保证数据较均匀的分散落在不同的库、表中,可以有效的避免热点数据集中问题
- 缺点:扩容不是很⽅便,需要数据迁移
# 分库分表中间件
业界常⻅分库分表中间件
Cobar(已经被淘汰没使⽤了)
TDDL
- 淘宝根据⾃⼰的业务特点开发了 TDDL (TaobaoDistributed Data Layer)
- 基于 JDBC 规范,没有 server,以 client-jar 的形式存在,引⼊项⽬即可使⽤
- 开源功能⽐较少,阿⾥内部使⽤为主
Mycat
- 地址 http://www.mycat.org.cn/
- Java 语⾔编写的 MySQL 数据库⽹络协议的开源中间件,前身 Cobar
- 遵守 Mysql 原⽣协议,跨语⾔,跨平台,跨数据库的通⽤中间件代理
- 是基于 Proxy,它复写了 MySQL 协议,将 Mycat Server 伪装成⼀个 MySQL 数据库
- 和 ShardingShere 下的 Sharding-Proxy 作⽤类似,需要单独部署

ShardingSphere 下的 Sharding-JDBC
- 地址:https://shardingsphere.apache.org/
- Apache ShardingSphere 是⼀套开源的分布式数据库中间件解决⽅案组成的⽣态圈
- 它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 3 个独⽴产品组合
- Sharding-JDBC
- 基于 jdbc 驱动,不⽤额外的 proxy,⽀持任意实现 JDBC 规范的数据库
- 它使⽤客户端直连数据库,以 jar 包形式提供服务,⽆需额外部署和依赖
- 可理解为加强版的 JDBC 驱动,兼容 JDBC 和各类 ORM 框架

# Mycat 和 ShardingJdbc 区别
Mycat 和 ShardingJdbc 区别
- 两者设计理念相同,主流程都是 SQL 解析 -->SQL 路由 -->SQL 改写 --> 结果归并
- sharding-jdbc
- 基于 jdbc 驱动,不⽤额外的 proxy,在本地应⽤层重写 Jdbc 原⽣的⽅法,实现数据库分⽚形式
- 是基于 JDBC 接⼝的扩展,是以 jar 包的形式提供轻量级服务的,性能⾼
- 代码有侵⼊性
- Mycat
- 是基于 Proxy,它复写了 MySQL 协议,将 Mycat Server 伪装成⼀个 MySQL 数据库
- 客户端所有的 jdbc 请求都必须要先交给 MyCat,再有 MyCat 转发到具体的真实服务器
- 缺点是效率偏低,中间包装了⼀层
- 代码⽆侵⼊性
# Apache ShardingSphere
ShardingSphere,已于 2020 年 4 ⽉ 16 ⽇成为 Apache 软件基⾦会的顶级项⽬
是⼀套开源的分布式数据库解决⽅案组成的⽣态圈,定位为 Database Plus,它由 JDBC、Proxy 和 Sidecar 这 3 款既能够独⽴部署,⼜⽀持混合部署配合使⽤的产品组成
三⼤构成
ShardingSphere-Sidecar
- 定位为 Kubernetes 的云原⽣数据库代理,以 Sidecar (边⻋) 的形式代理所有对数据库的访问
- 通过⽆中⼼、零侵⼊的⽅案提供与数据库交互的啮合层,即 Database Mesh ,⼜可称数据库⽹格
ShardingSphere-JDBC
- 它使⽤客户端直连数据库,以 jar 包形式提供服务
- ⽆需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架
- 适⽤于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate,Mybatis, 或直接使⽤ JDBC ⽀持任何第三⽅的数据库连接池,如:DBCP, C3P0,BoneCP, HikariCP 等;
- ⽀持任意实现 JDBC 规范的数据库,⽬前⽀持 MySQL,PostgreSQL,Oracle,SQLServer 以及任何可使⽤ JDBC 访问的数据库
- 采⽤⽆中⼼化架构,与应⽤程序共享资源,适⽤于 Java 开发的⾼性能的轻量级 OLTP 应⽤

ShardingSphere-Proxy
- 数据库代理端,提供封装了数据库⼆进制协议的服务端版本,⽤于完成对异构语⾔的⽀持
- 向应⽤程序完全透明,可直接当做 MySQL/PostgreSQL
- 它可以使⽤任何兼容 MySQL/PostgreSQL 协议的访问客户端(如:MySQL Command Client, MySQL Workbench,Navicat 等)操作数据

# Sharding-Jdbc
# Sharding-Jdbc 中组件概念
数据节点 Node
- 数据分⽚的最⼩单元,由数据源名称和数据表组成
- ⽐如:ds_0.product_order_0
真实表
- 在分⽚的数据库中真实存在的物理表
- ⽐如订单表 product_order_0、product_order_1、product_order_2
逻辑表
- ⽔平拆分的数据库(表)的相同逻辑和数据结构表的总称
- ⽐如订单表 product_order_0、product_order_1、product_order_2,逻辑表就是 product_order
绑定表
- 指分⽚规则⼀致的主表和⼦表
- ⽐如 product_order 表和 product_order_item 表,均按照 order_id 分⽚,则此两张表互为绑定表关系
- 绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将⼤⼤提升

⼴播表
- 指所有的分⽚数据源中都存在的表,表结构和表中的数据在每个数据库中均完全⼀致
- 适⽤于数据量不⼤且需要与海量数据的表进⾏关联查询的场景
- 例如:字典表、配置表
# Sharding-Jdbc 常⻅分⽚算法
- 数据库表分⽚(⽔平库、表)
- 包含分⽚键和分⽚策略
- 分⽚键 (PartitionKey)
- ⽤于分⽚的数据库字段,是将数据库 (表) ⽔平拆分的关键字段
- ⽐如 prouduct_order 订单表,根据订单号 out_trade_no 做哈希取模,则 out_trade_no 是分⽚键
- 除了对单分⽚字段的⽀持,ShardingSphere 也⽀持根据多个字段进⾏分⽚

分⽚策略
⾏表达式分⽚策略
InlineShardingStrategy只⽀持 **【单分⽚键】** 使⽤ Groovy 的表达式,提供对 SQL 语句中的 = 和 IN 的分⽚操作⽀持
可以通过简单的配置使⽤,⽆需⾃定义分⽚算法,从⽽避免繁琐的 Java 代码开发
prouduct_order_$->{user_id % 8}` 表示订单表根据 user_id模8,⽽分成8张表,表名称为 `prouduct_order_0`到`prouduct_order_71
2
3
标准分⽚策略
StandardShardingStrategy- 只⽀持 **【单分⽚键】**,提供 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分⽚算法
- PreciseShardingAlgorithm 精准分⽚ 是必选的,⽤于处理 = 和 IN 的分⽚
- RangeShardingAlgorithm 范围分配 是可选的,⽤于处理 BETWEEN AND 分⽚
- 如果不配置 RangeShardingAlgorithm,如果 SQL 中⽤了 BETWEEN AND 语法,则将按照全库路由处理,性能下降
复合分⽚策略
ComplexShardingStrategy- ⽀持 **【多分⽚键】**,多分⽚键之间的关系复杂,由开发者⾃⼰实现,提供最⼤的灵活度
- 提供对 SQL 语句中的 =, IN 和 BETWEEN AND 的分⽚操作⽀持
- prouduct_order_0_0、prouduct_order_0_1、prouduct_order_1_0、prouduct_order_1_1
Hint 分⽚策略
HintShardingStrategy这种分⽚策略⽆需配置分⽚健,分⽚健值也不再从 SQL 中解析,外部⼿动指定分⽚健或分⽚库,让 SQL 在指定的分库、分表中执⾏
⽤于处理使⽤ Hint ⾏分⽚的场景,通过 Hint ⽽⾮ SQL 解析的⽅式分⽚的策略
Hint 策略会绕过 SQL 解析的,对于这些⽐较复杂的需要分⽚的查询,Hint 分⽚策略性能可能会更好
不分⽚策略
NoneShardingStrategy
⾃⼰实现分⽚策略的优缺点
- 优点:可以根据分⽚策略代码⾥⾯⾃⼰拼装 真实的数据库、真实的表,灵活控制分⽚规则
- 缺点:增加了编码,不规范的 sql 容易造成全库表扫描,部分 sql 语法⽀持不友好
# 流量包模块⽔平分表
需求:未来 2 年,短链平台累计 5 百万⽤户
付费流量包记录:⼀个⽤户 10 条 / 年,总量就是 5 千万条
单表不超过 1 千万数据,需要分 5 张表
进⼀步延伸,进⾏⽔平分表,⽐如 2 张表、4 张表、8 张表、16 张表
流量包 traffic 表数据太多,选取可⽤流量包 会影响性能,需要降低单表数据量,进⾏⽔平分表
分表数量:线上分 8 张表,本地分 2 张表即可
分⽚ key: account_no,查询维度都是根据 account_no 进⾏查询
分⽚策略:⾏表达式分⽚策略 InlineShardingStrategy
新建表 traffic_0 、 traffic_1
CREATE TABLE `traffic_0` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`day_limit` int DEFAULT NULL COMMENT '每天限制多少条,短链',
`day_used` int DEFAULT NULL COMMENT '当天⽤了多少条,短链',
`total_limit` int DEFAULT NULL COMMENT '总次数,活码才⽤',
`account_no` bigint DEFAULT NULL COMMENT '账号',
`out_trade_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单号',
`level` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品层级:FIRST⻘铜、SECOND⻩⾦、THIRD钻⽯',
`expired_date` date DEFAULT NULL COMMENT '过期⽇期',
`plugin_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '插件类型',
`product_id` bigint DEFAULT NULL COMMENT '商品主键',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_trade_no` (`out_trade_no`,`account_no`) USING BTREE,
KEY `idx_account_no` (`account_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `traffic_1` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`day_limit` int DEFAULT NULL COMMENT '每天限制多少条,短链',
`day_used` int DEFAULT NULL COMMENT '当天⽤了多少条,短链',
`total_limit` int DEFAULT NULL COMMENT '总次数,活码才⽤',
`account_no` bigint DEFAULT NULL COMMENT '账号',
`out_trade_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '订单号',
`level` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '产品层级:FIRST⻘铜、SECOND⻩⾦、THIRD钻⽯',
`expired_date` date DEFAULT NULL COMMENT '过期⽇期',
`plugin_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '插件类型',
`product_id` bigint DEFAULT NULL COMMENT '商品主键',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_trade_no` (`out_trade_no`,`account_no`) USING BTREE,
KEY `idx_account_no` (`account_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
加⼊ sharding-jdbc 依赖包, account 项⽬注释下⾯的依赖排查

配置⽂件 (注释之前 jdbc 单库配置)
# datasource:
# url: jdbc:mysql://192.168.130.24:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: xdclass.net168
# 数据源 ds0 第一个数据库
shardingsphere:
datasource:
#数据源名称
names: ds0
ds0:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
props:
# 打印执行的数据库以及语句
sql:
show: true
sharding:
tables:
traffic:
# 指定traffic表的数据分布情况,配置数据节点,行表达式标识符使用 ${...} 或 $->{...},但前者与 Spring 本身的文件占位符冲突,所以在 Spring 环境中建议使用 $->{...}
actual-data-nodes: ds0.traffic_$->{0..1}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ⽔平分表策略配置
⽔平分表策略配置
#水平分表策略+行表达式分片
table-strategy:
inline:
algorithm-expression: traffic_$->{ account_no % 2 }
sharding-column: account_no
2
3
4
5
测试 TrafficTest
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class TrafficTest {
@Autowired
private TrafficMapper trafficMapper;
@Test
public void testSaveTraffic() {
Random random = new Random();
for (int i = 0; i < 10; i++) {
TrafficDO trafficDO = new TrafficDO();
trafficDO.setAccountNo(Long.valueOf(random.nextInt(100)));
trafficMapper.insert(trafficDO);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20


# ID 冲突和分布式 id ⽣成
单库下⼀般使⽤ Mysql ⾃增 ID, 但是分库分表后,会造成不同分⽚上的数据表主键会重复。
需求
- 性能强劲
- 全局唯⼀
- 防⽌恶意⽤户根据 id 的规则来获取数据
业界常⽤ ID 解决⽅案
数据库⾃增 ID
- 利⽤⾃增 id, 设置不同的⾃增步⻓,auto_increment_offset、auto-increment-increment
- DB1: 单数从 1 开始、每次加 2 DB2: 偶数从 2 开始,每次加 2
- 缺点:
- 依靠数据库系统的功能实现,但是未来扩容麻烦
- 主从切换时的不⼀致可能会导致重复发号
- 性能瓶颈存在单台 sql 上
UUID
- 性能⾮常⾼,没有⽹络消耗
- 缺点
- ⽆序的字符串,不具备趋势⾃增特性
- UUID 太⻓,不易于存储,浪费存储空间,很多场景不适⽤
Redis 发号器
- 利⽤ Redis 的 INCR 和 INCRBY 来实现,原⼦操作,线程安全,性能⽐ Mysql 强劲
- 缺点:需要占⽤⽹络资源,增加系统复杂度
Snowflake 雪花算法
- twitter 开源的分布式 ID ⽣成算法,代码实现简单、不占⽤宽带、数据迁移不受影响
- ⽣成的 id 中包含有时间戳,所以⽣成的 id 按照时间递增
- 部署了多台服务器,需要保证系统时间⼀样,机器编号不⼀样
- 缺点:依赖系统时钟(多台服务器时间⼀定要⼀样)
# 分布式 ID ⽣成算法 - Snowflake 原理
雪花算法 Snowflake 是 twitter ⽤ scala 语⾔编写的⾼效⽣成唯⼀ ID 的算法
- ⽣成的 ID 不重复
- 算法性能⾼
- 基于时间戳,基本保证有序递增
计算机的基础知识回顾
bit 与 byte
- bit (位):电脑中存储的最⼩单位,可以存储⼆进制中的 0 或 1
- byte (字节):⼀个 byte 由 8 个 bit 组成
常规 64 位系统⾥⾯ java 数据类型存储字节⼤⼩
- int:4 个字节
- short:2 个字节
- long:8 个字节
- byte:1 个字节
- float:4 个字节
- double:8 个字节
- char:2 个字节
数据类型在不同位数机器的平台下⻓度不同
- 16 位平台 int 2 个字节 16 位
- 32 位平台 int 4 个字节 32 位
- 64 位平台 int 4 个字节 32 位
雪花算法⽣成的数字,long 类,所以就是 8 个 byte,64bit
- 表示的值 -9223372036854775808(-2 的 63 次⽅) ~9223372036854775807(2 的 63 次⽅ - 1)
- ⽣成的唯⼀值⽤于数据库主键,不能是负数,所以值为 0~9223372036854775807(2 的 63 次⽅ - 1)

# Snowflake 使用注意事项
分布式 ID ⽣成器需求
- 性能强劲
- 全局唯⼀不能重复
- 防⽌恶意⽤户根据 id 的规则来获取数据
全局唯⼀不能重复
- 分布式部署就需要分配不同的 workId, 如果 workId 相同,可能会导致⽣成的 id 相同
- 分布式情况下,需要保证各个系统时间⼀致,如果服务器的时钟回拨,就会导致⽣成的 id 重复
- ⼈⼯去⽣产环境做了系统时间调整
- 业务需求,代码⾥⾯做了系统时间同步
⽅式⼀:订单 id 使⽤ MybatisPlus 的配置,TrafficDO 类配置
@TableId(value = "id", type = IdType.ASSIGN_ID)
// 默认实现类为DefaultIdentifierGenerator雪花算法
2
⽅式⼆:使⽤ Sharding-Jdbc 配置⽂件,注释 DO 类⾥⾯的 id 分配策略
key-generator:
column: id
props:
worker:
id: 0
#id⽣成策略
type: SNOWFLAKE
2
3
4
5
6
7
# ⾃定义 wrokId
进阶:动态指定 sharding jdbc 的雪花算法中的属性 work.id 属性
使⽤ sharding-jdbc 中的使⽤ IP 后⼏位来做 workId, 但在某些情况下会出现⽣成重复 ID 的情况
解决办法:
- 在启动时给每个服务分配不同的 workId, 引⼊ redis/zk 都⾏,缺点就是多了依赖
- 启动程序的时候,通过 JVM 参数去控制,覆盖变量
common 模块下 net.xdclass.config.SnowFlakeWordIdConfig
@Configuration
@Slf4j
public class SnowFlakeWordIdConfig {
/**
* 动态指定sharding jdbc 的雪花算法中的属性work.id属性
* 通过调⽤System.setProperty()的⽅式实现,可⽤容器的id 或者机器标识位
* workId最⼤值 1L << 100,就是1024,即 0<= workId < 1024
* {@link SnowflakeShardingKeyGenerator#getWorkerId()}
*
*/
static {
try {
InetAddress inetAddress = Inet4Address.getLocalHost();
String hostAddress = inetAddress.getHostAddress();
String workId = Math.abs(hostAddress.hashCode()) % 1024 + "";
System.setProperty("workId", workId);
log.info("workId:{}", workId);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
// public static void main(String[] args) {
// try {
// InetAddress inetAddress = Inet4Address.getLocalHost();
// String hostAddress = inetAddress.getHostAddress();
// String hostName = inetAddress.getHostName();
// System.out.println(hostName + " " + hostAddress);
// } catch (UnknownHostException e) {
// throw new RuntimeException(e);
// }
//
// }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
修改配置
key-generator:
column: id
props:
worker:
id: ${workId}
#id⽣成策略
type: SNOWFLAKE
2
3
4
5
6
7
# 时间回拨问题解决和封装 ID ⽣成器
shardingjdbc-Snowflake ⾥⾯解决时间回拨问题

需求:⽤户注册 - ⽣成的 account_no 需要是 long 类型,且全局唯⼀
利⽤ Sharding-Jdbc 封装 id ⽣成器
net.xdclass.util.IDUtil
public class IDUtil {
private static SnowflakeShardingKeyGenerator shardingKeyGenerator = new SnowflakeShardingKeyGenerator();
/**
* 雪花算法⽣成器,配置workId,避免重复
* @return
*/
public static Comparable<?> geneSnowFlakeID(){
return shardingKeyGenerator.generateKey();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
修改 AccountServiceImpl 中 register 方法
/**
* ⼿机验证码验证
* 密码加密(TODO)
* 账号唯⼀性检查
* 插⼊数据库
* 新注册⽤户福利发放(TODO)
*
* @param registerRequest
* @return
*/
@Override
public JsonData register(AccountRegisterRequest registerRequest) {
boolean checkCode = false;
// 判断验证码
if (StringUtils.isNoneBlank(registerRequest.getPhone())) {
checkCode = notifyService.checkCode(SendCodeEnum.USER_REGISTER, registerRequest.getPhone(), registerRequest.getCode());
}
// 验证码错误
if (!checkCode) {
return JsonData.buildResult(BizCodeEnum.CODE_ERROR);
}
AccountDO accountDO = new AccountDO();
BeanUtils.copyProperties(registerRequest, accountDO);
// 认证级别
accountDO.setAuth(AuthTypeEnum.DEFAULT.name());
// 用户唯⼀
accountDO.setAccountNo(Long.valueOf(IDUtil.geneSnowFlakeID().toString()));
// 密码 密钥 盐
accountDO.setSecret("$1$" + CommonUtil.getStringNumRandom(8));
String cryptPwd = Md5Crypt.md5Crypt(registerRequest.getPwd().getBytes(), accountDO.getSecret());
accountDO.setPwd(cryptPwd);
int rows = accountManager.insert(accountDO);
log.info("rows:{},注册成功:{}", rows, accountDO);
// 用户注册成功,发放福利
userRegisterInitTask(accountDO);
return JsonData.buildSuccess();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 短链服务介绍
短链组成:协议:// 短链域名 / 短链码
最简单的⽅式
⼀个短链编码,去数据库 select * from table where code=XXX,返回给⽤户就⾏
# 短链的⽣命周期
从需求出发带你详细⼀个短链的⽣命周期
# 创建者和访问者

# 创建者
# 流量包管理
- 免费流量包
- 付费流量包

# 分组管理
- 新增分组
- 删除分组
- 修改分组
- 查看分组下的短链

# 短链管理
- 创建短链
- ⽬标地址
- 短链标题
- 短链域名
- 所属分组
- 有效期

- 删除短链
- 修改短链
- 查看短链
- 访问 PV、UV
- 地域分布
- 时间分布
- 来源分布
- ...
# 访问者
- 访问短链
- 跳转⽬标站点
# 短链 URL ⽣成服务⾥⾯的问题
- ⻓链的关系和短链的关系
- ⼀对⼀?
- ⼀对多?
- 多对多?
- 前端访问短链是如何跳转到对应的⻚⾯的?
- 短链码是如何⽣成的
- SaaS 类型业务,数据量有多⼤,是否要分库分表
- 如果分库分表,PartitionKey 是哪个?使⽤怎样的策略
- 如果分库分表,访问短链怎么知道具体是哪个库哪个表?
- 如果分库分表,怎么查看某个账号创建的全部短链?
# ⻓链的关系和短链的关系是⼀对⼀还是⼀对多?
- ⼀个⻓链,在不同情况下,⽣成的短⽹址应该不⼀样,才不会造成冲突
- 多渠道推⼴下,也可以区分统计不同渠道的效果质量
- 所以是 ⼀个短链接只能对应⼀个⻓链接,当然⼀个⻓链接可以对应多个短链接
# 前端访问短链是如何跳转到对应的⻚⾯的?
服务端转发
- 由服务器端进⾏的⻚⾯跳转,刚学 Servlet 时, 从 OneServlet 中转发到 TwoServlet
- 地址栏不发⽣变化,显示的是上⼀个⻚⾯的地址
- 请求次数:只有 1 次请求
- 转发只能在同⼀个应⽤的组件之间进⾏,不可以转发给其他应⽤的地址
request.getRequestDispatcher("/two").forward(request, response);

⻚⾯的跳转 - 重定向
- 由浏览器端进⾏的⻚⾯跳转

# 重定向涉及到 3xx 状态码,访问跳转是 301 还是 302,301 和 302 代表啥意思?
- 301 是永久重定向会被浏览器硬缓存,第⼀次会经过短链服务,后续再访问直接从浏览器缓存中获取⽬标地址
- 302 是临时重定向不会被浏览器硬缓存,每次都是会访问短链服务短地址⼀经⽣成就不会变化,所以⽤ 301 是同时对服务器压⼒也会有⼀定减少
- 但是如果使⽤了 301,⽆法统计到短地址被点击的次数
- 所以选择 302 虽然会增加服务器压⼒,但是有很多数据可以获取进⾏分析
- 选择使⽤ 302,这个也可以对违规推⼴的链接进⾏实时封禁
# 数据脱敏
数据脱敏也叫数据的去隐私化,在给定脱敏规则和策略的情况下,对敏感数据⽐如 ⼿机号 、 身份证 等信息,进⾏转换或者修改的⼀种技术⼿段,防⽌敏感数据直接在不可靠的环境下使⽤和泄露、撞库等
技术分两类
静态数据脱敏:将⽣产数据导出,进⾏对外发送或者给开发、测试⼈员等
动态数据脱敏:程序直接连接⽣产数据的场景,如运维⼈员在运维的⼯作中直接连接⽣产数据库进⾏运维
客服⼈员在⽣产中通过后台查询的个⼈信息
⾃增 ID 暴露的商业秘密
通过业务接⼝抓包分析 (假定数据没做加密或者加密被破解了),被他发现了⾃增 id 这个事情,他好⼏个号注 册了,编号都是⼀百多万,每次 id 都是⾃增 1, ⽤户量最多 100 多万,就此暴雷了。 经过有经验的⼈知道不能对外暴露 id 这个事情,但是总有关联的业务,或者其他不知道的⼈员开发了对应 的功能,⽐如 订单表、记录表、收藏表或者其他和 user_id 有关联的,只要他操作业务,接⼝有返回 user_id,就容易出问题了。
【暴露了每⽇拉新数据】 如果是⽤户⾃增 id,⽼王 肯定不单停留这⾥这么简单,假如他想看这个产品每⽇新增的⽤户有多少。
【暴露了平台商品数量 - 订单数量】 同样的思路,去爬取电商平台的商品,爬取平台最⼤的商品 id, 第⼆天再次爬取,持续⼀段时间。就可以可以推断出新品发布数量。
正常的业务表,会⽤⾃增 id,但是也会加个业务 id,⽐如下⾯的
CREATE TABLE `USER` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`biz_id` varchar(64) DEFAULT NULL COMMENT '业务id',
`name` varchar(128) DEFAULT NULL COMMENT '昵称',
`pwd` varchar(124) DEFAULT NULL COMMENT '密码',
`head_img` varchar(524) DEFAULT NULL COMMENT '头像',
`phone` varchar(64) DEFAULT '' COMMENT '⼿机号',
`login_type` int(10) DEFAULT NULL COMMENT '登录类型',
`email` varchar(128) DEFAULT NULL COMMENT '邮箱',
`sex` tinyint(2) DEFAULT '1' COMMENT '0表示⼥,1表示男',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`roles` varchar(11) DEFAULT NULL COMMENT '1,2,3,数字权限,逗号分隔',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=100000 DEFAULT
CHARSET=utf8;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
增加了 biz_id,这个就是业务 id, 如果有关联,则⽤ biz_id 进⾏关联并返回,这个可以是 varchar 类型,long 雪花算法.
# 短链码⽣成解决⽅案
短链码特点
- ⽣成性能强劲
- 碰撞概率低
- 避免重复
- 恶意猜测
- 业务规则安全

实现思路
# ⾃增 ID
- 利⽤插⼊数据库,利⽤数据库⾃增 id
- 把⾃增 id 转成 62 进制作为短链码
- 短链码的⻓度不固定,随着 id 变⼤,短链码⻓度也增⻓
- 可以指定从某个⻓度开始增⻓,到百亿、千亿数量
- 转换⼯具:https://tool.lu/hexconvert/
- 是否存在重复:不重复
- 但短链码是有序的递增,存在【业务数据安全】问题
# MD5 内容压缩
⻓链接做 md5 加密
43E08496,9E5CF455,E6D2D2B3,3407A6D21加密串查询是否已经⽣成过短链接
- 如果已经存在,则拼接时间戳再 MD5 加密,插⼊数据库
- 如果不存在则把⻓链接、⻓链接加密串插⼊数据库
取 MD5 后 最后 1 个 8 位字符串作为短链码
是否存在重复:存在碰撞(重复)可能
是有损压缩算法,数据量超⼤情况碰撞概念越⼤
- ⽐如 一个班上有 300 多个人,每再多 1 个,再同⼀天⽣⽇的概率越⼤,就更加复杂
# 哈希算法
哈希算法:将⼀个元素映射成另⼀个元素
- 加密哈希,如 SHA256、MD5
- ⾮加密哈希,如 MurMurHash,CRC32
# MurMurHash
Murmur 哈希是⼀种⾮加密散列函数,适⽤于⼀般的基于散列的查找。它在 2008 年由 Austin Appleby 创建,在 Github 上托管,名为 “SMHasher” 的测试套件。它也存在许多变种,所有这些变种都已经被公开。该名称来⾃两个基本操作,乘法(MU)和旋转(R)-- 来⾃百科
是⼀种【⾮加密型】哈希函数且【随机分布】特征表现更良好
由于是⾮加密的哈希函数,性能会⽐ MD5 强
再很多地⽅都⽤到⽐如 Guava、Jedis、HBase、Lucence 等
存在两个版本
- MurmurHash2(产⽣ 32 位或 64 位值)
- MurmurHash3(产⽣ 32 位或 128 位值)
数据量
MurmurHash 的 32 bit 能表示的最⼤值近 43 亿的 10 进制
- 满⾜多数业务,如果接近 43 亿则冲突概率⼤
产品⽬标【超理想情况】
⾸年⽇活⽤户: 10万 ⾸年⽇新增短链数据:10万*50 = 500万 年新增短链数:500万 * 365天 = 18.2亿 年新增⽤户数:50万/1年 年营收⽬标: 10万付费⽤户 * 客单价200元 = 2千万 新增短链:50条/⽤户每⽇1
2
3
4
5
6MurMurHash 得到的数值是 10 进制,⼀般会转化为 62 进制进⾏缩短
- 10 进制:1813342104
- 转 62 进制:1YIB7i
- https://tool.lu/hexconvert/
常规短链码是 6~8 位数字 + ⼤⼩写字⺟组合
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 6 位 62 进制数可表示 568 亿个短链(62的6次⽅,每位都有 62个可能,如果扩⼤位数到7位,则可以⽀持3万5200亿)1
2
3MurmurHash 的 32 bit 满⾜多数业务 43 亿
- 拼接上库 - 表位则可以表示更多数据
- 7 位则可以到到 43 亿 * 62 = 2666 亿
- 8 位则可以到到 2666 亿 * 62 = 1.65 万亿条数据
- 结合短链过期数据归档,理论上满⾜未来全部需求了
数据库存储
- 单表 1 千万 * 62 个库 * 62 表 = 384 亿数据
# 短链服务开发
# Murmur 哈希算法封装组件
Guava 框⾥⾥⾯⾃带 Murmur 算法
测试
dcloud-link 模块下 net.xdclass.biz.ShortLinkTest
@Slf4j
public class ShortLinkTest {
@Test
public void testMurmurHash() {
for (int i = 0; i < 5; i++) {
String originalUrl = "https://xdclass.net?id=" + CommonUtil.generateUUID();
long murmur3_32 = Hashing.murmur3_32().hashUnencodedChars(originalUrl).padToLong();
log.info("murmur3_32:{}", murmur3_32);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
21:24:11.090 [main] INFO net.xdclass.biz.ShortLinkTest - murmur3_32:3041886427
21:24:11.098 [main] INFO net.xdclass.biz.ShortLinkTest - murmur3_32:1026413808
21:24:11.099 [main] INFO net.xdclass.biz.ShortLinkTest - murmur3_32:4038355292
21:24:11.099 [main] INFO net.xdclass.biz.ShortLinkTest - murmur3_32:2724856129
21:24:11.099 [main] INFO net.xdclass.biz.ShortLinkTest - murmur3_32:2147341465
2
3
4
5
在 CommonUtil ⼯具类添加 murmur 算法
net.xdclass.util.CommonUtil
/**
* murmur hash算法
*
* @param param
* @return
*/
public static long murmurHash32(String param) {
long murmur32 = Hashing.murmur3_32().hashUnencodedChars(param).padToLong();
return murmur32;
}
2
3
4
5
6
7
8
9
10
# 短链⽣成组件 ShortLinkComponent
在 dcloud-link 模块下创建
net.xdclass.component.ShortLinkComponent
@Component
public class ShortLinkComponent {
private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* 生成短链码
*
* @param param
* @return
*/
public String createShortLinkCode(String param) {
long murmurHash = CommonUtil.murmurHash32(param);
// 进制转换
return encodeToBase62(murmurHash);
}
/**
* 10进制转62进制
*
* @param num
* @return
*/
private static String encodeToBase62(long num) {
//StringBuffer:线程安全; StringBuilder:线程不安全
StringBuilder sb = new StringBuilder();
do {
int i = (int) (num % 62);
sb.append(CHARS.charAt(i));
num = num / 62;
} while (num > 0);
return sb.reverse().toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
application.yml
server:
port: 8003
spring:
application:
name: dcloud-link
cloud:
nacos:
discovery:
server-addr: 192.168.130.24:8848
username: nacos
password: nacos
# datasource:
# url: jdbc:mysql://192.168.130.24:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: xdclass.net168
# 数据源 ds0 第一个数据库
shardingsphere:
datasource:
#数据源名称
names: ds0
ds0:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_link?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
props:
# 打印执行的数据库以及语句
sql:
show: true
#配置plus打印sql⽇志
mybatis-plus:
configuration:
log-impl:
org.apache.ibatis.logging.stdout.StdOutImpl
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
启动类 AccountApplication
@MapperScan("net.xdclass.mapper")
@EnableTransactionManagement
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
单元测试
@Slf4j
@SpringBootTest(classes = LinkApplication.class)
@RunWith(SpringRunner.class)
public class ShortLinkTest {
@Autowired
private ShortLinkComponent shortLinkComponent;
@Test
public void testMurmurHash() {
for (int i = 0; i < 5; i++) {
String originalUrl = "https://xdclass.net?id=" + CommonUtil.generateUUID();
long murmur3_32 = Hashing.murmur3_32().hashUnencodedChars(originalUrl).padToLong();
log.info("murmur3_32:{}", murmur3_32);
}
}
@Test
public void testCreateShortLink() {
Random random = new Random();
for (int i = 0; i < 10; i++) {
int num1 = random.nextInt(10);
int num2 = random.nextInt(1000000);
int num3 = random.nextInt(1000000);
String originalUrl = num1 + "xdclass" + num2 + ".net" + num3;
String shortLinkCode = shortLinkComponent.createShortLinkCode(originalUrl);
log.info("originalUrl:" + originalUrl + ", shortLinkCode=" + shortLinkCode);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2024-02-05 21:44:11.437 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:5xdclass346537.net464583, shortLinkCode=3qrQKV
2024-02-05 21:44:11.437 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:8xdclass418519.net789621, shortLinkCode=1WOrme
2024-02-05 21:44:11.437 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:0xdclass370375.net308886, shortLinkCode=Bow33
2024-02-05 21:44:11.437 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:8xdclass67410.net57682, shortLinkCode=1hgsne
2024-02-05 21:44:11.437 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:6xdclass898076.net412199, shortLinkCode=GUfYk
2024-02-05 21:44:11.437 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:2xdclass143088.net43672, shortLinkCode=13hwsY
2024-02-05 21:44:11.437 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:3xdclass376128.net773590, shortLinkCode=3JQgIu
2024-02-05 21:44:11.438 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:7xdclass547476.net980940, shortLinkCode=ub3wM
2024-02-05 21:44:11.438 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:1xdclass734085.net225859, shortLinkCode=4e0Tmu
2024-02-05 21:44:11.438 INFO 80156 --- [ main] net.xdclass.biz.ShortLinkTest : originalUrl:7xdclass114979.net558009, shortLinkCode=2esXn2
2
3
4
5
6
7
8
9
10
为什么要⽤ 62 进制转换,不是 64 进制?
- 62 进制转换是因为 62 进制转换后只含数字 + ⼩写 + ⼤写字⺟
- ⽽ 64 进制转换会含有 /、+ 这样的符号(不符合正常 URL 的字符)
- 10 进制转 62 进制可以缩短字符,如果我们要 6 位字符的话,已经有 560 亿个组合了
看业务情况有些短链也会加⼊其它特殊字符
短链固定 6 位?肯定不是的,后续会进⾏分库分表改造

# 数据库表模型讲解 - 短链分组和短链
- ⼀个账号有多个分组
- ⼀个分组下有多个短链

创建 dcloud_link 数据库
在 dcloud_link 数据库创建以下表
短链 - 分组
CREATE TABLE `link_group` (
`id` bigint unsigned NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '组名',
`account_no` bigint DEFAULT NULL COMMENT '账号唯⼀编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
2
3
4
5
6
7
8
短链
CREATE TABLE `short_link` (
`id` bigint unsigned NOT NULL ,
`group_id` bigint DEFAULT NULL COMMENT '组',
`title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链标题',
`original_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '原始url地址',
`domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链域名',
`code` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '短链压缩码',
`sign` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '⻓链的md5码,⽅便查找',
`expired` datetime DEFAULT NULL COMMENT '过期时间,⻓久就是-1',
`account_no` bigint DEFAULT NULL COMMENT '账号唯⼀编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`del` int unsigned NOT NULL COMMENT '0是默认,1是删除',
`state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态,lock是锁定不可⽤,active是可⽤',
`link_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接产品层级:FIRST 免费⻘铜、SECOND⻩⾦、THIRD钻⽯',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用逆向工具生成相关的实体类
net.xdclass.db.MyBatisPlusGenerator
public class MyBatisPlusGenerator {
public static void main(String[] args) {
//1. 全局配置
GlobalConfig config = new GlobalConfig();
// 是否支持AR模式
config.setActiveRecord(true)
// 作者
.setAuthor("iekr")
// 生成路径,最好使用绝对路径,window路径是不一样的
//TODO TODO TODO TODO
.setOutputDir("D:\\xdclass\\demo\\src\\main\\java")
// 文件覆盖
.setFileOverride(true)
// 主键策略
.setIdType(IdType.AUTO)
.setDateType(DateType.ONLY_DATE)
// 设置生成的service接口的名字的首字母是否为I,默认Service是以I开头的
.setServiceName("%sService")
//实体类结尾名称
.setEntityName("%sDO")
//生成基本的resultMap
.setBaseResultMap(true)
//不使用AR模式
.setActiveRecord(false)
//生成基本的SQL片段
.setBaseColumnList(true);
//2. 数据源配置
DataSourceConfig dsConfig = new DataSourceConfig();
// 设置数据库类型
dsConfig.setDbType(DbType.MYSQL)
.setDriverName("com.mysql.cj.jdbc.Driver")
//TODO TODO TODO TODO
.setUrl("jdbc:mysql://192.168.130.24:3306/dcloud_link?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai")
.setUsername("root")
.setPassword("xdclass.net168");
//3. 策略配置globalConfiguration中
StrategyConfig stConfig = new StrategyConfig();
//全局大写命名
stConfig.setCapitalMode(true)
// 数据库表映射到实体的命名策略
.setNaming(NamingStrategy.underline_to_camel)
//使用lombok
.setEntityLombokModel(true)
//使用restcontroller注解
.setRestControllerStyle(true)
// 生成的表, 支持多表一起生成,以数组形式填写
//TODO TODO TODO TODO
.setInclude("link_group","short_link");
//4. 包名策略配置
PackageConfig pkConfig = new PackageConfig();
pkConfig.setParent("net.xdclass")
.setMapper("mapper")
.setService("service")
.setController("controller")
.setEntity("model")
.setXml("mapper");
//5. 整合配置
AutoGenerator ag = new AutoGenerator();
ag.setGlobalConfig(config)
.setDataSource(dsConfig)
.setStrategy(stConfig)
.setPackageInfo(pkConfig);
//6. 执行操作
ag.execute();
System.out.println("======= 相关代码生成完毕 ========");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
导⼊⽣成好的代码
- model (为啥不放 common 项⽬,如果是确定每个服务都⽤到的依赖或者类才放到 common 项⽬)
- 删除掉 model 实体类中的
@TableId注解
- 删除掉 model 实体类中的
- mapper 类接⼝拷⻉
- resource/mapper ⽂件夹 xml 脚本拷⻉
- controller
- service 不拷⻉
# 配置⽂件修改 - yml 转 properties
pringBoot 的配置⽂件有两种
- ⼀种是 properties 结尾的
- ⼀种是 yaml 或者 yml ⽂件结尾的
- 如果同时存在 properties 和 yml, application.properties ⾥⾯的属性就会覆盖⾥ application.yml 的属性
yml 的注意点
- yml 中缩进⼀定不能使⽤ TAB,否则会报很奇怪的错误
- yml 每个的冒号后⾯⼀定都要加⼀个空格
- 第⼀个是 yml 是⽀持中⽂内容的,properties 想使⽤中⽂需要 unicode 编码
在线转换⼯具:https://www.toyaml.com
# 短链分组管理
# 配置登录拦截器
net.xdclass.config.InterceptorConfig
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
// 添加拦截路径
.addPathPatterns("/api/link/*/**", "/api/group/*/**","/api/domain/*/**")
// 排除不拦截
.excludePathPatterns(
"");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 新增接⼝
添加雪花算法配置
server:
port: 8003
spring:
application:
name: dcloud-link
cloud:
nacos:
discovery:
server-addr: 192.168.130.24:8848
username: nacos
password: nacos
# datasource:
# url: jdbc:mysql://192.168.130.24:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: xdclass.net168
# 数据源 ds0 第一个数据库
shardingsphere:
datasource:
#数据源名称
names: ds0
ds0:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
props:
# 打印执行的数据库以及语句
sql:
show: true
sharding:
tables:
link_group:
# # 指定traffic表的数据分布情况,配置数据节点,行表达式标识符使用 ${...} 或 $->{...},但前者与 Spring 本身的文件占位符冲突,所以在 Spring 环境中建议使用 $->{...}
# actual-data-nodes: ds0.traffic_$->{0..1}
# #水平分表策略+行表达式分片
# table-strategy:
# inline:
# algorithm-expression: traffic_$->{ account_no % 2 }
# sharding-column: account_no
key-generator:
column: id
props:
worker:
id: ${workId}
#id⽣成策略
type: SNOWFLAKE
#配置plus打印sql⽇志
mybatis-plus:
configuration:
log-impl:
org.apache.ibatis.logging.stdout.StdOutImpl
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
net.xdclass.controller.request.LinkGroupAddRequest
@Data
public class LinkGroupAddRequest {
/**
* 组名
*/
private String title;
}
2
3
4
5
6
7
8
LinkGroupController
@RestController
@RequestMapping("/api/group/v1")
public class LinkGroupController {
@Autowired
private LinkGroupService linkGroupService;
/**
* 创建分组
*
* @param request
* @return
*/
@PostMapping("add")
public JsonData add(@RequestBody LinkGroupAddRequest request) {
int rows = linkGroupService.add(request);
return rows == 1 ? JsonData.buildSuccess() : JsonData.buildResult(BizCodeEnum.GROUP_ADD_FAIL);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LinkGroupService
public interface LinkGroupService {
/**
* 新增分组
* @param request
* @return
*/
int add(LinkGroupAddRequest request);
}
2
3
4
5
6
7
8
9
10
LinkGroupServiceImpl
@Service
@Slf4j
public class LinkGroupServiceImpl implements LinkGroupService {
@Autowired
private LinkGroupManager linkGroupManager;
@Override
public int add(LinkGroupAddRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
LinkGroupDO linkGroupDO = new LinkGroupDO();
linkGroupDO.setTitle(request.getTitle());
linkGroupDO.setAccountNo(accountNo);
int rows = linkGroupManager.add(linkGroupDO);
return rows;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LinkGroupManager
public interface LinkGroupManager {
int add(LinkGroupDO linkGroupDO);
}
2
3
LinkGroupManagerImpl
@Component
public class LinkGroupManagerImpl implements LinkGroupManager {
@Autowired
private LinkGroupMapper linkGroupMapper;
@Override
public int add(LinkGroupDO linkGroupDO) {
return linkGroupMapper.insert(linkGroupDO);
}
}
2
3
4
5
6
7
8
9
10
11
# 删除接⼝
LinkGroupController
/**
* 根据id删除分组
*/
@DeleteMapping("/del/{group_id}")
public JsonData del(@PathVariable("group_id") Long groupId) {
int rows = linkGroupService.del(groupId);
return rows == 1 ? JsonData.buildSuccess() : JsonData.buildResult(BizCodeEnum.GROUP_NOT_EXIST);
}
2
3
4
5
6
7
8
9
LinkGroupService
/**
* 根据id删除分组
* @param groupId
* @return
*/
int del(Long groupId);
2
3
4
5
6
LinkGroupServiceImpl
@Override
public int del(Long groupId) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
return linkGroupManager.del(groupId,accountNo);
}
2
3
4
5
6
LinkGroupManager
int del(Long groupId, Long accountNo);
LinkGroupManagerImpl
@Override
public int del(Long groupId, Long accountNo) {
return linkGroupMapper.delete(new QueryWrapper<LinkGroupDO>().eq("group_id", groupId)
.eq("account_no", accountNo));
}
2
3
4
5
6
# 查询详情
net.xdclass.vo.LinkGroupVO
@Data
@EqualsAndHashCode(callSuper = false)
public class LinkGroupVO implements Serializable {
private Long id;
/**
* 组名
*/
private String title;
/**
* 账号唯⼀编号
*/
private Long accountNo;
private Date gmtCreate;
private Date gmtModified;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LinkGroupController
/**
* 根据id查询分组详情
*/
@GetMapping("detail/{group_id}")
public JsonData detail(@PathVariable("group_id") Long groupId) {
LinkGroupVO linkGroupVO = linkGroupService.detail(groupId);
return JsonData.buildSuccess(linkGroupVO);
}
2
3
4
5
6
7
8
9
LinkGroupService
/**
* 根据id查询分组详情
* @param groupId
* @return
*/
LinkGroupVO detail(Long groupId);
2
3
4
5
6
LinkGroupServiceImpl
@Override
public LinkGroupVO detail(Long groupId) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
LinkGroupVO linkGroupVO = new LinkGroupVO();
BeanUtils.copyProperties(linkGroupDO, linkGroupVO);
return linkGroupVO;
}
2
3
4
5
6
7
8
LinkGroupManager
LinkGroupDO detail(Long groupId, Long accountNo);
LinkGroupManagerImpl
@Override
public LinkGroupDO detail(Long groupId, Long accountNo) {
return linkGroupMapper.selectOne(new QueryWrapper<LinkGroupDO>().eq("group_id", groupId).eq("account_no", accountNo));
}
2
3
4
# 查询⽤户全部分组
LinkGroupController
/**
* 查询用户全部分组
*/
@GetMapping("list")
public JsonData findUserAllLinkGroup() {
List<LinkGroupVO> linkGroupVOS= linkGroupService.listAllGroup();
return JsonData.buildSuccess(linkGroupVOS);
}
2
3
4
5
6
7
8
LinkGroupService
/**
* 查询用户全部分组
* @return
*/
List<LinkGroupVO> listAllGroup();
2
3
4
5
LinkGroupServiceImpl
@Override
public List<LinkGroupVO> listAllGroup() {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
List<LinkGroupDO> linkGroupDOS = linkGroupManager.listAllGroup(accountNo);
List<LinkGroupVO> groupVOCollection = linkGroupDOS.stream().map(linkGroupDO -> {
LinkGroupVO linkGroupVO = new LinkGroupVO();
BeanUtils.copyProperties(linkGroupDO, linkGroupVO);
return linkGroupVO;
}).collect(Collectors.toList());
return groupVOCollection;
}
2
3
4
5
6
7
8
9
10
11
LinkGroupManager
List<LinkGroupDO> listAllGroup(Long accountNo);
LinkGroupManagerImpl
@Override
public List<LinkGroupDO> listAllGroup(Long accountNo) {
return linkGroupMapper.selectList(new QueryWrapper<LinkGroupDO>().eq("account_no", accountNo));
}
2
3
4
# 更新分组接⼝
net.xdclass.controller.request.LinkGroupUpdateRequest
@Data
public class LinkGroupUpdateRequest {
/**
* 组id
*/
private Long id;
/**
* 组名
*/
private String title;
}
2
3
4
5
6
7
8
9
10
11
12
13
LinkGroupController
/**
* 根据id更新分组
*/
@PutMapping("update")
public JsonData update(@RequestBody LinkGroupUpdateRequest request) {
int rows = linkGroupService.updateById(request);
return rows == 1 ? JsonData.buildSuccess() : JsonData.buildResult(BizCodeEnum.GROUP_OPER_FAIL);
}
2
3
4
5
6
7
8
9
LinkGroupService
/**
* 根据id更新组名
* @param request
* @return
*/
int updateById(LinkGroupUpdateRequest request);
2
3
4
5
6
LinkGroupServiceImpl
@Override
public int updateById(LinkGroupUpdateRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
LinkGroupDO linkGroupDO = new LinkGroupDO();
linkGroupDO.setId(request.getId());
linkGroupDO.setTitle(request.getTitle());
linkGroupDO.setAccountNo(accountNo);
int rows = linkGroupManager.updateById(linkGroupDO);
return 0;
}
}
2
3
4
5
6
7
8
9
10
11
LinkGroupManager
int updateById(LinkGroupDO linkGroupDO);
LinkGroupManagerImpl
@Override
public int updateById(LinkGroupDO linkGroupDO) {
return linkGroupMapper.update(linkGroupDO, new QueryWrapper<LinkGroupDO>().eq("id", linkGroupDO.getId())
.eq("account_no", linkGroupDO.getAccountNo())
);
}
2
3
4
5
6
# 短链分组管理 - ⽔平分库分表
- 需要降低单表数据量,进⾏⽔平分库分表
- 分库数量:线上分 16 库,本地分 2 库即可
- 分⽚ key: account_no,查询维度都是根据 account_no 进⾏查询
- 分⽚策略:⾏表达式分⽚策略 InlineShardingStrategy
创建数据库 dcloud_link_0 和 dcloud_link_1
在两个库中创建表
CREATE TABLE `link_group` (
`id` bigint unsigned NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '组名',
`account_no` bigint DEFAULT NULL COMMENT '账号唯⼀编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
2
3
4
5
6
7
8
配置
server:
port: 8003
spring:
application:
name: dcloud-link
cloud:
nacos:
discovery:
server-addr: 192.168.130.24:8848
username: nacos
password: nacos
# datasource:
# url: jdbc:mysql://192.168.130.24:3306/dcloud_account?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
# driver-class-name: com.mysql.cj.jdbc.Driver
# username: root
# password: xdclass.net168
# 数据源 ds0 第一个数据库
shardingsphere:
datasource:
#数据源名称
names: ds0,ds1
ds0:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_link_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
ds1:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_link_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
props:
# 打印执行的数据库以及语句
sql:
show: true
sharding:
tables:
link_group:
# 短链组,策略:⽔平分库,不⽔平分表
# 先进⾏⽔平分库, ⽔平分库策略,⾏表达式分⽚
database-strategy:
inline:
algorithm-expression: ds$->{account_no % 2}
sharding-column: account_no
key-generator:
column: id
props:
worker:
id: ${workId}
#id⽣成策略
type: SNOWFLAKE
#配置plus打印sql⽇志
mybatis-plus:
configuration:
log-impl:
org.apache.ibatis.logging.stdout.StdOutImpl
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# ShortLink 分库分表
# 分库分表策略
分库分表
- 16 个库,每个库 64 个表,总量就是 1024 个表
- 分⽚键:短链码 code
- ⽐如 g1.fit/92AEva 的短链码 92AEva
- 分库分表算法:短链码进⾏ hash 取模
优点
- 保证数据较均匀的分散落在不同的库、表中,可以有效的避免
- 热点数据集中问题,
- 分库分表⽅式清晰易懂
问题
- 扩容不是很⽅便,需要数据迁移
- 需要⼀次性建⽴ 16 个库,每个库 64 个表,总量就是 1024 个
- 表,浪费资源
需要解决的问题
- 数据量增加,扩容避免迁移数据或者免迁移
- 前期数据量不多,不浪费库表系统资源
- 分库分表:16 个库,每个库 64 个表,总量就是 1024 个表
如何做?
- 从短链码⼊⼿ - 增加库表位
- 类似案例 - 阿⾥这边商品订单号 - ⾥⾯也包括了库表信息(规则不能说)

# 分库分表相关库表建⽴
为啥能做到免迁移扩容?
- A92AEva1
- 由于短链码的前缀和后缀是是固定的,所以扩容也不影响旧的数据
类似的免迁移扩容策略还有哪些?
- 时间范围分库分表
- id 范围分库分表
三个库
创建新库 dcloud_link_a
⼀个库两个表
CREATE TABLE `short_link_a` (
`id` bigint unsigned NOT NULL,
`group_id` bigint DEFAULT NULL COMMENT '组',
`title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链标题',
`original_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '原始url地址',
`domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链域名',
`code` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '短链压缩码',
`sign` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '⻓链的md5码,⽅便查找',
`expired` datetime DEFAULT NULL COMMENT '过期时间,⻓久就是-1',
`account_no` bigint DEFAULT NULL COMMENT '账号唯⼀编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`del` int unsigned NOT NULL COMMENT '0是默认,1是删除',
`state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态,lock是锁定不可⽤,active是可⽤',
`link_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接产品层级:FIRST 免费⻘铜、SECOND⻩⾦、THIRD钻⽯',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
配置多库表
shardingsphere:
datasource:
#数据源名称
names: ds0,ds1,dsa
ds0:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_link_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
ds1:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_link_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
dsa:
connectionTimeoutMilliseconds: 30000
driver-class-name: com.mysql.cj.jdbc.Driver
idleTimeoutMilliseconds: 60000
jdbc-url: jdbc:mysql://192.168.130.24:3306/dcloud_link_a?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
maintenanceIntervalMilliseconds: 30000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 50
minPoolSize: 50
password: xdclass.net168
type: com.zaxxer.hikari.HikariDataSource
username: root
props:
# 打印执行的数据库以及语句
sql:
show: true
sharding:
tables:
link_group:
# 短链组,策略:⽔平分库,不⽔平分表
# 先进⾏⽔平分库, ⽔平分库策略,⾏表达式分⽚
database-strategy:
inline:
algorithm-expression: ds$->{account_no % 2}
sharding-column: account_no
key-generator:
column: id
props:
worker:
id: ${workId}
#id⽣成策略
type: SNOWFLAKE
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 标准分⽚策略 - 精准分⽚算法
# ⽔平分库
⽔平分库 - 标准分⽚策略 - 精准分⽚算法 Ae23asa1
net.xdclass.strategy.CustomDBPreciseShardingAlgorithm
public class CustomDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
/**
* @param availableTargetNames 数据源集合在分库时值为所有分⽚库的集合 databaseNames分表时为对应分⽚库中所有分⽚表的集合 tablesNames
* @param shardingValue 分⽚属性,包括logicTableName 为逻辑表,columnName 分⽚健(字段),value 为从 SQL 中解析出的分⽚健的值
* @return
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
//获取短链码第⼀位,即库位
String codePrefix = shardingValue.getValue().substring(0, 1);
for (String targetName : availableTargetNames) {
// 获取库名最后一位
String targetNameSuffix = targetName.substring(targetName.length() - 1);
// 如果一致则返回
if (codePrefix.equals(targetNameSuffix)) {
return targetName;
}
}
throw new BizException(BizCodeEnum.DB_ROUTE_NOT_FOUND);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
配置使用
show_link:
# 短链,策略:分库+分表
# 先进⾏⽔平分库,然后再⽔平分表
database-strategy:
inline:
algorithm-expression: net.xdclass.strategy.CustomDBPreciseShardingAlgorithm
sharding-column: code
2
3
4
5
6
7
# 水平分表
net.xdclass.strategy.CustomTablePreciseShardingAlgorithm
public class CustomTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
/**
* @param availableTargetNames 数据源集合
* 在分库时值为所有分⽚
* 库的集合 databaseNames
* 分表时为对应分⽚库中
* 所有分⽚表的集合 tablesNames
* @param shardingValue 分⽚属性,包括
* logicTableName 为
* 逻辑表,
* columnName 分⽚健
* (字段),
* value 为从 SQL 中解
* 析出的分⽚健的值
* @return
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
// 逻辑逻辑表
String targetName = availableTargetNames.iterator().next();
// 短链码
String value = shardingValue.getValue();
// 获取短链码最后一位
String codeSuffix = value.substring(value.length() - 1);
// 拼装actual table
return targetName + "_" + codeSuffix;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
配置⽂件
# ⽔平分表策略,⾃定义策略。 真实库.逻辑表
actual-data-nodes: ds0.short_link,ds1.short_link,dsa.short_link
table-strategy:
standard:
sharding-column: code
precise-algorithm-class-name: net.xdclass.strategy.CustomTablePreciseShardingAlgorithm
key-generator:
column: id
props:
worker:
id: ${workId}
#id⽣成策略
type: SNOWFLAKE
2
3
4
5
6
7
8
9
10
11
12
13
# 短链码配置⽣成库表位
# 分库位
net.xdclass.strategy.ShardingDBConfig
public class ShardingDBConfig {
/**
* 存储数据库位置编号
*/
private static final List<String> dbPrefixList = new ArrayList<>();
private static Random random = new Random();
//配置启⽤哪些库的前缀
static {
dbPrefixList.add("0");
dbPrefixList.add("1");
dbPrefixList.add("a");
}
/**
* 获取随机的前缀
* @return
*/
public static String getRandomDBPrefix() {
int index = random.nextInt(dbPrefixList.size() );
return dbPrefixList.get(index);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 分表位
net.xdclass.strategy.ShardingTableConfig
public class ShardingTableConfig {
/**
* 存储数据表位置编号
*/
private static final List<String> tablePrefixList = new ArrayList<>();
private static Random random = new Random();
//配置启⽤哪些表的前缀
static {
tablePrefixList.add("0");
tablePrefixList.add("1");
tablePrefixList.add("a");
}
/**
* 获取随机的前缀
* @return
*/
public static String getRandomDBPrefix() {
int index = random.nextInt(tablePrefixList.size() );
return tablePrefixList.get(index);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 修改生成短链码逻辑
使用库位 + murmur 哈希 + 表位拼接短链
ShortLinkComponent
/**
* 生成短链码
*
* @param param
* @return
*/
public String createShortLinkCode(String param) {
long murmurHash = CommonUtil.murmurHash32(param);
// 进制转换
String code = encodeToBase62(murmurHash);
String shortLinkCode = ShardingDBConfig.getRandomDBPrefix() + code + ShardingTableConfig.getRandomDBPrefix();
return shortLinkCode;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Manager 层模块
ShortLinkManager
public interface ShortLinkManager {
/**
* 新增短链
* @param shortLinkDO
* @return
*/
int addShortLink(ShortLinkDO shortLinkDO);
/**
* 根据短链码查找短链
*/
ShortLinkDO findByShortLinkCode(String shortLinkCode);
/**
* 根据短链码删除短链
*/
int del(ShortLinkDO shortLinkDO);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ShortLinkManagerImpl
@Component
@Slf4j
public class ShortLinkManagerImpl implements ShortLinkManager {
@Autowired
private ShortLinkMapper shortLinkMapper;
@Override
public int addShortLink(ShortLinkDO shortLinkDO) {
return shortLinkMapper.insert(shortLinkDO);
}
@Override
public ShortLinkDO findByShortLinkCode(String shortLinkCode) {
return shortLinkMapper.selectOne(new QueryWrapper<ShortLinkDO>().eq("code", shortLinkCode).eq("del", 0));
}
@Override
public int del(ShortLinkDO shortLinkDO) {
int rows = shortLinkMapper.update(null,
new UpdateWrapper<ShortLinkDO>().eq("code", shortLinkDO.getCode()).set("del", 1));
return rows;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 单元测试
ShortLinkTest
@Autowired
private ShortLinkManager shortLinkManager;
@Test
public void testSaveShortLink() {
Random random = new Random();
int num1 = random.nextInt(10);
int num2 = random.nextInt(1000000);
int num3 = random.nextInt(1000000);
String originalUrl = num1 + "xdclass" + num2 + ".net" + num3;
String shortLinkCode = shortLinkComponent.createShortLinkCode(originalUrl);
ShortLinkDO shortLinkDO = new ShortLinkDO();
shortLinkDO.setCode(shortLinkCode);
shortLinkDO.setAccountNo(Long.valueOf(num3));
shortLinkDO.setSign(CommonUtil.MD5(originalUrl));
shortLinkDO.setDel(0);
shortLinkManager.addShortLink(shortLinkDO);
}
@Test
public void testFindShortLink() {
String shortLinkCode = "04DzCFLa";
ShortLinkDO shortLinkDO = shortLinkManager.findByShortLinkCode(shortLinkCode);
log.info("shortLinkDO:{}", shortLinkDO);
}
@Test
public void testDelShortLink() {
String shortLinkCode = "04DzCFLa";
shortLinkManager.del(shortLinkCode, 1L);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 加权负载均衡思想应⽤ - 数据库表扩容
# 数据不均匀问题解决⽅案
问题
- 假如前期分三个库,⼀个库两个表,项⽬⽕爆,数据量激增,进⾏扩容
- 增加了新的数据库表位,会导致旧的库表⽐新的库表数据量多,且容易出现超载情况

Nginx 加权负载均衡的应⽤
- 不同的库表位分配的概率不⼀样,类似的中间件应⽤场景有 nginx
Nginx 常⻅的负载均衡策略
节点轮询(默认)
weight 权重配置
简介:weight 和访问⽐率成正⽐,数字越⼤,分配得到的流量越⾼
场景:服务器性能差异⼤的情况使⽤
upstream lbs { server 192.168.159.133:8080 weight=5; server 192.168.159.133:8081 weight=10; }1
2
3
4
5
加权解决⽅式
- 库表位可以使⽤对象形式,配置权重,避免数据倾斜、数据集中
- 编写算法,根据不同的,配置权重,不同的库表位配置不同的权重
- 加权配置,list 重复添加出现的⾼频的库表位
可以把这个亮点记录下
⾯试的时候再⾯试官前⾯说下项⽬难点和你的解决⽅案
你想到的解决⽅案,⽅便⼜清晰,还省服务器资源和避免了问题
- 业务量超过评估量,分库分表 - ⼆次扩容的时候避免数据迁移
- 不⽤⼀次性建⽴很多个库表,可以动态添加,节省服务器资源
- 使⽤加权库表位算法,解决扩容后数据倾斜不均匀问题
# 分库分表多维度查询解决⽅案
# 短链 URL 跳转 302 跳转开发
需求
- 接收⼀个短链码
- 解析获取原始地址
- 302 进⾏跳转
common 模块下 net.xdclass.enums.ShortLinkStateEnum
public enum ShortLinkStateEnum {
/**
* 锁定
*/
LOCK,
/**
* 可用
*/
ACTIVE;
}
2
3
4
5
6
7
8
9
10
11
net.xdclass.vo.ShortLinkVO
@Data
@EqualsAndHashCode(callSuper = false)
public class ShortLinkVO implements Serializable {
private Long id;
/**
* 组
*/
private Long groupId;
/**
* 短链标题
*/
private String title;
/**
* 原始url地址
*/
private String originalUrl;
/**
* 短链域名
*/
private String domain;
/**
* 短链压缩码
*/
private String code;
/**
* ⻓链的md5码,⽅便查找
*/
private String sign;
/**
* 过期时间,⻓久就是-1
*/
private Date expired;
/**
* 账号唯⼀编号
*/
private Long accountNo;
/**
* 创建时间
*/
private Date gmtCreate;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 0是默认,1是删除
*/
private Integer del;
/**
* 状态,lock是锁定不可⽤,active是可⽤
*/
private String state;
/**
* 链接产品层级:FIRST 免费⻘铜、SECOND⻩⾦、THIRD钻⽯
*/
private String linkType;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
LinkApiController
@Controller
@Slf4j
public class LinkApiController {
@Autowired
private ShortLinkService shortLinkService;
/**
* 解析 301还是302,这边是返回http code是302
* <p>
* 知识点⼀,为什么要⽤ 301 跳转⽽不是 302 呐?
* <p>
* 301 是永久重定向,302 是临时重定向。
* <p>
* 短地址⼀经⽣成就不会变化,所以⽤ 301 是同时对服务器压
* ⼒也会有⼀定减少
* <p>
* 但是如果使⽤了 301,⽆法统计到短地址被点击的次数。
* <p>
* 所以选择302虽然会增加服务器压⼒,但是有很多数据可以获
* 取进⾏分析
*
* @param shortLinkCode
* @param request
* @param response
* @return
*/
@GetMapping(path = "/{shortLinkCode}")
public void dispatch(@PathVariable("shortLinkCode") String shortLinkCode, HttpServletRequest request, HttpServletResponse response) {
try {
log.info("短链码:{}", shortLinkCode);
// 判断短链是否合法
if (isLetterDigit(shortLinkCode)) {
// 查找短链
ShortLinkVO shortLinkVO = shortLinkService.parserShortLinkCode(shortLinkCode);
// 判断是否过期和可用
if (isVisible(shortLinkVO)) {
response.setHeader("Location", shortLinkVO.getOriginalUrl());
// 302
response.setStatus(HttpStatus.FOUND.value());
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
}
}
} catch (Exception e) {
log.error("短链码:{}", shortLinkCode, e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
/**
* 判断短链是否可用
*
* @param shortLinkVO
* @return
*/
private static boolean isVisible(ShortLinkVO shortLinkVO) {
if (shortLinkVO != null && shortLinkVO.getExpired().getTime() > CommonUtil.getCurrentTimestamp()) {
if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
return true;
}
} else if (shortLinkVO != null && shortLinkVO.getExpired().getTime() == -1) {
return true;
}
return false;
}
/**
* 验证字符串是否包含数字和字母
*
* @param str
* @return
*/
private static boolean isLetterDigit(String str) {
String regex = "^[a-z0-9A-Z]+$";
return str.matches(regex);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
ShortLinkService
public interface ShortLinkService {
/**
* 解析短链
* @param shortLinkCode
* @return
*/
ShortLinkVO parserShortLinkCode(String shortLinkCode);
}
2
3
4
5
6
7
8
ShortLinkServiceImpl
@Service
@Slf4j
public class ShortLinkServiceImpl implements ShortLinkService {
@Autowired
private ShortLinkManager shortLinkManager;
@Override
public ShortLinkVO parserShortLinkCode(String shortLinkCode) {
ShortLinkDO shortLinkDO = shortLinkManager.findByShortLinkCode(shortLinkCode);
if (shortLinkDO != null) {
ShortLinkVO shortLinkVO = new ShortLinkVO();
BeanUtils.copyProperties(shortLinkDO, shortLinkVO);
return shortLinkVO;
}
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 短链服务多维度查询问题
问题来了,商家怎么看⾃⼰的全部短链呢?????
- 普通⽤户根据短链码可以路由到对应的库表
- 但是商家创建的短链码都是没规律,分布再不同的库表上,咋整???
不同维度查看数据,场景是不⼀样的,主要是分:有 PartitionKey,没 PartitionKey 两个场景
电商订单案例⼀:
- 订单表 的 partionKey 是 user_id,⽤户查看⾃⼰的订 单列表⽅便
- 但商家查看⾃⼰店铺的订单列表就麻烦,分布在不同数据节点

短链访问案例:
- 普通⽤户访问短链,根据短链码 code 可以解析到对应的库表
- 但短链商家,查看⾃⼰全部的短链就麻烦了,分布再不同的库下⾯

这个是【 通⽤的业务场景痛点 】,⼤家学会总结
- 除了上⾯的电商业务、短链业务,还有更多
- 招聘⽹站业务
- 企业查看⾃⼰某个岗位的⾯试记录
- 应聘者查看⾃⼰的全部⾯试记录
- 痛点:
- 根据 user_id 进⾏ hash 分库分表
- 但是企业的岗位存在不同 user_id 进⾏⾯试
解决⽅案
- 字段解析配置
- NOSQL 冗余
- 本身库表冗余双写⽅案
- 部分字段冗余
- 全量内容冗余
# 解决⽅案⼀ - 字段解析配置
字段解析配置:
- 建⼀个表,存储 account_no 对应的库表位,商家⽣成的【短链码】固定前缀或者后缀
- 即【短链码】⾥⾯包括了商家的信息

# 解决⽅案⼆ - NOSQL ⽅案
电商订单案例
- 订单表 的 partionKey 是 user_id,⽤户查看⾃⼰的订单列表⽅便
- 但商家查看⾃⼰店铺的订单列表就麻烦,分布在不同数据节点
- 订单冗余存储在 es 上⼀份

短链平台案例
- 短链表的 partionKey 是短链码,⽤户访问短码⽅便解析
- 但商家查看⾃⼰某个分组下全部短链列表就麻烦,分布在不同数据节点
- 短链码冗余存储在 es 上⼀份

# 解决⽅案三 - 冗余双写⽅案
电商场景
- b2b 平台,⽐如淘宝、京东,买家和卖家都要能够看到⾃⼰的订单列表
- ⽆论是按照买家 id 切分订单表,还是按照卖家 id 切分订单表都没法满⾜要求
- 拆分买家库和卖家库
- 买家库,按照⽤户的 id 来分库分表
- 卖家库,按照卖家的 id 来分库分表
- 数据冗余
- 下订单的时候写两份数据
- 在买家库和卖家库各写⼀份

短链场景

# 冗余双写⽅案和分布式事务
冗余双写会代来什么问题?
- 存储空间更多(属于空间换时间,需要更多存储空间,减少库表数据量,提升性能)
- 冗余双写怎么实现问题
- 分布事务问题
# 解决⽅案⼀
- 直接 RPC 调⽤ + Seata 分布式事务框架
- 优点:强⼀致性,代码逻辑简单,业务侵⼊性⼩
- 缺点:性能下降,seata 本身存在⼀定的性能损耗 Seata ⽀持 AT、TCC、Saga 三种模式
- AT:隔离性好和低改造成本,但性能低
- TCC:性能和隔离性,但改造成本⼤
- Saga:性能和低改造成本,但隔离性不好

# 解决⽅案⼆
- 使⽤ MQ, ⽣产者确认消息发送成功后,不同的消费者订阅消息消费
- 同时保证消息处理的幂等性
- 保证 Broker 的⾼可⽤
- 优点
- 实现简单,改造成本⼩
- 性能⾼,没有全局锁
- 缺点
- 弱⼀致性,需要强⼀致性的场景不适⽤
- 消费者消费失败,需要额外写接⼝回滚⽣产者业务逻辑

# 冗余双写库表创建 + 基础模块开发
分库分表策略 分库分表
- 8 个库,每个库 128 个表,总量就是 1024 个表
- 本地开发 2 库,每个库 2 个表
分⽚键:
- 分库 PartitionKey:account_no
- 分表 PartitionKey:group_id
接⼝访问量
- C 端解析,访问量⼤
- B 端查询,访问量少,单个表的存储数据可以多点
冗余双写库表设计 group_code_mapping (与 short_link 保持⼀样)
在 dcloud_link_0 和 dcloud_link_1 两个库创建下面两张表
CREATE TABLE `group_code_mapping_0` (
`id` bigint unsigned NOT NULL,
`group_id` bigint DEFAULT NULL COMMENT '组',
`title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链标题',
`original_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '原始url地址',
`domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链域名',
`code` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '短链压缩码',
`sign` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '⻓链的md5码,⽅便查找',
`expired` datetime DEFAULT NULL COMMENT '过期时间,⻓久就是-1',
`account_no` bigint DEFAULT NULL COMMENT '账号唯⼀编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`del` int unsigned NOT NULL COMMENT '0是默认,1是删除',
`state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态,lock是锁定不可⽤,active是可⽤',
`link_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接产品层级:FIRST 免费⻘铜、SECOND⻩⾦、THIRD钻⽯',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_bin;
CREATE TABLE `group_code_mapping_1` (
`id` bigint unsigned NOT NULL,
`group_id` bigint DEFAULT NULL COMMENT '组',
`title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链标题',
`original_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '原始url地址',
`domain` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链域名',
`code` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '短链压缩码',
`sign` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '⻓链的md5码,⽅便查找',
`expired` datetime DEFAULT NULL COMMENT '过期时间,⻓久就是-1',
`account_no` bigint DEFAULT NULL COMMENT '账号唯⼀编号',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`del` int unsigned NOT NULL COMMENT '0是默认,1是删除',
`state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '状态,lock是锁定不可⽤,active是可⽤',
`link_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '链接产品层级:FIRST 免费⻘铜、SECOND⻩⾦、THIRD钻⽯',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_bin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
短链域名表(前期不分库分表,默认 ds0)
只在 dcloud_link_0 创建
CREATE TABLE `domain` (
`id` bigint unsigned NOT NULL ,
`account_no` bigint DEFAULT NULL COMMENT '⽤户⾃⼰绑定的域名',
`domain_type` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '域名类型,⾃建custom, 官⽅offical',
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`del` int(1) unsigned zerofill DEFAULT '0' COMMENT'0是默认,1是禁⽤',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_bin;
2
3
4
5
6
7
8
9
10
11
# 逆向⼯程⽣成相关实体类
public class MyBatisPlusGenerator {
public static void main(String[] args) {
//1. 全局配置
GlobalConfig config = new GlobalConfig();
// 是否支持AR模式
config.setActiveRecord(true)
// 作者
.setAuthor("iekr")
// 生成路径,最好使用绝对路径,window路径是不一样的
//TODO TODO TODO TODO
.setOutputDir("D:\\xdclass\\demo\\src\\main\\java")
// 文件覆盖
.setFileOverride(true)
// 主键策略
.setIdType(IdType.AUTO)
.setDateType(DateType.ONLY_DATE)
// 设置生成的service接口的名字的首字母是否为I,默认Service是以I开头的
.setServiceName("%sService")
//实体类结尾名称
.setEntityName("%sDO")
//生成基本的resultMap
.setBaseResultMap(true)
//不使用AR模式
.setActiveRecord(false)
//生成基本的SQL片段
.setBaseColumnList(true);
//2. 数据源配置
DataSourceConfig dsConfig = new DataSourceConfig();
// 设置数据库类型
dsConfig.setDbType(DbType.MYSQL)
.setDriverName("com.mysql.cj.jdbc.Driver")
//TODO TODO TODO TODO
.setUrl("jdbc:mysql://192.168.130.24:3306/dcloud_link_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai")
.setUsername("root")
.setPassword("xdclass.net168");
//3. 策略配置globalConfiguration中
StrategyConfig stConfig = new StrategyConfig();
//全局大写命名
stConfig.setCapitalMode(true)
// 数据库表映射到实体的命名策略
.setNaming(NamingStrategy.underline_to_camel)
//使用lombok
.setEntityLombokModel(true)
//使用restcontroller注解
.setRestControllerStyle(true)
// 生成的表, 支持多表一起生成,以数组形式填写
//TODO TODO TODO TODO
.setInclude("domain","group_code_mapping_0");
//4. 包名策略配置
PackageConfig pkConfig = new PackageConfig();
pkConfig.setParent("net.xdclass")
.setMapper("mapper")
.setService("service")
.setController("controller")
.setEntity("model")
.setXml("mapper");
//5. 整合配置
AutoGenerator ag = new AutoGenerator();
ag.setGlobalConfig(config)
.setDataSource(dsConfig)
.setStrategy(stConfig)
.setPackageInfo(pkConfig);
//6. 执行操作
ag.execute();
System.out.println("======= 相关代码生成完毕 ========");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
表
- group_code_mapping
- domain
拷⻉ model/mapper/xml,需要去掉 group_code_mapping_0 后面的 _0 ,以及主键注解删掉
xml 与 mapper 里面也需要去掉_0,xml 中的 domain 为 sql 关键字需要用反引号包裹
# B 端查询短链 Manager 层开发
常⽤接⼝
- 新增
- 详情
- 删除
- 分⻚
- 更新状态
common 模块下 net.xdclass.enums.ShortLinkStateEnum
public enum ShortLinkStateEnum {
/**
* 锁定
*/
LOCK,
/**
* 可用
*/
ACTIVE;
}
2
3
4
5
6
7
8
9
10
11
common 模块下 net.xdclass.config.MybatisPlusPageConfig
@Configuration
public class MybatisPlusPageConfig {
/**
* 分页配置
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
GroupCodeMappingVO
@Data
@EqualsAndHashCode(callSuper = false)
public class GroupCodeMappingVO implements Serializable {
private Long id;
/**
* 组
*/
private Long groupId;
/**
* 短链标题
*/
private String title;
/**
* 原始url地址
*/
private String originalUrl;
/**
* 短链域名
*/
private String domain;
/**
* 短链压缩码
*/
private String code;
/**
* ⻓链的md5码,⽅便查找
*/
private String sign;
/**
* 过期时间,⻓久就是-1
*/
private Date expired;
/**
* 账号唯⼀编号
*/
private Long accountNo;
/**
* 创建时间
*/
private Date gmtCreate;
/**
* 修改时间
*/
private Date gmtModified;
/**
* 0是默认,1是删除
*/
private Integer del;
/**
* 状态,lock是锁定不可⽤,active是可⽤
*/
private String state;
/**
* 链接产品层级:FIRST 免费⻘铜、SECOND⻩⾦、THIRD钻⽯
*/
private String linkType;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
GroupCodeMappringManager
public interface GroupCodeMappringManager {
/**
* 查找详情
* @param mappingId
* @param accountNo
* @param groupId
* @return
*/
GroupCodeMappingDO findByGroupIdAndMappingId(Long mappingId, Long accountNo,Long groupId);
/**
* 新增
*
* @param groupCodeMappingDO
* @return
*/
int add(GroupCodeMappingDO groupCodeMappingDO);
/**
* 根据短链码删除
* @param groupCodeMappingDO
* @return
*/
int del(GroupCodeMappingDO groupCodeMappingDO);
/**
* 分页查找
* @param page
* @param size
* @param accountNo
* @param groupId
* @return
*/
Map<String, Object> pageShortLinkByGroupId(Integer page, Integer size, Long accountNo, Long groupId);
/**
* 更新短链码状态
* @param accountNo
* @param groupId
* @param shortLinkCode
* @param shortLinkStateEnum
* @return
*/
int updateGroupCodeMappingState(Long accountNo, Long groupId, String shortLinkCode, ShortLinkStateEnum shortLinkStateEnum);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
GroupCodeMappringManagerImpl
@Component
@Slf4j
public class GroupCodeMappringManagerImpl implements GroupCodeMappringManager {
@Autowired
private GroupCodeMappingMapper groupCodeMappingMapper;
@Override
public GroupCodeMappingDO findByGroupIdAndMappingId(Long mappingId, Long accountNo, Long groupId) {
GroupCodeMappingDO groupCodeMappingDO = groupCodeMappingMapper.selectOne(new QueryWrapper<GroupCodeMappingDO>()
.eq("mapping_id", mappingId)
.eq("account_no", accountNo)
.eq("group_id", groupId));
return groupCodeMappingDO;
}
@Override
public int add(GroupCodeMappingDO groupCodeMappingDO) {
return groupCodeMappingMapper.insert(groupCodeMappingDO);
}
@Override
public int del(GroupCodeMappingDO groupCodeMappingDO) {
int rows = groupCodeMappingMapper.update(null, new UpdateWrapper<GroupCodeMappingDO>()
.eq("id", groupCodeMappingDO.getId())
.eq("account_no", groupCodeMappingDO.getAccountNo())
.eq("group_id", groupCodeMappingDO.getGroupId())
.set("del", 1)
);
return rows;
}
@Override
public Map<String, Object> pageShortLinkByGroupId(Integer page, Integer size, Long accountNo, Long groupId) {
Page<GroupCodeMappingDO> pageInfo = new Page<>(page, size);
Page<GroupCodeMappingDO> groupCodeMappingDOPage = groupCodeMappingMapper.selectPage(pageInfo, new QueryWrapper<GroupCodeMappingDO>()
.eq("account_no", accountNo).eq("del",0)
);
Map<String, Object> pageMap = new HashMap<>(3);
pageMap.put("total_record", groupCodeMappingDOPage.getTotal());
pageMap.put("total_page", groupCodeMappingDOPage.getCurrent());
pageMap.put("current_data", groupCodeMappingDOPage.getRecords().stream()
.map(obj -> beanProcess(obj))
.collect(Collectors.toList()));
return pageMap;
}
@Override
public int updateGroupCodeMappingState(Long accountNo, Long groupId, String shortLinkCode, ShortLinkStateEnum shortLinkStateEnum) {
int rows = groupCodeMappingMapper.update(null, new UpdateWrapper<GroupCodeMappingDO>()
.eq("code", shortLinkCode)
.eq("account_no", accountNo)
.eq("group_id", groupId)
.eq("del", 0)
.set("state", shortLinkStateEnum.name())
);
return rows;
}
private GroupCodeMappingVO beanProcess(GroupCodeMappingDO groupCodeMappingDO) {
GroupCodeMappingVO groupCodeMappingVO = new GroupCodeMappingVO();
BeanUtils.copyProperties(groupCodeMappingDO, groupCodeMappingVO);
return groupCodeMappingVO;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# Domain 短链域名模块
预判能⼒,给⾃⼰留条后路
- 部分表有进⾏分库分表,部分没,但是不确保未来是否会有,预留字段
- 数据库设计的时候,参考同⾏竞品
- 很多情况下产品经理是会做⽐较多功能,⽐如⾃定义域名
- 但是迫于⼯期,就会缩减功能,但是未来⼀定是会加上的(只要是靠谱的功能)
net.xdclass.enums.DomainTypeEnum
public enum DomainTypeEnum {
/**
* 自建
*/
CUSTOM,
/**
* 官方
*/
OFFICIAL;
}
2
3
4
5
6
7
8
9
10
11
12
net.xdclass.vo.DomainVO
@Data
@EqualsAndHashCode(callSuper = false)
public class DomainVO implements Serializable {
private Long id;
/**
* ⽤户⾃⼰绑定的域名
*/
private Long accountNo;
/**
* 域名类型,⾃建custom, 官⽅offical
*/
private String domainType;
private String value;
/**
* 0是默认,1是禁⽤
*/
private Integer del;
private Date gmtCreate;
private Date gmtModified;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
DomainController
@RestController
@RequestMapping("/api/domain/v1")
public class DomainController {
@Autowired
private DomainService domainService;
/**
* 例举全部域名
* @return
*/
@GetMapping("list")
public JsonData listAll(){
List<DomainVO> list = domainService.listAll();
return JsonData.buildSuccess(list);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DomainManager
public interface DomainManager {
/**
* 根据id查找域名
* @param id
* @param accountNo
* @return
*/
DomainDO findById(Long id,Long accountNo);
/**
* 根据域名和id查找域名
* @param id
* @param domainTypeEnum
* @return
*/
DomainDO findByDomainTypeAndId(Long id, DomainTypeEnum domainTypeEnum);
/**
* 新增域名
* @param domainDO
* @return
*/
int addDomain(DomainDO domainDO);
/**
* 查询全部官方域名
* @return
*/
List<DomainDO> listOfficialDomain();
/**
* 查询全部自建域名
* @param accountNo
* @return
*/
List<DomainDO> listCustomDomain(Long accountNo);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
DomainManagerImpl
@Component
@Slf4j
public class DomainManagerImpl implements DomainManager {
@Autowired
private DomainMapper domainMapper;
@Override
public DomainDO findById(Long id, Long accountNo) {
return domainMapper.selectOne(new QueryWrapper<DomainDO>()
.eq("id", id)
.eq("account_no", accountNo)
);
}
@Override
public DomainDO findByDomainTypeAndId(Long id, DomainTypeEnum domainTypeEnum) {
return domainMapper.selectOne(new QueryWrapper<DomainDO>()
.eq("id", id)
.eq("domain_type", domainTypeEnum.name())
);
}
@Override
public int addDomain(DomainDO domainDO) {
return domainMapper.insert(domainDO);
}
@Override
public List<DomainDO> listOfficialDomain() {
return domainMapper.selectList(new QueryWrapper<DomainDO>()
.eq("domain_type", DomainTypeEnum.OFFICIAL.name()));
}
@Override
public List<DomainDO> listCustomDomain(Long accountNo) {
return domainMapper.selectList(new QueryWrapper<DomainDO>()
.eq("domain_type", DomainTypeEnum.CUSTOM.name())
.eq("account_no", accountNo));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
DomainService
public interface DomainService {
/**
* 列举全部可用域名
* @return
*/
List<DomainVO> listAll();
}
2
3
4
5
6
7
DomainServiceImpl
@Service
@Slf4j
public class DomainServiceImpl implements DomainService {
@Autowired
private DomainManager domainManager;
@Override
public List<DomainVO> listAll() {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
List<DomainDO> customDomainList = domainManager.listCustomDomain(accountNo);
List<DomainDO> officialDomainList = domainManager.listOfficialDomain();
customDomainList.addAll(officialDomainList);
return customDomainList.stream().map(obj -> beanProcess(obj)).collect(Collectors.toList());
}
private DomainVO beanProcess(DomainDO domainDO) {
DomainVO domainVO = new DomainVO();
BeanUtils.copyProperties(domainDO, domainVO);
return domainVO;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
设置默认数据库策略
default-data-source-name: ds0
default-key-generator:
column: id
type: SNOWFLAKE
props:
worker:
id: ${workId}
2
3
4
5
6
7
# 冗余双写 MQ 架构
通过 MQ 如何实现冗余双写?

# Kafka+RabbitMQ
Kafka 实现

RabbitMQ 实现

选择 RabbitMQ 理由
- 业务开发团队本身熟悉 RabbitMQ(对内,省了学习成本、运维成本、现有基础设施)
- RabbitMQ ⾃带延迟队列,更适合业务这块,⽐如定时任务、分布式事务处理
- Kafka ⽐较适合在⼤数据领域流式计算
# RabbitMQ 交换机
RabbitMQ 交换机类型
- ⽣产者将消息发送到 Exchange,交换器将消息路由到⼀个或者多个队列中,交换机有多个类型,队列和交换机是多对多的关系。
- 交换机只负责转发消息,不具备存储消息的能⼒,如果没有队列和 exchange 绑定,或者没有符合的路由规则,则消息会被丢失
- RabbitMQ 有四种交换机类型,分别是 Direct exchange、Fanout exchange、Topic exchange、Headers exchange,最后的基本不⽤

# Direct Exchange 定向
- 将⼀个队列绑定到交换机上,要求该消息与⼀个特定的路由键完全匹配
- 例⼦:如果⼀个队列绑定到该交换机上要求路由键 “aabb”,则只有被标记为 “aabb” 的消息才被转发,不会转发 aabb.cc,也不会转发 gg.aabb,只会转发 aabb
- 处理路由健
# Fanout Exchange ⼴播
- 只需要简单的将队列绑定到交换机上,⼀个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像⼦⽹⼴播,每台⼦⽹内的主机都获得了⼀份复制的消息
- Fanout 交换机转发消息是最快的,⽤于发布订阅,⼴播形式,中⽂是扇形
- 不处理路由健
# Topic Exchange 通配符
- 主题交换机是⼀种发布 / 订阅的模式,结合了直连交换机与扇形交换机的特点
- 将路由键和某模式进⾏匹配。此时队列需要绑定要⼀个模式上
- 符号 “#” 匹配⼀个或多个词,符号 “*” 匹配不多不少⼀个词
- 例⼦:因此 “abc.#” 能够匹配到 “abc.def.ghi”,但是 “abc.*” 只会匹配到 “abc.def”。
# 应用场景
- Fanout Exchange ⼴播(做幂等性)
- Topic Exchange 通配符 (推荐)
# 交换机和队列绑定配置
# Topic Exchange 通配符

⽤ topic 模式解决分布式事务 - 最终⼀致性
交换机和队列绑定时⽤的 binding 使⽤通配符的路由健
⽣产者发送消息时需要使⽤具体的路由健
BindingKey 是 Exchange 和 Queue 绑定的规则描述
RoutingKey,Exchange 就据这个 RoutingKey 和当前 Exchange 所有绑定的 BindingKey 做匹配,符合规则则发送过去
真实情况下参数名都是 RoutingKey,没有 BindingKey 这个参数,为了区别⽤户发送的和绑定的概念,才说 RoutingKey 和 BindingKey
⽬的:解决短链新增数据⼀致性问题
- 新增短链 -》发送 topic 消息 -》新增短链、新增映射两个消费者进⾏监听
RabbitMQConfig
@Configuration
@Data
public class RabbitMQConfig {
/**
* 交换机
*/
private String shortLinkEventExchange="short_link.event.exchange";
/**
* 创建交换机 Topic类型
* 一般一个微服务一个交换机
* @return
*/
@Bean
public Exchange shortLinkEventExchange(){
return new TopicExchange(shortLinkEventExchange,true,false);
}
//新增短链相关配置====================================
/**
* 新增短链 队列
*/
private String shortLinkAddLinkQueue="short_link.add.link.queue";
/**
* 新增短链映射 队列
*/
private String shortLinkAddMappingQueue="short_link.add.mapping.queue";
/**
* 新增短链具体的routingKey,【发送消息使用】
*/
private String shortLinkAddRoutingKey="short_link.add.link.mapping.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
*/
private String shortLinkAddLinkBindingKey="short_link.add.link.*.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
*/
private String shortLinkAddMappingBindingKey="short_link.add.*.mapping.routing.key";
/**
* 新增短链api队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkAddApiBinding(){
return new Binding(shortLinkAddLinkQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddLinkBindingKey,null);
}
/**
* 新增短链mapping队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkAddMappingBinding(){
return new Binding(shortLinkAddMappingQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddMappingBindingKey,null);
}
/**
* 新增短链api 普通队列,用于被监听
*/
@Bean
public Queue shortLinkAddLinkQueue(){
return new Queue(shortLinkAddLinkQueue,true,false,false);
}
/**
* 新增短链mapping 普通队列,用于被监听
*/
@Bean
public Queue shortLinkAddMappingQueue(){
return new Queue(shortLinkAddMappingQueue,true,false,false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 短链和 mapping 消费者配置
common 模块下 net.xdclass.model.EventMessage
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class EventMessage {
/**
* 消息队列id
*/
private String messageId;
/**
* 事件类型
*/
private String eventMessageType;
/**
* 业务id
*/
private String bizId;
/**
* 账号
*/
private Long accountNo;
/**
* 消息体
*/
private String content;
/**
* 异常备注
*/
private String remark;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
short_link 消费者配置
ShortLinkAddLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.add.link.queue")
public class ShortLinkAddLinkMQListener {
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddLinkMQListener:message:{}", message);
long tag = message.getMessageProperties().getDeliveryTag();
try {
// TODO 处理业务逻辑
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ShortLinkAddMappingLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.add.mapping.queue")
public class ShortLinkAddMappingLinkMQListener {
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingLinkMQListener:message:{}", message);
long tag = message.getMessageProperties().getDeliveryTag();
try {
// TODO 处理业务逻辑
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 链路测试和异常消息处理
修改配置
rabbitmq:
host: 192.168.130.24
port: 5672
# 需要⼿⼯创建虚拟主机
virtual-host: dev
username: admin
password: password
listener:
simple:
# 消息确认⽅式,manual(⼿动ack) 和auto(⾃动ack)
acknowledge-mode: manual
2
3
4
5
6
7
8
9
10
11
进入网页管理手动创建 http://192.168.130.24:15672/#/vhosts
⼤家遇到的问题 (不会⾃动创建队列)
- 加了 @bean 配置交换机和 queue,启动项⽬却没⾃动化创建队列
- RabbitMQ 懒加载模式, 需要配置消费者监听才会创建
@RabbitListener(queues ="short_link.add.link.queue")
另外种⽅式(若 Mq 中⽆相应名称的队列,会⾃动创建 Queue)
@RabbitListener(queuesToDeclare = {@Queue("short_link.add.link.queue") })
链路测试 - 多节点启动
# MQ 消费者异常处理⽅案
消费者异常情况处理
- 业务代码⾃⼰重试
- 组件重试
RabbitMQ 配置重试
listener:
simple:
# 消息确认⽅式,manual(⼿动ack) 和auto(⾃动ack)
acknowledge-mode: auto
retry:
enabled: true
max-attempts: 4
initial-interval: 5000
2
3
4
5
6
7
8
问题:多次重试失败怎么处理?

解决⽅式:RepublishMessageRecoverer
- 消息重试⼀定次数后,⽤特定的 routingKey 转发到指定的交换机中,⽅便后续排查和告警
- 消息消费确认使⽤⾃动确认⽅式
RabbitMQErrorConfig
@Configuration
@Slf4j
public class RabbitMQErrorConfig {
/**
* 异常交换机
*/
private String shortLinkErrorExchange = "short_link.error.exchange";
/**
* 异常队列
*/
private String shortLinkErrorQueue = "short_link.error.queue";
/**
* 异常routing.key
*/
private String shortLinkErrorRoutingKey = "short_link.error.routing.key";
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 异常交换机
*
* @return
*/
@Bean
public TopicExchange errorTopicExchange() {
return new TopicExchange(shortLinkErrorExchange, true, false);
}
/**
* 异常队列
*
* @return
*/
@Bean
public Queue errorQueue() {
return new Queue(shortLinkErrorQueue, true);
}
/**
* 队列与交换机进⾏绑定
*
* @return
*/
@Bean
public Binding bindingErrorQueueAndExchange() {
return BindingBuilder.bind(errorQueue()).to(errorTopicExchange()).with(shortLinkErrorRoutingKey);
}
/**
* 配置 RepublishMessageRecoverer
* ⽤途:消息重试⼀定次数后,⽤特定的routingKey转发到指定的交换机中,⽅便后续排查和告警
* 顶层是 MessageRecoverer接⼝,多个实现类
* @return
*/
@Bean
public MessageRecoverer messageRecoverer() {
return new RepublishMessageRecoverer(rabbitTemplate, shortLinkErrorExchange, shortLinkErrorRoutingKey);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
模拟异常重试,关闭手动 ack
ShortLinkAddLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.add.link.queue")
public class ShortLinkAddLinkMQListener {
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddLinkMQListener:message:{}", message);
long tag = message.getMessageProperties().getDeliveryTag();
try {
// TODO 处理业务逻辑
int i = 1/0;
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
//channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ShortLinkErrorLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.error.link.queue")
public class ShortLinkErrorLinkMQListener {
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.error("异常消息队列-ShortLinkErrorLinkMQListener:message:{}", message);
}
}
2
3
4
5
6
7
8
9
10
# 短链码⽣成端问题
短链码是哪⾥⽣成?⽣产者端 还是消费者端
⽣产者⽣成短链码,下⾯的情况
- ⽤户 A ⽣成短链码 AABBCC,查询数据库不存在,发送 MQ,插⼊数据库成功
- ⽤户 B ⽣成短链码 AABBCC,查询数据库不存在,发送 MQ,插⼊数据库失败
消费者⽣成短链码,下⾯的情况
- ⽤户 A ⽣成短链码 AABBCC ,C 端先插⼊,B 端还没插⼊
- ⽤户 B 也⽣成短链码 AABBCC ,B 端先插⼊,C 端还没插⼊
- ⽤户 A ⽣成短链码 AABBCC ,B 端插⼊
- ⽤户 B ⽣成短链码 AABBCC ,C 端插⼊

# C 端消费者开发
ShortLinkServiceImpl
/**
* 处理短链新增
* //判断短链域名是否合法
* //判断组名是否合法
* //⽣成⻓链摘要
* //⽣成短链码
* //加锁
* //查询短链码是否存在
* //构建短链对象
* //保存数据库
*
* @param eventMessage
* @return
*/
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkAddRequest shortLinkAddRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
//判断短链域名是否合法
DomainDO domainDO = checkDomain(shortLinkAddRequest.getDomainType(), shortLinkAddRequest.getDomainId(), accountNo);
//判断组名是否合法
LinkGroupDO linkGroupDO = checkLinkGroup(shortLinkAddRequest.getGroupId(), accountNo);
//⽣成⻓链摘要
String originalUrlDigest = CommonUtil.MD5(shortLinkAddRequest.getOriginalUrl());
//⽣成短链码
String shortLinkCode = shortLinkComponent.createShortLinkCode(shortLinkAddRequest.getOriginalUrl());
//加锁 todo
//查询短链码是否存在 todo
//构建短链对象
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.accountNo(accountNo)
.code(shortLinkCode)
.title(shortLinkAddRequest.getTitle())
.originalUrl(shortLinkAddRequest.getOriginalUrl())
.domain(domainDO.getValue())
.groupId(linkGroupDO.getId())
.expired(shortLinkAddRequest.getExpired())
.sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name())
.del(0)
.build();
//保存数据库
shortLinkManager.addShortLink(shortLinkDO);
return true;
}
/**
* 校验域名
*
* @param domainType
* @param domainId
* @param accountNo
* @return
*/
private DomainDO checkDomain(String domainType, Long domainId, Long accountNo) {
DomainDO domainDO;
if (DomainTypeEnum.CUSTOM.name().equalsIgnoreCase(domainType)) {
domainDO = domainManager.findById(domainId, accountNo);
} else {
domainDO = domainManager.findByDomainTypeAndId(domainId, DomainTypeEnum.OFFICIAL);
}
Assert.notNull(domainDO, "短链域名不合法");
return domainDO;
}
/**
* 检验组名
* @param groupId
* @param accountNo
* @return
*/
private LinkGroupDO checkLinkGroup(Long groupId, Long accountNo) {
LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
Assert.notNull(linkGroupDO,"组名不合法");
return linkGroupDO;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
ShortLinkService
/**
* 处理新增短链消息
* @param eventMessage
* @return
*/
boolean handlerAddShortLink(EventMessage eventMessage);
2
3
4
5
6
net.xdclass.enums.EventMessageType
public enum EventMessageType {
/**
* 短链创建
*/
SHORT_LINK_ADD,
/**
* 短链创建 C端
*/
SHORT_LINK_ADD_LINK,
/**
* 短链创建 B端
*/
SHORT_LINK_ADD_MAPPING;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ShortLinkAddMappingLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.add.mapping.queue")
public class ShortLinkAddMappingLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingLinkMQListener:message:{}", message);
long tag = message.getMessageProperties().getDeliveryTag();
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_MAPPING.name());
shortLinkService.handlerAddShortLink(eventMessage);
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
// channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ShortLinkAddLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.add.link.queue")
public class ShortLinkAddLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddLinkMQListener:message:{}", message);
long tag = message.getMessageProperties().getDeliveryTag();
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_LINK.name());
shortLinkService.handlerAddShortLink(eventMessage);
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
//channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# B 端消费者开发
ShortLinkServiceImpl
@Autowired
private GroupCodeMappringManager groupCodeMappringManager;
/**
* 处理短链新增
* //判断短链域名是否合法
* //判断组名是否合法
* //⽣成⻓链摘要
* //⽣成短链码
* //加锁
* //查询短链码是否存在
* //构建短链对象
* //保存数据库
*
* @param eventMessage
* @return
*/
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkAddRequest shortLinkAddRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
//判断短链域名是否合法
DomainDO domainDO = checkDomain(shortLinkAddRequest.getDomainType(), shortLinkAddRequest.getDomainId(), accountNo);
//判断组名是否合法
LinkGroupDO linkGroupDO = checkLinkGroup(shortLinkAddRequest.getGroupId(), accountNo);
//⽣成⻓链摘要
String originalUrlDigest = CommonUtil.MD5(shortLinkAddRequest.getOriginalUrl());
//⽣成短链码
String shortLinkCode = shortLinkComponent.createShortLinkCode(shortLinkAddRequest.getOriginalUrl());
//加锁(加锁再查,不然查询后,加锁前有线程刚好新增) todo
//查询短链码是否存在
ShortLinkDO shortLinkDOInDB = shortLinkManager.findByShortLinkCode(shortLinkCode);
if (shortLinkDOInDB == null) {
// 判断是B端还是C端生成的
if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {
//构建短链对象
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.accountNo(accountNo)
.code(shortLinkCode)
.title(shortLinkAddRequest.getTitle())
.originalUrl(shortLinkAddRequest.getOriginalUrl())
.domain(domainDO.getValue())
.groupId(linkGroupDO.getId())
.expired(shortLinkAddRequest.getExpired())
.sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name())
.del(0)
.build();
//保存数据库
shortLinkManager.addShortLink(shortLinkDO);
return true;
} else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
.accountNo(accountNo)
.code(shortLinkCode)
.title(shortLinkAddRequest.getTitle())
.originalUrl(shortLinkAddRequest.getOriginalUrl())
.domain(domainDO.getValue())
.groupId(linkGroupDO.getId())
.expired(shortLinkAddRequest.getExpired())
.sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name())
.del(0)
.build();
groupCodeMappringManager.add(groupCodeMappingDO);
return true;
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 同个 URL ⽣成短链码随机库表位问题
需求
- App 的下载链接,需要进⾏投放⼴告,并验证不同渠道的效果质量
- 渠道:抖⾳、百度、新浪微博、知乎、B 站、头条等
- 最终下载地址⼀样,但是需要区分不通渠道效果质量
问题
- MurmurHash 对同个 url 产⽣后的值是⼀样的,但是随机拼接了库表位,最终⽣成的短链码就导致可能不⼀致的情况,怎么解决?
解决
- MurmurHash 后的短链码,拼接随机库表位需要固定
- 采⽤ hashCode 取模⽣成对应的库表位
# ⽣成固定库表位编码
修改 ShardingDBConfig
/**
* 获取随机的前缀
* @return
*/
public static String getRandomDBPrefix(String code) {
int hashCode = code.hashCode();
int index = Math.abs(hashCode) % dbPrefixList.size();
return dbPrefixList.get(index);
}
2
3
4
5
6
7
8
9
10
修改 ShardingTableConfig
/**
* 获取随机的前缀
*
* @return
*/
public static String getRandomDBPrefix(String code) {
int hashCode = code.hashCode();
int index = Math.abs(hashCode) % tablePrefixList.size();
return tablePrefixList.get(index);
}
2
3
4
5
6
7
8
9
10
修改 ShortLinkComponent
/**
* 生成短链码
*
* @param param
* @return
*/
public String createShortLinkCode(String param) {
long murmurHash = CommonUtil.murmurHash32(param);
// 进制转换
String code = encodeToBase62(murmurHash);
String shortLinkCode = ShardingDBConfig.getRandomDBPrefix(code) + code + ShardingTableConfig.getRandomDBPrefix(code);
return shortLinkCode;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成重复短链问题
解决了随机库表问题后,⼀个 URL 怎么⽣成多个不⼀样的短链码
URL 重复⽣成短链问题
- 如果原始 URL 不做处理,则重复概率很⾼
- ⽅案:原始 url 拼接随机串,访问前去除
编码步骤
- ⽣产者发送消息携带⼀个时间戳 或 随机 id
- 原始 URL 开头拼接特殊字段
- 原⽣ https://xdclass.net
- 拼接后 1469558440337604610||https://xdclass.net
- 如果冲突,则编号递增 1
- 访问前截取去除
# ⽣成不唯⼀短链码
⼯具类添加方法
CommonUtil
/**
* URL增加前缀
*
* @param url
* @return
*/
public static String addUrlPrefix(String url) {
return IDUtil.geneSnowFlakeID() + "&" + url;
}
/**
* URL移除前缀
*
* @param url
* @return
*/
public static String removeUrlPrefix(String url) {
return url.substring(url.indexOf("&") + 1);
}
/**
* 如果短链码重复,则调⽤这个⽅法
* url前缀编号递增1,如果还是⽤雪花算法,则容易C和B端不⼀致,所以采⽤原先的id递增1
*
* @param url
* @return
*/
public static String addUrlPrefixVersion(String url) {
String prefix = url.substring(0, url.indexOf("&"));
String original = url.substring(url.indexOf("&") + 1);
Long version = Long.parseLong(prefix) + 1;
return version + "&" + original;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
修改创建短链时添加前缀
ShortLinkServiceImpl
@Override
public JsonData createShortLink(ShortLinkAddRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
// 添加前缀
String prefixUrl = CommonUtil.addUrlPrefix(request.getOriginalUrl());
request.setOriginalUrl(prefixUrl);
EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
.content(JsonUtil.obj2Json(request))
.messageId(IDUtil.geneSnowFlakeID().toString())
.eventMessageType(EventMessageType.SHORT_LINK_ADD.name())
.build();
rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(), rabbitMQConfig.getShortLinkAddRoutingKey(), eventMessage);
return JsonData.buildSuccess();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
修改解析短链时去除前缀
LinkApiController
/**
* 解析 301还是302,这边是返回http code是302
* <p>
* 知识点⼀,为什么要⽤ 301 跳转⽽不是 302 呐?
* <p>
* 301 是永久重定向,302 是临时重定向。
* <p>
* 短地址⼀经⽣成就不会变化,所以⽤ 301 是同时对服务器压
* ⼒也会有⼀定减少
* <p>
* 但是如果使⽤了 301,⽆法统计到短地址被点击的次数。
* <p>
* 所以选择302虽然会增加服务器压⼒,但是有很多数据可以获
* 取进⾏分析
*
* @param shortLinkCode
* @param request
* @param response
* @return
*/
@GetMapping(path = "/{shortLinkCode}")
public void dispatch(@PathVariable("shortLinkCode") String shortLinkCode, HttpServletRequest request, HttpServletResponse response) {
try {
log.info("短链码:{}", shortLinkCode);
// 判断短链是否合法
if (isLetterDigit(shortLinkCode)) {
// 查找短链
ShortLinkVO shortLinkVO = shortLinkService.parserShortLinkCode(shortLinkCode);
// 判断是否过期和可用
if (isVisible(shortLinkVO)) {
String originalUrl = CommonUtil.removeUrlPrefix(shortLinkVO.getOriginalUrl());
response.setHeader("Location", originalUrl);
// 302
response.setStatus(HttpStatus.FOUND.value());
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
}
}
} catch (Exception e) {
log.error("短链码:{}", shortLinkCode, e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Redis 分布式锁
# 短链码是⽣成端选择
# ⽣产者端与消费者端⽅案对⽐
⽅案⼀:⽣产者端⽣成短链码 code
- 加分布式锁 key=code,配置过期时间 (加锁失败则重新⽣成)
- 需要查询⼀次数据库或其他存储源,判断是否存在
- 发送 MQ
- C 端插⼊
- B 端插⼊
- 解分布式锁(锁过期⾃动解锁)

⽅案⼆:消费者端⽣成短链码 code
- ⽣产者发送消息
- C 端⽣成
- 加锁 key=code
- 查询数据库,如果存在,则 ver 版本递增,重新⽣成短链码
- 保存数据库
- 解分布式锁
- 加锁 key=code
- B 端⽣成
- 加锁 key=code
- 查询数据库,如果存在,则 ver 版本递增,重新⽣成短链码
- 保存数据库
- 解分布式锁
- 加锁 key=code
# 可重⼊锁
- 本地锁:synchronize、lock 等,锁在当前进程内,集群部署下依旧存在问题
- 分布式锁:redis、zookeeper 等实现,虽然还是锁,但是多个进程共⽤的锁标记,可以⽤ Redis、Zookeeper、Mysql 等都可以

设计分布式锁应该考虑的东⻄
- 排他性:在分布式应⽤集群中,同⼀个⽅法在同⼀时间只能被⼀台机器上的⼀个线程执⾏
- 容错性:分布式锁⼀定能得到释放,⽐如客户端奔溃或者⽹络中断
- 满⾜可重⼊、⾼性能、⾼可⽤
- 注意分布式锁的开销、锁粒度
单节点可重⼊锁
- 可重⼊锁: JDK 指的是以线程为单位,当⼀个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,⽽其他的线程是不可以的,synchronized 和 ReentrantLock 都是可重⼊锁
分布式下的可重⼊锁
- 进程单位,当⼀个线程获取对象锁之后,其他节点的同个业务线程可以再次获取本对象上的锁
# 基于 Redis 实现的分布式锁
实现分布式锁 可以⽤ Redis、Zookeeper、Mysql 数据库这⼏种,性能最好的是 Redis 且是最容易理解
基于 redis 实现分布式锁,⽂档:http://www.redis.cn/commands.html#string
key 是锁的唯⼀标识,⼀般按业务来决定命名,⽐如想要给⼀种商品的秒杀活动加锁,key 命名为 “seckill_商品 ID” 。value 就可以使⽤固定值,⽐如设置成 1。 短链码可以:short_link:code:xxxx
加锁 SETNX key value
setnx 的含义就是 SET if Not Exists,有两个参数 setnx (key, value),该⽅法是原⼦性操作如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0
解锁 del (key)
得到锁的线程执⾏完任务,需要释放锁,以便其他线程可以进⼊,调⽤ del (key)
配置锁超时 expire (key,30s)
客户端奔溃或者⽹络中断,资源将会永远被锁住,即死锁,因此需要给 key 配置过期时间,以保证即使没有被显式释放,这把锁也要在⼀定时间后⾃动释放
逻辑代码
methodA(){
String key = "short_link:code:abcdef"
if(setnx(key, 1) == 1){
expire(key,30,TimeUnit.MILLISECONDS)
try {
//做对应的业务逻辑
} finally {
del(key)
}
}else{
//睡眠100毫秒,然后⾃旋调⽤本⽅法
methodA()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
存在什么问题?
多个命令之间不是原⼦性操作,如 setnx 和 expire 之间,如果 setnx 成功,但是 expire 失败,且宕机了,则这个资源就是死锁
// 使⽤原⼦命令:设置和配置过期时间 setnx / setex如: set key 1 ex 30 nx
String key = "short_link:code:abcdef"
redisTemplate.opsForValue().setIfAbsent(key,1,30,TimeUnit.MILLISECONDS)
2
3
4
解决了命令之间原子性问题,还有其他更多问题
- 业务超时,如何避免其他线程勿删
- 业务执⾏时间过⻓,如何实现锁的⾃动续期
- ... 更多问题
之前说的⽅案⼆,消费者端⽣成短链码 code,那 C 端或者 B 端其中⼀个加锁成功后,另外⼀个怎么加锁?
通过 value 判断是否是同个账号,如果是则认为是加锁成功
C 端⽣成
- 加锁 key=code,value=account
- 查询数据库,如果存在,则 ver 版本递增,重新⽣成短链码
- 保存数据库
- 解分布式锁(锁过期⾃动解锁)
B 端⽣成
- 加锁 key=code,value=account
- 查询数据库,如果存在,则 ver 版本递增,重新⽣成短链码
- 保存数据库
- 解分布式锁(锁过期⾃动解锁)
流程:加锁的⽅式需要保证原⼦性
- 先判断是否有,如没这个 key,则设置 key-value,配置过期时间,加锁成功
- 如果有这个 key, 判断 value 是否是同个账号,是同个账号则返回加锁成功
- 如果不是同个账号则加锁失败
- 解决⽅式,配置 key 过期时间久,⽐如 2~5 天
# Lua 脚本分布式重⼊锁 + redis 原⽣
前⾯说了 redis 做分布式锁存在的问题核⼼是保证多个指令原⼦性,加锁使⽤ setnx setex 可以保证原⼦性,那解锁使⽤判断和设置等怎么保证原⼦性
多个命令的原⼦性:采⽤ lua 脚本 + redis, 由于【判断和删除】是 lua 脚本执⾏,所以要么全成功,要么全失败
流程
- 先判断是否有,如没这个 key,则设置 key-value,配置过期时间,加锁成功
- 如果有这个 key, 判断 value 是否是同个账号,是同个账号则返回加锁成功
- 如果不是同个账号则加锁失败
在 dcloud-link 项目加入 redis 配置
#-------redis连接配置-------
redis:
client-type: jedis
host: 192.168.130.24
port: 6379
password: xdclass.net
jedis:
pool:
max-active: 100
max-idle: 100
min-idle: 100
max-wait: 60000
2
3
4
5
6
7
8
9
10
11
12
代码
//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间单位为秒
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]);redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
" elseif redis.call('get',KEYS[1])== ARGV[1] then return 2;" +
" else return 0; end;";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);
2
3
4
5
添加到业务中
ShortLinkServiceImpl#handlerAddShortLink
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
/**
* 处理短链新增
* //判断短链域名是否合法
* //判断组名是否合法
* //⽣成⻓链摘要
* //⽣成短链码
* //加锁
* //查询短链码是否存在
* //构建短链对象
* //保存数据库
*
* @param eventMessage
* @return
*/
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkAddRequest shortLinkAddRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
//判断短链域名是否合法
DomainDO domainDO = checkDomain(shortLinkAddRequest.getDomainType(), shortLinkAddRequest.getDomainId(), accountNo);
//判断组名是否合法
LinkGroupDO linkGroupDO = checkLinkGroup(shortLinkAddRequest.getGroupId(), accountNo);
//短链码重复标记
boolean duplicateCodeFlag = false;
//⽣成⻓链摘要
String originalUrlDigest = CommonUtil.MD5(shortLinkAddRequest.getOriginalUrl());
//⽣成短链码
String shortLinkCode = shortLinkComponent.createShortLinkCode(shortLinkAddRequest.getOriginalUrl());
//加锁(加锁再查,不然查询后,加锁前有线程刚好新增)
//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间单位为秒
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]);redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
" elseif redis.call('get',KEYS[1])== ARGV[1] then return 2;" +
" else return 0; end;";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);
// 判断加锁成功
if (result > 0) {
// 判断是B端还是C端生成的
if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {
//查询短链码是否存在
ShortLinkDO shortLinkDOInDB = shortLinkManager.findByShortLinkCode(shortLinkCode);
if (shortLinkDOInDB == null) {
//构建短链对象
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.accountNo(accountNo)
.code(shortLinkCode)
.title(shortLinkAddRequest.getTitle())
.originalUrl(shortLinkAddRequest.getOriginalUrl())
.domain(domainDO.getValue())
.groupId(linkGroupDO.getId())
.expired(shortLinkAddRequest.getExpired())
.sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name())
.del(0)
.build();
//保存数据库
shortLinkManager.addShortLink(shortLinkDO);
return true;
} else {
log.error("C端短链码重复:{}", eventMessage);
duplicateCodeFlag = true;
}
} else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {
//查询短链码是否存在
GroupCodeMappingDO groupCodeMappingDOInDb = groupCodeMappringManager.findByCodeAndGroupId(shortLinkCode, linkGroupDO.getId(), accountNo);
if (groupCodeMappingDOInDb == null) {
// b端处理
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
.accountNo(accountNo)
.code(shortLinkCode)
.title(shortLinkAddRequest.getTitle())
.originalUrl(shortLinkAddRequest.getOriginalUrl())
.domain(domainDO.getValue())
.groupId(linkGroupDO.getId())
.expired(shortLinkAddRequest.getExpired())
.sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name())
.del(0)
.build();
groupCodeMappringManager.add(groupCodeMappingDO);
return true;
} else {
log.error("B端短链码重复:{}", eventMessage);
duplicateCodeFlag = true;
}
}
} else {
// 加锁失败,自旋100毫秒,再调用;失败的可能是短链码已经被占用,需要重新生成短链码
log.error("加锁失败:{}", eventMessage);
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
duplicateCodeFlag = true;
}
// 短链码重复 重新生成
if(duplicateCodeFlag){
String newOriginalUrl = CommonUtil.addUrlPrefixVersion(shortLinkAddRequest.getOriginalUrl());
shortLinkAddRequest.setOriginalUrl(newOriginalUrl);
eventMessage.setContent(JsonUtil.obj2Json(shortLinkAddRequest));
log.warn("短链码保存失败,重新生成:{}",eventMessage);
handlerAddShortLink(eventMessage);
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
GroupCodeMappringManager
/**
* 查找是否存在
* @param shortLinkCode
* @param id
* @param accountNo
* @return
*/
GroupCodeMappingDO findByCodeAndGroupId(String shortLinkCode, Long groupId, Long accountNo);
2
3
4
5
6
7
8
GroupCodeMappringManagerImpl
@Override
public GroupCodeMappingDO findByCodeAndGroupId(String shortLinkCode, Long groupId, Long accountNo) {
GroupCodeMappingDO groupCodeMappingDO = groupCodeMappingMapper.selectOne(new QueryWrapper<GroupCodeMappingDO>()
.eq("code", shortLinkCode)
.eq("account_no", accountNo)
.eq("group_id", groupId)
.eq("del", 0));
return groupCodeMappingDO;
}
2
3
4
5
6
7
8
9
10
11
# B 端分库分表
# GroupCodeMapping 表分库分表
分库分表策略
分库分表
- 8 个库,每个库 128 个表,总量就是 1024 个表
- 本地开发 2 库,每个库 2 个表
分⽚键:
- 分库 PartitionKey:account_no
- 分表 PartitionKey:group_id
接⼝访问量
- C 端解析,访问量⼤
- B 端查询,访问量少,单个表的存储数据可以多点
# 组+短链码mapping表,策略:分库+分表
group_code_mapping:
database-strategy:
inline:
algorithm-expression: ds$->{account_no % 2}
sharding-column: account_no
actual-data-nodes: ds$->{0..1}.group_code_mapping_$->{0..1}
table-strategy:
inline:
sharding-column: group_id
algorithm-expression: group_code_mapping_$->{group_id % 2}
2
3
4
5
6
7
8
9
10
11
# B 端接⼝ - 分⻚查找短链
ShortLinkPageRequest
@Data
public class ShortLinkPageRequest {
/**
* 组
*/
private Long groupId;
/**
* 页码
*/
private int page;
/**
* 每页大小
*/
private int size;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ShortLinkController
/**
* 分页查找短链
* @param request
* @return
*/
@RequestMapping("page")
public JsonData pageByGroupId(@RequestBody ShortLinkPageRequest request) {
Map<String,Object> result = shortLinkService.pageByGroupId(request);
return JsonData.buildSuccess(result);
}
2
3
4
5
6
7
8
9
10
ShortLinkService
/**
* 分页查询短链
* @param request
* @return
*/
Map<String, Object> pageByGroupId(ShortLinkPageRequest request);
2
3
4
5
6
ShortLinkServiceImpl
/**
* 从B端查找,group_code_mapping表
*
* @param request
* @return
*/
@Override
public Map<String, Object> pageByGroupId(ShortLinkPageRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
Map<String, Object> result = groupCodeMappringManager.pageShortLinkByGroupId(request.getPage(), request.getSize(), accountNo, request.getGroupId());
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 删除短链

EventMessageType 添加删除和更新常量
public enum EventMessageType {
/**
* 短链创建
*/
SHORT_LINK_ADD,
/**
* 短链创建 C端
*/
SHORT_LINK_ADD_LINK,
/**
* 短链创建 B端
*/
SHORT_LINK_ADD_MAPPING,
/**
* 短链删除
*/
SHORT_LINK_DEL,
/**
* 短链删除 C端
*/
SHORT_LINK_DEL_LINK,
/**
* 短链删除 B端
*/
SHORT_LINK_DEL_MAPPING,
/**
* 短链更新
*/
SHORT_LINK_UPDATE,
/**
* 短链更新 C端
*/
SHORT_LINK_UPDATE_LINK,
/**
* 短链更新 B端
*/
SHORT_LINK_UPDATE_MAPPING;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
RabbitMQConfig 添加删除交换机
//删除短链相关配置====================================
/**
* 删除短链 队列
*/
private String shortLinkDelLinkQueue="short_link.del.link.queue";
/**
* 删除短链映射 队列
*/
private String shortLinkDelMappingQueue="short_link.del.mapping.queue";
/**
* 删除短链具体的routingKey,【发送消息使用】
*/
private String shortLinkDelRoutingKey="short_link.del.link.mapping.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
*/
private String shortLinkDelLinkBindingKey="short_link.del.link.*.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
*/
private String shortLinkDelMappingBindingKey="short_link.del.*.mapping.routing.key";
/**
* 删除短链api队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkDelApiBinding(){
return new Binding(shortLinkDelLinkQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkDelLinkBindingKey,null);
}
/**
* 删除短链mapping队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkDelMappingBinding(){
return new Binding(shortLinkDelMappingQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkDelMappingBindingKey,null);
}
/**
* 删除短链api 普通队列,用于被监听
*/
@Bean
public Queue shortLinkDelLinkQueue(){
return new Queue(shortLinkDelLinkQueue,true,false,false);
}
/**
* 删除短链mapping 普通队列,用于被监听
*/
@Bean
public Queue shortLinkDelMappingQueue(){
return new Queue(shortLinkDelMappingQueue,true,false,false);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
ShortLinkDelRequest
@Data
public class ShortLinkDelRequest {
/**
* 组
*/
private Long groupId;
/**
* 映射id
*/
private Long mappingId;
/**
* 短链码
*/
private String code;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ShortLinkController
/**
* 删除短链
*
* @param request
* @return
*/
@PostMapping("del")
public JsonData del(@RequestBody ShortLinkDelRequest request) {
JsonData jsonData = shortLinkService.del(request);
return jsonData;
}
2
3
4
5
6
7
8
9
10
11
ShortLinkService
/**
* 删除短链
* @param request
* @return
*/
JsonData del(ShortLinkDelRequest request);
/**
* 处理删除短链消息
* @param eventMessage
* @return
*/
boolean handlerDelShortLink(EventMessage eventMessage);
2
3
4
5
6
7
8
9
10
11
12
13
ShortLinkServiceImpl
@Override
public JsonData del(ShortLinkDelRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
.content(JsonUtil.obj2Json(request))
.messageId(IDUtil.geneSnowFlakeID().toString())
.eventMessageType(EventMessageType.SHORT_LINK_DEL.name())
.build();
rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(), rabbitMQConfig.getShortLinkDelRoutingKey(), eventMessage);
return JsonData.buildSuccess();
}
@Override
public boolean handlerDelShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkDelRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkDelRequest.class);
if (EventMessageType.SHORT_LINK_DEL_LINK.name().equalsIgnoreCase(messageType)) {
ShortLinkDO shortLinkDO = ShortLinkDO.builder().code(request.getCode()).build();
int rows = shortLinkManager.del(shortLinkDO);
log.debug("删除c端短链:{}", rows);
return true;
} else if (EventMessageType.SHORT_LINK_DEL_MAPPING.name().equalsIgnoreCase(messageType)) {
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
.id(request.getMappingId()).accountNo(accountNo).groupId(request.getGroupId()).build();
int rows = groupCodeMappringManager.del(groupCodeMappingDO);
log.debug("删除b端短链:{}", rows);
return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
消费消息
ShortLinkDelMappingLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.del.mapping.queue")
public class ShortLinkDelMappingLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkDelMappingLinkMQListener:message:{}", message);
long tag = message.getMessageProperties().getDeliveryTag();
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_DEL_MAPPING.name());
shortLinkService.handlerDelShortLink(eventMessage);
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
// channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ShortLinkDelLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.del.link.queue")
public class ShortLinkDelLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkDelLinkMQListener:message:{}", message);
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_DEL_LINK.name());
shortLinkService.handlerDelShortLink(eventMessage);
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
//channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 更新短链

RabbitMQConfig
//更新短链相关配置====================================
/**
* 更新短链 队列
*/
private String shortLinkUpdateLinkQueue="short_link.update.link.queue";
/**
* 更新短链映射 队列
*/
private String shortLinkUpdateMappingQueue="short_link.update.mapping.queue";
/**
* 更新短链具体的routingKey,【发送消息使用】
*/
private String shortLinkUpdateRoutingKey="short_link.update.link.mapping.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
*/
private String shortLinkUpdateLinkBindingKey="short_link.update.link.*.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
*/
private String shortLinkUpdateMappingBindingKey="short_link.update.*.mapping.routing.key";
/**
* 更新短链api队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkUpdateApiBinding(){
return new Binding(shortLinkUpdateLinkQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkUpdateLinkBindingKey,null);
}
/**
* 更新短链mapping队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkUpdateMappingBinding(){
return new Binding(shortLinkUpdateMappingQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkUpdateMappingBindingKey,null);
}
/**
* 更新短链api 普通队列,用于被监听
*/
@Bean
public Queue shortLinkUpdateLinkQueue(){
return new Queue(shortLinkUpdateLinkQueue,true,false,false);
}
/**
* 更新短链mapping 普通队列,用于被监听
*/
@Bean
public Queue shortLinkUpdateMappingQueue(){
return new Queue(shortLinkUpdateMappingQueue,true,false,false);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
ShortLinkUpdateRequest
@Data
public class ShortLinkUpdateRequest {
/**
* 组
*/
private Long groupId;
/**
* 映射id
*/
private Long mappingId;
/**
* 短链码
*/
private String code;
/**
* 标题
*/
private String title;
/**
* 域名id
*/
private Long domainId;
/**
* 域名类型
*/
private String domainType;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ShortLinkService
/**
* 更新短链
* @param request
* @return
*/
JsonData update(ShortLinkUpdateRequest request);
/**
* 处理更新短链消息
* @param eventMessage
* @return
*/
boolean handlerUpdateShortLink(EventMessage eventMessage);
2
3
4
5
6
7
8
9
10
11
12
13
ShortLinkServiceImpl
@Override
public JsonData update(ShortLinkUpdateRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
.content(JsonUtil.obj2Json(request))
.messageId(IDUtil.geneSnowFlakeID().toString())
.eventMessageType(EventMessageType.SHORT_LINK_UPDATE.name())
.build();
rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(), rabbitMQConfig.getShortLinkUpdateRoutingKey(), eventMessage);
return JsonData.buildSuccess();
}
@Override
public boolean handlerUpdateShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkUpdateRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkUpdateRequest.class);
// 检验短链域名
DomainDO domainDO = checkDomain(request.getDomainType(), request.getDomainId(), accountNo);
// c端
if (EventMessageType.SHORT_LINK_UPDATE_LINK.name().equalsIgnoreCase(messageType)) {
ShortLinkDO shortLinkDO = ShortLinkDO.builder().code(request.getCode())
.title(request.getTitle()).domain(domainDO.getValue()).build();
int rows = shortLinkManager.update(shortLinkDO);
log.debug("短链C端更新成功:rows={}", rows);
return true;
} else if (EventMessageType.SHORT_LINK_UPDATE_MAPPING.name().equalsIgnoreCase(messageType)) {
// b端
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder().id(request.getMappingId())
.groupId(request.getGroupId()).accountNo(accountNo).title(request.getTitle())
.domain(domainDO.getValue()).build();
int rows = groupCodeMappringManager.update(groupCodeMappingDO);
log.debug("短链B端更新成功:rows={}", rows);
return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
ShortLinkController
/**
* 更新短链
*
* @param request
* @return
*/
@PostMapping("update")
public JsonData update(@RequestBody ShortLinkUpdateRequest request) {
JsonData jsonData = shortLinkService.update(request);
return jsonData;
}
2
3
4
5
6
7
8
9
10
11
消费消息
ShortLinkUpdateLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.update.link.queue")
public class ShortLinkUpdateLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkUpdateLinkMQListener:message:{}", message);
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_LINK.name());
shortLinkService.handlerUpdateShortLink(eventMessage);
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
//channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ShortLinkUpdateMappingLinkMQListener
@Component
@Slf4j
@RabbitListener(queues = "short_link.update.mapping.queue")
public class ShortLinkUpdateMappingLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkUpdateMappingLinkMQListener:message:{}", message);
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_MAPPING.name());
shortLinkService.handlerUpdateShortLink(eventMessage);
} catch (Exception e) {
// 处理业务异常 记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
// 确认消息消费成功
// channel.basicAck(tag, false);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GroupCodeMappringManager
/**
* 更新短链
* @param groupCodeMappingDO
* @return
*/
int update(GroupCodeMappingDO groupCodeMappingDO);
2
3
4
5
6
GroupCodeMappringManagerImpl
@Override
public int update(GroupCodeMappingDO groupCodeMappingDO) {
int rows = groupCodeMappingMapper.update(null, new UpdateWrapper<GroupCodeMappingDO>()
.eq("id", groupCodeMappingDO.getId())
.eq("account_no", groupCodeMappingDO.getAccountNo())
.eq("group_id", groupCodeMappingDO.getGroupId())
.eq("del", 0)
.set("title", groupCodeMappingDO.getTitle())
.set("domain", groupCodeMappingDO.getDomain()));
return rows;
}
2
3
4
5
6
7
8
9
10
11
12
ShortLinkManager
/**
* 更新短链
* @param shortLinkDO
* @return
*/
int update(ShortLinkDO shortLinkDO);
2
3
4
5
6
ShortLinkManagerImpl
@Override
public int update(ShortLinkDO shortLinkDO) {
int rows = shortLinkMapper.update(null, new UpdateWrapper<ShortLinkDO>().eq("code", shortLinkDO.getCode())
.eq("del", 0)
.set("title", shortLinkDO.getTitle())
.set("domain", shortLinkDO.getDomain())
);
return rows;
}
2
3
4
5
6
7
8
9
10