第十八章 短链服务-分库分表多维度查询解决方案《钻石玩法》
第1集 短链服务-短链URL跳转302跳转接口开发实战
简介: 短链URL 跳转302跳转接口开发实战
- 需求
- 接收一个短链码
- 解析获取原始地址
- 302进行跳转
- 编码实战
@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 linkCode
* @return
*/
@GetMapping(path = "/{shortLinkCode}")
public void dispatch(@PathVariable(name = "shortLinkCode") String shortLinkCode,
HttpServletRequest request, HttpServletResponse response) {
try {
log.info("短链码:{}", shortLinkCode);
//判断短链码是否合规
if (isLetterDigit(shortLinkCode)) {
//查找短链
ShortLinkVO shortLinkVO = shortLinkService.parseShortLinkCode(shortLinkCode);
//判断是否过期和可用
if (isVisitable(shortLinkVO)) {
response.setHeader("Location", shortLinkVO.getOriginalUrl());
//302跳转
response.setStatus(HttpStatus.FOUND.value());
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
}
} catch (Exception e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
/**
* 判断短链是否可用
*
* @param shortLinkVO
* @return
*/
private static boolean isVisitable(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)) {
if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
return true;
}
}
return false;
}
/**
* 仅包括数字和字母
*
* @param str
* @return
*/
private static boolean isLetterDigit(String str) {
String regex = "^[a-z0-9A-Z]+$";
return str.matches(regex);
}
}
第2集 遇到的短链服务多维度查询问题【业界通用难点】
简介: 老王的问题-短链服务多维度查询问题
-
一切都是那么顺利
- 创建短链、新增、分库分表、查询
-
但是问题来了,商家怎么看自己的全部短链呢?????
- 普通用户根据短链码可以路由到对应的库表
- 但是商家创建的短链码都是没规律,分布再不同的库表上,咋整???
不同维度查看数据,场景是不一样的,
-
主要是分:有PartitionKey,没PartitionKey两个场景
- 电商订单案例一:
- 订单表 的partionKey是user_id,用户查看自己的订 单列表方便
- 但商家查看自己店铺的订单列表就麻烦,分布在不同数据节点
- 电商订单案例一:
-
短链访问案例
- 普通用户访问短链,根据短链码code可以解析到对应的库表
- 但短链商家,查看自己全部的短链就麻烦了,分布再不同的库下面
第3集 分库分表多维度查询解决方案一之【字段解析配置】
简介: 分库分表多维度查询解决方案一之【字段解析配置】
-
分库分表后的查询问题
-
有PartitionKey,没PartitionKey两个场景
-
不同维度查询是不一样的,怎么解决?
-
解决方案
-
字段解析配置
-
NOSQL冗余
-
本身库表冗余双写方案
- 部分字段冗余
- 全量内容冗余
-
-
-
解决方式一(字段解析配置):
- 建一个表,存储account_no对应的库表位,商家生成的【短链码】固定前缀或者后缀
- 即【短链码】里面包括了商家的信息
第4集 分库分表多维度查询解决方案二之【NOSQL方案】
简介: 分库分表多维度查询解决方案二之【NOSQL方案】
-
分库分表后的查询问题
-
有PartitionKey,没PartitionKey两个场景
-
不同维度查询是不一样的,怎么解决?
-
解决方案
- 字段解析配置
- NOSQL冗余
- 本身库表冗余双写方案
- 部分字段冗余
- 全量内容冗余
-
-
解决方式二:
- 电商订单案例
- 订单表 的partionKey是user_id,用户查看自己的订单列表方便
- 但商家查看自己店铺的订单列表就麻烦,分布在不同数据节点
- 订单冗余存储在es上一份
- 业务架构流程
- 电商订单案例
- 短链平台案例
- 短链表的partionKey是短链码,用户访问短码方便解析
- 但商家查看自己某个分组下全部短链列表就麻烦,分布在不同数据节点
- 短链码冗余存储在es上一份
- 业务架构流程
第5集 分库分表多维度查询解决方案三之【冗余双写方案】
简介: 分库分表多维度查询解决方案三之【冗余双写方案】
-
分库分表后的查询问题
- 有PartitionKey,没PartitionKey两个场景
- 不同维度查询是不一样的,怎么解决?
-
解决方式三:
- 电商场景
- b2b平台,比如淘宝、京东,买家和卖家都要能够看到自己的订单列表
- 无论是按照买家id切分订单表,还是按照卖家id切分订单表都没法满足要求
- 拆分买家库和卖家库
- 买家库,按照用户的id来分库分表
- 卖家库,按照卖家的id来分库分表
- 数据冗余
- 下订单的时候写两份数据
- 在买家库和卖家库各写一份
- 电商场景
- 短链场景
问题:
-
冗余双写会代来什么问题?
-
是时间换空间还是空间换时间?
第6集 分库分表-冗余双写方案和分布式事务问题解决《上》
简介: 分库分表-冗余双写方案和分布式事务问题解决
- 冗余双写会代来什么问题?
- 存储空间更多(属于空间换时间,需要更多存储空间,减少库表数据量,提升性能)
- 冗余双写怎么实现问题
- 分布事务问题
冗余双写
- 解决方案一
- 直接RPC调用+Seata分布式事务框架
- 优点:强一致性,代码逻辑简单,业务侵入性小
- 缺点:性能下降,seata本身存在一定的性能损耗
- Seata支持AT、TCC、Saga 三种模式
- AT:隔离性好和低改造成本, 但性能低
- TCC:性能和隔离性,但改造成本大
- Saga:性能和低改造成本,但隔离性不好
- Seata支持AT、TCC、Saga 三种模式
第7集 分库分表-冗余双写方案和分布式事务问题解决《下》
简介: 分库分表-冗余双写方案和分布式事务问题解决
- 解决方案二
- 使用MQ, 生产者确认消息发送成功后,不同的消费者订阅消息消费
- 同时保证消息处理的幂等性
- 保证Broker的高可用
- 优点
- 实现简单,改造成本小
- 性能高,没有全局锁
- 缺点
- 弱一致性,需要强一致性的场景不适用
- 消费者消费失败,需要额外写接口回滚生产者业务逻辑
- 使用MQ, 生产者确认消息发送成功后,不同的消费者订阅消息消费
上图商家生成短链码保存冗余库的解决方案,先写队列,然后发送给不同的消费者消费,如图所示各消费各的消息并存库。
第十九章 短链服务-冗余双写库表创建+基础模块开发
第1集 短链服务-分库分表策略+冗余双写库表架构设计
简介: 短链服务-分库分表冗余双写库表架构设计
-
数据量预估
- 首年日活用户: 10万
- 首年日新增短链数据:10万*50 = 500万
- 年新增短链数:500万 * 365天 = 18.2亿
- 往高的算就是100亿,支撑3年
-
分库分表策略
- 分库分表
- 8个库, 每个库128个表,总量就是 1024个表
- 本地开发 2库,每个库2个表
- 分片键:
- 分库PartitionKey:account_no
- 分表PartitionKey:group_id
- 分库分表
-
接口访问量
- C端解析,访问量大
- B端查询,访问量少,单个表的存储数据可以多点
-
冗余双写库表设计 group_code_mapping (short_link一样)
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;
- 短链域名表(前期不分库分表,默认ds0)
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,COMMENT '用户自己绑定的域名'
`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集 短链服务-MybatisPlus逆向工程生成相关实体类
简介: 短链服务-分库分表冗余双写库表架构设计
- 表
- group_code_mapping
- domain
- 拷贝model/mapper/xml
第3集 短链服务-B端查询短链Manager层开发实战
简介: 短链服务-B端查询短链Manager层开发实战
-
常用接口
- 新增
- 详情
- 删除
- 分页
- 更新状态
-
其他接口用的时候再更新
第4集 短链服务-Domain短链域名模块开发
简介: 短链服务-Domain短链域名模块开发
-
预判能力,给自己留条后路
-
部分表有进行分库分表,部分没,但是不确保未来是否会有,预留字段
-
数据库设计的时候,参考同行竞品
- 很多情况下产品经理是会做比较多功能,比如自定义域名
- 但是迫于工期,就会缩减功能,但是未来一定是会加上的(只要是靠谱的功能)
-
-
开发Controller-Service-Manager层接口
@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);
}
}
- 其他接口用的时候再加
第5集 短链服务-sharding-jdbc默认数据源配置实战
简介: 短链服务-分库分表默认数据源配置实战
- 某些表并不需要进行分表分库,未配置分片规则的表将通过默认数据源定位
#----------配置默认数据库,比如短链域名,不分库分表--------------
spring.shardingsphere.sharding.default-data-source-name=ds0
#默认id生成策略
spring.shardingsphere.sharding.default-key-generator.column=id
spring.shardingsphere.sharding.default-key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.default-key-generator.props.worker.id=${workerId}
- domain模块单元测试
第二十章 短链服务-冗余双写MQ架构和开发实战
第1集 冗余双写MQ架构讲解-Kafka+RabbitMQ方案
简介: 冗余双写MQ架构实现讲解-Kafka+RabbitMQ方案
- 通过MQ如何实现冗余双写?
-
Kafka实现
-
选择RabbitMQ理由
- 业务开发团队本身熟悉RabbitMQ(对内,省了学习成本、运维成本、现有基础设施)
- RabbitMQ自带延迟队列,更适合业务这块,比如定时任务、分布式事务处理
- Kafka比较适合在大数据领域流式计算
第2集 冗余双写MQ架构实现-RabbitMQ交换机知识点回顾
简介: 冗余双写MQ架构实现 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”。
- Direct Exchange 定向
-
我们这个冗余双写应该采用哪种交换机?
- Fanout Exchange 广播(做幂等性)
- Topic Exchange 通配符 (推荐)
第3集 冗余双写MQ架构实现-交换机和队列绑定配置讲解
简介: 冗余双写MQ架构实现-RabbitMQ交换机和队列绑定配置讲解
- RabbitMQ交换机配置讲解
- Topic Exchange 通配符
- 注释说明
/**
* 用topic模式解决分布式事务-最终一致性
*
* 交换机和队列绑定时用的binding使用通配符的路由健
* 生产者发送消息时需要使用具体的路由健
*
* BindingKey是Exchange和Queue绑定的规则描述
* RoutingKey,Exchange就据这个RoutingKey和当前Exchange所有绑定的BindingKey做匹配,符合规则则发送过去
* 真实情况下参数名都是RoutingKey,没有BindingKey这个参数,
* 为了区别用户发送的和绑定的概念,才说RoutingKey和BindingKey
*
*
*
* 目的:解决短链新增数据一致性问题
* 新增短链-》发送topic消息-》新增短链、新增映射两个消费者进行监听
*/
第4集 冗余双写MQ架构RabbitMQ配置开发实战
简介: 冗余双写MQ架构RabbitMQ配置开发实战
- 配置实操
- 前期避免配置太多,不容易理解或者搞混,就先不抽取到配置文件里面
@Configuration
@Data
public class RabbitMQConfig {
/**
* 交换机
*/
private String shortLinkEventExchange="short_link.event.exchange";
/**
* 创建交换机 Topic类型
* 一般一个微服务一个交换机
* @return
*/
@Bean
public Exchange shortLinkEventExchange(){
return new TopicExchange(shortLinkEventExchange,true,false);
//return new FanoutExchange(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);
}
}
第5集 冗余双写MQ架构-短链和mapping消费者配置
简介: 冗余双写MQ架构-短链和mapping消费者配置
- 消息对象封装
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class EventMessage implements Serializable {
/**
* 消息队列id
*/
private String messageId;
/**
* 事件类型
*/
private String eventMessageType;
/**
* 业务id
*/
private String bizId;
/**
* 账号
*/
private Long accountNo;
/**
* 消息体
*/
private String content;
/**
* 异常备注
*/
private String remark;
}
- short_link消费者配置
@Component
@Slf4j
@RabbitListener(queues = "short_link.add.link.queue")
public class ShortLinkAddLinkMQListener {
/**
*
*
* @param eventMessage
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddLinkMQListener:message消息内容:{}", message);
long msgTag = message.getMessageProperties().getDeliveryTag();
try {
//TODO 处理业务
} catch (Exception e) {
// 处理业务失败,还要进行其他操作,比如记录失败原因
log.error("消费失败{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功{}", eventMessage);
//确认消息消费成功
channel.basicAck(msgTag, false);
}
}
- mapping消费者配置
@Component
@Slf4j
@RabbitListener(queues = "short_link.add.mapping.queue")
public class ShortLinkAddMappingMQListener {
/**
* @param eventMessage
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingMQListener:message消息内容:{}", message);
long msgTag = message.getMessageProperties().getDeliveryTag();
try {
//TODO 处理业务
} catch (Exception e) {
// 处理业务失败,还要进行其他操作,比如记录失败原因
log.error("消费失败{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功{}", eventMessage);
//确认消息消费成功
channel.basicAck(msgTag, false);
}
}
ortLinkAddMappingMQListener {
/**
* @param eventMessage
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingMQListener:message消息内容:{}", message);
long msgTag = message.getMessageProperties().getDeliveryTag();
try {
//TODO 处理业务
} catch (Exception e) {
// 处理业务失败,还要进行其他操作,比如记录失败原因
log.error("消费失败{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功{}", eventMessage);
//确认消息消费成功
channel.basicAck(msgTag, false);
}
}