设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间12 现在时间12 缓存数据 还是老妈说的话最经典,她说:“你们这代人呐,就是活的太明白了,所以什么都得不到。我们当年什么都糊里糊涂,该结婚结婚,该工作工作,现在什么都有。”

还是老妈说的话最经典,她说:“你们这代人呐,就是活的太明白了,所以什么都得不到。我们当年什么都糊里糊涂,该结婚结婚,该工作工作,现在什么都有。” -- 好心分手

查看: 1134|回复: 2

详解如何解决golang定时器引发的id重复问题

[复制链接]

  离线 

TA的专栏

  • 打卡等级:即来则安
  • 打卡总天数:16
  • 打卡月天数:0
  • 打卡总奖励:241
  • 最近打卡:2025-03-07 18:42:00
等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
36
主题
28
精华
0
金钱
351
积分
80
注册时间
2023-8-13
最后登录
2025-5-31

发表于 2024-6-16 04:25:49 来自手机 | 显示全部楼层 |阅读模式
目录


  • 问题描述
  • 简述根因
  • 问题代码
  • 根因分析
  • 验证

问题描述

线上服务日志中突然出现很多主键冲突的异常,而这个主键是一个int64的id,这个id的生成依赖了秒级时间戳和机器码.那么下面先把问题代码贴出来,由于具体分析较长,这里先简述下根因,后面不感兴趣可以不看

简述根因

本质上是golang运行时的单调时钟和物理世界的墙上时钟不一致导致的。
当golang的单调时钟跑过1s后,会获取墙上时钟并塞到ticker.C中,上述两个操作并不同时,也不是同一个时间。
【单调时钟没有单位】

  • 在单调时钟1000的时候,隔了0,调用了time.Now(),得到了XX:XX:11:9999;
  • 在单调时钟2000的时候,隔了2,调用了time.Now(),得到了XX:XX:12:0001;
  • 在单调时钟3000的时候,隔了0,调用了time.Now(),得到了XX:XX:12:9999;
  • 在单调时钟4000的时候,隔了3,调用了time.Now(),得到了XX:XX:14:0002;
我们对比1,2,发现间隔超过了1s。对比2,3,发现间隔小于1s。

问题代码
  1. func init() {
  2.     // 先设置最初的时间,保证基本的正确性
  3.     now := time.Now()
  4.     updateUnixTimestamp(uint64(now.Unix()))

  5.     var err error
  6.     g, err = NewGenerator()
  7.     if err != nil {
  8.        panic(fmt.Sprintf("init default generator failed. err=%v", err))
  9.     }
  10.     g2, err = NewGenerator()
  11.     if err != nil {
  12.        panic(fmt.Sprintf("init second default generator failed. err=%v", err))
  13.     }
  14.     go func() {
  15.        // sleep到下一秒开始,再创建一个ticker,尽量从某一秒的开始
  16.        time.Sleep(time.Until(time.Now().Truncate(time.Second).Add(time.Second)))
  17.        tk := time.NewTicker(time.Second)
  18.        // ticker的更新会从下一秒开始,当前的这一秒还是需要立刻更新
  19.        now = time.Now()
  20.        updateUnixTimestamp(uint64(now.Unix()))
  21.        for {
  22.           // 拿到这个ticker chan返回的时间
  23.           now = <-tk.C
  24.           updateUnixTimestamp(uint64(now.Unix()))
  25.        }
  26.     }()
  27. }

  28. // 为所有的generator设置时间counter
  29. func updateUnixTimestamp(timestamp uint64) {
  30.     atomic.StoreUint64(&gUnixTimestamp, timestamp)
  31.     tsHigh := timestamp << 32
  32.     gGeneratorsMutex.Lock()
  33.     for i := range gGenerators {
  34.        atomic.StoreUint64(&gGenerators[i].timestampCounter, tsHigh)
  35.     }
  36.     gGeneratorsMutex.Unlock()
  37. }

  38. func (i *Generator) NextUint64() uint64 {
  39.     c := atomic.AddUint64(&i.timestampCounter, 1)
  40.     return (c & high32) | ((c & low16) << 16) | i.Config.workerID16
  41. }
复制代码
根因分析

从上面的方法中可以看出,这个id生成依赖了秒级时间戳和机器码,机器码我们已经排查了不会重复,那么最可能得原因就是时间戳重复导致,起初我们怀疑是ntp服务问题导致的时间回退,但是排查后发现ntp并没有问题,我们把怀疑的方向转向go的timer实现,下面我们来看go的timer实现
timer实现
目前线上的服务使用的是Go 1.20版本,我们看下go 1.20版本的go ticker如何触发运行的,这里不会展示完整的timer实现链,如果想了解timer整体实现可以参考 深入解析go Timer 和Ticker实现原理
NewTicker
我们先看下ticker初始化,重点关注sendTime(也就是后续的f)
可以看到ticker是触发sendTime时才去获得的最新时间,并尝试塞给了channel,如果channel满了则丢弃
startTimer这个实现不再展示(使用的是runtime包的startTimer),大致逻辑是把这个timer绑定到proccesser上,并放到这个processer的timer堆中相应的位置上
  1. func NewTicker(d Duration) *Ticker {
  2.     if d <= 0 {
  3.        panic(errors.New("non-positive interval for NewTicker"))
  4.     }
  5.     // Give the channel a 1-element time buffer.
  6.     // If the client falls behind while reading, we drop ticks
  7.     // on the floor until the client catches up.
  8.     c := make(chan Time, 1)
  9.     t := &Ticker{
  10.        C: c,
  11.        r: runtimeTimer{
  12.           when:   when(d),
  13.           period: int64(d),
  14.           f:      sendTime,
  15.           arg:    c,
  16.        },
  17.     }
  18.     startTimer(&t.r)
  19.     return t
  20. }

  21. // sendTime does a non-blocking send of the current time on c.
  22. func sendTime(c any, seq uintptr) {
  23.     select {
  24.     case c.(chan Time) <- Now():
  25.     default:
  26.     }
  27. }
