短链接项目

该项目是一个go语言的短链接项目,这里附上gitee的地址,欢迎大家学习。短链系统: 这是一个go语言实现的短链接生成系统

一.目录结构

在日常的项目开发,首先要做的就是判断我们的目录结构,设计模式,建表等操作

最重要的就是设计结构。

首先关于这个项目的开发,不管如何都应该先从目录结构和设计的逻辑开始设计

下面是我设计的一个目录结构:

  1. 采用的基本都是SC的模式,分为database,server,client的架构
  2. server就是服务层,database就是sql的表,client就是应用层

1.1 server层

├─ go.mod // 项目依赖
│  main.go //主程序
│  
│           
├─internal																	    
│     ├─controllers                          //控制层 
│ 		│ 		└─api
│     ├─config                               // 项目配置入口   
│     ├─dao                                  // 数据库连接(初始化)
│  		│	├──mysql
│			│	└──redis
│     ├─cache                                // 缓存
│     ├─global                               // 全局变量
│     ├─middleware                           // 中间件
│     ├─models                               // 模型结构体
│     ├─service                              // 业务逻辑层
│     ├─routers                              // 路由入口
│			├─repositories												 // 仓储层(数据访问层)
│     ├─pkg                                	 // 项目各类模块
│  		│  └─logger,moddlewares,vaildator...
│     │  
│     └─server															 // 服务入口 
│           

上述就是我的一个服务层整体的一个目录架构

主要的实现流程:

控制层(controller) -> 逻辑层(service) -> 数据访问层(repository)

控制层:主要的操作就是处理前端传入的信息,将请求传给下一层

逻辑层:主要就是处理业务逻辑方面的问题,将要和数据库交互的信息传给下一层

数据访问层:主要的操作就是和数据库进行交互的一层

1.2 database

这个就是一个sql语句建的表,没啥别的内容。

1.3 client层

前端内容比较少,所以架构比较简单,下面是一个通用的架构

├── node_modules/        # 项目依赖的 node 模块
├── public/              # 公共资源目录
│   ├── favicon.ico      # 网页图标
│   └── index.html       # html模板(单页面应用)
├── src/                 # 源代码目录
│   ├── assets/          # 静态资源文件夹,如图片、字体等
│   ├── components/      # 公共组件
│   ├── router/          # 路由文件夹
│   ├── store/           # Vuex状态管理文件夹
│   ├── views/           # 视图层组件
│   ├── App.vue          # Vue 根组件,也是整个应用的入口
│   └── main.js          # Vue 实例化入口文件,也是整个应用的入口
├── .babelrc             # Babel 配置文件
├── .gitignore           # Git管理忽略文件
├── package.json         # 项目配置文件
├── README.md            # 项目说明文件
└── webpack.config.js    # Webpack配置文件

二. 设计架构和逻辑

2.1 设计架构

就是类似这个MCV架构,只不过我这里的技术框架是gin,gorm,mysql和redis这些内容

viper来处理配置文件

2.2 设计的逻辑

设计的逻辑的化,应该也算是一种依赖注入的方式,通过global的设计,获取各类配置的全局变量

这里如果依赖注入,会出现一个问题:

由于go语言是值传递,如果你直接将这个全局变量传过去,会出现传的是副本,这里对应的操作就是传入这些全局变量的指针(这里你会发现就是你的gorm对象本身就是指针,所以就要传入指针的指针)

写一个函数来处理各类错误,直接报panic即可

package server

import (
	"MyWeb/internal/config"
	"MyWeb/internal/dao/mysql"
	"MyWeb/internal/dao/redis"
	"MyWeb/internal/global"
	"MyWeb/internal/pkg/logger"
	"MyWeb/internal/pkg/snowflake"
	"MyWeb/internal/routers"
)

func PrintMsg(Error error) {
	if Error != nil {
		panic(Error)
	}
}

