雪花算法 Redis 分布式管理机器码

该博客介绍了如何结合Redis优化雪花算法,通过lua脚本保证原子性,实现分布式环境下服务的唯一序列号分配。服务启动时占用Redis中的一个序列号并定时续期,服务关闭时自动释放,确保资源的有效利用。同时提供了Mybatis-Plus中初始化雪花算法的示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

雪花算法是64位的二进制组成,展示如下:

292E7FB9-D3CB-4425-A6D9-70B57C5EEB44

留给我们自定义的就只剩下机器id服务id号了, 也就是说这中间的10位可以由我们自定义

虽然雪花算法重复概率已经非常低了,但是《墨菲定律》说:你不想发生的事,一定会发生

所以写代码不能抱有侥幸心理

思路

  1. 目前雪花算法主要生成数据库的ID,一般在分布式环境下,一个服务都是单独一个库,自定义中间10位的话,单服务最多可以部署1024个节点,一般服务应该达不到这么多节点(反正目前我没见过)
  2. 以服务名+序号存入redis,服务活跃时,参照Redission实现分布式锁一样,开一个线程做完看门狗来续期
  3. 用lua脚本获取一个唯一且有效的序列号,设置过期时间为30秒
  4. 获取序列化之后,开启一个守护线程,每10秒延长过期时间为30秒,服务健康就一直占用,服务关闭,这个Id会自动释放

实现

所有实现都参考reids实现分布式锁来做的

因为所有操作要保证原子性,所以获取服务Idlua脚本实现

循环0-1023,获取一个空闲的序号,修改过期时间为30秒

local now = redis.call('TIME')[1]
local idWordsKey = KEYS[1]
local serviceName = KEYS[2]
local sp = ':'
for i = 0, 1023 do
    local serviceKey = idWordsKey..sp..serviceName..sp..i
    if redis.call('SETNX', serviceKey, now) == 1 then
        redis.call('Expire', serviceKey, 30)
        return i;
    end
end
return -1

存入Redis格式是:

key=idWordsKey:serviceName:unm  value=time

# 说明
idWordsKey  :IdKey标识,可以随便命名
serviceName :服务名称
unm         :服务序列号,取值范围是2^0-1 至 2^10-1,即1至1023
time        :当前时间戳

例如:

image-20220722235123367

java 代码

从Redis中获取空闲ID

		
   	/**
     * key标识,可以随便定义
     */
		private final String idWorker = "WorkerId";

   	/**
     * 服务名称
     */
    @Value("${app.idKey}")
    private String serviceName;

   	/**
     * lua 脚本
     */
    private final String script =
            "local now = redis.call('TIME')[1]\n" +
                    "local idWordsKey = KEYS[1]\n" +
                    "local serviceName = KEYS[2]\n" +
                    "local sp = ':'\n" +
                    "for i = 0, 1023 do\n" +
                    "    local serviceKey = idWordsKey..sp..serviceName..sp..i\n" +
                    "    if redis.call('SETNX', serviceKey, now) == 1 then\n" +
                    "        redis.call('Expire', serviceKey, 30)\n" +
                    "        return i;\n" +
                    "    end\n" +
                    "end\n" +
                    "return -1";


    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 获取机器标识号
     * @param serviceName 服务名称
     */
    private Long getWorkerIdNum(String serviceName) {
        // 实例化脚本对象
        DefaultRedisScript<Long> lua = new DefaultRedisScript<>();
        lua.setResultType(Long.class);
        lua.setScriptText(script);
        List<String> keys = List.of(idWorker, serviceName);

        // 获取序列号
        Long num = (Long)redisTemplate.execute(lua, keys, keys.size());
        String key = String.join(":", keys) + ":" + num;

        // -1 代表机器用完了,重试
        if (num < 0){
            log.error("目前Id已用完,请重新启动试试");
            System.exit(0);
        }

        // 自动续期
        this.autoExpire(key);

        return num;
    }

    /**
     * 自动续期
     */
    private void autoExpire(String key) {
        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
                new BasicThreadFactory.Builder().namingPattern("id_auto_expire-%d").daemon(true).build());
        executorService.scheduleAtFixedRate(() ->  {
            redisTemplate.expire(key, 30, TimeUnit.SECONDS);
            log.info("自动续期id成功:{}", key);
        }, 0, 10, TimeUnit.SECONDS);
    }

