《HBase权威指南》一3.4 行锁

3.4 行锁

像put()、delete()、checkAndPut()这样的修改操作是独立执行的,这意味着在一个串行方式的执行中,对于每一行必须保证行级别的操作是原子性的。region服务器提供了一个行锁(row lock)的特性,这个特性保证了只有一个客户端能获取一行数据相应的锁,同时对该行进行修改。在实践中,大部分客户端应用程序都没有提供显式的锁,而是使用这个机制来保障每个操作的独立性。

文字用户应该尽可能地避免使用行锁。就像在RDBMS中,两个客户端很可能在拥有对方要请求的锁时,又同时请求对方已拥有的锁,这样便形成了一个死锁。

锁超时之前,两个被阻塞的客户端会占用一个服务器端的处理线程(handler),而这个线程是一种十分稀缺的资源。如果在一个频繁操作的行上发生了这种情况,那么很多其他的客户端会占用掉其所有的处理线程,阻塞所有其他客户端访问这台服务器,导致这个region服务器将不能为其负责的region内的行提供服务。

重申一下:在不必要的情况下,尽量不要使用行锁。如果必须使用,那么一定要节约占用锁的时间!
比如,当使用put()访问服务器时,Put实例可以通过以下构造函数生成:

Put(byte[] row)

这个构造函数就没有RowLock实例参数,所以服务器会在调用期间创建一个锁。实际上,通过客户端的API,得不到这个生存期短暂的服务器端的锁的实例。

除了服务器端隐式加锁之外,客户端也可以显式地对单行数据的多次操作进行加锁,通过以下调用便可以做到:

RowLock lockRow(byte[] row) throws IOException
void unlockRow(RowLock rl) throws IOException

第一个调用lockRow()需要一个行键作为参数,返回一个RowLock的实例,这个实例可以供后续的Put或者Delete的构造函数使用。一旦不再需要锁时,必须通过unlockRow()调用来释放它。

每一个排他锁(unique lock),无论是由服务器提供的,还是通过客户端API传入的,都能保护这一行不被其他锁锁定。换句话说,锁必须针对整个行,并且指定其行键,一旦它获得锁定权就能防止其他的并发修改。

当一个锁被服务器端或客户端显式获取之后,其他所有想要对这行数据加锁的客户端将会等待,直到当前锁被释放,或者锁的租期超时。后者是为了确保错误进程不会占用锁太长时间或无限期占用。

图像说明文字默认的锁超时时间是一分钟,但是可以在hbase-site.xml文件中添加以下配置项来修改这个默认值,时间以毫秒为单位:

< property>
  < name>hbase.regionserver.lease.period< /name>
  < value>120000< /value>
< /property>

通过添加以上代码,超时时间被设置为原来的两倍——120秒也就是2分钟。小心不要将这个值设得太大,因为每一个想获取被锁住的行的客户端都会阻塞并等待锁的恢复。
例3.17展示了如何在行上创建一个锁,该锁阻塞所有的并发读取。

例3.17 显式使用行锁

static class UnlockedPut implements Runnable {
  @Override
   public void run() {
   try {
     HTable table = new HTable(conf,"testtable");
     Put put = new Put(ROW1);
     put.add(COLFAM1,QUAL1,VAL3);
       long time = System.currentTimeMillis();
     System.out.println("Thread trying to put same row now...");
     table.put(put);
     System.out.println("Wait time: " +
      (System.currentTimeMillis() - time)+ "ms");
    } catch(IOException e){
     System.err.println("Thread error: " + e);
  }
 }
}

System.out.println("Taking out lock...");
RowLock lock = table.lockRow(ROW1);
System.out.println("Lock ID: " + lock.getLockId());

Thread thread = new Thread(new UnlockedPut());
thread.start();

try {
  System.out.println("Sleeping 5secs in main()...");
  Thread.sleep(5000);
} catch(InterruptedException e){
 // ignore
}

try {
  Put put1 = new Put(ROW1,lock);
  put1.add(COLFAM1,QUAL1,VAL1);
  table.put(put1);

  Put put2 = new Put(ROW1,lock);
  put2.add(COLFAM1,QUAL1,VAL2);
  table.put(put2);
} catch(Exception e){
  System.err.println("Error: " + e);
} finally {
 System.out.println("Releasing lock...");
 table.unlockRow(lock);
}

使用一个异步的线程更新同一个行,但是不显式加锁。

put()调用会阻塞,直到锁被释放。

给整行加锁。

启动那个会阻塞的异步线程。

休眠一会儿,以阻塞其他写入操作。

在拥有锁的情况下创建Put。

在拥有锁的情况下创建另外一个Put。

释放锁,让阻塞线程继续执行。

执行这个例子代码时,应该能在控制台看到以下输出:

Taking out lock...
Lock ID: 4751274798057238718
Sleeping 5secs in main()...
Thread trying to put same row now...
Releasing lock...
Wait time: 5007ms
After thread ended...
KV: row1/colfam1:qual1/1300775520118/Put/vlen=4,Value: val2
KV: row1/colfam1:qual1/1300775520113/Put/vlen=4,Value: val1
KV: row1/colfam1:qual1/1300775515116/Put/vlen=4,Value: val3

从这个例子能看出,一个显示的锁是如何阻塞另一个使用隐式锁的线程的。主线程休眠了5秒,一醒来就调用了两次put(),分别将同一列设置为两个不同的数值。

主线程的锁一释放,阻塞线程的run()方法就继续执行并调用了第三个put。观察put操作在服务器端的执行情况,会觉得很有意思。读者可能注意到了,KeyValue实例的时间戳显示第三个put拥有最小的时间戳,虽然这个put表面上是最后执行的。这是因为线程中的put()调用是在两个主线程中的put()之前执行的,这之后主线程休眠了5秒。当put被发送到服务器时,如果它的时间戳没有被显式指定,服务器端会帮它设定时间戳,同时试图获得这一行的锁。但是示例代码中主线程已经获得了该行的锁,因此服务器端的处理一直等待了5秒多,锁被释放才得已继续。从上面的输出可以看出,主线程中两个put调用的执行以及行的解锁只花费了7毫秒的时间。

Get需要锁吗?

修改行时锁定行是有意义的,那么获取数据时是否需要加锁呢?Get类有一个构造器允许用户指定一个显式的锁:

Get(byte[] row,RowLock rowLock)

这是遗留的方法,但服务器端根本用不着这种方法,因为在获取数据的过程中,服务器根本不需要任何锁,而是应用了一个多版本的并发控制(multiversion concurrencycontrol-style)⑦机制来保证行级读操作。例如,get()调用永远不会返回写了一半的数据,比如当这些数据是另一个线程或者客户端写的。

这个就像是小规模的事务系统:只有当一个变动被应用到整个行之后,客户端才能读出这个改动。当改动在进行中时,所有的客户端读取操作得到的都将是所有列以前的状态。
当用户试图使用之前申请的显式锁,但锁的租约已经超时并恢复,用户将会从服务器得到一个以UnknownRowLockException形式报告的错误。这个异常告诉用户服务器已经废弃了用户尝试使用的锁。用户应该在代码中丢弃这个锁,然后请求一个新的锁再试图恢复锁定状态。

时间: 2024-11-02 05:30:06

《HBase权威指南》一3.4 行锁的相关文章

《HBase权威指南》一导读

前 言 HBase权威指南 你阅读本书的理由可能有很多.可能是因为听说了Hadoop,并了解到它能够在合理的时间范围内处理PB级的数据,在研读Hadoop的过程中发现了一个处理随机读写的系统,它叫做HBase.或者将其称为目前流行的一种新的数据存储架构,传统数据库解决大数据问题时成本更高,更适合的技术范围是NoSQL. 无论你是如何来到这里的,我都希望你能够了解并学习如何在企业或组织中使用HBase解决海量数据问题.你可能有关系型数据库的背景,但更希望去研究这个"列式存储"系统:也许你

《HBase权威指南》一第1章 简介

第1章 简介 HBase权威指南 在探究HBase的功能之前,我认为有必要先来思考一下为什么要设计出这样一套新的存储架构.传统的关系型数据库管理系统(Relational Database Management System,RDBMS)早在20世纪70年代已经出现,并且帮助无数的公司和机构实现了给定问题的解决方案,时至今日,RDBMS仍旧非常有用.很多情况下关系模型都能够非常完美地阐述问题,但是在面对一些特殊的场景时关系模型并不是最佳的解决方案.①

《HBase权威指南》一1.5 HBase:Hadoop数据库

1.5 HBase:Hadoop数据库 看过BigTable的架构之后,我们可能会简单地认为HBase完全是Google的BigTable的开源实现.但是这个说法可能过于简单,因为两者之间还有些差异(大多是细微的)值得一提. 1.5.1 历史 HBase是Powerset㉑在2007年创建的,最初是Hadoop的一部分.之后,它逐步成为Apache软件基金会旗下的顶级项目,具备Apache软件许可证,版本为2.0. HBase项目的主页是http://hbase.apache.org/,通过这个

《HBase权威指南》一3.2 CRUD操作

3.2 CRUD操作 数据库的初始基本操作通常被称为CRUD(Create,Read,Update,Delete),具体指增.查.改.删.HBase中有与之相对应的一组操作,随后我们会依次介绍.这些方法都由HTable类提供,本章后面将直接引用这个类的方法,不再特别提到这个包含类. 接下来介绍的操作大多都能不言自明,但本书有一些细节需要大家注意.这意味着,对于书中出现的一些重复的模式,我们不会多次赘述. 文字你所看到的示例源代码都可以从GitHub的公用源中下载,具体地址为https://git

《HBase权威指南》一3.5 扫描

3.5 扫描 在讨论过基本的CRUD类型的操作之后,现在来看一下扫描(scan)技术,这种技术类似于数据库系统中的游标(cursor),并利用到了HBase提供的底层顺序存储的数据结构.⑧ 3.5.1 介绍 扫描操作的使用跟get()方法非常类似.同样,和其他函数类似,这里也提供了Scan类.但是由于扫描操作的工作方式类似于迭代器,所以用户无需调用scan()方法创建实例,只需调用HTable的getScanner()方法,此方法在返回真正的扫描器(scanner)实例的同时,用户也可以使用它迭

《HBase权威指南》一1.4 结构

1.4 结构 本节首先介绍HBase的架构,然后介绍一些关于HBase起源的背景资料,之后将介绍其数据模型的一般概念和可用的存储API,最后在一个更高的层次上对其实现细节进行分析. 1.4.1 背景 2003年,Google发表了一篇论文,叫"The Google File System"(http://labs.google.com/papers/gfs.html).这个分布式文件系统简称GFS,它使用商用硬件集群存储海量数据.文件系统将数据在节点之间冗余复制,这样的话,即使一台存储

《HBase权威指南》一1.1 海量数据的黎明

1.1 海量数据的黎明 我们生活在一个互联网时代,无论是想搜索最佳的火鸡菜谱,还是送妈妈什么样的生日礼物,都希望能够通过互联网迅速地检索到问题的答案,同时希望查询到的结果有用,而且非常切合我们的需要. 因此,很多公司开始致力于提供更有针对性的信息,例如推荐或在线广告,这种能力会直接影响公司在商业上的成败.现在类似Hadoop②这样的系统能够为公司提供存储和处理PB级数据的能力,随着新机器学习算法的不断发展,收集更多数据的需求也在与日俱增. 以前,因为缺乏划算的方式来存储所有信息,很多公司会忽略某

《HBase权威指南》一3.6 各种特性

3.6 各种特性 在深入介绍客户端可以利用的特性之前,让我们先介绍一下HBase和其客户端API提供的各种特性或功能. 3.6.1 HTable的实用方法 客户端API是由HTable类的实例提供的,用户可以用它来操作HBase表.除了之前提到过的一些主要特性外,还有以下一些值得注意的方法. void close() 这个方法之前提到过,不过为了它的完整性和重要性,我们在这儿要重新讨论一下.用户使用完一个HTable实例之后,需要调用一次close().这个方法会刷写所有客户端缓冲的写操作:cl

《HBase权威指南》一3.3 批量处理操作

3.3 批量处理操作 现在我们已经介绍过添加.检索和删除表中数据的操作了,不过前面介绍的操作都是基于单个实例或基于列表的操作.这一节将会介绍一些API调用,这些调用可以批量处理跨多行的不同操作. 文字事实上,许多基于列表的操作,如delete(List deletes)或者get(List gets),都是基于batch()方法实现的.它们都是一些为了方便用户使用而保留的方法.如果你是新手,推荐使用batch()方法进行所有操作. 下面的客户端API方法提供了批量处理操作.用户可能注意到这里引入