背景
在对数据库进行分库分表后,原本一个数据库上的自增id的结果,在分库分表下并不是全局唯一的. 所以,分库分表后需要有一种技术可以生成全局的唯一id。
TDDL是基于第三种思路进行实现的
SequenceDAO实现介绍: 因为需要对id进行持久化,所以需要在数据库中创建一个数据表来进行存储. sequence建表sql:
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值
获取下一个可用的id区间
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,更新数据库
获取一个可用id
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;
}
}
复制代码
根据id区间,获取下一个id
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获取,实现高可用