短链
# 环境配置
4c8g,CentOS_7,虚拟机
vim /etc/sysconfig/network-scripts/ifcfg-ens32
TYPE="Ethernet"
PROXY_METHOD="none"
BROWSER_ONLY="no"
BOOTPROTO="static"
DEFROUTE="yes"
IPV4_FAILURE_FATAL="no"
IPV6INIT="yes"
IPV6_AUTOCONF="yes"
IPV6_DEFROUTE="yes"
IPV6_FAILURE_FATAL="no"
IPV6_ADDR_GEN_MODE="stable-privacy"
NAME="ens32"
UUID="22d6a4b9-11a3-45b7-8373-f0a43eec3ffc"
DEVICE="ens32"
ONBOOT="yes"
IPADDR="192.168.80.136"
PREFIX="24"
NETMASK="255.255.255.0"
GATEWAY="192.168.83.2"
DNS1="192.168.83.2"
# Docker
https://docs.docker.com/engine/install/centos/ (opens new window)
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
sudo yum install -y yum-utils
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
sudo systemctl enable docker
# MySQL
docker run \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=shortLink \
-v /home/data/mysql/data:/var/lib/mysql:rw \
-v /etc/localtime:/etc/localtime:ro \
--name mysql \
--restart=always \
-d mysql:8.0
配置连接数
show variables like '%max_connections%';
set GLOBAL max_connections=5000;
set GLOBAL mysqlx_max_connections=5000;
# Redis
docker run -d --name redis -p 6379:6379 -v /mydata/redis/data:/data redis:6.2.4 --requirepass shortLink
# Nacos
所需 sql
/******************************************/
/*https://github.com/alibaba/nacos/blob/master/distribution/conf/nacos-mysql.sql */
/* 数据库全名 = nacos_config */
/* 表名称 = config_info */
/******************************************/
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
开启认证
docker run -d \
-e NACOS_AUTH_ENABLE=true \
-e MODE=standalone \
-e JVM_XMS=128m \
-e JVM_XMX=128m \
-e JVM_XMN=128m \
-p 8848:8848 \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=172.31.179.44 \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=shortLink \
-e MYSQL_SERVICE_DB_NAME=nacos_config \
-e MYSQL_SERVICE_DB_PARAM='characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false' \
--restart=always \
--privileged=true \
-v /home/data/nacos/logs:/home/nacos/logs \
--name nacos \
nacos/nacos-server:2.0.2
# RabbitMQ
docker run -d --name rabbitmq \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=shortLink \
-p 15672:15672 \
-p 5672:5672 \
--restart=always \
rabbitmq:3.8.15-management
⽹络安全组开放端⼝
- 4369 erlang 发现⼝
- 5672 client 端通信⼝
- 15672 管理界⾯ ui 端⼝
- 25672 server 间内部通信⼝
# 业务 sql
CREATE TABLE `account` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`account_no` bigint DEFAULT NULL,
`head_img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '头像',
`phone` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '手机号',
`pwd` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`secret` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '盐,用于个人敏感信息处理',
`mail` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '邮箱',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
`auth` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '认证级别,DEFAULT,REALNAME,ENTERPRISE,访问次数不一样',
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`) USING BTREE,
UNIQUE KEY `uk_account` (`account_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `traffic` (
`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_task` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`account_no` bigint DEFAULT NULL,
`traffic_id` bigint DEFAULT NULL,
`use_times` int DEFAULT NULL,
`lock_state` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '锁定状态锁定LOCK 完成FINISH-取消CANCEL',
`message_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin 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_msg_id` (`message_id`) USING BTREE,
KEY `idx_release` (`account_no`,`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
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;
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;
# 业务模块
# 发送短信
- (http)池化 + (线程)异步
- 图像验证码 + 防刷
# 分库分表算法:哈希取模
库ID = 短链码hash值 % 库数ᰁ 表ID = 短链码hash值 / 库数 % 表数
# 分布式 ID
- 雪花算法,指定
worker.id
- shardingjdbc-Snowflake ⾥⾯解决时间回拨问题,如果发生时钟回拨,就 sleep 到上次生成 id 的时间之后再次生成
# 短链生成算法
MurMurHash 得到 10 进制数字,转成 62 进制(数字 + 大小写字母)
10 进制:1813342104 62 进制:1YIB7i
- 短链大多是 6~8 位,每位又有 62 个可能,即 6^62 种可能,568 亿个短链
- 再拼接库表位,能表示更多数据
- 头拼接库,尾拼接表,即 8^62,根本用不完
# 自定义分库分表算法
生成短链时携带库表基因,查询时根据基因路由
public class CustomDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
//获取短链码第一位,即库位
String codePrefix = shardingValue.getValue().substring(0, 1);
for (String targetName : availableTargetNames) {
//数据源名称最后一位,ds
String targetNameSuffix = targetName.substring(targetName.length() - 1);
//返回对应数据源名称,ds0
if (codePrefix.equals(targetNameSuffix)) {
return targetName;
}
}
//抛异常
throw new BizException(BizCodeEnum.DB_ROUTE_NOT_FOUND);
}
}
public class CustomTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
//获取逻辑表
String targetName = availableTargetNames.iterator().next();
//短链码 A23Ad1
String value = shardingValue.getValue();
//获取短链码最后一位
String codeSuffix = value.substring(value.length()-1);
//拼接Actual table
return targetName+"_"+codeSuffix;
}
}
工具类,短链生成时添加基因
public class ShardingDBConfig {
/**
* 存储数据库位置编号
*/
private static final List<String> DB_PREFIX_LIST = new ArrayList<>();
//配置启用那些库的前缀
static {
DB_PREFIX_LIST.add("0");
DB_PREFIX_LIST.add("1");
DB_PREFIX_LIST.add("a");
}
/**
* 获取随机的前缀
* @return
*/
public static String getRandomDBPrefix(){
return DB_PREFIX_LIST.get(ThreadLocalRandom.current().nextInt(DB_PREFIX_LIST.size()));
}
}
public class ShardingTableConfig {
/**
* 存储数据表位置编号
*/
private static final List<String> TABLE_SUFFIX_LIST = new ArrayList<>();
//配置启用那些表的后缀
static {
TABLE_SUFFIX_LIST.add("0");
TABLE_SUFFIX_LIST.add("a");
}
/**
* 获取随机的后缀
* @return
*/
public static String getRandomTableSuffix(){
return TABLE_SUFFIX_LIST.get(ThreadLocalRandom.current().nextInt(TABLE_SUFFIX_LIST.size()));
}
}
# 数据库扩容
假如前期分三个库,⼀个库两个表,项⽬⽕爆,数据激增,进⾏扩容,增加了新的数据库表位,会导致旧的库表⽐新的库表数据多,且容易出现超载情况。如何解决 给数据源配置权重
- 数据扩容避免数据迁移
- 配置权重解决数据倾斜不均匀的问题
- 避免一次建很多库和表,节省资源
# 多维度查询
- 普通用户(匿名用户,不用注册,就只是访问短链)通过短链码可以路由到对应库表
- 平台用户(商家)如何查询对应的所有的短链,商家的短链分布在不同的库表,如何查询?
数据冗余,写 2 份
# 双写事务如何保证
- seata:强一致性,AT 模式全局锁,性能不高,不适合高并发业务
- mq:生产者发送消息到 mq,消费者订阅消息,消费幂等处理,broker 做高可用,失败重试,多次失败告警
# 短链生成在生产者还是消费者
生产者:
- ⽤户 A ⽣成短链码 AABBCC,查询数据库不存在,发送 MQ,插⼊数据库成功
- ⽤户 B ⽣成短链码 AABBCC,查询数据库不存在,发送 MQ,插⼊数据库失败
由于是双写,写入数据库是在消费者,就需要保证生产者生成短链和消费者插入数据库是原子的,不同的进程加锁不太好加
消费者:
- ⽤户 A ⽣成短链码 AABBCC ,C 端先插⼊,B 端还没插⼊
- ⽤户 B 也⽣成短链码 AABBCC ,B 端先插⼊,C 端还没插⼊
- ⽤户 A ⽣成短链码 AABBCC ,B 端插⼊失败,数据不一致
- ⽤户 B ⽣成短链码 AABBCC ,C 端插⼊失败,数据不一致
保证 2 个消费者的插入是加锁的,加的是同一个锁,即可重入分布式锁
# 消费者如何加锁
由于是 2 个消费者,如果要保证短链不重复,就需要操作时加锁,多个消费者如何使用同一个把锁,使用分布式可重入锁,即同一个 key,通过条件判断能否重入
- C 端
- 加锁,key:短链码,value:账户;设置过期时间
- 业务操作
- 查询短链是否存在
- 如果存在标识 +1
- 保存到数据库
- 解锁,不做删除,通过过期时间来实现解锁
- B 端
- 加锁,key:短链码,value:账户;设置过期时间
- 业务操作
- 查询短链是否存在
- 如果存在标识 +1
- 保存到数据库
- 解锁,不做删除,通过过期时间来实现解锁
加锁:
- 通过 code 判断是否有这个 key
- 没有,设置 value:account,设置过期时间
- 有,判断 value 是否和当前 account 一致
- 一致:加锁成功(重入)
- 不一致:加锁失败
- 加锁需要原子性
- 只有生成需要加锁,修改和删除不用加锁
解锁:
- 设置过期时间
- 不能直接删除,防止另外一端未处理完,又来一个重复的短链
- 设置的过期时间需要保证双端的逻辑可以处理完,防止消息积压,比如 2~3 天
# 消费者生成如何保证双写短链码一致
消费者生成短链码,B 端和 C 端是不同的消费者,如何保证生成的短链码一致? MurmurHash 对同个 url 产⽣后的值是⼀样的,但是随机拼接了库表位,最终⽣成的短链码就可能不⼀致的情况,如何解决?
- 改为固定库表位
- 对 url 的
hashCode()
进行取模得到对应的库表位
# 同一个 URL 如何生成多个短链码
商家广告投放,不同渠道需要生成不同的短链码,同一个 URL 如何生成多个短链码
- url 前面拼接时间戳或者唯一标识
- 用户访问短链的时候再移除前面的标识
- 如果冲突,就进行数字 +1
1469558440337604610&http://baidu.com
# 下单防重(幂等)
- 下单前获取唯一标识(token),下单时携带唯一标识。服务端删除 token 来判断是否重复订单
- 删除成功:不重复
- 删除失败:重复请求
- 根据 ip + method + account + userAgent 生成一个 key,设置过期时间,请求时判断 key 是否存在
- 存在:重复请求
- 不存在:非重复请求
# 下单流程
- 用户下单
- 订单服务
- 生成订单
- 保存数据库
- 发送延迟消息(关闭订单)
- 创建三方支付信息
- 返回客户端支付信息
- 客户端支付成功
- 三方回调后台接口,修改订单状态及后续操作
- 未收到三方回调(网络波动等),收到 MQ 延迟消息,查询订单信息
- 已支付,不做处理
- 未支付,查询三方支付结果
- 已支付,修改订单状态及后续操作
- 未支付,取消订单
# 二维码哪里生成
- 服务器生成
- 图片生成占用大量服务器资源,占用服务器带宽
- 返回的是二进制流,无法一并返回其他参数
- 客户端生成
- 降低服务器压力
- 可携带其他参数,比如订单号
- 需要考虑浏览器兼容问题
# 超时关闭订单
使用 RabbitMQ 的延迟队列 + 死信队列
- 延迟队列:声明队列时额外添加参数
Map<String,Object> args = new HashMap<>(3);
// 死信交换机
args.put("x-dead-letter-exchange",orderEventExchange);
// 死信队列
args.put("x-dead-letter-routing-key",orderCloseRoutingKey);
// 消息存活时间
args.put("x-message-ttl",ttl);
return new Queue(orderCloseDelayQueue,true,false,false,args);
- 死信队列:和声明普通队列一样。死信队列是相当于其他队列而言的,其他队列的消息被拒绝或者过期后重新投递到另外一个队列,这个队列即死信队列
# 支付回调如何处理
- 幂等处理,微信可能多次回调,使用回调过来的订单号作为 key,设置 3 天的过期时间,处理回调前先判断能不能将 key 设置到缓存中
sexnx
- 业务处理
- 更新订单状态
- 调用账户服务,发放流量包
- 如何调用账户服务
- rpc 远程调用,同步调用,处理完所有的业务逻辑才返回给微信返回收到回调,处理过慢,微信可能多次通知
- 分布式事务如何保证?
- rpc 远程调用,同步调用,处理完所有的业务逻辑才返回给微信返回收到回调,处理过慢,微信可能多次通知
- 使用 mq 双写
- 收到回调生成 mq 消息,投递到交换机就给微信返回返回收到回调
- 订单 mq 收到消息,更新订单状态
- 账户 mq 收到消息,发放流量包
- 分布式事务如何保证?
- 靠 mq 的 ack 机制,失败重试,失败次数达上限,路由到异常处理队列
# 微信支付 QPS 有限制,如何解决
https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/ico-guide/chapter1_5.shtml (opens new window)
统一下单 qps,单个商户限制 600
- 配置多个商户信息,使用负载均衡
- 使用账户取模,下单、查询、退款,路由到同一个商户上
- 做好频率和监控
# 免费流量包如何发放
新用户注册,会有一定数量的免费流量包,如何进行发放
- 直接远程调用商品服务,查询流量包详情,数据库新增流量包记录
- 同步调用,性能不高
- 扩展不强,增加业务时需要修改原编码
- 注册成功发送 mq 消息
- 分布式事务如何保证?
- 账户注册失败,mq 消费成功,没关系,只是多了一个流量包记录(无对应账户),只是多了一条无用的记录,不影响业务
- 账户注册成功,mq 消费失败,靠 mq 的 ack 来保证,失败重试路由等
- 接口性能高,易扩展,监听者模式,解耦
- 分布式事务如何保证?
# 流量包发放幂等
流量包的发放是靠 mq 的消费者来处理,可能出现消息多次投递,如何保证流量包不发放多次?
消息投递
- 使用 redis 进行了限制,当时不能保证一定不重复投递
消息消费
- 使用数据库的唯一索引兜底
- 数据库是根据账户来进行分表,即同一个账户的流量包会落在同一张表,只需保证分表(物理表)的唯一索引,而不是整个逻辑表的唯一索引
UNIQUE KEY `uk_trade_no`
(`out_trade_no`,`account_no`) USING BTREE;
- 使用订单号和账户做唯一索引
- 付费流量包有订单号,每个订单不同
- 免费流量包无订单号,索引尽量不要给 null 值,使用一个固定的值来填充订单号
# 过期流量包如何处理
分布式定时任务删除过期流量包,直接将过期时间
# 流量包每天使用次数如何维护
付费流量包:每天一定短链创建次数 免费流量包:每天少量短链创建次数
如何维护更新每天的流量包次数?有几千万的用户
- 直接使用 update sql 更新,数据量大时,直接卡着,后续的 sql 无法执行
- 几千万的用户都是活跃用户?全部更新,不活跃的也更新,浪费服务器资源
借鉴 redis 的 key 过期策略
定期 + 惰性删除。
- 随机抽取一批带有 ttl 的 key 查看是否过期
- 下次再次访问时再判断 key 是否过期
扣减流量包时更新维护流量包每天的额度
# 流量包如何扣减
- 查询用户所有可用流量包
- 遍历所有可用流量包,通过修改日期判断当天有没有更新过
- 没更新过
- 批量更新待重置流量包数据
- 更新过
- 当天额度是否用完
- 没更新过
- 扣减流量包
- 设置当天剩余额度到缓存,供短链服务来预扣减,过期时间就是当天晚上12点
# 创建短链流程
- 流量包额度预扣减,是否有足够的流量包额度(账户服务维护的缓存,redis)
- 发送 mq 消息
- 监听消息,生成短链,加锁
- 查询短链是否重复
- 远程调用账户服务,扣减流量包(db)
- 保存到数据库(db)
- 短链重复,时间戳+1,自旋
- 不手动处理解锁,ttl,自动解锁
缓存是账户服务维护,短链服务器使用同一个 key 来进行预扣减,远程调用账户服务器真正扣款时会更新缓存的值。 缓存没有值,说明当天的额度还没更新,用户有免费的额度,可用直接给前端返回创建短链成功;或者用户购买了新的流量包,又有了新的额度也是可用直接返回成功。
# 短链创建和流量包扣减事务如何保证
短链服务和流量包服务是 2 个不同的服务,如何保证事务
使用本地消息表,最终一致性
- 流量包服务扣减库存后,保存一个 task 任务,记录扣减的流量包(同一个事务)
- 发送延迟队列来检查任务
- 消费者监听到消息,rpc 远程调用,查询短链是否创建成功
- 成功:删除 task 任务
- 失败:回滚流量包,删除 task 任务
- 还需要有定时任务来兜底,防止 mq 投递/消费失败
- 保证最终一致性