1. 短链系统概述
短链服务是现代互联网中不可或缺的基础设施之一。它通过将冗长的URL转换为简短易记的短链接,解决了社交媒体分享、短信发送等场景下URL过长的问题。一个典型的短链系统需要处理每天数百万甚至上亿次的请求,这对系统的设计和实现提出了很高的要求。
我在最近的一个个人项目中,从零开始构建了一个完整的短链系统。这个系列文章将详细记录整个开发过程,分享我在架构设计、技术选型和性能优化等方面的实践经验。Day01主要聚焦于系统的基础架构设计和核心功能实现。
2. 核心需求分析
2.1 功能需求
一个完整的短链系统需要满足以下核心功能:
- 长URL转短链:将任意长度的URL转换为固定长度的短码
- 短链跳转:用户访问短链时,自动重定向到原始长URL
- 访问统计:记录每个短链的访问次数、时间等数据
- 自定义短码:允许用户指定特定的短码(如果可用)
- 有效期管理:设置短链的过期时间
2.2 非功能需求
除了基本功能外,系统还需要考虑:
- 高可用性:99.9%以上的可用性保证
- 低延迟:跳转响应时间控制在100ms以内
- 高并发:支持每秒数万次的跳转请求
- 数据持久化:确保短链数据不丢失
- 安全性:防止恶意短链和滥用
3. 技术架构设计
3.1 整体架构
基于上述需求,我采用了分层架构设计:
客户端层 → 接入层 → 服务层 → 存储层- 客户端层:Web界面和API调用
- 接入层:Nginx反向代理和负载均衡
- 服务层:核心业务逻辑处理
- 存储层:MySQL + Redis组合
3.2 关键组件选型
3.2.1 短码生成算法
比较了几种常见的短码生成方案:
自增ID+Base62编码
- 优点:实现简单,无冲突
- 缺点:可预测,安全性较低
哈希算法(如MD5截取)
- 优点:分布均匀
- 缺点:可能存在哈希冲突
预生成随机码
- 优点:高性能
- 缺点:需要预生成和存储
最终选择了方案1,因为实现简单且能满足初期需求。Base62编码使用字符集:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
3.2.2 存储设计
MySQL表结构设计:
CREATE TABLE short_url ( id BIGINT PRIMARY KEY AUTO_INCREMENT, short_code VARCHAR(10) NOT NULL UNIQUE, original_url VARCHAR(2048) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NULL, user_id INT NULL, status TINYINT DEFAULT 1, INDEX idx_short_code (short_code) );Redis用于缓存热点数据,采用String类型存储:
key: s:{short_code} value: {original_url}3.2.3 跳转实现
HTTP 302重定向是最常用的跳转方式:
@app.route('/<short_code>') def redirect(short_code): original_url = cache.get(f's:{short_code}') if not original_url: original_url = db.query_url(short_code) cache.set(f's:{short_code}', original_url, ttl=3600) return redirect(original_url, code=302)4. 核心功能实现
4.1 短链生成服务
def generate_short_url(original_url, custom_code=None, user_id=None, ttl=None): if custom_code: if db.exists_short_code(custom_code): raise ValueError('Custom code already exists') short_code = custom_code else: id = db.get_next_id() short_code = base62_encode(id) expires_at = datetime.now() + timedelta(seconds=ttl) if ttl else None db.create_short_url( short_code=short_code, original_url=original_url, user_id=user_id, expires_at=expires_at ) cache.set(f's:{short_code}', original_url, ttl=min(3600, ttl) if ttl else 3600) return f'https://short.url/{short_code}'4.2 跳转服务优化
为提高性能,采用多级缓存策略:
- Nginx层缓存:对热点短链设置1分钟缓存
- Redis缓存:所有短链缓存1小时
- 本地缓存:服务进程内缓存高频访问的短链
@lru_cache(maxsize=10000) def get_original_url(short_code): original_url = cache.get(f's:{short_code}') if not original_url: original_url = db.query_url(short_code) if original_url: cache.set(f's:{short_code}', original_url, ttl=3600) return original_url5. 性能优化实践
5.1 存储优化
- 使用连接池管理数据库连接
- 对短码字段创建唯一索引
- 对大表进行分库分表设计
5.2 缓存策略
- 热点数据预加载
- 缓存穿透防护:
def get_url(short_code): # 布隆过滤器快速判断是否存在 if not bloom_filter.might_contain(short_code): return None # 缓存空值防止穿透 original_url = cache.get(f's:{short_code}') if original_url is None: if cache.add(f'lock:{short_code}', 1, timeout=10): original_url = db.query_url(short_code) cache.set(f's:{short_code}', original_url or '', ttl=300 if not original_url else 3600) cache.delete(f'lock:{short_code}') else: time.sleep(0.1) return get_url(short_code) return original_url if original_url else None6. 监控与统计
实现基础的访问统计功能:
CREATE TABLE access_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, short_code VARCHAR(10) NOT NULL, access_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, referrer VARCHAR(512), user_agent VARCHAR(512), client_ip VARCHAR(45), INDEX idx_short_code_time (short_code, access_time) );为避免高频写入影响性能,采用异步批处理方式:
@background_task def log_access(short_code, request): access_log = { 'short_code': short_code, 'referrer': request.headers.get('Referer'), 'user_agent': request.headers.get('User-Agent'), 'client_ip': request.remote_addr } redis.rpush('access_log_queue', json.dumps(access_log)) # 后台消费者 def consume_access_log(): while True: logs = redis.lpop('access_log_queue', count=100) if logs: db.bulk_insert_access_logs(logs) time.sleep(1)7. 安全防护措施
恶意URL检测:
- 检查黑名单域名
- 使用第三方安全API验证
- 限制单个用户创建频率
防爬虫:
- 限制单个IP访问频率
- 设置合理的Robots.txt
- 对异常流量进行人机验证
数据安全:
- 敏感操作记录审计日志
- 数据库定期备份
- 关键信息加密存储
8. 部署架构
采用容器化部署方案:
- Docker容器封装应用
- Kubernetes集群管理
- Nginx Ingress处理流量
- Prometheus + Grafana监控
- ELK日志收集
# Kubernetes Deployment示例 apiVersion: apps/v1 kind: Deployment metadata: name: short-url-service spec: replicas: 3 selector: matchLabels: app: short-url template: metadata: labels: app: short-url spec: containers: - name: app image: short-url:v1.0 ports: - containerPort: 8000 resources: limits: cpu: "1" memory: 512Mi9. 测试方案
9.1 单元测试
使用pytest编写核心逻辑测试:
def test_base62_encode(): assert base62_encode(0) == '0' assert base62_encode(61) == 'z' assert base62_encode(62) == '10' def test_short_url_creation(): url = 'https://example.com' short_url = generate_short_url(url) assert len(short_url.split('/')[-1]) == 69.2 性能测试
使用Locust模拟高并发场景:
from locust import HttpUser, task class ShortUrlUser(HttpUser): @task def redirect(self): self.client.get("/abc123") @task(3) def create(self): self.client.post("/api/shorten", json={ "url": "https://example.com/" + random_string(20) })测试指标:
- 创建接口QPS ≥ 1000
- 跳转接口QPS ≥ 5000
- 平均延迟 < 50ms
- P99延迟 < 200ms
10. 遇到的问题与解决方案
10.1 短码冲突问题
初期实现中,自定义短码和自动生成短码可能冲突。解决方案:
- 使用单独的命名空间
- 添加前缀区分(如'c-'表示自定义)
- 查询时优先检查自定义短码
10.2 缓存一致性问题
当原始URL更新时,缓存可能过期不及时。解决方案:
- 更新数据库时同步删除缓存
- 设置较短的缓存TTL
- 使用消息队列通知各节点失效缓存
10.3 数据库热点问题
自增ID导致写入集中在单个数据节点。解决方案:
- 使用分段ID生成
- 改用UUID或雪花算法
- 提前批量生成ID缓冲池
11. 项目演进方向
- 增加API限流和鉴权
- 支持批量创建短链
- 添加更丰富的统计报表
- 实现短链分组和标签管理
- 开发浏览器插件和移动端应用
- 支持域名自定义和HTTPS
- 增加团队协作功能
这个短链系统虽然基础,但涵盖了Web开发的许多核心概念和技术点。在Day01的实现中,我特别注重了系统的可扩展性和性能表现,为后续功能迭代打下了良好基础。