Go实战:短链接服务
2026/8/19 12:54:30 网站建设 项目流程

Go实战:短链接服务

摘要: 本篇讲解Go短链接服务设计,实现base62和雪花算法两种短码生成,Redis缓存热点URL加速跳转,布隆过滤器防止缓存穿透,分库分表支撑海量数据,分享短码冲突导致跳转错误的踩坑经验,对比base62、雪花算法、自增ID三种短码方案。

开篇故事

去年我们给营销部门做了短链接服务,把长推广URL转成短码发短信。上线第一天发了50万条短信,陆续收到用户反馈说点开短链接跳转到了别人的页面。营销部门炸了,短链接跳错意味着用户看到别人的广告,投放费用全白花。

排查发现是短码冲突。我们用的base62算法把自增ID转成短码,生成短码时没有做唯一性校验。并发场景下两个不同的长URL拿到了同一个自增ID,转出来的短码一样,后写的覆盖了先写的,先写的用户跳转到了后写的页面。

这次我把短链接服务的设计写清楚,重点讲短码生成怎么做防冲突。

一、短码生成算法

短码是把长URL映射成6到7位字符。主流方案有两种,base62编码和雪花算法。base62用0-9、a-z、A-Z共62个字符表示数字,把自增ID编码成短字符串。雪花算法生成全局唯一ID,再编码成短码。

packageshorturlimport("errors""fmt""sync""time")// Base62Generator base62短码生成器// 把自增ID转成base62编码typeBase62Generatorstruct{mu sync.Mutex counterint64// 自增计数器}// 字符集: 0-9, a-z, A-Zconstcharset="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"// NewBase62Generator 创建生成器// start: 起始计数器值,多实例时不同实例用不同起始值funcNewBase62Generator(startint64)*Base62Generator{return&Base62Generator{counter:start}}// Generate 生成短码// 把自增ID转成base62字符串func(g*Base62Generator)Generate()(string,error){g.mu.Lock()deferg.mu.Unlock()g.counter++num:=g.counterifnum<=0{return"",errors.New("计数器溢出")}// 数字转base62字符串returnencodeBase62(num),nil}// encodeBase62 数字转base62编码funcencodeBase62(numint64)string{ifnum==0{return"0"}result:=make([]byte,0,8)base:=int64(len(charset))fornum>0{// 取余数作为字符索引result=append(result,charset[num%base])num/=base}// 反转字符串fori,j:=0,len(result)-1;i<j;i,j=i+1,j-1{result[i],result[j]=result[j],result[i]}returnstring(result)}// SnowflakeGenerator 雪花算法生成器// 生成全局唯一ID,不依赖自增计数器typeSnowflakeGeneratorstruct{mu sync.Mutex workerIDint64// 工作节点IDsequenceint64// 序列号lastStampint64// 上次时间戳}const(workerIDBits=10// 工作节点ID位数sequenceBits=12// 序列号位数maxWorkerID=-1^(-1<<workerIDBits)maxSequence=-1^(-1<<sequenceBits)// 时间戳左移22位(workerID+sequence位数)timeShift=workerIDBits+sequenceBits workerShift=sequenceBits)// NewSnowflakeGenerator 创建雪花生成器// workerID: 节点ID,多实例必须不同funcNewSnowflakeGenerator(workerIDint64)(*SnowflakeGenerator,error){ifworkerID<0||workerID>maxWorkerID{returnnil,fmt.Errorf("workerID超出范围: %d",workerID)}return&SnowflakeGenerator{workerID:workerID,lastStamp:time.Now().UnixMilli(),},nil}// Generate 生成雪花ID再转base62短码func(s*SnowflakeGenerator)Generate()(string,error){s.mu.Lock()defers.mu.Unlock()now:=time.Now().UnixMilli()ifnow==s.lastStamp{// 同一毫秒内,序列号递增s.sequence=(s.sequence+1)&maxSequenceifs.sequence==0{// 序列号用完,等到下一毫秒fornow<=s.lastStamp{now=time.Now().UnixMilli()}}}else{s.sequence=0}s.lastStamp=now// 拼接: 时间戳 + workerID + 序列号id:=(now<<timeShift)|(s.workerID<<workerShift)|s.sequencereturnencodeBase62(id),nil}

base62简单直接,6位编码能表示620亿个短码(62的6次方)。雪花算法不依赖自增计数器,多实例各自生成不冲突,但短码位数更多(因为ID值大)。

二、Redis缓存与布隆过滤器

短链接服务是读多写少,每次跳转都要查长URL。全走数据库,数据库扛不住。用Redis缓存热点URL,大部分请求直接命中缓存。但缓存有个问题,不存在的短码也会穿透到数据库,恶意请求能打挂数据库。布隆过滤器挡在缓存前面,先判断短码是否存在。

