短链接生成源码实战案例:3种方案对比,拒绝被外包坑

改个需求建站公司拖一周,这种痛苦谁懂?上周帮朋友看个企业站,加个短链跳转功能,对方报价三千,工期五天。朋友急了,自己翻GitHub,发现这事儿其实挺简单。今天不聊虚的,直接上实战案例,拆解三种主流短链接生成源码的技术选型。你是后端初学者,还是被外包忽悠过的受害者?往下看,教你用代码夺回控制权。

方案一:基于Redis的纯后端生成

很多小白觉得短链接难,其实核心就两点:唯一ID生成和映射存储。最轻量级的玩法,直接上Redis。

定位:适合中小流量、追求极致性能的场景。无需复杂数据库,启动快,内存读写速度微秒级。

核心差异: | 维度 | Redis纯后端 | MySQL关系型 | 云SaaS服务 | | :--- | :--- | :--- | :--- | | 读写性能 | 极高 (10w+ QPS) | 中等 (受索引影响) | 取决于服务商 | | 数据持久化 | 需配置RDB/AOF | 原生支持 | 黑盒 | | 开发复杂度 | 低 | 中 | 极低 | | 运维成本 | 中 (需监控内存) | 低 | 无 | | 扩展性 | 垂直扩展为主 | 垂直+水平 | 自动 |

代码实现: 这里给出一段Go语言的精简实现。注意,Redis的INCR是原子操作,保证ID不重复,这是关键。

package mainimport ("context""fmt""github.com/go-redis/redis/v8""time"
)type ShortLinkService struct {rdb *redis.Client
}func NewShortLinkService(addr string) *ShortLinkService {rdb := redis.NewClient(&redis.Options{Addr: addr,})return &ShortLinkService{rdb: rdb}
}// GenerateShortCode 生成短码
func (s *ShortLinkService) GenerateShortCode(ctx context.Context, longURL string) (string, error) {// 1. 自增生成唯一IDid, err := s.rdb.Incr(ctx, "shortlink:counter").Result()if err != nil {return "", err}// 2. 将ID转换为Base62字符串 (短链接常用编码,去除了易混淆字符)shortCode := base62Encode(id)// 3. 存储映射关系: key=短码, value=长链接// 设置过期时间,例如180天,避免内存无限膨胀err = s.rdb.Set(ctx, "shortlink:"+shortCode, longURL, 180*24*time.Hour).Err()if err != nil {return "", err}return shortCode, nil
}func base62Encode(n int64) string {const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"if n == 0 {return "0"}result := ""for n > 0 {result = string(chars[n%62]) + resultn /= 62}return result
}

适用场景:

  • 内部工具系统,流量可预测。
  • 对延迟敏感的活动页,如秒杀、投票。
  • 团队有运维能力,能维护Redis集群。

选型建议: 如果你懂点Linux,能装个Redis,选这个。别被“分布式ID生成器”这种词吓住,小规模业务,Redis INCR就是王道。但记住,务必配置RDB持久化,不然重启服务,映射关系全丢,用户点链接直接404,那才叫尴尬。

方案二:MySQL+ShardingSphere分库分表

当流量上来,或者老板要求“数据必须永久保存,不能丢”,Redis就不够了。这时候,传统关系型数据库登场。但单表存几亿条记录,查询慢如蜗牛,怎么办?分库分表。

定位:适合中大型业务,数据合规要求高,需要复杂查询(如按时间统计、按用户查询历史链接)的场景。

核心差异: MySQL方案的最大优势是ACID事务和灵活查询。你可以轻松写出“查询某用户最近7天生成的所有短链”这种SQL,而Redis很难做到。

代码实现: 这里展示MyBatis-Plus结合ShardingSphere-JDBC的配置片段。注意分片键的选择,通常用ID或User_ID。

// application.yml 配置示例
spring:shardingsphere:datasource:names: ds0,ds1ds0:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://127.0.0.1:3306/db0?useSSL=false&serverTimezone=UTCusername: rootpassword: rootds1:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://127.0.0.1:3306/db1?useSSL=false&serverTimezone=UTCusername: rootpassword: rootrules:sharding:tables:t_short_link:actual-data-nodes: ds$->{0..1}.t_short_link_$->{0..1}table-strategy:standard:sharding-column: idsharding-algorithm-name: mod_shardingkey-generate-strategy:column: idkey-generator-name: snowflakesharding-algorithms:mod_sharding:type: MODprops:sharding-count: 2key-generators:snowflake:type: SNOWFLAKEprops:worker-id: 1// Mapper 接口,无感知使用,框架自动路由
public interface ShortLinkMapper extends BaseMapper<ShortLink> {
}

