前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TDDL分库分表生成全局唯一ID原理

TDDL分库分表生成全局唯一ID原理

作者头像
周同学
发布2020-12-23 11:43:51
1.9K0
发布2020-12-23 11:43:51
举报
文章被收录于专栏:一块自留地一块自留地

背景

在对数据库进行分库分表后,原本一个数据库上的自增id的结果,在分库分表下并不是全局唯一的. 所以,分库分表后需要有一种技术可以生成全局的唯一id。

要求

  • 全局唯一
  • 高性能
  • 高可用

几种常见的全局唯一ID实现思路

  • oracle sequence : 基于第三方oracle的SEQ.NEXTVAL来获取一个ID 优势:简单可用 缺点:需要依赖第三方oracle数据库
  • mysql id区间隔离 : 不同分库设置不同的起始值和步长,比如2台mysql,就可以设置一台只生成奇数,另一台生成偶数. 或者1台用0到10亿,另一台用10到20亿. 优势:利用mysql自增id 缺点:运维成本比较高,数据扩容时需要重新设置步长
  • 基于数据库更新+内存分配: 在数据库中维护一个ID,获取下一个ID时,会对数据库进行ID=ID+100 WHERE ID=XX,拿到100个ID后,在内存中进行分配 优势:简单高效 缺点:无法保证自增顺序

实现原理

TDDL是基于第三种思路进行实现的

sequence模型

SequenceDAO实现介绍: 因为需要对id进行持久化,所以需要在数据库中创建一个数据表来进行存储. sequence建表sql:

