Skip to content

Latest commit

 

History

111 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

短链接管理平台

image-20240305133043087

image-20240305133026604

image-20240305133110605

项目介绍

短链接管理平台采用 SpringCloudAlibaba + RocketMQ + ShardingSphere + Redis + MySQL + Sentinel 等技术栈,旨在实现高并发访问和海量数据存储能力。核心能力:

  • 短链生成与跳转:布隆过滤器前置拦截无效短码,Redis KV 承接读路径,ShardingSphere 分片库作为真相源
  • 访问监控异步化:RocketMQ 削峰 + 消费幂等,统计存储与跳转主流程解耦
  • 并发与一致性:Redisson 读写锁保护热点短链,先更 DB 再删缓存
  • 实时监控:PV/UV/UIP 统计(HyperLogLog 去重)、24 小时分时统计

已部署至云端双服务器(Java 服务 + 中间件分离),无生产流量,压测口径见文末。

Q & A

  1. Q:项目是自己做的吗?

    A:自己逐模块实现并提交的(本仓库 110 次提交记录可查)。

  2. Q:项目是参考网上教程做的吗?

    A:设计上参考了马丁的 SaaS 短连接教程,但本人做了很多扩展——读写锁 + 延迟队列重构、DB 与布隆过滤器一致性修复、IO 密集型线程池调优、UV 统计的 HyperLogLog 键设计修复等,详见下方「开发项目遇到的问题」。

  3. Q:项目做了多久?

    A:如果你跟着教程视频做不思考的话我认为要10-12天左右,但是vvv(作者)本人做了将近一个月。

  4. Q:项目做了那些扩展?

    A:非常多!详细可以看 Q & A 下面的章节,几乎我分析的都是教程中没有的!

  5. Q:后序计划?

    A:本项目虽然叫做SaaS短链接,但是和SaaS相关的东西几乎没有,除了做了表的水平拆分,后序的话vvv 可能会去看一些SaaS系统的文章去优化一下项目,包括但不限数据隔离等。

项目业务简介

数据库表逻辑

t_user, t_group, t_link的关系:

t_user, t_group, t_link的关系:

  • t_user:存放用户基本信息,分片键为username
  • t_group:存放用户的分组信息,分片键为user_id,也就是同个user_id的分组放到一张表上
  • t_link:存放短链接信息,分片键为user_id,同个user_id的短链放到同一张表上

t_user, t_group, t_link的业务逻辑:

  • 一个用户有多个分组,一个分组下有多个短链接

  • gid为分组标识,保证同个用户下的gid不相同,gid全局不唯一

  • t_link:一个用户的不同分组下可以有相同的短链接,这些短链是相同的

  • 本质上来说t_link是和user_id挂钩的,分组只是用来管理短链接而已

Q:表t_user为什么不用user_id作为分片键呢?

A:username的使用场景比user_id多,比如注册时是没有user_id

Q:如何选择分片键?

A:我认为分片键的选择必须是后端可以控制的字段,并且是不能修改的字段,如果分片键能被修改的话,那么必然会出现数据迁移的问题。

Q:后端可以控制的字段?

A:这样我们就能掌控每张表的堆积程度,如果说分片键是用户控制的,那么如果一个字段被多个用户使用,那么那个字段哈希过后的表必然是很多数据的。

Q:为什么不能修改?

A:分片键是定位那张表的关键,如果分片键修改的话(如马哥的教程),那么业务逻辑就非常麻烦(扩展1 将一些表的user_id作为分片键),业务逻辑包括:

  1. 取出原来分片键表中的全部修改前的数据
  2. 填入新的分片键
  3. 插入新的分片键hash过后的表
  4. 删除原来分片键hash过后的表的原来分片键的数据
  5. 解决新分片键hash后和原来分片键hash到同一张表中发生hash碰撞进而产生数据库唯一索引冲突问题

这是人能搞的定的事情??

路由表 link_goto

背景:

一条短链接是可以被仍和人访问的,也就是说我们在后端能拿到用户的信息几乎只有完整的短链接url

为了能够判断被访问的短链接是否已经失效,已经对这条短链接做监控,我们就要引入路由表

Q:既然完整短链接是全局唯一的,为什么不直接在t_link表中找呢

A:考虑到海量存储问题,t_link表做了水平拆分处理,并且分片键是user_id

