Skip to content

Instantly share code, notes, and snippets.

@itchyny
Last active January 27, 2022 11:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itchyny/63869949999a08ca829736d37e0a7911 to your computer and use it in GitHub Desktop.
Save itchyny/63869949999a08ca829736d37e0a7911 to your computer and use it in GitHub Desktop.
Lock-free snowflake-like id generator in Golang
package main
import (
"fmt"
"os"
"sync"
"sync/atomic"
"time"
)
func main() {
g := &IdGenerator{worker: 0}
var wg sync.WaitGroup
var sm sync.Map
count := 10000
for i := 0; i < count; i++ {
wg.Add(1)
go func() {
defer wg.Done()
id := g.NewID()
if _, loaded := sm.LoadOrStore(id, true); loaded {
fmt.Fprintf(os.Stderr, "id conflict: %v\n", id)
}
}()
}
wg.Wait()
var smsize int
sm.Range(func(_, _ interface{}) bool { smsize++; return true })
if count != smsize {
fmt.Fprintf(os.Stderr, "%v %v\n", count, smsize)
os.Exit(1)
}
}
type IdGenerator struct {
worker, last uint64
}
func (g *IdGenerator) NewID() uint64 {
const (
seqbits = 12
workerbits = 10
timeoffset = workerbits + seqbits
basetime = 1640995200000 // time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli()
)
var now, sequence uint64
var d time.Duration
for {
now = uint64(time.Now().UnixMilli()) - basetime
last := atomic.AddUint64(&g.last, 1)
if old := last >> timeoffset; old == now {
// If the timestamps are identical and sequence is acceptable, use it.
if sequence = last & (1<<timeoffset - 1); sequence <= 1<<seqbits-1 {
break
}
if sequence > 1<<(seqbits+2) {
// Try to set sequence to the maximum to avoid overflow.
atomic.CompareAndSwapUint64(&g.last, last, now<<timeoffset|1<<seqbits-1)
}
// No more sequence in this timestamp is available so sleep for a bit.
time.Sleep(100 * time.Microsecond)
} else if old < now {
// The timestamp is updated so reset the sequence.
if atomic.CompareAndSwapUint64(&g.last, last, now<<timeoffset) {
// If saving the new sequence is accepted, use it.
sequence = 0
break
}
} else {
// Newer timestamp is set by different goroutine, sequence overflow, or clock rewind.
if d == 0 {
d = 100 * time.Microsecond
} else if d *= 2; d >= time.Second {
// Clock rewind happened, unlikely to able to generate a new sequence so give up.
panic("IdGenerator.NewID: clock rewind")
}
time.Sleep(d)
}
}
v := now<<timeoffset | g.worker<<seqbits | sequence
fmt.Printf("%d\t%d\t%d\t%4d\n", v, now, g.worker, sequence) // for debugging
return v
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment