一、应用背景
微博:用户表users、微博表weibos、用户关系表relations,和具体哪个公司的微博没关系。
微博中的用户想关注其他用户的微博,首先要维护一个特定用户的关注列表,例如张三关注了李四和王五。
为了要的得到张三应该看到的所有微博,你需要查找列表{李四、王五},然后读出列表中每个用户的所有微博,这个信息需要保存在hbase中。
二、表模式(Schema)设计应该考虑的问题
- 这个表应该保存多少个列族
- 列族使用什么数据
- 每个列族应该有多少列
- 列名应该是什么
- 每个单元(Cell)存储多少时间版本
- 行健结构是什么?应该包括什么信息?
三、问题建模
问题分析:关系表需要存储一个特定用户关注什么用户的数据。
表的访问模式:
- 读出全部用户列表。张三关注人的列表
- 查询某指定用户是否在列表里。如张三是否关注了李四
需要几个列族呢?
张三的关注对象里的所有用户,都有可能被确认是否存在,从访问模式看无法区分彼此,不能假定一个用户比其他用户的被访问的可能性大,推断被关注用户需要同一个列族。
原因:同一个列族的数据存储在同一个物理存储(store)里面,这个物理存储被分为多个Hfile,理想情况下合并为一个Hfile。一个列族的所有列在物理上存在一起,使用这种模式可以把不同访问模式的列放在不同的列族,以便隔离他们。即Hbase被称为是面向列族存储的。
初始表设计为:
说明:follws:1--->TheRealMT.
三、定义访问模式(读模式,写模式)
这张表用来干嘛?
- 张三关注了谁?
- 张三关注李四了吗?
- 谁关注了张三?
- 李四关注张三了吗?
表创建脚本:
1.创建关系表 create \'relation\',{NAME => \'follows\', VERSIONS => 2} 2.添加数据 put \'relation\',\'zhansan\',\'follows:1\',\'lisi\' put \'relation\',\'zhansan\',\'follows:2\',\'wangwu\'
回答第一个问题:张三关注了谁?
@Test public void demo2() throws Exception{ Configuration config = HBaseConfiguration.create(); config.set("hbase.zookeeper.quorum", "node1,node2,node3"); HTable table = new HTable(config, "relation"); String rowKey = "zhansan"; Get get = new Get(Bytes.toBytes(rowKey)); List<String> fllowed = new ArrayList<String>(); Result rs = table.get(get); List<KeyValue> list = rs.list(); Iterator<KeyValue> it = list.iterator(); while(it.hasNext()){ KeyValue kv = it.next(); System.out.println(Bytes.toString(kv.getValue())); fllowed.add(Bytes.toString(kv.getValue())); } table.close(); }
回答第二个问题:张三关注李四了谁?
public static boolean demo3() throws Exception{ Configuration config = HBaseConfiguration.create(); config.set("hbase.zookeeper.quorum", "node1,node2,node3"); HTable table = new HTable(config, "relation"); String rowKey = "zhansan"; Get get = new Get(Bytes.toBytes(rowKey)); //要查询的对象 String followUser = "lisi"; Result rs = table.get(get); List<KeyValue> list = rs.list(); Iterator<KeyValue> it = list.iterator(); while(it.hasNext()){ KeyValue kv = it.next(); System.out.println(followUser.equals(Bytes.toString(kv.getValue()))); //followUser if(followUser.equals(Bytes.toString(kv.getValue()))); return true; } return false; }
现在我们这样的回答能简单回答第一个和第二个问题,另外另个不确定,但是这两个问题的回答都属于表的读模式。
让我么来看看表的写模式:以下应用场景会引发hbase表的写模式
- 一个用户关注了某人
- 一个用户取消关注了某人
当用户新增一个新的关注的时候,需要在用户的关注列表里新增一个对象,如之前的表设计,TheFakeMT新增一个关注用户的时候,需要知道这个
用户是用户列表里的第5个,如果不查询hbase客户端并不知道这个信息,还有不指定列限定符,也没办法要求Hbase在已有的行上增加一个单元。
不清楚要插入的列的位置,会出现数据覆盖。
解决办法:
在同一行维护一个计数器。如下图所示;
优点:count列能有效的让任何用户知道所关注的用户数量,避免遍历整个关注列表。
基于现在的表设计,插入关注用户的步骤如下:
这种表设计的确定:
- 增加了客户端代码的复杂性,写入需要四步
- hbase不支持事务,线程不安全
怎么解决????--->去掉count计数器
新的表设计如下:
表设计说明:
- 列限定符设计为被关注人的用户名(用户id)
- cell的内容可以随便存储内容,可以存储1
- hbase把一切数据存储为byte[](字节数组),可以存储任意数量的列
- 这就是传说中的宽表设计,宽表的设计会便于检索数据
- 由于用户id的是唯一的,所以不会出现数据覆盖
客户端代码:
public static void demo4() throws Exception{ Configuration config = HBaseConfiguration.create(); config.set("hbase.zookeeper.quorum", "node1,node2,node3"); HTable table = new HTable(config, "relation"); String rowKey = "zhansan"; Put put = new Put(Bytes.toBytes(rowKey)); //要存储的对象 String followUser = "chaosju"; put.add(Bytes.toBytes("follows"), Bytes.toBytes(followUser), Bytes.toBytes(1)); table.put(put); table.close(); }
hbase(main):018:0> scan \'relation\' ROW COLUMN+CELL zhansan column=follows:1, timestamp=1442106287998, value=lisi zhansan column=follows:2, timestamp=1442106352630, value=wangwu zhansan column=follows:chaosju, timestamp=1442109774792, value=\x00\x00\x00\x01 1 row(s) in 0.1640 seconds
学到的东西:
- hbase没有跨行事务的概念,避开客户端使用复杂的业务逻辑
四、均衡分布数据和负载
背景分析:基于上述的表设计
- 一个人可能关注很多人,这个表会特别长,这本身不是问题,但会影响读模式。如TheFakeMT关注了TheRealMT吗?如何使用这个表回答这个问题呢,行健指定TheFakeMT,列限定符指定TheRealMT,一个Get请求即可。
follows表的另一种设计——高表(high table)设计
优点:
- 短的列族和列限定名,减少网络io和磁盘空间占用
具体的存储数据样式:
回答张三是否关注了李四???
先进性一次索引查找,先找到第一个以张三为前缀的第一个数据块,然后以张三开头的行健对所有数据行进行最后一次扫描。
优化设计,及高表的优点:
- MD5(user1ID)MD5(user2ID)作为rowkey,使得数据均匀的分布在region上,避免热点问题
- rowkey长度统一,便于预测读写性能
- 去掉分隔符,更容易扫描计算起始和停止键
hbase热点问题???数据倾斜
热点:数据集中分布在一小部分region上。
例如插入时间序列数据,行健开头是时间戳,因为任何写入的数据的时间戳都是大于之前写入的时间戳,数据总是追加在表的尾部,最后的region会成为热点。
基于MD5_rowkey的设计
如果需要查找,用户id,客户把用户id保存为列限定符。
表设计总结: