第三节:分库分表下订单ID的生成的几种方案

一. 背景

 主流架构一般分库分表都会涉及,追求性能的同时,带来各种痛点。

   比如订单id的生成,在分表的情况下,使用int自增,两张分表都自增,直接会导致主键ID的重复,这是错误的,本节主要就是解决分库分表情况下Id的生成规则。

 下面先补充一下常见的分库分表中间件:

1. DB层次的,针对DB做代理。

ShardingSphere-Proxy的架构图

 

2. 代码层次的  

   最常见的是 sharding-jdbc (他和 shardingsphere-proxy是同一产品下的东西,只是作用的层次不同),如下图:

二者的区别详见:https://shardingsphere.apache.org/document/legacy/3.x/document/cn/overview/

 

二.  方案剖析

1. 自增方案

方案1:起始点分段

  比如设置分表2的起始点从10000开始

ALTER TABLE incorder_1 AUTO_INCREMENT=10000;

优缺点:

  简单容易,数据库层面设置,代码是不需要动的

  边界的切分人为维护,操作复杂,触发器自动维护可以实现但在高并发下不推荐

方案2:分段步长的自增 

--查看
show session variables like 'auto_inc%';
show global variables  like 'auto_inc%';
--设定自增步长
set session auto_increment_increment=2;
--设置起始值
set session auto_increment_offset=1;
--全局的
set global auto_increment_increment=2;
set global auto_increment_offset=1;

分析:

影响范围不可控,要么session每次设置,忘记会出乱子。要么全局设置,影响全库所有表

结论:不可取!!!

方案3:Sequence特性 

仅限于oracle和sqlserver,主流mysql不支持

-- 创建一个sequence:
create sequence sequence_name as int minvalue 1 maxvalue 1000000000 start with 1
increment by 1 no cache;
-- sequence的使用:
sequence_name.nextval
sequence_name.currval
-- 在表中使用sequence:
insert into incorder_0 values(sequence_name.nextval,userid);

 

2. 基于业务规则自定义生成

 不用自增,自定义id,加上业务属性,从业务细分角度对并发性降维。例如淘宝,在订单号中加入用户id。

 加上用户id后,并发性维度降低到单个用户,每个用户的下单速度变的可控

 时间戳+userid,业务角度,一个正常用户不可能1毫秒内下两个单子,即便有说明是刻意刷单,应该被前端限流。

    [HttpPost]
    public IActionResult CreateIdWay1()
    {
        //时间戳+userId
        // 转换为毫秒时间戳
        long unixTimestampMilliseconds = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds;
        string userId = "user001";
        string orderId = unixTimestampMilliseconds + "_" + userId;
        return Json(new { status = "ok", msg = "获取成功", data = orderId });
    } 

 

3. 集中式分配

方案1-MaxId表 (了解思路即可)

(1) 创建一张maxId表

CREATE TABLE `maxid` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`nextid` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
);
insert into maxid(name,nextid) values ('myOrderId',1000);

(2) 创建一个函数,方便调用,生成orderId

DROP FUNCTION IF EXISTS getid;

-- 创建函数
CREATE FUNCTION getid(myOrderId VARCHAR(50))
RETURNS BIGINT(20)
BEGIN
    -- 定义变量
    DECLARE id BIGINT(20);
    
    -- 给定义的变量赋值
    UPDATE maxid SET nextid = nextid + 1 WHERE name = myOrderId;
    SELECT nextid INTO id FROM maxid WHERE name = myOrderId;
    
    -- 返回函数处理结果
    RETURN id;
END;

(3) 调用函数,获取orderId 

SELECT getid('myOrderId') from dual

(4). 通过程序代码调用getId函数即可

(省略)

 

方案2-redis缓存  【推荐】

利用redis的单线程、原子性、自增性来生成id使用。

   [HttpPost]
   public IActionResult CreateIdWay3()
   {
       long orderId = RedisHelper.IncrBy("orderId");
       return Json(new { status = "ok", msg = "获取成功", data = orderId });
   }

分析:

 需要额外的中间件redis

 与db相比不够直观,不方便查看当前增长的id值,需要额外连接redis服务器读取

 性能不是问题,redis得到业界验证和认可

 对redis集群的可靠性要求很高,禁止出现故障,否则全部入库被阻断

 数据一致性需要注意,尽管redis有持久策略,down机恢复时需要确认和当前库中最大id的一致性

 

4. uuid和guid

java中uuid

public Strorder uuid(int userid){
  Strorder order = new Strorder();
  order.setId(UUID.randomUUID().toString());
  order.setUserid(userid);
  strorderMapper.save(order);
  return order;
}