考虑到多种原因,我们把路由表t_link_goto也进行了水平拆分处理,并且分片键为full_short_url

并且给full_short_url添加了唯一索引,这样我们就可以通过完整短链接获取创建改短链接的用户信息了,以及其他信息

Q:另一大难点就是如何对这条短链接进行监控 pv uv uip ?

A:对于uv uip 我们的做法是,通过给首次访问该短链的用户的cookie上加入唯一值,并且利用redis的 HyperLogLog数据结构进行判断

我们的监控表并没有进行分片处理,因为表数量太多。

如果要进行分片处理的话,分片键应该是usre_id,逻辑是尽量让同一个用户占一个表

开发项目遇到的问题

优化创建短链接口

需求:

功能异步获取短链图标,如果获取超时或者获取失败则使用默认小黑子图标

实现:

使用@Async注解异步执行

遇到的问题:

  1. @Async不起作用方法是同步进行的,后面发现因为**@Async功能是通过Spring的AOP(面向切面编程)代理来实现的**,所以如果在本类调用本类的@Async方法其实就是this去调用,没有走代理,

解决的方式有3种 1. 自我注入(Self Injection) 2. 拆分服务 3. 通过ApplicationContext获取代理

最后使用了拆分服务的方法,防止循环依赖的问题。

  1. 发现异步方法更新数据库失败,后面发现创建的短链还没插入数据库,就执行了异步获取图标的方法,所以更新失败

uv 统计

需求:

给每一条短链都做独立的uv统计

问题:

所以人都可以调用短链接,我们能拿到的用户信息几乎没有,那么如何进行uv统计?

一开始的实现:

  1. 给用户的cookie中植入随机值
  2. 如果用户cookie中有值,并且该值在对应短链中的hyperloglog中存在,则认为是老用户uv不用+1
  3. 如果没有值,或者对应短链的hyperloglog中不存在,则认为是新用户,uv + 1,并且给该用户植入随机值

问题:

这样就会有一个很严重的问题,用户先调了短链1,cookie中被植入值了,然后调用短链2,cookie被植入了新值,然后再调用短链1,ok这时短链1会认为该用户是新用户,因为cookie中的值是短链2随机生成的,短链1中的hyperloglog是没有的,所以短链1 uv 又加了 1

解决:

插入hyperloglog时要插入uv + 该完整短链拼接的字符串。

Docker 部署导致时间出现错误问题

因为我是全用docker部署的,无论是Java应用还是中间件

然后我们有一个是24小时统计的功能,因为Docker里面是美国时间,所以对不上

还有一个是获取今日统计数据的接口,也是因为mysql的CURDATE()返回的是美国时间导致的错误

项目部署遇到的问题

背景

整个项目部署用到了两台服务器,总共耗时1天半

第一台是阿里云服务器用于部署java服务 有gateway网关组件,aggregation聚合服务

第二台是腾讯云服务器用于部署各种中间件有Mysql,Redis,RocketMQ,Sentinel、Nacos

nacos问题

  1. 不知道为什么nacos需要去打开9848端口,这个错浪费了30分钟
docker run --name nacos -e MODE=standalone -p 8848:8848 -p 9848:9848 -d nacos/nacos-server
  1. 阿里云需要在安全组开放端口才能用,而腾讯云是全部端口都是开放的,只有配置防火墙才会去拦截

前端 DockerFile 部署问题

思路:

  1. 将前端项目 build 成 dist
  2. 写个Nginx 的配置文件
  3. 写一个DockerFile
  4. 整个文件直接拉倒服务器上去构建镜像 docker build -t 镜像名 .
  5. 启动容器的docker run

思路很完美,但是部署这个前端花费了就一天

首先

Dockerfile

一开始不知道有这个CMD的启动命令,所以导致nginx一直跑不起来


解释一下这段Dockerfile

# 使用官方Nginx镜像的最新版本作为基础镜像
FROM nginx:latest

# 将项目根目录下dist文件夹下的所有文件复制到镜像中的 /usr/share/nginx/html/ 目录下
# 这一步通常用于部署静态网站或前端项目的构建结果
COPY dist/ /usr/share/nginx/html/