复制代码
runtimer
我们看下1.20的go如何运行的timer
  1. func runtimer(pp *p, now int64) int64 {
  2.     for {
  3.        t := pp.timers[0]
  4.        if t.pp.ptr() != pp {
  5.           throw("runtimer: bad p")
  6.        }
  7.        switch s := t.status.Load(); s {
  8.        case timerWaiting:
  9.           if t.when > now {
  10.              // Not ready to run.
  11.              return t.when
  12.           }

  13.           if !t.status.CompareAndSwap(s, timerRunning) {
  14.              continue
  15.           }
  16.           // 重点就是这个方法
  17.           runOneTimer(pp, t, now)
  18.           return 0

  19.        case timerDeleted:
  20.        // 下面的逻辑对这个问题没有影响 忽略
  21.           .....
  22.     }
  23. }

  24. func runOneTimer(pp *p, t *timer, now int64) {
  25.     f := t.f
  26.     arg := t.arg
  27.     seq := t.seq

  28.     if t.period > 0 {
  29.        // 对于ticker 会先设置下次运行的时间,然后重新触发堆排序
  30.        delta := t.when - now // t.when 一定小于等于 now,所以delta是个负数
  31.        // 整数除整数,得到的还是整数。
  32.        // delta一般会比t.period小特别多 (在1s的ticker下,t.period也已经是10^6了)
  33.        // 所以这个除法的结果大概率是0,所以这里的加减不太影响 t.when
  34.        t.when += t.period * (1 + -delta/t.period)
  35.        if t.when < 0 { // check for overflow.
  36.           t.when = maxWhen
  37. }
  38.        siftdownTimer(pp.timers, 0)
  39.        if !t.status.CompareAndSwap(timerRunning, timerWaiting) {
  40.           badTimer()
  41.        }
  42.        updateTimer0When(pp)
  43.     } else {
  44.        // Remove from heap.
  45.        dodeltimer0(pp)
  46.        if !t.status.CompareAndSwap(timerRunning, timerNoStatus) {
  47.           badTimer()
  48.        }
  49.     }

  50.     unlock(&pp.timersLock)
  51.     // 触发sendTimer
  52.     f(arg, seq)

  53.     lock(&pp.timersLock)
  54. }
复制代码
从上面的代码其实就可以看到问题了,下次触发的时间和sendTime拿到的时间不是一致的,也就是说如果unlock或者其他操作执行的较慢,那很可能sendTime这次拿到的时间是比预期晚,而下次拿到的时间比预期早,正好这个id生成器尽量从整秒开始,当出现上面描述的情况就会出现两次在同一秒的情况,导致id重复,同时当go调度器较忙时,可能触发runtimer的时间比预期晚,这个时候相当于返回的时间大于1s了,很可能又把之前小于1s的误差追平了,这个时候如果再出现小于1s的情况,可能又会触发id重复。所以日志中会看到多次出现id重复问题

验证

我们写一个很简单的ticker
  1. package main

  2. import (
  3.     "fmt"
  4.     "time"
  5. )

  6. func main() {
  7.     // 为了更容易复现问题,这里尽量从接近整秒但不足整秒开始
  8.     time.Sleep(time.Until(time.Now().Truncate(time.Second).Add(999099999 * time.Nanosecond)))
  9.     tick := time.NewTicker(1 * time.Second)
  10.     for i := 0; i < 5; i++ {
  11.        c := <-tick.C
  12.        fmt.Println("tick", i, ":", c.Format(time.StampNano))
  13.     }
  14. }
复制代码
然后修改sendTime方法,我们记录下上次触发的时间戳,然后和这次的时间戳比较
  1. var pre int64

  2. // sendTime does a non-blocking send of the current time on c.
  3. func sendTime(c any, seq uintptr) {
  4.     var n = runtimeNano()
  5.     println("send", n-pre)
  6.     pre = n
  7.     select {
  8.     case c.(chan Time) <- Now():
  9.     default:
  10.     }
  11. }
复制代码
测试结果
可以看到这个sendTime的间隔先是不足1s后又超过1s,5次ticker中出现了2次落到同一秒的情况
1.webp

观察上述的输出,和【简述根因】中的推演结果一致。结论成立。
到此这篇关于详解如何解决golang定时器引发的id重复问题的文章就介绍到这了,更多相关golang定时器引发id重复内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
12
积分
4
注册时间
2024-1-23
最后登录
2024-1-23

发表于 2024-9-20 02:50:43 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
21
积分
22
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2025-4-19 01:01:33 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表