单独引入

如果你想单独引入雪花算法,可以参考:https://gitee.com/yu120/sequence

Mybatis-plus 内部源码就是引入的这个工具类

Mybais-Plus引入

我是直接在Mybatia-plus配置文件中直接初始化的,代码如下:

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


/**
 * @author chang
 */
@Slf4j
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {

    private final String idWorker = "WorkerId";

    @Value("${app.idKey}")
    private String serviceName;

    private final String script =
            "local now = redis.call('TIME')[1]\n" +
                    "local idWordsKey = KEYS[1]\n" +
                    "local serviceName = KEYS[2]\n" +
                    "local sp = ':'\n" +
                    "for i = 0, 1023 do\n" +
                    "    local serviceKey = idWordsKey..sp..serviceName..sp..i\n" +
                    "    if redis.call('SETNX', serviceKey, now) == 1 then\n" +
                    "        redis.call('Expire', serviceKey, 30)\n" +
                    "        return i;\n" +
                    "    end\n" +
                    "end\n" +
                    "return -1";


    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

     /**
     * 初始化雪花ID
     */
    @PostConstruct
    public void initMybatisPlus(){
        if (serviceName == null || serviceName.length() == 0){
            log.error("雪花算法初始化失败,【 app.idKey 】配置为空。");
            return;
        }

        long num = getWorkerIdNum(serviceName);
        // 获取前5位
        long workerId = num >> 5;
        // 获取后5位
        long dataCenterId = num & (~(-1L << 5L));
        // 自定义初始化雪花算法
        IdWorker.initSequence(workerId, dataCenterId);
    }

    /**
     * 获取机器标识号
     * @param serviceName 服务名称
     */
    private Long getWorkerIdNum(String serviceName) {
        // 实例化脚本对象
        DefaultRedisScript<Long> lua = new DefaultRedisScript<>();
        lua.setResultType(Long.class);
        lua.setScriptText(script);
        List<String> keys = List.of(idWorker, serviceName);

        // 获取序列号
        Long num = (Long)redisTemplate.execute(lua, keys, keys.size());
        String key = String.join(":", keys) + ":" + num;

        // -1 代表机器用完了,重试
        if (num < 0){
            log.error("目前Id已用完,请重新启动试试");
            System.exit(0);
        }

        // 自动续期
        this.autoExpire(key);

        return num;
    }

    /**
     * 自动续期
     */
    private void autoExpire(String key) {
        ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
                new BasicThreadFactory.Builder().namingPattern("id_auto_expire-%d").daemon(true).build());
        executorService.scheduleAtFixedRate(() ->  {
            redisTemplate.expire(key, 30, TimeUnit.SECONDS);
            log.info("自动续期id成功:{}", key);
        }, 0, 10, TimeUnit.SECONDS);
    }

    public String getServiceName() {
        return serviceName;
    }

    public void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }
}

相关Maven

<!-- 依赖父类版本号 -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
</dependency>

效果展示

每10秒自动续期

image-20220723001125402

<think>好的,我现在需要帮用户用Gin框架写一个短链生成系统的后端部分,并且使用雪花算法生成分布式唯一ID。首先,我得理清楚用户的需求是什么。他们需要的是一个后端服务,能够接收原始URL,生成对应的短链,并且存储起来,当访问短链时重定向到原始URL。同时,要确保生成的短链ID在分布式环境下是唯一的,所以需要用到雪花算法。 首先,我得回忆一下Gin框架的基本用法。Gin是一个高性能的HTTP框架,用Go语言写的,路由和中间件的处理都很方便。接下来是雪花算法的实现,我需要确认雪花算法的结构,通常包括时间戳、机器ID、序列号等部分,确保在分布式系统中生成唯一ID。 然后,考虑项目的结构。通常一个Gin项目会有main.go作为入口,定义路由和启动服务。可能还需要一个单独的包来处理雪花算法,比如snowflake,里面实现ID生成逻辑。另外,数据存储部分,用户可能暂时不需要数据库,但为了持久化,可能需要使用内存存储或者文件存储,不过根据用户的要求,可能先用内存存储,方便演示。 接下来是路由设计。用户需要两个主要的端点:一个用于创建短链(POST请求),另一个用于重定向(GET请求)。例如,POST /api/shorten 接收原始URL,返回短链;GET /:shortID 根据短链重定向到原始URL。 然后考虑数据存储结构。可以用一个map来存储短链ID到原始URL的映射,这样查找起来快速。不过要注意并发安全,因为Gin处理请求是并发的,所以需要使用sync.Map或者加锁机制。不过sync.Map可能在性能上更好,特别是在读多写少的情况下。 雪花算法的实现是关键。需要确保每个实例的机器ID不同,否则可能生成重复ID。通常雪花算法中的机器ID需要配置,或者通过某种方式自动获取,比如环境变量。这里可能需要用户自行配置,或者通过其他方式确保不同实例的机器ID不同,比如使用Kubernetes的Pod ID或者IP地址的一部分。但在示例中,可能先写死或者通过环境变量传入。 接下来是短链的生成。雪花算法生成的ID是int64,需要将其转换为更短的字符串,比如Base62编码,这样比Base64更友好,没有特殊字符。Base62编码可以使用数字和大小写字母,缩短字符串长度。例如,一个int64的ID转换为Base62后大概是11位字符,取决于具体数值。 然后是处理流程。当用户POST一个原始URL时,生成雪花ID,转换为短字符串,存储到map中,然后返回短链。当用户访问短链时,查找map中对应的原始URL,返回302重定向。 还需要处理可能的错误,比如URL格式不正确,或者短链不存在的情况。Gin提供了方便的返回JSON和状态码的方法,可以在这里使用。 现在需要测试代码的正确性。比如,生成ID是否唯一,转换后的短链是否正确,重定向是否正常工作。可能需要写单元测试,但用户可能只需要示例代码,所以可以暂时省略测试部分。 最后,考虑部署和配置。比如机器ID如何设置,是否需要环境变量,或者配置文件。在示例中,可能需要从环境变量读取,或者默认一个值,但提醒用户在生产环境中需要正确配置。 可能遇到的问题包括:雪花算法的时间回拨问题,如何处理?通常雪花算法实现需要考虑这种情况,比如等待时间追上或者抛出异常。但示例中可能暂时不处理,假设时间正常。另外,短链的存储如果重启会丢失,所以用户可能需要持久化存储,但根据当前需求,可能只需要内存存储,用户可以根据需要扩展。 总结步骤: 1. 实现雪花算法生成唯一ID。 2. 创建Gin路由,处理POST和GET请求。 3. 使用Base62将ID转换为短字符串。 4. 使用并发安全的数据结构存储映射。 5. 处理错误和重定向。 现在需要将这些步骤转化为代码,确保每个部分都正确,并且有必要的注释,方便用户理解。同时,用户可能需要知道如何运行和测试,比如使用curl命令测试API。</think>我将以模块化方式为您构建基于Gin的短链系统,包含分布式ID生成和核心功能实现: 一、项目结构 ``` shorturl/ ├── main.go # 入口文件 ├── go.mod ├── handler/ # 业务处理 │ └── shorten.go ├── pkg/ │ ├── snowflake/ # 雪花算法实现 │ │ └── id_gen.go │ └── base62/ # 进制转换 │ └── encode.go └── storage/ # 存储抽象 └── memory.go ``` 二、核心代码实现 1. 雪花算法实现 (`pkg/snowflake/id_gen.go`) ```go package snowflake import ( "sync" "time" ) const ( epoch = 1640995200000 // 2022-01-01 00:00:00 UTC machineIDBits = 10 sequenceBits = 12 maxMachineID = -1 ^ (-1 << machineIDBits) ) type IDGenerator struct { machineID int sequence int lastStamp int64 mu sync.Mutex } func New(machineID int) *IDGenerator { if machineID < 0 || machineID > maxMachineID { panic("invalid machine ID") } return &IDGenerator{machineID: machineID} } func (g *IDGenerator) Generate() int64 { g.mu.Lock() defer g.mu.Unlock() now := time.Now().UnixNano() / 1e6 if now < g.lastStamp { panic("clock moved backwards") } if now == g.lastStamp { g.sequence = (g.sequence + 1) & ((1 << sequenceBits) - 1) if g.sequence == 0 { for now <= g.lastStamp { now = time.Now().UnixNano() / 1e6 } } } else { g.sequence = 0 } g.lastStamp = now return ((now - epoch) << (machineIDBits + sequenceBits)) | (int64(g.machineID) << sequenceBits) | int64(g.sequence) } ``` 2. Base62编码 (`pkg/base62/encode.go`) ```go package base62 const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" func Encode(n int64) string { if n == 0 { return string(charset[0]) } var result []byte for n > 0 { result = append(result, charset[n%62]) n /= 62 } // 反转数组 for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { result[i], result[j] = result[j], result[i] } return string(result) } ``` 3. 存储实现 (`storage/memory.go`) ```go package storage import "sync" type MemoryStorage struct { mu sync.RWMutex store map[string]string } func NewMemoryStorage() *MemoryStorage { return &MemoryStorage{ store: make(map[string]string), } } func (s *MemoryStorage) Save(shortURL, originalURL string) { s.mu.Lock() defer s.mu.Unlock() s.store[shortURL] = originalURL } func (s *MemoryStorage) Get(shortURL string) (string, bool) { s.mu.RLock() defer s.mu.RUnlock() val, exists := s.store[shortURL] return val, exists } ``` 4. 主逻辑 (`main.go`) ```go package main import ( "net/http" "os" "shorturl/pkg/base62" "shorturl/pkg/snowflake" "shorturl/storage" "github.com/gin-gonic/gin" ) var ( idGen *snowflake.IDGenerator store *storage.MemoryStorage ) func init() { // 从环境变量获取机器ID,默认0 machineID := 0 if envID := os.Getenv("MACHINE_ID"); envID != "" { machineID = // 添加转换逻辑 } idGen = snowflake.New(machineID) store = storage.NewMemoryStorage() } func main() { r := gin.Default() r.POST("/shorten", func(c *gin.Context) { type request struct { URL string `json:"url" binding:"required,url"` } var req request if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid URL"}) return } // 生成唯一ID id := idGen.Generate() shortKey := base62.Encode(id) // 存储映射关系 store.Save(shortKey, req.URL) c.JSON(http.StatusOK, gin.H{ "short_url": "http://localhost:8080/" + shortKey, }) }) r.GET("/:key", func(c *gin.Context) { key := c.Param("key") if original, exists := store.Get(key); exists { c.Redirect(http.StatusMovedPermanently, original) return } c.JSON(http.StatusNotFound, gin.H{"error": "URL not found"}) }) r.Run(":8080") } ``` 三、运行与测试 1. 启动服务: ```bash export MACHINE_ID=1 # 不同节点设置不同ID go run main.go ``` 2. 测试请求: ```bash # 生成短链 curl -X POST http://localhost:8080/shorten \ -H "Content-Type: application/json" \ -d '{"url":"https://www.example.com/some/long/url"}' # 访问短链 curl -v http://localhost:8080/Abc123 ``` 四、系统优化方向 1. 性能提升: $$ \text{QPS} = \frac{\text{可用连接数}}{\text{平均响应时间}} $$ - 添加Redis缓存层 - 使用连接池 2. 扩展功能: ```go // 添加过期时间 type URLRecord struct { Original string ExpireAt time.Time } // 定时清理任务 func startCleanupTask() { ticker := time.NewTicker(1 * time.Hour) go func() { for range ticker.C { // 清理过期记录 } }() } ``` 3. 监控指标: - 使用Prometheus监控接口成功率 $$ \text{成功率} = \frac{\text{成功请求数}}{\text{总请求数}} \times 100\% $$ 需要补充实现分布式锁机制或数据库持久化存储方案吗?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值