# 将当前目录下的default.conf文件复制到镜像中的/etc/nginx/conf.d/目录下
# 这个配置文件用于替换Nginx的默认配置,可以用来定义服务器行为、重定向规则等
COPY default.conf /etc/nginx/conf.d/default.conf

# 声明容器运行时监听的端口号为80
# 这是HTTP通信的默认端口,意味着Nginx将在此端口上接收外部HTTP请求
EXPOSE 80

# 容器启动时执行的命令
# 这里的命令用于启动Nginx服务器,并配置为非守护进程模式运行
# 非守护进程模式意味着Nginx将在前台运行,这样Docker可以监控Nginx的输出日志
CMD ["nginx", "-g", "daemon off;"]
  1. FROM nginx:latest: 这行指定了容器的基础镜像。这里使用的是Docker Hub上的官方Nginx镜像的最新版本。基础镜像是构建自己的镜像的起点,包含了运行应用所需的操作系统和预安装的软件。
  2. COPY dist/ /usr/share/nginx/html/: 这条指令将构建你的静态网站或前端项目后生成的所有文件(通常位于项目的dist/目录)复制到Nginx镜像的/usr/share/nginx/html/目录中。Nginx默认会从这个目录提供静态文件服务,使得你的网站或前端项目可以通过浏览器访问。
  3. COPY default.conf /etc/nginx/conf.d/default.conf: 这行将你自定义的Nginx配置文件(default.conf)复制到镜像中的Nginx配置目录(/etc/nginx/conf.d/)。这允许你覆盖Nginx的默认配置,例如配置反向代理、设置缓存策略等。
  4. EXPOSE 80: 通过这个指令,Docker会在容器运行时监听80端口。这意味着你的Nginx服务器将在这个端口上接受HTTP请求。这不会自动将端口映射到宿主机,但是可以在运行容器时使用-p标志来实现映射。
  5. CMD ["nginx", "-g", "daemon off;"]: 定义了容器启动时执行的默认命令。这里的命令nginx -g "daemon off;"启动Nginx服务器并使其在前台运行。这样做的目的是让Docker能够直接管理Nginx进程,而不是作为守护进程在后台运行,这样当Nginx进程停止时,Docker容器也会相应地停止。

Nginx.conf