// Init 注入依赖 global用来解耦,降低复杂度。
func Init() {
	// 加载配置文件
	config.InitConfig(global.Cfg)
	// 加载日志库
	PrintMsg(logger.InitLogger(global.Cfg, &global.Log))
	global.Log.Info("Log init successfully")
	// 加载mysql
	PrintMsg(mysql.InitMysqlDb(global.Cfg, &global.Db))
	global.Log.Info("Mysql init successfully")
	// 加载 redis
	PrintMsg(redis.InitRedisDb(global.Cfg, &global.Rdb))
	global.Log.Info("Redis init successfully")
	// 生成雪花对象
	PrintMsg(snowflake.SnowInit(1))
	global.Log.Info("Snow init successfully")

	routers.InitRouter(global.Cfg)
}

三:短链系统的设计思路

短链系统主要要做到两个方面的设计:

  1. POST :将request传给handler,生成shortURL,在返回respense
  2. GET : 获取传入的shortURL,进行一个重定向操作即可。

其次里面还涉及各类接口,着重讲一下生成短链接的接口。

这里有一个关键问题:就是短链的结构?

短链其实是由基础url和生成的短链code组合而成

下面是一个完整的POST和GET请求的图

其实在查缓存之前,可以加入布隆过滤器,在redis之中进行。

3.1 POST请求的处理

3.2 GET请求的处理

这是处理GET请求的操作。

3.3 短链接算法的接口

其实最关键还是说这个算法接口:

可能会好奇我为什么会使用接口,我个人认为接口其实就是c++多态的抽象,通过这个抽象的操作,我们可以设置多个实现这个接口的对象,从而初始化不同的对象,实现不同短链接算法,这样很方便。

这里主要涉及一点就是:传入实现接口的实例对象

换句话说就是父类指针指向子类的一个实现吧。

这个接口,会在后续讨论。

四.对应结构体的设计

4.1 请求和响应结构体

// 字段含义 :长链,用户自定义,有效时间
type UrlRequest struct {
	LongUrl    string `json:"long_url" validate:"required,url"`
	CustomCode string `json:"custom_code,omitempty" validate:"omitempty,min=4,max=10,alphanum" label:"用户定义后缀"`
	UsedTime   *int   `json:"used_time,omitempty" validate:"omitempty,min=1,max=10" label:"有效时间"`
}
//	字段含义:返回短链和过期时间
type UrlResponse struct {
	ShortUrl  string    `json:"short_url"`
	ExpiresAt time.Time `json:"expires_at"`
}

需要是由tag为其设置约束。

简单说明一下这些tag的作用:

long_url:就是转化为json的字段名

omitempty:作用就是该字段可以为空

alphanum:

4.2 UrlMap表

这个就是一个url数据库存储的表的形式

//	字段分别是id,长链,短链,是否是用户自定义的短链,创建时间,过期时间
type UrlMap struct {
	ID          int64     `gorm:"primary_key;column:id"`
	LongUrl     string    `json:"long_url" gorm:"not null;column:long_url"`
	ShortUrl    string    `json:"short_url" gorm:"not null;unique;column:short_url"`
	UserDefined bool      `json:"user_defined" gorm:"column:user_defined"`
	CreatedAt   time.Time `gorm:"column:created_at"`
	ExpiresAt   time.Time `json:"expires_at" gorm:"column:expires_at"`
}

4.3 缓存结构

我们存入缓存也需要一个结构体,避免将整个map存入,浪费内存

加入ExpiredAt 就是读取时,直接可以判断是否过去,不需要再去查询数据库

type CacheSL struct {
    ShortURL  string    `json:"short_url"`
    LongURL   string    `json:"long_url"`
    ExpiresAt time.Time `json:"expires_at" gorm:"column:expires_at"`
}

五.三层架构的实现

所谓的三层架构就是

controller -> service -> repository

这里就展示一下架构的结构体的实现:

5.1 controller层

这一层的话,主要就是处理器,gin框架方面的内容

需要一个实例生成函数,因为我这里全是方法。

这个结构体提供一个Service的接口实现。

这里解释一下:接口,其实就好比我们手机的数据线,管他是啥类型,只要他是Type-c的就行,而他的内容就是充电器的头,可以是20w,或者200w

type URLHandler struct {
	uRLService URLService
	//可以任意切换接口的实现
}

type URLService interface {
	CreateURL(c *gin.Context, req models.UrlRequest) (*models.UrlResponse, error)
	GetLongURL(c *gin.Context, shortcode string) (string, error)
}

func NewURLHandler(service URLService) *URLHandler {
	return &URLHandler{
		uRLService: service,
	}
}

5.2 service层

这个就是逻辑业务层,涉及的东西可能会比较多一些。

这里说明一下,我的Service的接口定义是在controller包下

从而在这个service来实现这个接口

// shortCodeGenerator 最短后缀的生成接口
type shortCodeGenerator interface {
	CreateShortCode() string
}

// repo数据交互层的接口
type UrlMapRepo interface {
	CreateSHortURL(url *models.UrlMap) error
	FindShortURL(UserShortURL string) error
	FindLongURL(ShortURL string) (*models.UrlMap, error)
	IsExpiredAt(url *models.UrlMap) bool
}

// Cacher 缓存接口
type Cacher interface {
	SetURL(c *gin.Context, url *models.CacheSL) error
	GetURL(c *gin.Context, shortUrl string) (*models.CacheSL, error)
}

type UrlService struct {
	baseURL         string
	defaultDuration time.Duration
	// 抽象出三个接口:1.数据交互层的接口  2.生成最短算法的接口, 3.缓存接口
	query              repositories.UrlMapRepo
	shortCodeGenerator shortCodeGenerator
	cache              Cacher
}

func NewUrlService(repo repositories.UrlMapRepo) *UrlService {
	return &UrlService{
		query:              repo,
		defaultDuration:    1 * time.Hour,
		baseURL:            "http://ShortURL.com",
		shortCodeGenerator: shortcode.NewSnowURL(),
		cache:              cache.NewShortCache(),
	}
}

5.3 repository层

这一层,就是实现上述接口即可

这里面之所以要加上 Db

因为这一层是要属于数据库做对接的,所以要把全局变量引入

type UrlMapRepoUse struct {
	Db *gorm.DB
}

// NewUrlMapRepoUse 加载全局DB
func NewUrlMapRepoUse() *UrlMapRepoUse {
	return &UrlMapRepoUse{
		Db: global.Db,
	}
}

六.前端架构

由于前端页面比较简单,主要就是记录一下vue的使用

├── node_modules/        # 项目依赖的 node 模块
├── public/              # 公共资源目录
│   ├── favicon.ico      # 网页图标
│   └── index.html       # html模板(单页面应用)
├── src/                 # 源代码目录
│   ├── assets/          # 静态资源文件夹,如图片、字体等
│   ├── components/      # 公共组件
│   ├── router/          # 路由文件夹
│   ├── views/           # 视图层组件
│   ├── App.vue          # Vue 根组件,也是整个应用的入口
│   └── main.js          # Vue 实例化入口文件,也是整个应用的入口
├── .gitignore           # Git管理忽略文件
├── package.json         # 项目配置文件
├── README.md            # 项目说明文件
└── webpack.config.js    # Webpack配置文件

6.1 路由引入

路由引入:主要就是引入router,对main.js做一个修改,将路由引入并使用。

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'


// createApp(App).mount('#app')

// 引入路由
import router from './router/index.js'

const app = createApp(App)

// 加载
app.use(router)

app.mount('#app')

6.2 axios引入

这里是全局引入

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// createApp(App).mount('#app')

import router from './router/index.js'  // 引入路由
import axios from "axios"               // 引入网络

const app = createApp(App)

// 加载路由
app.use(router)
app.config.globalProperties.$axios = axios


app.mount('#app')

这里就是将$axios作为全局的变量。

七.唯一ID

7.1 常见唯一id概念

在了解雪花ID之前,首先要了解一下什么是唯一id

唯一id顾名思义就是唯一的id

有哪些常见的唯一id呢?

就比如:数据库的主键自增id,或者redis的INCR自增原子操作

这些都是唯一id,但是这些id都存在一些问题

就是这些id做不到全局唯一。

意思就是一张表内的id和另一张表内的id会重复,做不到全局唯一

7.2 全局唯一id

这个时候就有一个大佬发明了一种雪花算法来生成全局唯一id。

我们接下来重点也就是说一下这个雪花id的生成。

八.雪花id

8.1 概念

雪花算法(Snowflake Algorithm)是一种在分布式系统中生成唯一ID的方法,最初由Twitter内部使用。它生成的是一个64位的长整型(long)数字,由以下几部分组成:

  • 最高位是符号位,通常为0,因为ID通常是正数。
  • 41位用于存储毫秒级的时间戳,这部分不是存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 开始时间戳),可以支持大约69年的时间。
  • 10位用于存储机器码,可以支持最多1024台机器。如果在同一毫秒内有多个请求到达同一台机器,机器码可以用于区分不同的请求。
  • 12位用于存储序列号,用于同一毫秒内的多个请求,每台机器每毫秒可以生成最多4096个ID。

雪花算法的优点包括:

  • 在高并发的分布式系统中,能够保证ID的唯一性。
  • 基于时间戳,ID基本上是有序递增的。
  • 不依赖于第三方库或中间件,减少了系统复杂性。
  • 生成ID的效率非常高。

我们不采用MySQL的主键自增ID和redsi的incr的自增ID,而是使用本地雪花算法的形式直接生成ID,这 样性能更高

8.2 雪花算法实现

下面是一个我写的比较基础的原始的雪花算法以及一些注解

package main

import (
	"errors"
	"fmt"
	"log"
	"sync"
	"time"
)

// todo 雪花算法的结构
const (
	timesIDBits   = 41 // 时间戳(ms)
	machineIdBits = 10 // 机器id
	sequenceBits  = 12 // 同一毫秒内的序列号id

	// 最大值计算
	timesIDMaxValue   = -1 ^ (-1 << timesIDBits)
	machineIdMaxValue = -1 ^ (-1 << machineIdBits) // 机器 ID 最大值
	sequenceMask      = -1 ^ (-1 << sequenceBits)  // 序列号掩码

	// 位移
	timestampShift = machineIdBits + sequenceBits
	machineShift   = sequenceBits
)

// Snowflake 一个雪花结构体
type Snowflake struct {
	mu        *sync.Mutex // 互斥锁
	StartTime time.Time   // 初始时间
	Timestamp int64       // 时间戳
	machineId int64       // 机器号
	sequence  int64       // 序列号
}

// NewSnowflake 创建的雪花对象,要输入数据中心id和机器id
func NewSnowflake(StartTime time.Time, machineID int64) (*Snowflake, error) {
	// 判断传入的初始化时间是否合理
	if StartTime.After(time.Now()) {
		return nil, errors.New("start time is ahead of now")
	}

	// 如果传入的时间是零值,则默认初始化
	if StartTime.IsZero() {
		StartTime = time.Date(2014, 9, 1, 0, 0, 0, 0, time.UTC)
	}

	// 判断传入的机械id是否合理
	if machineID < 0 || machineID > machineIdMaxValue {
		return nil, errors.New("machineID out of range")
	}

	return &Snowflake{
		mu:        new(sync.Mutex),
		StartTime: StartTime,
		Timestamp: 0,
		machineId: machineID,
		sequence:  0,
	}, nil

}

// NextID = 0 + timestamp + dataCenterId + machineId + sequence
func (s *Snowflake) NextID() (int64, error) {
	// 加锁
	s.mu.Lock()
	defer s.mu.Unlock()

	// 获取当前时间戳
	Epoch := time.Since(s.StartTime).Milliseconds()

	// 如果时间回退
	if Epoch < s.Timestamp {
		//return 0, errors.New("时钟回拨!")
		log.Println("发生了时钟回退")
	}

	// 如果在同一个时间点,就对序列号进行自增
	if Epoch == s.Timestamp {
		// 序列号从1开始
		s.sequence = (s.sequence + 1) & sequenceMask
		// sequence,再加1,就为0。即表示再这个时间单位内,不能再生成更多的id了,需要等待到下一个时间单位内。
		if s.sequence == 0 {
			for Epoch <= s.Timestamp {
				// 计算初始时间到现在的现在的毫秒量
				Epoch = time.Since(s.StartTime).Milliseconds()
			}
		}
	} else {
		// 如果是新的一毫秒 自增id设为0
		s.sequence = 0
	}

	// 具体的时间戳,通过这个方法动态获取
	s.Timestamp = Epoch

	// 计算唯一 ID
	timePart := Epoch << timestampShift
	machinePart := s.machineId << machineShift
	sequencePart := s.sequence

	snowID := timePart | machinePart | sequencePart
	return snowID, nil
}