.Net中Guid

    [HttpPost]
    public IActionResult CreateIdWay4()
    {
        var orderId0 = Guid.NewGuid();
        string orderId1 = Guid.NewGuid().ToString();
        string orderId2 = Guid.NewGuid().ToString("N");
        return Json(new { status = "ok", msg = "获取成功", data = new { orderId0, orderId1, orderId2 } });
    }

分析:

 最简单的方案,数据迁移方便

 缺点也是非常明显的,太过冗长,非常的不友好,可读性极差

 需要使用字符串存储,占用大量存储空间

 在建立索引和基于索引进行查询时性能不如数字

 

5. 雪花算法【推荐!!】

  详见下面

 

 

三. 雪花算法

1. 简介

  UUID 能保证时空唯一,但是过长且是字符,雪花算法由Twitter发明,是一串数字。

  Snowflake是一种约定,它把时间戳、工作组 ID、工作机器 ID、自增序列号组合在一起,生成一个64bits 的整数 ID,能够使用 (2^41)/(1000*60*60*24*365) = 69.7 年,每台机器每毫秒理论最多生成 2^12 个 ID

1 bit:固定为0

  二进制里第一个bit如果是 1,表示负数,但是我们生成的 id都是正数,所以第一个 bit 统一都是 0。

41 bit:时间戳,单位毫秒

  41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值。

注意:这个时间不是绝对时间戳,而是相对值,所以需要定义一个系统开始上线的起始时间

10 bit:哪台机器产生的

  代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。官方定义,前5 个 bit 代表机房 id,后5 个 bit 代表机器 id。这10位是机器维度,可以根据公司的实际情况自由定制。

12 bit:自增序列

   同1毫秒内,同一机器,可以产生2 ^ 12 - 1 = 4096个不同的 id。

优缺点:

 不依赖第三方介质例如 Redis、数据库,本地程序生成分布式自增 ID

 只能保证在工作组中的机器生成的 ID 唯一,不同组下可能会重复

 时间回拨后,生成的 ID 就会重复,所以需要保持时间是网络同步的。

2. 实操

代码实现

using System;


/// <summary>
/// 雪花算法
/// 已经考虑了多线程问题 和 时钟回拨问题
/// </summary>
public class Snowflake
{
    private static readonly object lockObj = new object();

    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    private long maxWorkerId = -1L ^ (-1L << 5);
    private long maxDatacenterId = -1L ^ (-1L << 5);
    private long sequenceMask = -1L ^ (-1L << 12);

    private static readonly long twepoch = DateTime.Parse("2021-01-01").Ticks;

    public Snowflake(long workerId, long datacenterId)
    {
        if (workerId > maxWorkerId || workerId < 0)
            throw new ArgumentException($"worker Id can't be greater than {maxWorkerId} or less than 0");

        if (datacenterId > maxDatacenterId || datacenterId < 0)
            throw new ArgumentException($"datacenter Id can't be greater than {maxDatacenterId} or less than 0");

        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public long NextId()
    {
        lock (lockObj)
        {
            long timestamp = GetTimestamp();

            if (timestamp < lastTimestamp)
                throw new Exception($"Clock moved backwards. Refusing to generate id for {lastTimestamp - timestamp} milliseconds");

            if (lastTimestamp == timestamp)
            {
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0)
                    timestamp = TilNextMillis(lastTimestamp);
            }
            else
            {
                sequence = 0;
            }

            lastTimestamp = timestamp;

            return ((timestamp - twepoch) << 22)
                | (datacenterId << 17)
                | (workerId << 12)
                | sequence;
        }
    }

    private long TilNextMillis(long lastTimestamp)
    {
        var timestamp = GetTimestamp();
        while (timestamp <= lastTimestamp)
        {
            timestamp = GetTimestamp();
        }
        return timestamp;
    }

    private long GetTimestamp()
    {
        return DateTime.UtcNow.Ticks - twepoch;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var snowflake = new Snowflake(workerId: 1, datacenterId: 1);

        for (int i = 0; i < 10; i++)
        {
            long id = snowflake.NextId();
            Console.WriteLine($"Generated ID: {id}");
        }
    }
}
View Code

调用

   [HttpPost]
   public IActionResult CreateIdWay5()
   {
       var snowflake = new Snowflake(workerId: 1, datacenterId: 1);
       long orderId = snowflake.NextId();
       return Json(new { status = "ok", msg = "获取成功", data = orderId });
   }

返回结果

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2023-08-23 09:26  Yaopengfei  阅读(377)  评论(1编辑  收藏  举报