代码语言:javascript
复制
CREATE TABLE `sequence` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(64) NOT NULL,
    `value` bigint(20) NOT NULL,
    `gmt_create` timestamp DEFAULT CURRENT_TIMESTAMP,
    `gmt_modified` timestamp NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `unique_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

PS:

表中的name字段,对应于你自定义的一个sequence name,要求唯一. 比如用户可以为每张逻辑表定义一个sequence,不同sequence之间id分配互不干扰.表中的value就是对应的当前已配置的id值

源码
DefaultSequenceDao#nextRange()

获取下一个可用的id区间

代码语言:javascript
复制
public class DefaultSequenceDao implements SequenceDao {
	private static final Log log = LogFactory.getLog(DefaultSequenceDao.class);

	private static final int MIN_STEP = 1;
	private static final int MAX_STEP = 100000;
	private static final int DEFAULT_STEP = 1000;
	private static final int DEFAULT_RETRY_TIMES = 150;

	private static final String DEFAULT_TABLE_NAME = "sequence";
	private static final String DEFAULT_NAME_COLUMN_NAME = "name";
	private static final String DEFAULT_VALUE_COLUMN_NAME = "value";
	private static final String DEFAULT_GMT_MODIFIED_COLUMN_NAME = "gmt_modified";

	private static final long DELTA = 100000000L;

	private DataSource dataSource;

	private volatile String selectSql;
	private volatile String updateSql;

	//获取下一个id区间
	public SequenceRange nextRange(String name) throws SequenceException {
		if (name == null) {
			throw new IllegalArgumentException("");
		}

		long oldValue;
		long newValue;

		Connection conn = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;

		for (int i = 0; i < retryTimes + 1; ++i) {
			try {
				conn = dataSource.getConnection();
                		//拼装sql
				stmt = conn.prepareStatement(getSelectSql());
				stmt.setString(1, name);
				rs = stmt.executeQuery();
				rs.next();
				oldValue = rs.getLong(1);
				
                		//异常处理
				if (oldValue < 0) {
					//。。

					throw new SequenceException(message.toString());
				}

				if (oldValue > Long.MAX_VALUE - DELTA) {
					//。。

					throw new SequenceException(message.toString());
				}
				
                		//新的区间id最大值
				newValue = oldValue + getStep();
			} catch (SQLException e) {
				throw new SequenceException(e);
			} finally {
				//。。。
			}

			try {
				conn = dataSource.getConnection();
                		//sql 参数
				stmt = conn.prepareStatement(getUpdateSql());
				stmt.setLong(1, newValue);
				stmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
				stmt.setString(3, name);
				stmt.setLong(4, oldValue);
                		//通过乐观锁的方式,更新数据库
                		//update sequence set value = 2000 ,updateTime = now() where name = ? and value = 1000
				int affectedRows = stmt.executeUpdate();
				if (affectedRows == 0) {
					// retry
					continue;
				}

				return new SequenceRange(oldValue + 1, newValue);
			} catch (SQLException e) {
				throw new SequenceException(e);
			} finally {
				closeStatement(stmt);
				stmt = null;
				closeConnection(conn);
				conn = null;
			}
		}

		throw new SequenceException("Retried too many times, retryTimes = " + retryTimes);
	}

	private String getSelectSql() {
		if (selectSql == null) {
			synchronized (this) {
				if (selectSql == null) {
					StringBuilder buffer = new StringBuilder();
					buffer.append("select ").append(getValueColumnName());
					buffer.append(" from ").append(getTableName());
					buffer.append(" where ").append(getNameColumnName()).append(" = ?");

					selectSql = buffer.toString();
				}
			}
		}

		return selectSql;
	}

	private String getUpdateSql() {
		if (updateSql == null) {
			synchronized (this) {
				if (updateSql == null) {
					StringBuilder buffer = new StringBuilder();
					buffer.append("update ").append(getTableName());
					buffer.append(" set ").append(getValueColumnName()).append(" = ?, ");
					buffer.append(getGmtModifiedColumnName()).append(" = ? where ");
					buffer.append(getNameColumnName()).append(" = ? and ");
					buffer.append(getValueColumnName()).append(" = ?");

					updateSql = buffer.toString();
				}
			}
		}

		return updateSql;
	}

	public int getStep() {
		return step;
	}

	public void setStep(int step) {
		if (step < MIN_STEP || step > MAX_STEP) {
			StringBuilder message = new StringBuilder();
			message.append("Property step out of range [").append(MIN_STEP);
			message.append(",").append(MAX_STEP).append("], step = ").append(step);

			throw new IllegalArgumentException(message.toString());
		}

		this.step = step;
	}
}
复制代码

DAO层的核心就是: 获取数据库中的value值,然后给这个值加上步长step,组成id区间 如value=0,stpe=1000,则id区间为0~1000 获取区间之后,更新value为1000,更新数据库

DefaultSequence#nextValue()

获取一个可用id

代码语言:javascript
复制
public class DefaultSequence implements Sequence {
	private final Lock lock = new ReentrantLock();

	private SequenceDao sequenceDao;

	private String name;

	private volatile SequenceRange currentRange;

	//获取下一个可用的id
	public long nextValue() throws SequenceException {
		if (currentRange == null) {
        		//加锁,获取id区间
			lock.lock();
			try {
				if (currentRange == null) {
					currentRange = sequenceDao.nextRange(name);
				}
			} finally {
				lock.unlock();
			}
		}
		
       		//通过id区间,获取下一个可用id
		long value = currentRange.getAndIncrement();
		if (value == -1) {
        		//如果可用id大于区间最大值
                	//加锁,获取新的id区间
			lock.lock();
			try {
				for (;;) {
					if (currentRange.isOver()) {
						currentRange = sequenceDao.nextRange(name);
					}
					//从新的id区间,获取新的id
					value = currentRange.getAndIncrement();
					if (value == -1) {
						continue;
					}

					break;
				}
			} finally {
				lock.unlock();
			}
		}

		if (value < 0) {
			throw new SequenceException("Sequence value overflow, value = " + value);
		}
		return value;
	}
}

复制代码
SequenceRange

根据id区间,获取下一个id

代码语言:javascript
复制
public class SequenceRange {
	private final long min;
	private final long max;

	private final AtomicLong value;

	private volatile boolean over = false;

	public SequenceRange(long min, long max) {
		this.min = min;
		this.max = max;
		this.value = new AtomicLong(min);
	}

	public long getAndIncrement() {
    		//利用AtomicLong,获取下一个id
		long currentValue = value.getAndIncrement();
		if (currentValue > max) {
        		//如果超过区间最大值,则返回-1
			over = true;
			return -1;
		}

		return currentValue;
	}
}
复制代码

以上就是实现唯一ID的主要源码,需要注意的是,用此方法生成的id不是自增的。

总结

通过内存分配的方式,实现高性能 保证生成id的数据库可以是多机,其中一个或者多个数据库挂了,不能影响id获取,实现高可用

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 要求
  • 几种常见的全局唯一ID实现思路
  • 实现原理
    • sequence模型
      • 源码
        • DefaultSequenceDao#nextRange()
          • DefaultSequence#nextValue()
            • SequenceRange
            • 总结
            相关产品与服务
            云数据库 SQL Server
            腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档