func main() {
	// 初始化 Snowflake
	startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
	sf, err := NewSnowflake(startTime, 1)
	if err != nil {
		panic(err)
	}

	// 生成 ID
	for i := 0; i < 10; i++ {

		id, err := sf.NextID()
		if err != nil {
			panic(err)
		}

		fmt.Println(id)
	}
}

8.3 雪花算法的缺点(时钟回拨)

在上述内容中,只涉及了雪花算法的优点,接下来说一下雪花算法的缺点

雪花算法的最关键缺点:

  1. 强依赖于时钟,会出现时钟回拨的情况

为什么会出现时钟回拨的问题呢?

  1. 网络时间校准
  2. 人工设置
  3. 出现负闰秒(了解就行)

那针对这个问题,有没有什么解决方法呢?

有的,兄弟有的,解决的方法有很多。

常见的解决方案:

  1. 直接抛出异常,人为处理
  2. 延迟等待,直到时间校准

(上述这两种方法其实没啥用,关键还是下面的)

  1. 基于时钟序列的解决方式
  2. seata框架里面的一个解决方法
  3. 还有很多,比如美团百度开源的算法,开源可以自己看看

九.解决时间回拨的方案

9.1 基于时钟序列的解决方案

这里介绍的是一种基于修改扩展位的思路,基于时钟序列的雪花算法

设计思路

  • 如上图,将原本10位的机器码拆分成3位时钟序列及7位机器码
  • 发生时间回拨的时候,时间已经发生了变化,那么这时将时钟序列新增1位,重新定义整个雪花Id
  • 为了避免实例重启引起时间序列丢失,因此时钟序列最好通过DB/缓存等方式存储起来

算法支持

  • 还是支持最长 69 年多的运行时间
  • 分布式实例规模由210(1024)降至27(128)
  • 单实例每毫秒仍然支持 4096次请求
  • 每个分布式实例支持最多 2^3(8) 次时间回拨

带来的问题就是这个处理并发的机器数下降,应该根据实际情况选择合适的方式

9.2 seata框架的解决方法

https://seata.apache.org/zh-cn/blog/seata-analysis-UUID-generator/

上面这个是原链接,下面会做一个简单的介绍

原版的雪花结构:

改成(即时间戳和节点ID换个位置):

为什么要做这样的修改呢?

它主要这样做的原因是一个超前消费的一个想法:(将时间戳和序列号作为一个整体)

主要有两个好处:

  1. 首先就是保证序列号可以超过4096,避免高峰期超过4096导致系统崩掉,它可与向下一毫秒接着申请序列号,做到一个超前使用。
  2. 其次就是可以解决时钟回拨的问题,也是通过对该时间剩余的序列号的一个使用来避免一个时钟回拨的问题。

这里比没有完全解决,这里只是将强依赖改为了弱依赖

这里有一句很关键的话:就是系统仅在启动时获取一遍起始时间戳就可以了。

就算发生了时钟回拨也无所谓。

十.改良版的雪花算法

package main

import (
	"errors"
	"fmt"
	"log"
	"sync"
	"time"
)

// todo 雪花算法的结构
const (
	timesIDBits   = 41 // 时间戳(ms)
	machineIdBits = 10 // 机器id
	sequenceBits  = 12 // 同一毫秒内的序列号id

	// 最大值计算
	timesIDMaxValue   = -1 ^ (-1 << timesIDBits)
	machineIdMaxValue = -1 ^ (-1 << machineIdBits) // 机器 ID 最大值
	sequenceMask      = -1 ^ (-1 << sequenceBits)  // 序列号掩码

	// 位移
	timestampShift = machineIdBits + sequenceBits
	machineShift   = sequenceBits
)

// Snowflake 一个雪花结构体
type Snowflake struct {
	mu        *sync.Mutex // 互斥锁
	StartTime time.Time   // 初始时间
	Timestamp int64       // 时间戳
	machineId int64       // 机器号
	sequence  int64       // 序列号
}

