关于id generator

前几日,徐XX同学翻出了一篇mixi engineer blog上的老文,地址是
http://alpha.mixi.co.jp/blog/?p=272。其中一部分提到了如下的情况。

mixi的日记服务,在表设计上是采用了sharding的方式,即日记并不是全部存放
在一张表内的,而是根据member_id放入不同库的相同表结构中。因为不同的库
可以放在不同的物理机器上,所以这样做不仅减少了每张表的大小,也能够同时
利用起多台机器的资源,加速对表的读写。

Sharding带来了好处,也会带来负面影响。因为日记存放在不同的数据表中,又
要保持日记ID的全局唯一性,那么我们就无法再利用每张表各自的
auto_increment来生成主键了。

既然要生成全局唯一的ID,一个容易想到的方法是虽然日记被写入了不同的表,
但是我们仍然可以在一张表内来生成ID。Mixi也是这么做的。

表结构如下:
create table id_pot (
    id bigint unsigned not null
) engine=innodb;
//注意,这张表没有申明主建。Mysql会隐含地帮你生成主键。

获取唯一ID的方法:
UPDATE id_pot SET id = LAST_INSERT_ID(id+1);
SELECT LAST_INSERT_ID();
//关于LAST_INSERT_ID可以查阅MySQL的官方文档

然后,就如blog上所说的,在过年的时候,大量的用户在写日记,而瓶颈就出现
在了这获取唯一主键上。经过调查发现,是因为有大量的数据库连接在并行着获
取ID而产生了死锁。

其实我觉得这里并不是真正意义上的死锁,死锁是指线程A需要获得X,Y两个资源
的锁来完成一个事务,而线程B也是同样,只是A和B在获取锁的顺序上不一致,如
果A先获得了X的锁,而B先获得了Y的锁,在这种情况下就会产生死锁。(补充:
在Innodb的事务中,锁是在真正需要的时候才会去获得,并在整个事务结束后才
会释放。)而在这里,所有的线程都只是想获得一行数据的锁,这个时候就需要
排队,同时Mysql也会尝试判断是否产生了死锁。而这里发生的“死锁”就源于尝
试去解决死锁。简单看了下源代码,大致的调用关系如下:

lock_rec_lock -> lock_rec_lock_slow -> lock_rec_enqueue_waiting ->
lock_deadlock_occurs -> lock_deadlock_recursiv -> return
LOCK_EXCEED_MAX_DEPTH

在源代码中有这样一个常值LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK, 值为200死锁检
查是一个递归的过程,当深度大于这个常值时就会放弃检查,返回
LOCK_EXCEED_MAX_DEPTH。这样做的原因应该是处于对cpu资源的考量。

存疑:按理说一句update只要加一个排它锁,对于没有持有锁的transaction是
不是没有必要检查死锁

原因发现了,那么解决方法就是减少对一行数据的并发执行数。blog的作者采取
了将原本多台application server(比如有200台,每台apache的max_client为
100, 每个apache进程持有一个数据库链接)直接连mysql,改为连接N台ID
generate API Server,然后由API Server去连数据库。API Server的
max_client设为10,保证这台server对mysql的并发更新数最大为10,另外因为
tcp协议中连接是可以排队的并且获取一个id所花费的时间是很短的,所以不会
有什么大的影响,实际测试下来一台机器可以达到2000qps (之前mybench的测试
结果挺奇怪的,qps才200?)

在这之后呢,kzys又写了一篇相关的文章
http://alpha.mixi.co.jp/blog/?p=2453
在这篇文章中提到,上述的做法后来遇到了too many connections的情况(mysql
有一个变量叫max_connections, 可以通过show global variables like
'max_connections';来查看。当连接数超过这个数值时,就会报too many
connections)。原因文章里没有细说,比较奇怪,照我的理解如果1台api
server保持着10个数据库连接,而mysql的max_connections是知道的,所以最多
放几台api server是完全可以计算的。当application server向api server的请
求非常大时,整体的响应时间会增大。但是应该也不至于在mysql上出现too
many connections阿...