packageshorturlimport("context""errors""github.com/redis/go-redis/v9")// ShortURLService 短链接服务typeShortURLServicestruct{client*redis.Client bloom*BloomFilter// 布隆过滤器gen*SnowflakeGenerator}// NewShortURLService 创建服务funcNewShortURLService(client*redis.Client,gen*SnowflakeGenerator)*ShortURLService{return&ShortURLService{client:client,// 布隆过滤器: 容量1亿,误判率0.01%bloom:NewBloomFilter(100000000,0.0001),gen:gen,}}// Create 创建短链接func(s*ShortURLService)Create(ctx context.Context,longURLstring)(string,error){// 生成短码code,err:=s.gen.Generate()iferr!=nil{return"",err}// 写入Redis缓存key:="shorturl:"+codeiferr:=s.client.Set(ctx,key,longURL,0).Err();err!=nil{return"",err}// 写入布隆过滤器s.bloom.Add(code)// 异步写入数据库(省略)returncode,nil}// Resolve 解析短链接,返回长URLfunc(s*ShortURLService)Resolve(ctx context.Context,codestring)(string,error){// 1. 布隆过滤器先判断短码是否存在if!s.bloom.Exists(code){// 一定不存在,直接返回,不查缓存和DBreturn"",errors.New("短链接不存在")}// 2. 查Redis缓存key:="shorturl:"+code longURL,err:=s.client.Get(ctx,key).Result()iferr==nil{returnlongURL,nil// 缓存命中}iferr!=redis.Nil{return"",err// Redis异常}// 3. 缓存未命中,查数据库(省略DAO调用)// 4. 数据库查到后回写缓存// 5. 数据库也没有,返回不存在return"",errors.New("短链接不存在")}// BloomFilter 布隆过滤器// 用多个hash函数判断元素是否可能存在typeBloomFilterstruct{bits[]uint64// 位数组sizeuint// 位数组大小hashNumuint// hash函数个数}// NewBloomFilter 创建布隆过滤器// n: 预期元素数量, p: 误判率funcNewBloomFilter(nint,pfloat64)*BloomFilter{// 计算位数组大小和hash函数个数// 公式: m = -n*ln(p) / (ln2)^2, k = m/n * ln2size:=uint(float64(n)*1.44/0.693)// 简化计算hashNum:=uint(7)// 经验值wordCount:=(size+63)/64return&BloomFilter{bits:make([]uint64,wordCount),size:size,hashNum:hashNum,}}// Add 添加元素到布隆过滤器func(b*BloomFilter)Add(keystring){fori:=uint(0);i<b.hashNum;i++{// 用不同种子计算多个hash值pos:=b.hash(key,i)wordIdx:=pos/64bitIdx:=pos%64b.bits[wordIdx]|=1<<bitIdx}}// Exists 判断元素是否存在// 返回true: 可能存在(有误判)// 返回false: 一定不存在func(b*BloomFilter)Exists(keystring)bool{fori:=uint(0);i<b.hashNum;i++{pos:=b.hash(key,i)wordIdx:=pos/64bitIdx:=pos%64ifb.bits[wordIdx]&(1<<bitIdx)==0{returnfalse// 任意一位为0,一定不存在}}returntrue// 所有位都为1,可能存在}// hash 简单hash函数,用不同种子区分func(b*BloomFilter)hash(keystring,seeduint)uint{varhuint=0for_,c:=rangekey{h=h*131+uint(c)+seed}returnh%b.size}

布隆过滤器的特点是判断不存在就一定不存在,判断存在有误判率。短链接场景正好需要这个特性,不存在的短码直接挡掉,存在的再查缓存和数据库。

三、踩坑经验:短码冲突导致跳转错误

开篇提到的短码冲突问题,根因是base62生成器用自增ID,多实例部署时各自维护自己的计数器,两个实例的计数器可能同时到同一个值。生成的短码一样,后写的覆盖先写的。

修复方案有三个要点。第一,短码生成后做唯一性校验,冲突了重新生成。第二,多实例用雪花算法替代自增ID,天生不冲突。第三,数据库层面加唯一索引兜底。

packageshorturlimport("context""errors""github.com/redis/go-redis/v9")// SafeShortURLService 带冲突检测的短链接服务typeSafeShortURLServicestruct{client*redis.Client gen*SnowflakeGenerator// 用雪花算法避免冲突}// NewSafeShortURLService 创建安全短链接服务funcNewSafeShortURLService(client*redis.Client,gen*SnowflakeGenerator)*SafeShortURLService{return&SafeShortURLService{client:client,gen:gen}}// CreateWithCheck 创建短链接,带冲突检测// 生成短码后检查是否已存在,冲突则重试func(s*SafeShortURLService)CreateWithCheck(ctx context.Context,longURLstring,)(string,error){maxRetry:=3fori:=0;i<maxRetry;i++{// 生成短码code,err:=s.gen.Generate()iferr!=nil{continue}key:="shorturl:"+code// SETNX: 只在key不存在时设置// 返回true说明短码可用,false说明已被占用ok,err:=s.client.SetNX(ctx,key,longURL,0).Result()iferr!=nil{return"",err}ifok{// 设置成功,短码唯一returncode,nil}// 短码冲突,重试生成}return"",errors.New("短码生成冲突,重试次数用尽")}

SETNX天然适合做冲突检测。短码存在就设置失败,不存在才成功。雪花算法加SETNX双重保证,冲突概率降到几乎为零。再配合数据库唯一索引,三层防线确保短码不冲突。

四、对比分析

短码方案冲突风险性能多实例短码长度
base62自增极高不支持6位
雪花算法极低支持8位
MD5哈希支持6位(截断)
UUID支持20位+

base62自增最短最快,但多实例会冲突。雪花算法多实例不冲突,短码稍长但可接受。MD5哈希固定长度但截断后有冲突风险。UUID完全不冲突但太长不适合做短链接。综合看雪花算法加SETNX校验是最佳方案。

总结

短链接服务的核心是短码生成和缓存加速。base62自增简单但多实例冲突,雪花算法天生不冲突是首选。布隆过滤器挡在不存在的短码前面,防止缓存穿透打挂数据库。短码冲突必须三层防护: 雪花算法避免生成冲突,SETNX检查运行时冲突,唯一索引兜底数据库冲突。分库分表支撑海量数据,按短码首字母分表均衡分布。下一篇我们聊分布式任务调度平台。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询