适用场景:

  • 电商平台,需要追踪每个点击来源。
  • 金融类应用,数据审计要求严格。
  • 需要生成报表,分析链接热度、地域分布。

选型建议: 这是最“重”的方案。如果你没有专职DBA,慎用分库分表。配置错了,数据分布不均,性能反而下降。但对于追求稳定和数据完整性的企业,这是必经之路。记得在工信部ICP备案系统提交网站信息时,确保你的服务器IP与备案主体一致,尤其是当你使用多个数据库分片服务器时,备案信息的准确性直接影响网站能否正常访问。别等网站被屏蔽了,才想起备案信息填错了域名。

方案三:Go+Etcd分布式协调服务

还有一种折中方案,适合微服务架构。用Etcd管理ID分配,避免中心点瓶颈。

定位:适合云原生环境,K8s部署,多实例横向扩展的场景。

核心差异: Etcd提供强一致性,多个服务实例可以并发从Etcd获取ID段,本地缓存后再生成短码。相比直接查Redis,它减少了网络IO;相比MySQL,它响应更快。

代码实现: 使用go-etcd客户端,实现ID段分配。

package mainimport ("context""fmt"clientv3 "go.etcd.io/etcd/client/v3""time"
)type EtcdIDGenerator struct {client *clientv3.Clientkey    stringstep   int64
}func NewEtcdIDGenerator(endpoints []string) *EtcdIDGenerator {client, _ := clientv3.New(clientv3.Config{Endpoints:   endpoints,DialTimeout: 5 * time.Second,})return &EtcdIDGenerator{client: client,key:    "/shortlink/id/counter",step:   1000, // 每次分配1000个ID}
}// GetID 获取一个ID,内部维护本地缓存
func (g *EtcdIDGenerator) GetID(ctx context.Context) (int64, error) {// 简化逻辑:实际生产中需加锁或使用atomic包保证并发安全resp, err := g.client.Txn(ctx).If(clientv3.Version(g.key) == 0).Then(clientv3.OpPut(g.key, "1")).Commit()if err != nil {return 0, err}if resp.Succeeded {return 1, nil}// 读取当前值getResp, err := g.client.Get(ctx, g.key)if err != nil {return 0, err}currentID := parseInt(string(getResp.Kvs[0].Value))newID := currentID + 1// 更新Etcd_, err = g.client.Txn(ctx).If(clientv3.Compare(clientv3.Version(g.key), "=", 0)).Then(clientv3.OpPut(g.key, fmt.Sprintf("%d", newID))).Commit()return newID, err
}

适用场景:

  • 容器化部署,Pod随时扩缩容。
  • 对一致性要求高于性能,但不能忍受MySQL的延迟。
  • 已有Etcd集群,不想引入Redis。

选型建议: 这是架构师层面的选择。如果你刚起步,别碰这个。Etcd运维复杂,集群配置、网络分区处理都是坑。但对于大厂级应用,这是标配。

实操避坑与部署优化

技术选型只是第一步,上线才是硬仗。

  1. 301 vs 302 跳转:

    • 301永久重定向:SEO友好,搜索引擎会收录目标页,但缓存严重,修改长链接后,老链接可能还指向旧地址。
    • 302临时重定向:SEO不友好,但灵活,随时可改。
    • 建议:营销类、临时活动用302;官网、品牌链接用301。代码中通过Header控制:http.Redirect(w, r, longURL, http.StatusMovedPermanently) 或 http.StatusFound。
  2. 防刷与限流:

    • 短链接是DDoS攻击的常见入口。必须在网关层(Nginx/Kong)配置限流。
    • 示例Nginx配置:
    limit_req_zone $binary_remote_addr zone=shortlink:10m rate=10r/s;server {location /s/ {limit_req zone=shortlink burst=20 nodelay;proxy_pass http://backend;}
    }
    
  3. 安全性:

    • 短码不能包含敏感词,避免被CDN拦截。Base62编码天然规避了大部分特殊字符。
    • 对高频访问的短链,可考虑在CDN层做缓存,减少回源压力。

总结与互动

选方案,别只看代码帅不帅,要看你的业务体量和运维能力。

  • 个人项目/小站:Redis方案,简单粗暴,够用。
  • 中型企业/电商:MySQL分库分表,稳,数据全。
  • 云原生/大厂:Etcd/分布式ID,架构先进,但门槛高。

别再为加个短链功能付三千块外包费了。代码就在上面,复制、修改、部署,半小时搞定。

你踩过哪些建站的坑?是遇到外包改需求拖延,还是自己部署时被备案、SSL证书卡住?评论区交流,看看谁更惨,顺便分享下你的解决方案。