分布式 ID 生成策略(一)

前言

在分库、分表之后,主键唯一ID的生成将不能基于数据库本身的自增方式生成,因为对于全局而言可能会重复,下面我们将构建一个外部服务统一提供 ID 的生成。

思路

我们采用一种基于数据库的分布式ID生成策略,每个节点维护一个本地的ID池,当节点的本地ID池耗尽时,再通过数据库去获取新的ID段,再放入 ID 池。

业务步长配置表

sql 复制代码
CREATE TABLE `sequence_step` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `barrier` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前序列值最大值',
  `step` int(11) NOT NULL DEFAULT '100' COMMENT '每次递增量',
  `sn` varchar(128) NOT NULL DEFAULT '' COMMENT '业务字段',
  `version` bigint(20) NOT NULL DEFAULT '1' COMMENT '乐观锁版本',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_sn` (`sn`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='全局ID生成表';
  • barrier:目前应用程序已经获取的最大ID值(即 ID 池的最大值);
  • step:每次获取时,递增的步长;
  • sn:业务字段名称,全局唯一。

可以通过表来配置每个业务字段的 ID 生成策略,比如 user_id,初始值是 1,步长是 100。

第一次的 ID 区间为【1,101),初始值更新为 101,则 ID 池子的数字就是【1,101)

系统优先去 ID 池子去取数据,当 ID 池子消耗完了后,再请求数据库,拿到新的初始值,并生成数据。

第二次的 ID 区间为【101,201),初始值更新为 201

...

可以看到,如果区间设置的太小,访问数据库就很频繁;区间设置的太大,万一系统崩溃了,断续的间隔就比较大了。

代码实现

整体流程如下

  1. 根据 sn 获取锁对象;
  2. 根据 sn 去本地内存获取 ID 段,如果不为空,则返回;否则,进入下一步;
  3. 根据 sn 去数据库获取 ID 段,并放入内存 ID 池。

ID 段

java 复制代码
@NotThreadSafe
public class IDSequence implements Serializable {

    private String sn;//此sequence的唯一标识

    private int step;

    private AtomicLong current;//当前值

    private long barrier = -1;//栅栏,当前sequence所在step中的最大值。

    public IDSequence() {}

    public IDSequence(String sn) {
        this.sn = sn;
    }

    public IDSequence(String sn, long start, long barrier, int step) {
        this.sn = sn;
        this.current = new AtomicLong(start);
        this.barrier = barrier;
        this.step = step;
    }


    public String getSn() {
        return sn;
    }

    public void setSn(String sn) {
        this.sn = sn;
    }

    public long getCurrent() {
        if (current == null) {
            return -1;
        }
        return current.get();
    }

    public boolean valid() {
        return current != null;
    }

    /**
     * 此方法将会在并发环境中执行,需要注意控制数据安全性
     *
     * @return 0:表示超限,则需要重试
     * 负1:表示超限,需要重新fetch新的step,当前Sequence已不可用
     * 大于1:正常,此值可以使用。
     */
    public long increment() {
        if (current == null) {
            return -1;
        }
        long i = current.getAndIncrement();
        //边界
        if (i >= barrier) {
            current = null;
            return -1;//强制上层重置
        }
        return i;

    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("sn:").append(sn)
                .append(",step:").append(step)
                .append(",current:").append(current)
                .append(",barrier:").append(barrier);
        return sb.toString();
    }
}

ID 生成器

java 复制代码
public class StepBasedIDGenerator{
    protected static final int DEFAULT_RETRY = 6;

    protected Map<String, IDSequence> cachedPool = new HashMap<>();

    private static Map<String,Object> locks = new ConcurrentHashMap<>(); //业务字段对应的锁

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取锁对象
     * @param sn
     * @return
     */
    private Object getLock(String sn) {
        return locks.computeIfAbsent(sn, k -> new Object());
    }


    private IDSequence getSequence(String sn) {
        IDSequence sequence = cachedPool.get(sn);
        //正常,返回
        if (sequence != null && sequence.valid()) {
            return sequence;
        }
        int i = 0;
        while (i < DEFAULT_RETRY) {
            sequence = fetch(sn);
            if (sequence != null) {
                cachedPool.put(sn, sequence);
                break;
            }
            i++;
        }
        return sequence;
    }

    /**
     * ID 生成器的入口
     */
    public long next(String sn) {

        Object lock = getLock(sn);

        synchronized (lock) {
            IDSequence sequence = getSequence(sn);
            if (sequence == null || !sequence.valid()) {
                throw new IllegalStateException("sn:" + sn + ",cant fetch sequence!");
            }

            try {
                long v = sequence.increment();
                if (v > 0) {
                    return v;
                }
                return next(sn);
            } catch (Exception e) {
                throw new RuntimeException("fetch nextStep error:", e);
            }
        }
    }


    private Connection getConnection() {
        // 数据库URL,用户名和密码
        String url = "";
        String username = "";
        String password = "";

        Connection conn = null;
        try {
            // 加载数据库驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 创建数据库连接
            conn = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }

    protected IDSequence fetch(String sn) {
        Connection connection = null;
        PreparedStatement ps = null;
        try {
            connection = getConnection();
            connection.setAutoCommit(true);//普通操作
            connection.setReadOnly(false);

            ps = connection.prepareStatement("select barrier,step,`version` from `sequence_step` where `sn` = ? limit 1");
            ps.setString(1, sn);
            ResultSet rs = ps.executeQuery();
            if (!rs.next()) {
                throw new IllegalStateException(sn + " is not existed,So fetch sequence cant be executed! Please check it!");
            }
            long _b = rs.getLong(1);//barrier
            int _s = rs.getInt(2);//step
            long _v = rs.getLong(3);//version

            ps.close();

            ps = connection.prepareStatement("update `sequence_step` set barrier = ?,update_time = ?,`version` = `version` + 1 where `sn` = ? and `version` = ?");
            long barrier = _b + _s;

            ps.setLong(1, barrier);
            ps.setDate(2, new Date(System.currentTimeMillis()));
            ps.setString(3, sn);
            ps.setLong(4, _v);
            int row = ps.executeUpdate();

            ps.close();
            if (row > 0) {
                //更新成功
                return new IDSequence(sn, _b, barrier, _s);
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (ps != null) {
                    ps.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (Exception ex) {
                //
            }
        }
    }
}
相关推荐
月光水岸New1 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6751 小时前
数据库基础1
数据库
我爱松子鱼1 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo1 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser2 小时前
【SQL】多表查询案例
数据库·sql
Galeoto2 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
人间打气筒(Ada)3 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231113 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
喝醉酒的小白3 小时前
PostgreSQL:更新字段慢
数据库·postgresql
敲敲敲-敲代码3 小时前
【SQL实验】触发器
数据库·笔记·sql