// NewSnowflake 创建的雪花对象,要输入数据中心id和机器id
func NewSnowflake(StartTime time.Time, machineID int64) (*Snowflake, error) {
	// 判断传入的初始化时间是否合理
	if StartTime.After(time.Now()) {
		return nil, errors.New("start time is ahead of now")
	}

	// 如果传入的时间是零值,则默认初始化
	if StartTime.IsZero() {
		StartTime = time.Date(2014, 9, 1, 0, 0, 0, 0, time.UTC)
	}

	// 判断传入的机械id是否合理
	if machineID < 0 || machineID > machineIdMaxValue {
		return nil, errors.New("machineID out of range")
	}

	return &Snowflake{
		mu:        new(sync.Mutex),
		StartTime: StartTime,
		Timestamp: 0,
		machineId: machineID,
		sequence:  0,
	}, nil

}

// NextID = 0 + timestamp + dataCenterId + machineId + sequence
func (s *Snowflake) NextID() (int64, error) {
	// 加锁
	s.mu.Lock()
	defer s.mu.Unlock()

	// 获取当前时间戳
	Epoch := time.Since(s.StartTime).Milliseconds()

	// 如果时间回退
	if Epoch < s.Timestamp {
		Epoch = s.Timestamp
		log.Println("发生了时钟回退")
	}

	// 如果在同一个时间点,就对序列号进行自增
	if Epoch == s.Timestamp {
		// 一个循环的自增序列号
		s.sequence = (s.sequence + 1) & sequenceMask
		// sequence,再加1,就为0。即表示再这个时间单位内,不能再生成更多的id了,需要等待到下一个时间单位内。
		if s.sequence == 0 {
			for Epoch <= s.Timestamp {
				// 序列号溢出,等待下一毫秒
				Epoch = time.Since(s.StartTime).Milliseconds()
			}
		}
	} else {
		// 如果是新的一毫秒 自增id设为0
		s.sequence = 0
	}

	// 具体的时间戳,通过这个方法动态获取
	s.Timestamp = Epoch

	// 计算唯一 ID
	timePart := Epoch << timestampShift
	machinePart := s.machineId << machineShift
	sequencePart := s.sequence

	snowID := timePart | machinePart | sequencePart
	return snowID, nil
}

func main() {
	// 初始化 Snowflake
	startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
	sf, err := NewSnowflake(startTime, 1)
	if err != nil {
		panic(err)
	}

	// 生成 ID
	now := time.Now().Add(1 * time.Hour).UnixMilli()
	for i := 0; i < 10; i++ {

		if mark := i; mark == 5 {
			sf.Timestamp = now
		}

		id, err := sf.NextID()
		if err != nil {
			panic(err)
		}

		fmt.Println(id)
	}
}

10.1 基于seata的改良版雪花算法

package main

import (
	"errors"
	"fmt"
	"log"
	"sync"
	"time"
)

const (
	timesIDBits   = 41 // 时间戳(ms)
	machineIdBits = 10 // 机器id
	sequenceBits  = 12 // 同一毫秒内的序列号id

	// 最大值计算
	timesIDMaxValue   = -1 ^ (-1 << timesIDBits)
	machineIdMaxValue = -1 ^ (-1 << machineIdBits) // 机器 ID 最大值
	sequenceMask      = -1 ^ (-1 << sequenceBits)  // 序列号掩码

	// 位移
	timestampShift = machineIdBits + sequenceBits
	machineShift   = sequenceBits
)

// Snowflake 一个雪花结构体
type Snowflake struct {
	mu        *sync.Mutex // 互斥锁
	StartTime time.Time   // 启动时间
	Timestamp int64       // 时间戳
	machineId int64       // 机器号
	sequence  int64       // 序列号
}

// NewSnowflake 创建的雪花对象,要输入机器id
func NewSnowflake(StartTime time.Time, machineID int64) (*Snowflake, error) {
	// 判断传入的初始化时间是否合理
	if StartTime.After(time.Now()) {
		return nil, errors.New("start time is ahead of now")
	}

	// 判断传入的机器id是否合理
	if machineID < 0 || machineID > machineIdMaxValue {
		return nil, errors.New("machineID out of range")
	}

	// 计算时间戳
	Epoch := time.Since(StartTime).Milliseconds()

	return &Snowflake{
		mu:        new(sync.Mutex),
		StartTime: StartTime,
		Timestamp: Epoch, // 初始化时将时间戳设为 启动项目时的时间戳
		machineId: machineID,
		sequence:  0,
	}, nil
}

// NextID 生成唯一 ID
func (s *Snowflake) NextID() (int64, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	// 如果序列号已经达到最大值,需要进位到下一个时间戳
	if s.sequence == sequenceMask {
		s.sequence = 0 // 重置序列号
		s.Timestamp++  // 时间戳加 1
	} else {
		s.sequence++ // 序列号加 1
	}

	// 如果时间戳超出最大值,表示生成器溢出
	if s.Timestamp > timesIDMaxValue {
		return 0, errors.New("timestamp overflow")
	}

	// 计算唯一 ID
	timePart := s.Timestamp << timestampShift
	machinePart := s.machineId << machineShift
	sequencePart := s.sequence

	snowID := timePart | machinePart | sequencePart
	return snowID, nil
}

func main() {
	// 初始化 Snowflake
	startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
	sf, err := NewSnowflake(startTime, 1)
	if err != nil {
		panic(err)
	}

	// 生成 ID
	for i := 0; i < 10; i++ {
		id, err := sf.NextID()
		if err != nil {
			log.Printf("生成 ID 时出错: %v", err)
			continue
		}
		fmt.Println(id)
	}
}

我来说一下这个的思路:

首先只需要获取第一次的启动时间即可。

后续的操作就是对序列号进行一个增加,不在对时间进行一个处理了

当序列号到4095+1时就向时间戳上加1就可以了。

这里即使发生了时间回拨也不会影响他的id生成,因为它还是在你一开始的启动基础之上对其序列号进行增加,所以就算时间回拨也不会有任何影响。

只需要重启系统,让世界回复正常,他就会跳跃到一个新的时间戳

还有一点就是他的QPS是4096/ms,不可能就是会用完的,所以不用担心就是重启后会和新的时间戳重复。

十一.基于雪花算法的常见问题

11.1. 雪花算法支持的并发数最大多少?

  • 这个是由序列号的位数决定的,原生雪花算法序列号12位,也就是1毫秒最大可生成2^12(4096),相当于1秒可生成 4096 * 1000 个ID,也就是QPS可以到 409.6 w/s

11.2. 雪花算法支持最多支持系统运行多少年?

  • 这个是由时间戳位数决定的,原生雪花算法时间戳占41位,也就是支持最大的时间戳为2^41(2199023255552),而1年的总毫秒数为3600 * 1000 * 24 * 365 = 31,536,000,000,因此2^41 / 1年的总毫秒数≈69.7年

  • 其实衍生出另一个问题,41位能表示的最大的时间戳为2^41(2199023255552)对应的时间应该是2039-09-07 23:47:35,距离现在只有不到20年的时间,为什么算出来的是69年呢?
  • 其实时间戳的算法是1970年1月1日到指点时间所经过的毫秒或秒数,那咱们把开始时间从2021年开始,就可以延长41位时间戳能表达的最大时间,所以这里实际指的是相对自定义开始时间的时间戳

11.3. 用了雪花Id,出现负闰秒为什么会导致系统大量抛异常?

  • 闰秒是偶尔运用于协调世界时(UTC)的调整,经由增加或减少一秒,以消弥精确的时间(使用原子钟测量)和不精确的观测太阳时(称为UT1),之间的差异
  • 这种做法已被证明具有破坏性,特别是在二十一世纪,尤其是在依赖精确时间戳或时间关键程序控制的服务中
  • 而雪花算法严重依赖时间戳,当出现负闰秒也就是时间减少一秒时(时间往前回拨1秒),雪花Id就可能出现重复,而原生的雪花算法出现时间回拨的处理方式是直接抛异常
  • 2022年11月,在第27届国际计量大会上,科学家和政府代表投票决定到2035年取消闰秒

11.4. 雪花算法的时钟回拨问题

这个的方法上面已经说过了,就不再过多介绍了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值