这里遇到的问题就我没怎么学过Nginx,然后对里面的配置不是很熟悉,然后在location /api { 这一块中加了一个rewriteimage-20240305124150075

导致前端发给后端的请求路径中少了/api


upstream my_server{
  server 120.79.40.135:8000; # 后端server 地址
  keepalive 2000;
}

server {
    listen       80; #这里的端口号不要动,80端口指的是容器的端口,最后我们会将容器的端口映射到我们宿主服务器的端口,比如映射到8888端口
    server_name  120.79.40.135; # 修改为docker服务宿主机的ip/域名
    
    #charset koi8-r;
    access_log  /var/log/nginx/host.access.log  main;
    error_log  /var/log/nginx/error.log  error;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html =404;
    }
     #  这里就是和vue本地代理的意思一样,已api开头的路径都代理到本机的3000端口
    location /api {
        proxy_pass http://my_server/api;
        proxy_set_header Host $host:$server_port;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
    
}

这段配置是Nginx的配置文件的一部分,用于定义一个反向代理服务器。它包含两个主要部分:upstream定义和server定义。下面是对这些部分的详细说明:

Upstream 定义
nginxCopy codeupstream my_server{
  server 120.79.40.135:8000; # 后端server 地址
  keepalive 2000;
}
  • upstream my_server:这个块定义了一个名为my_server的上游服务器组,Nginx可以向这组服务器转发请求。my_server可以在proxy_pass指令中使用,以指定请求应该被转发到哪个服务器或服务器组。
  • server 120.79.40.135:8000;:这行指定了上游服务器的地址和端口。在这个例子中,后端服务运行在IP地址120.79.40.1358000端口上。
  • keepalive 2000;:这个指令设置了持久连接的数量,Nginx将会为向这个上游服务器的连接保持这么多的持久连接。这有助于减少建立连接的开销。
Server 定义
nginxCopy codeserver {
    listen       80; #这里的端口号不要动,80端口指的是容器的端口
    server_name  120.79.40.135; # 修改为docker服务宿主机的ip/域名
    
    access_log  /var/log/nginx/host.access.log  main;
    error_log  /var/log/nginx/error.log  error;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html =404;
    }

    location /api {
        proxy_pass http://my_server/api;
        proxy_set_header Host $host:$server_port;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}
  • listen 80;:这行指定Nginx监听80端口。这是HTTP的默认端口,用于接受客户端的请求。
  • server_name 120.79.40.135;:这里设置了服务器的名称或IP地址,可以用于基于名称的虚拟主机。
  • location /:这个块定义了根URL(/)的处理规则。它设置了静态文件的根目录为/usr/share/nginx/html,并指定了默认的首页文件。try_files指令用于尝试依次访问请求的URI、URI对应的目录、或者重定向到/index.html,如果都找不到,则返回404错误。
  • location /api:这个块特别处理所有以/api开头的请求。这些请求被转发到上面定义的my_server上游服务器组,实现了API请求的反向代理。proxy_set_header Host $host:$server_port;设置了转发请求的Host头部,保持它不变。
  • error_page 500 502 503 504 /50x.html;和随后的location = /50x.html:这些指令定义了当服务器遇到500系列的错误时,应该显示的错误页面。

总之,这个配置文件通过Nginx实现了一个静态文件服务器和API请求的反向代理,支持持久连接,并处理了错误页面。在Docker容器中运行时,外部请求先到达容器的80端口,然后由Nginx根据请求的路径将请求转发到相应的服务。

nacos 配置中心的小坑

因为我们是做了聚合服务,也就是用一个模块去引入其他两个模块,所以说我们必须要在聚合服务里面去引入nacos的配置中心maven,而不是在两个模块中各自引入配置中心的maven,否则不生效

还有一个小坑就是

nacos的配置要这样写

spring:
  cloud:
    nacos:
      server-addr: 175.178.77.29:8848

这样写才会在注册中心和配置中心都生效

其他知识

关于Saas化的相关问题

要多去看下SaaS架构:多租户系统架构设计的文章才行,目前还是一头雾水

我发现我对Saas的很多东西还是不是很了解啊

image-20240227172955863

数据隔离问题

通常来说多租户的做法有三种。

1.完全独立的设计。每个租户有自己完全独立的服务和数据。

2.独立的数据分区,共享的服务。多租户的服务是共享的,但数据是分开隔离的。

3.共享的服务,共享的数据分区。每个租户的数据和服务都是共享的

所以,一般来说,技术方案会使用折衷方案,也就是中间方案,服务是共享的,数据通过分区来隔离,而对于一些比较重要的租户(需要好的隔离性),则使用完全独立的方式。

数据隔离的三种方案:

image-20240227173829114

  1. 多端登录:因为我们做的是Saas系统,所以说

@Autowired @Resource @RequiredArgsConstructor 区别

  1. @Autowired springboot的注解,是先以类型进行注入,类型冲突的话会抛出异常,除非配合使用@Qualifier注解明确指定要装配的bean的名称。
  2. @Rescourcejavax的注解,是先以名字进行注入,名字冲突后会尝试使用类型进行注入,如果还是冲突会抛异常,但是jdk17以后@Resource换了个包,就很恶心
  3. @RequiredArgsConstructor注解,就是构造器注入,只能够支持类型注入

url,uri,域名,路径,协议,www.,.com的区别

例子:

https://www.example.com:443/path/to/resource?query=123#section
  • 协议https 表示安全的超文本传输协议。
  • www.www 通常作为一个子域名来指代托管网页内容的服务器。
  • 域名example.com 每个域名都是独一无二的,指向一个特定的IP地址。
  • .com.com 表明这是一个商业实体的网站。
  • 端口:443 指定了安全通信的默认端口,虽然这部分通常在使用标准端口时可以省略。
  • 路径/path/to/resource 指出了在服务器上资源的确切位置。
  • 查询字符串?query=123 提供了额外的参数来访问或请求资源。
  • 片段#section 指向页面内的一个特定部分。

https://www.example.com:443/path/to/resource?query=123#section就是一个URL

URL由协议,域名,路径组成

uri不用管,只需要记住所有的 URL 都是 URI,但不是所有的 URI 都是 URL。

About

短链接管理平台:SpringCloudAlibaba + RocketMQ + ShardingSphere + Redis + Sentinel(布隆前置 / Redis KV 读路径 / 分片真相源 / MQ 异步监控)

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages