1 行锁简介
在事务特性方面,hbase只支持单row的事务,不能保证跨row(cross-row)的事务。hbase通过行锁来实现单row事务。客户端进行操作时,可以显式对某一个行加锁,但是大部分情况下是没有必要的,因为如果没有显式的加行锁,hbase在内部会默认的加行锁。为了描述方便,用户显式加的行锁简称为显式行锁。
源码1:客户端使用显式行锁的示例代码
HTable table = new HTable(config, tablename);
RowLock lock = table.lockRow(rowkey);//首先获得rowkey对应的行锁,可能阻塞
Put p1 = new Put(rowkey,lock);
p1.add(cf, qua,value); //在行rowkey上,写数据
table.put(p1);
table.unlockRow(lock);//解锁
hbase不推荐客户端显式加行锁,因为可能造成regionserver死锁。并且现在正就"客户端不支持显式加行锁"进行讨论。
引自hbase官网:
RowLocks are still in the client APIhowever they are discouraged because if not managed properly thesecan lock up the RegionServers. There is an oustanding ticketHBASE-2332 to remove this feature from the client.
2 实现行锁
如下图所示,客户端看到的行锁由两部分组成:rowkey和外部锁id。外部锁id是向regionserver申请行锁时,由regionserver返回的。外部锁id在对应的regionserver上全局唯一。
图1 行锁的定义
如图2所示。一个regionserver对应一个HRegionServer实例,一个分片对应一个HRegion实例。
在HregionServer中提供两个接口lockRow和unlockRow,供客户端显式加锁和解锁。rowlocks存储了<客户端所见的锁id,内部锁id>的映射关系。因为内部锁id在分片内部是唯一的,但是在regionserver上不唯一,所以需要这个映射关系。
在HRegion中,lockedRows记录了所有已经加锁的行,lockIds记录了映射关系<内部锁id,rowkey>。
图2
为了处理客户端忘记释放锁的情况,显式行锁有一个租约(即有效期,默认是60秒,可配置,对应的配置项是hbase.regionserver.lease.period),如果显式行锁超过了租约时间,regionserver会自动释放行锁。在租约时间内,如果又收到了在这个行上的写请求,会延长租约时长。
加显式行锁的算法
1 判断目标行是否已经加锁,如果已经加锁,则等待至解锁。
while (lockedRows.contains(row))
lockedRows.wait();
2 分配内部锁id,记为lockId。并把映射关系<lockIds,row>加入至lockIds。lockId在region内是唯一的。
保证内部锁id唯一性的常规的解决办法是:找出一个没有使用过的id,然后分配给行锁。hbase采用的方法比较有意思:在内部维护一个id生成器,每次分配时,把id生成器的值分配给行锁,并且id生成器自增1。如果没有出现冲突(冲突:当前分配的id不唯一),分配完成。 如果出现冲突,为id生成器赋一随机值,然后重复的上面的流程,直到没有冲突,见源码2。
方法对比:如果使用常规方法,必然要使用到锁来进行并发控制,锁的开销还是比较大的。hbase认为出现冲突的可能性非常小,这种方法的开销应该远小于常规方法。这种提供性能的方法值得学习。
源码2:分配内部锁id。 见HRegion. internalObtainRowLock()
byte[] prev = null;
Integer lockId = null;
do
{
lockId = new Integer(lockIdGenerator++);//生成锁id
prev = lockIds.put(lockId, row);
if (prev != null) //如果lockId已经分配了,即发生冲突
{
lockIds.put(lockId, prev);// 恢复被破坏的映射关系
lockIdGenerator = rand.nextInt();//生成新的锁id
}
} while (prev != null);
源码:添加映射关系
lockIds.put(lockId, row)
3 把目标行加入至lockedRows。
lockedRows.add(row);
4 为行锁分配外部锁id,这个id在regionserver内是唯一的。
lockId = rand.nextLong();//这种方法会给hbase带来问题,见第三节
5 在行锁上添加行锁监听器。
行锁监听器的主要功能是处理行锁的租约,租约过期时,自动释放行锁,收到新的写请求延长租约时长。
下图是实现行锁的类图。
图3 实现行锁类图
3 设计缺陷
在加显式行锁算法的第4步中,因为Random对象可能产生重复的值,所以分配的外部锁id可能不唯一。
通过修改regionserver的代码,模拟生成重复锁id的情况,测试流程如下:
1修改regionserver的代码使得每次分配的外部锁id都为999
2线程1为rowkey1申请行锁;线程2为rowkey2申请行锁。线程1和线程2同时并发运行。
测试发现只要只要rowkey1对应的行锁不释放,线程2会永远阻塞。
修正方法:保证外部锁id的唯一性。