其实改善id generator的一个比较简单的方法就是把engine从Innodb改成MyIsam。
MyIsam相比Innodb而言,不支持事务,也就是不会存在死锁,不需要去检查是否
有死锁。只能表锁不能行锁。这两点其实很适合于高并发的id generator。不会
死锁,也就没有了Innodb200个并发的限制,而表锁的实现逻辑必然大大简单于多
版本控制的行锁,速度也会更快。

我做了一些简单的测试:

方案1:
create table id_pot(id int unsigned not null, counter bigint unsigned
not null, primary key(id));
insert into id_pot values(1, 0);

update id_pot set counter = last_insert_id(counter+1) where id = 1;
select last_insert_id();

进程数     100        200       300       400       每个进程执行200次
Innodb   2213       2157    deadlock    deadlock     单位QPS
MyIsam   2121       2180      2098      2090

Innodb在200以下和MyIsam不相上下,超过以后就如预期的开始产生死锁。而
MyIsam因为是表锁,多大的并发其实还是在顺序的一个个处理的,在各个并发数下都很稳定。

方案2:
create table id_pot(id bigint unsignet not null auto_increment, stub
char(1) not null, primary key(id), unique key(stub));
insert into id_pot(0, 'a');

replace into id_pot(stub) values('a');
select last_insert_id();

进程数     100        200       300       400       每个进程执行200次
Innodb    415        353    deadlock    deadlock     单位QPS
MyIsam   2248       2185      2131      2136

MyIsam依然稳定,而Innodb就较之前的方法慢了很多。这次采用的是replace并
利用auto_increment来获得id。之前还试过一个直接在主键上id =
last_insert_id(id+1),速度更慢,惨不忍睹。所以也许是和频繁改变主键导致
簇索引(B+tree一个)频繁结构变化有关。
       
如果2200左右的QPS无法满足需求了呢,除了提高硬件以外,一种简单的方式就
是使用两台mysql,并初始化设定一台从1开始,一台从2开始,并且步长都是2。
这样保证了两台生成的ID不会重复。但是这样做的一个缺点时,id对应的记录不
再是按时间顺序排列的了。

Mixi的日记服务到目前为止还是在使用一张MyIsam表来提供主键生成。

Mixi blog里还有很多料,有些我们现在看起来会觉得有点奇怪的地方都还是有其
历史原因的。我们会发现echo这个功能的主键是member_id + timestamp, 这个功
能发布于日记id生成的瓶颈还没有解决的情况下。这样的主键有2个缺点,一个是
功能上的限制了一个member_id在同一timestamp下不能有两条echo,一个是因为所
有的secondary索引(就是我们平时定义的索引)中必然包含着簇索引(主键索
引),所以主键越大,所有的secondary索引也都变大。考虑到echo的发布频率一
定比日记还要高很多,所以采取了member_id + timestamp这个没有主键生成瓶颈
的策略。member_id + timestamp有两种形式,一种就是member_id +
timestamp,还有一种是timestamp + member_id,这两种主键是有比较大的区别
的。因为Innodb的簇索引是一颗B+tree,按照主键的的大小顺序排序。前者是
member_id在前,所以插入的数据是非有序的,B+tree的结构调整会比较多,而如
果是timestamp在前,那么插入的记录是有序的概率会大很多。光论插入的性
能,timestamp放前会比较好。但是如果是读取,通常是select * from xxx
where member_id = x order by timestamp desc。这时候如果是member_id +
timestamp做主键,就会直接用到主键索引。再加上由于Mysql之前放了一个Q4M队
列来缓解插入压力,所以最终的这个选择在当时还是非常正确的。

那么如果需求继续增长,2台MyIsam的id generator满足不了需求了,该怎么办?
使用UUID?使用UUID会直接消除id generator的瓶颈,但是之前提到无序的主键
插入会对mysql的插入有很大的影响,而UUID正是无序的。最终瓶颈可能就真的
出在真正写记录的时候了。(具体的以后做个试验模拟下看看)

twitter给出了一个解决方案: https://github.com/twitter/snowflake

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值