TDDL 在分布式下的SEQUENCE原理

TDDL大家应该很熟悉了,淘宝分布式数据层。很好的为我们实现了分库分表、Master/Salve、动态数据源配置等功能。

那么分布式之后,数据库自增序列肯定用不了了,如何方便快捷的解决这个问题呢?TDDL也提供了SEQUENCE的解决方案。

总述

在数据库中创建 sequence 表,用于记录,当前已被占用的id最大值。

每台客户端主机取一个id区间(比如 1000~2000)缓存在本地,并更新 sequence 表中的id最大值记录。

客户端主机之间取不同的id区间,用完再取,使用乐观锁机制控制并发。

第一步:创建一张sequence对应的表

CREATE TABLE `imp_sequence` (
  `BIZ_NAME` varchar(45) NOT NULL COMMENT '业务名称',
  `CURRENT_VALUE` int(11) NOT NULL COMMENT '当前最大值',
  `GMT_CREATE` datetime DEFAULT NULL COMMENT '创建时间',
  `GMT_MODIFIED` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`BIZ_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据序列表';

表名和字段可以按各自规则定义,定义之后需要与第二步DAO中的定义相对应!

几张逻辑表需要声明几个sequence。

第二步:配置sequenceDao

<bean id="sequenceDao" class="com.taobao.tddl.client.sequence.impl.DefaultSequenceDao">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource" />
<!-- 步长-->
<property name="step" value="1000" />
<!-- 重试次数-->
<property name="retryTimes" value="1" />
<!-- sequence 表名-->
<property name="tableName" value="gt_sequence" />
<!-- sequence 名称-->
<property name="nameColumnName" value="BIZ_NAME" />
<!-- sequence 当前值-->
<property name="valueColumnName" value="CURRENT_VALUE" />
<!-- sequence 更新时间-->
<property name="gmtModifiedColumnName" value="gmt_modified" />
</bean>

第三步:配置sequence生成器

<bean id="businessSequence"  class="com.taobao.tddl.client.sequence.impl.DefaultSequence">
<property name="sequenceDao" ref="sequenceDao"/>
<property name="name" value="business_sequence" />
</bean>

第四步:调用

public class IbatisSmDAO extends SqlMapClientDaoSupport implements SmDAO {

  /**smSequence*/
private DefaultSequence businessSequence; public int insert(SmDO sm) throws DataAccessException {
if (sm == null) {
throw new IllegalArgumentException("Can't insert a null data object into db.");
} try {
sm.setId((int)businessSequence.nextValue());
} catch (SequenceException e) {
throw new RuntimeException("Can't get primary key.");
} getSqlMapClientTemplate().insert("MS-SM-INSERT", sm); return sm.getId();
}
}

从调用配置中,我们可以发现其中涉及到二个重要类DefaultSequenceDao和DefaultSequence,这二个都是TDDL的默认实现。DefaultSequenceDao:序列DAO默认实现,JDBC方式。DefaultSequence:序列默认实现。

先来看DefaultSequenceDao,TDDL中提供了默认的表名,列名和步长等,第一步的建表可以参照默认方式。

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 int retryTimes = DEFAULT_RETRY_TIMES; /**
* 步长
*/
private int step = DEFAULT_STEP; /**
* 序列所在的表名
*/
private String tableName = DEFAULT_TABLE_NAME; /**
* 存储序列名称的列名
*/
private String nameColumnName = DEFAULT_NAME_COLUMN_NAME; /**
* 存储序列值的列名
*/
private String valueColumnName = DEFAULT_VALUE_COLUMN_NAME; /**
* 存储序列最后更新时间的列名
*/
private String gmtModifiedColumnName = DEFAULT_GMT_MODIFIED_COLUMN_NAME;

接下来看一下nextRange方法:取得下一个可用的序列区间:

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();
stmt = conn.prepareStatement(getSelectSql());
stmt.setString(1, name);
rs = stmt.executeQuery();
rs.next();
oldValue = rs.getLong(1); if (oldValue < 0) {
StringBuilder message = new StringBuilder();
message.append("Sequence value cannot be less than zero, value = ").append(oldValue);
message.append(", please check table ").append(getTableName()); throw new SequenceException(message.toString());
} if (oldValue > Long.MAX_VALUE - DELTA) {
StringBuilder message = new StringBuilder();
message.append("Sequence value overflow, value = ").append(oldValue);
message.append(", please check table ").append(getTableName()); throw new SequenceException(message.toString());
} newValue = oldValue + getStep();
} catch (SQLException e) {
throw new SequenceException(e);
} finally {
closeResultSet(rs);
rs = null;
closeStatement(stmt);
stmt = null;
closeConnection(conn);
conn = null;
} try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(getUpdateSql());
stmt.setLong(1, newValue);
stmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
stmt.setString(3, name);
stmt.setLong(4, oldValue);
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);
}

通过getSelectSql查询最新的value值,然后加上步点,通过getUpdateSql更新到数据库中

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;
}

有一个特殊需要说明的,在update语句中,where需要把之前的value当成条件传入。实现了类型version的乐观锁操作。如果同一个时间AB二台机器同时请求获取到相同的value,进行update操作只有可能一条成功。失败的会按retryTimes进行重试。

接下来看DefaultSequence,比较简单,就不说明了

public class DefaultSequence implements Sequence {
private final Lock lock = new ReentrantLock(); private SequenceDao sequenceDao; /**
* 序列名称
*/
private String name; private volatile SequenceRange currentRange; public long nextValue() throws SequenceException {
if (currentRange == null) {
lock.lock();
try {
if (currentRange == null) {
currentRange = sequenceDao.nextRange(name);
}
} finally {
lock.unlock();
}
} long value = currentRange.getAndIncrement();
if (value == -1) {
lock.lock();
try {
for (;;) {
if (currentRange.isOver()) {
currentRange = sequenceDao.nextRange(name);
} value = currentRange.getAndIncrement();
if (value == -1) {
continue;
} break;
}
} finally {
lock.unlock();
}
} if (value < 0) {
throw new SequenceException("Sequence value overflow, value = " + value);
} return value;
} public SequenceDao getSequenceDao() {
return sequenceDao;
} public void setSequenceDao(SequenceDao sequenceDao) {
this.sequenceDao = sequenceDao;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
}

TDDL生成全局ID原理的更多相关文章

  1. 分布式系统中生成全局ID的总结与思考

    世间万物,都有自己唯一的标识,比如人,每个人都有自己的指纹(白夜追凶给我科普的,同卵双胞胎DNA一样,但指纹不一样).又如中国人,每个中国人有自己的身份证.对于计算机,很多时候,也需要为每一份数据生成 ...

  2. mysql生成全局id(转)

    由于数据量以及IO效率的因素,很多项目对数据支持的数据库会采取分库分表的方式.使用了分库分表之后需要解决的一个问题就是主键的生成.多个表之间的主键就不能用数据库本身的自增主键来支持,因为不同表之间生成 ...

  3. 高并发分布式系统中生成全局唯一Id汇总

    数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:   1 不能有单点故障.   2 以时间为序,或者ID里包含时间 ...

  4. 分布式高并发下全局ID生成策略

    数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求:   1 不能有单点故障.   2 以时间为序,或者ID里包含时间 ...

  5. 【php】mysql全局ID生成方案

    生产系统随着业务增长总会经历一个业务量由小变大的过程,可扩展性是考量数据库系统高可用性的一个重要指标;在单表/数据库数据量过大,更新量不断飙涨时,MySQL DBA往往会对业务系统提出sharding ...

  6. 高并发情况下,如何生成分布式全局id

    1.使用UUID生成全局id,不占用宽带 2.基于数据库自增或者序列生成全局id,占用宽带,设置自增步长实现集群,但可扩展性差 3.基于redis生成全局id,占用宽度,设置自增步长实现集群,性能比数 ...

  7. 常见的生成全局唯一id有哪些?他们各有什么优缺点?

    分布式系统中全局唯一id是我们经常用到的,生成全局id方法由很多,我们选择的时候也比较纠结.每种方式都有各自的使用场景,如果我们熟悉各种方式及优缺点,使用的时候才会更方便.下面我们就一起来看一下常见的 ...

  8. 高并发情况下分布式全局ID

    1.高并发情况下,生成分布式全局id策略2.利用全球唯一UUID生成订单号优缺点3.基于数据库自增或者序列生成订单号4.数据库集群如何考虑数据库自增唯一性5.基于Redis生成生成全局id策略6.Tw ...

  9. 分布式全局ID生成器设计

    项目是分布式的架构,需要设计一款分布式全局ID,参照了多种方案,博主最后基于snowflake的算法设计了一款自用ID生成器.具有以下优势: 保证分布式场景下生成的ID是全局唯一的 生成的全局ID整体 ...

随机推荐

  1. hdfs中删除文件、文件夹、抓取内容

    删除文件   bin/hdfs dfs -rm output2/* 删除文件夹   bin/hdfs dfs -rm -r output2 抓取内容     bin/hdfs dfs -cat /us ...

  2. nginx利用fastcgi_cache模块缓存

    nginx不仅有个大家很熟悉的缓存代理后端内容的proxy_cache,还有个被很多人忽视的fastcgi_cache.proxy_cache的作用是缓存后端服务器的内容,可能是任何内容,包括静态的和 ...

  3. MFC BASE64加解密 算法

    unsigned char * base64 = (unsigned char *)"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ...

  4. 《Fluid Engine Development》 学习笔记4-预测校正不可压缩SPH-PCISPH

    传统SPH方案的主要问题之一是时间步长限制.在原始的SPH中,我们首先从当前设置计算密度,使用EOS计算压强,应用压力梯度,然后运行时间积分.这个过程意味着只需要一定的压缩量就可以触发内核半径内的压力 ...

  5. 排序算法的实现之Javascript(常用)

    排序算法的实现之Javascript 话不多说,直接代码. 1.冒泡排序 1.依次比较相邻的两个数,如果前一个比后一个大,则交换两者的位置,否则位置不变 2.按照第一步的方法重复操作前length-1 ...

  6. JDK替换掉系统自带的gij编译工具

    解决办法: 在终端里面依次输入以下两句话 alternatives --install /usr/bin/java java /usr/java/jdk1.6.0_11/bin/java 300 al ...

  7. 机器学习_第一节_numpy

    今天学了机器学习第一节, 希望能够坚持下去,其实不在乎课程是什么?关键要坚持下去 今天主要学了对矩阵的一些操作, 用的库是numpy 开始从头到尾捋一遍, 作者说的很有道理,学计算机,动手能力要强,所 ...

  8. v-bind 绑定属性

    与mustache相区别,他是对内容(content内部)进行修改的.v-bind的语法糖写法是   : v-bind 动态绑定class属性:v-bind:class="对象名" ...

  9. MAVEN(二)

    1.本地仓库?Maven到底有哪些仓库?它们什么关系? Maven仓库: 本地仓库路径配置: 包查找路径:本地——>私服——>中央仓库,然后将查找到的jar同步到私服——>本地仓库 ...

  10. Scala调用Kafka的生产者和消费者Demo,以及一些配置参数整理

    kafka简介 Kafka是apache开源的一款用Scala编写的消息队列中间件,具有高吞吐量,低延时等特性. Kafka对消息保存时根据Topic进行归类,发送消息者称为Producer,消息接受 ...