自增id一般情况下有两种方案,一个是使用数据库自增功能,另外一种就是oracle这样的sequance机制。
个人觉得,无论你的系统是否考虑日后分布式扩展,建议统一采用sequance方式获取。这样对系统日后可能产生的数据库移植也是很好的支持。
比如说系统需要获得一个表的 新id,可以这样统一封装一个方法:seuHelper.getTableSeq(tableName);
public long getTableSeq(String tableName) { synchronized(tableName)//重要,确保每个表一个锁 { //自己实现sequance机制 } }
对于oracle有自己的sequance机制,实现起来就很容易了。但是对于mysql来说,它只有自增方式。如果是做大型集群、分库分表又如何实现一个统一的sequance机制呢?
经过网上查找,发现flicker采用了一种非常巧妙的既简单又可行的实现方案:
1、建一个存放对应表的sequance表
CREATE TABLE `order_seq` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`stub` char(1) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB AUTO_INCREMENT=102951 DEFAULT CHARSET=utf8;
这里有几个关键点:
每个表,一个sequance表,确保性能
UNIQUE KEY,确保每个表,只有一条记录,避免数据无限增大,提高性能
2、自增和获取最新id
REPLACE INTO pro_seq (stub) VALUES ('a')
SELECT LAST_INSERT_ID()
这里需要注意的是,这两个操作,需要放在一个连接里面执行,否则SELECT LAST_INSERT_ID()可能查询不到正确结果。另外还有一个重要原因,SELECT LAST_INSERT_ID()只会查询到当前连接最新的自增id,
也就是说,其他连接更新其他seq表,互不干扰。正是这种基础,保证了我们能建立多个seq表实现seq服务。
测试:
为了测试多个表,多并发下,是否正常互不干扰产生自增id,并且不重复,我们再建立几个表
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`oid` bigint(20) NOT NULL,
PRIMARY KEY (`oid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入新id,检测是否有主键重复
CREATE TABLE `pro_seq` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`stub` char(1) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB AUTO_INCREMENT=22207 DEFAULT CHARSET=utf8;
商品表seq
CREATE TABLE `pros` (
`pid` bigint(20) NOT NULL,
PRIMARY KEY (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入商品id,检测是否重复主键
数据库操作代码
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void addOrder(long id) throws DataAccessException { String sql = "insert into orders(oid) values(?)"; jdbcTemplate.update( sql, new Object[] { id }); } @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public synchronized long getOrdersSeq() throws DataAccessException { jdbcTemplate.update("REPLACE INTO order_seq (stub) VALUES ('a')"); Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class); return id.longValue(); } @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public synchronized long getProSeq() throws DataAccessException { jdbcTemplate.update("REPLACE INTO pro_seq (stub) VALUES ('a')"); Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class); return id.longValue(); } @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void addPro(long id) throws DataAccessException { String sql = "insert into pros(pid) values(?)"; jdbcTemplate.update( sql, new Object[] { id }); }
并发测试代码
final SeqHelper seqHelper = new FileSystemXmlApplicationContext(Utils.getRootPath()+"/conf/app-context.xml").getBean(SeqHelper.class); new Thread(new Runnable() { @Override public void run() { for (int i=0; i<10; i++) { for (int j=0; j<2000; j++) { new Thread(new Runnable() { @Override public void run() { long id = seqHelper.getOrdersSeq(); System.out.println("order------>"+id); seqHelper.addOrder(id); } }).start(); } } } }).start(); for (int i=0; i<10; i++) { for (int j=0; j<2000; j++) { new Thread(new Runnable() { @Override public void run() { long id = seqHelper.getProSeq(); System.out.println("pro ------>"+id); seqHelper.addPro(id); } }).start(); } }
观察测试过程,并没有出现主键冲突异常。
order------>122943
order------>122944
order------>122945
order------>122946
order------>122947
order------>122948
pro ------>42202
pro ------>42203
order------>122949
pro ------>42204
order------>122950
pro ------>42205
pro ------>42206
从测试结果看到,order表和pro表,产生的id是互相对立的,并不是顺序一起的。
结论是这个方案确实可行,在大型分片集群中,我们可以使用一台独立的数据库,作为专门的seq服务器。对于单点问题,flicker也给出一个很巧妙的方法,就是两台服务器做一个轮训负载,分别设置两台数据库产生的id方式为奇、偶,这样即使一台出现当机,也不会出现id混乱问题。对于小型无分布式应用,我们可以把seq表直接建立在同一个库中,这样以后扩展也是非常方便的。