HBase案例-谷粒微博(尚硅谷)
1 需求分析
模拟微博运行过程,可以实现发布微博、关注/取关用户、查看用户微博内容等功能。
2 数据表设计
共设计3张表,分别是微博内容表、用户关系表、微博收件箱表。
2.1 微博内容表
设计一个列族info,包含一个列Content,用于存放发布的微博内容。行键设计为用户id+时间戳,每行数据表示某个用户在某一时刻发布的微博内容。所有用户发布的所有微博内容均存储在一张表中。
2.2 用户关系表
设计两个列族attends、fans,表示当前用户的关注者和粉丝。每个列族可以有很多列,每个列表示一个具体的用户,列的值与列名保持相同,仅起到标识用户的作用。行键设计为用户id。如果某个用户没有关注任何其他用户,则该行数据的attends列族为空;同样地,如果没有粉丝,则fans列族为空。
2.3 微博收件箱表(初始化页面表)
设计为一个列族info,可以包括多个列,分别表示当前用户关注的其他用户。行键设计为用户id,每行数据表示当前用户关注的其他用户所发布的微博内容,每列的值设置3个版本,内容为关注者所发布的微博内容的行键(uid+ts)。即列的值是第一张表(微博内容表)的外键。
数据表示例如下:
3 项目架构
创建Maven工程,在pom.xml文件中添加项目所需要的依赖。
<dependencies> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-server</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-client</artifactId> <version>1.3.1</version> </dependency> </dependencies>
在src/main/resources文件夹下添加配置文件hbase-site.xml,其中包含项目要连接的集群信息,这样就不用在代码里再设置了。
在src/main/java下创建4个Java包:constants、utils、dao、test,分别表示常量、工具类、数据操作、测试。
4 定义常量
在constants包下创建Constants类,存放项目需要用到的常量。包括HBase配置信息、命名空间、微博内容表及其列族信息、用户关系表及其列族信息、收件箱表及其列族信息。
public class Constants { // HBase配置信息 public static final Configuration CONFIGURATION = HBaseConfiguration.create(); // 命名空间 public static final String NAMESPACE = "weibo"; // 微博内容表 public static final String CONTENT_TABLE = "weibo:content"; public static final String CONTENT_TABLE_CF = "info"; public static final int CONTENT_TABLE_VERSIONs = 1; // 用户关系表 public static final String RELATION_TABLE = "weibo:relation"; public static final String RELATION_TABLE_CF1 = "attends"; public static final String RELATION_TABLE_CF2 = "fans"; public static final int RELATION_TABLE_VERSIONs = 1; // 收件箱表 public static final String INBOX_TABLE = "weibo:inbox"; public static final String INBOX_TABLE_CF = "info"; public static final int INBOX_TABLE_VERSIONS = 2; }
5 HBaseUtil封装
在utils包下创建HBaseUtil类,提供项目通用的功能,包括创建命名空间、判断表是否存在、创建表。
public class HBaseUtil { // 1 创建命名空间 public static void createNamespace(String nameSpace) throws IOException { // 1 获取Connection对象 Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); // 2 获取Admin对象 Admin admin = connection.getAdmin(); // 3 构建命名空间描述器 NamespaceDescriptor namespaceDescriptor = NamespaceDescriptor.create(nameSpace).build(); // 4 创建命名空间 admin.createNamespace(namespaceDescriptor); // 5 关闭资源 admin.close(); connection.close(); } // 2 判断表是否存在 private static boolean isTableExist(String tableName) throws IOException { // 1 获取Connection对象 Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); // 2 获取Admin对象 Admin admin = connection.getAdmin(); // 3 判断是否存在 boolean exists = admin.tableExists(TableName.valueOf(tableName)); // 4 关闭资源 admin.close(); connection.close(); // 5 返回结果 return exists; } // 3 创建表 public static void createTable(String tableName, int versions, String... cfs) throws IOException { // 1 判断是否传入了列族信息 if(cfs.length <= 0){ System.out.println("请传入列族信息"); return; } // 2 判断表是否存在 if(isTableExist(tableName)){ System.out.println("表" + tableName + "已存在"); return; } // 3 获取Connection对象 Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); // 4 获取Admin对象 Admin admin = connection.getAdmin(); // 5 创建表描述器 HTableDescriptor hTableDescriptor = new HTableDescriptor(TableName.valueOf(tableName)); // 6 添加列族信息 for (String cf : cfs) { HColumnDescriptor hColumnDescriptor = new HColumnDescriptor(cf); // 7 设置版本 hColumnDescriptor.setMaxVersions(versions); hTableDescriptor.addFamily(hColumnDescriptor); } // 8 创建表对象 admin.createTable(hTableDescriptor); // 9 关闭资源 admin.close(); connection.close(); } }
6 HBaseDao封装
在dao包下创建HBaseDao类,实现与业务相关的数据操作功能。
6.1 发布微博
// 1 发布微博 public static void publishWeiBo(String uid, String content) throws IOException { Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); // 第一部分:操作微博内容表 Table contTable = connection.getTable(TableName.valueOf(Constants.CONTENT_TABLE)); long ts = System.currentTimeMillis(); String rowKey = uid + "_" + ts; Put contPut = new Put(Bytes.toBytes(rowKey)); contPut.addColumn(Bytes.toBytes(Constants.CONTENT_TABLE_CF), Bytes.toBytes("content"), Bytes.toBytes(content)); contTable.put(contPut); // 第二部分:操作微博收件箱表 Table relaTable = connection.getTable(TableName.valueOf(Constants.RELATION_TABLE)); Get relaGet = new Get(Bytes.toBytes(uid)); relaGet.addFamily(Bytes.toBytes(Constants.RELATION_TABLE_CF2)); Result result = relaTable.get(relaGet); ArrayList<Put> inboxPuts = new ArrayList<>(); for (Cell cell : result.rawCells()) { Put inboxPut = new Put(CellUtil.cloneQualifier(cell)); inboxPut.addColumn(Bytes.toBytes(Constants.INBOX_TABLE_CF), Bytes.toBytes(uid), Bytes.toBytes(rowKey)); inboxPuts.add(inboxPut); } // 判断当前uid是否有粉丝 if (inboxPuts.size() > 0){ Table inboxTable = connection.getTable(TableName.valueOf(Constants.INBOX_TABLE)); inboxTable.put(inboxPuts); inboxTable.close(); } // 关闭资源 relaTable.close(); contTable.close(); connection.close(); }
难点是要同时操作3张表,要处理3张表之间的联系。某个uid用户发布了一条微博,系统做了这些事情:在微博内容表中,添加一条数据;在用户关系表中,查找uid的fans用户;在微博收件箱表中,为所有fans用户更新当前uid用户的微博发布信息。
微博内容表中,行键设计为uid+时间戳。
Table contTable = connection.getTable(TableName.valueOf(Constants.CONTENT_TABLE)); long ts = System.currentTimeMillis(); String rowKey = uid + "_" + ts; Put contPut = new Put(Bytes.toBytes(rowKey)); contPut.addColumn(Bytes.toBytes(Constants.CONTENT_TABLE_CF), Bytes.toBytes("content"), Bytes.toBytes(content)); contTable.put(contPut);
在用户关系表中,查找当前uid的fans信息。遍历每一个粉丝,在微博收件箱表中为每个粉丝更新uid用户的微博发布信息。构建Get对象,行键为uid,列族为fans,传入用户关系表中,返回一个result对象,其中包含uid的所有粉丝信息。
Table relaTable = connection.getTable(TableName.valueOf(Constants.RELATION_TABLE)); Get relaGet = new Get(Bytes.toBytes(uid)); relaGet.addFamily(Bytes.toBytes(Constants.RELATION_TABLE_CF2)); Result result = relaTable.get(relaGet);
解析result对象,获取每一个cell对象,每个cell对象包含列族fans中的各列的列名及列的值。将获取到的粉丝信息作为新的行键,构建微博收件箱表的Put对象,列族为info,列名为当前用户的uid,值是当前新添加微博的行键(uid+时间戳)。每一个Put对象存放在创建的ArrayList列表中。循环遍历每一个粉丝,进行如上操作。
ArrayList<Put> inboxPuts = new ArrayList<>(); for (Cell cell : result.rawCells()) { Put inboxPut = new Put(CellUtil.cloneQualifier(cell)); inboxPut.addColumn(Bytes.toBytes(Constants.INBOX_TABLE_CF), Bytes.toBytes(uid), Bytes.toBytes(rowKey)); inboxPuts.add(inboxPut); }
如果当前uid用户没有粉丝(ArrayList列表为空),则不对微博收件箱表做任何操作。否则,获取微博收件箱表对象,并将ArrayList列表中的Put对象传入到微博收件箱表中。
// 判断当前uid是否有粉丝 if (inboxPuts.size() > 0){ Table inboxTable = connection.getTable(TableName.valueOf(Constants.INBOX_TABLE)); inboxTable.put(inboxPuts); inboxTable.close(); }
最后,关闭资源。
// 关闭资源 relaTable.close(); contTable.close(); connection.close();
6.2 关注用户
// 2 关注用户 public static void addAttends(String uid, String... attends) throws IOException { if (attends.length <= 0) { System.out.println("请传入要关注的人"); return; } Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); // 添加uid用户的关注人,且同步更新关注人的粉丝信息 Table relaTable = connection.getTable(TableName.valueOf(Constants.RELATION_TABLE)); Put attendPut = new Put(Bytes.toBytes(uid)); ArrayList<Put> relaPuts = new ArrayList<>(); for (String attend : attends) { // 对于每一个attend对象,要同时做两件事情 attendPut.addColumn(Bytes.toBytes(Constants.RELATION_TABLE_CF1), Bytes.toBytes(attend), Bytes.toBytes(attend)); Put fanPut = new Put(Bytes.toBytes(attend)); fanPut.addColumn(Bytes.toBytes(Constants.RELATION_TABLE_CF2), Bytes.toBytes(uid), Bytes.toBytes(uid)); relaPuts.add(fanPut); } relaPuts.add(attendPut); // 对于同一行数据,可以一次添加多个列 relaTable.put(relaPuts); // 更新微博收件箱表信息 Table contTable = connection.getTable(TableName.valueOf(Constants.CONTENT_TABLE)); Put inboxPut = new Put(Bytes.toBytes(uid)); for (String attend : attends) { Scan scan = new Scan(Bytes.toBytes(attend + "_"), Bytes.toBytes(attend + "|")); ResultScanner resultScanner = contTable.getScanner(scan); // 定义一个时间戳,以解决多条数据同一时间戳的问题 long ts = System.currentTimeMillis(); for (Result result : resultScanner) { inboxPut.addColumn(Bytes.toBytes(Constants.INBOX_TABLE_CF), Bytes.toBytes(attend), ts++, result.getRow()); } } // 判断uid所关注的人是否有发过微博 if (!inboxPut.isEmpty()) { Table inboxTable = connection.getTable(TableName.valueOf(Constants.INBOX_TABLE)); inboxTable.put(inboxPut); inboxTable.close(); } // 关闭资源 relaTable.close(); contTable.close(); connection.close(); }
关注用户同样需要操作3张表。在用户关系表中,当前uid用户在attends列族中添加若干个用户,同时这些用户在fans列族中添加当前uid用户。此外,在微博收件箱表中,以当前uid用户为行键,在列族info中添加若干个列,每列表示一个关注人,每列的值存放此关注人发布的微博的行键(版本存放2条)。
循环遍历传入的attends参数,在每一次遍历中要同时做两件事情。一是为uid添加关注人,二是为关注人添加粉丝(uid)。这个地方要注意,对于同一行数据,Put对象可以分步骤添加多个列。而对于不同行数据,需要分别创建Put对象并添加到ArrayList<Put>列表中,之后再传入至用户关系表对象中。
// 添加uid用户的关注人,且同步更新关注人的粉丝信息 Table relaTable = connection.getTable(TableName.valueOf(Constants.RELATION_TABLE)); Put attendPut = new Put(Bytes.toBytes(uid)); ArrayList<Put> relaPuts = new ArrayList<>(); for (String attend : attends) { // 对于每一个attend对象,要同时做两件事情 attendPut.addColumn(Bytes.toBytes(Constants.RELATION_TABLE_CF1), Bytes.toBytes(attend), Bytes.toBytes(attend)); Put fanPut = new Put(Bytes.toBytes(attend)); fanPut.addColumn(Bytes.toBytes(Constants.RELATION_TABLE_CF2), Bytes.toBytes(uid), Bytes.toBytes(uid)); relaPuts.add(fanPut); } relaPuts.add(attendPut); // 对于同一行数据,可以一次添加多个列 relaTable.put(relaPuts);
在微博收件箱表中,对于当前uid用户,为其添加新关注的人,并更新这些人发布的微博信息。这两个信息分别由列名和列值承载,所以可以通过一个Put对象同时添加。新关注人发布的微博信息通过Scan对象获取,指定起始和终止的行键,根据字典序的排序规则以及微博内容表中“uid+时间戳”的行键设计规则,设计起始行键为“attend_”,终止行键为“attend|”,其中attend是要关注的人。这样设计可以获取到某个用户所发布的全部微博的行键(字典序,有比没有大,而且字符|大于字符_)。微博内容表的getScanner()方法返回ResultScanner对象,其中包含若干个Result对象。每一个Result对象表示一行数据,包含所需要的行键信息。手动定义一个时间戳,以解决多条数据同一时间戳的问题。最后,如果当前uid用户新关注的人都没有发布过微博,则不用对微博收件箱表做任何操作;当至少有一条微博时,才进行后续操作。
// 更新微博收件箱表信息 Table contTable = connection.getTable(TableName.valueOf(Constants.CONTENT_TABLE)); Put inboxPut = new Put(Bytes.toBytes(uid)); for (String attend : attends) { Scan scan = new Scan(Bytes.toBytes(attend + "_"), Bytes.toBytes(attend + "|")); ResultScanner resultScanner = contTable.getScanner(scan); // 定义一个时间戳,以解决多条数据同一时间戳的问题 long ts = System.currentTimeMillis(); for (Result result : resultScanner) { inboxPut.addColumn(Bytes.toBytes(Constants.INBOX_TABLE_CF), Bytes.toBytes(attend), ts++, result.getRow()); } } // 判断uid新关注的人是否有发过微博 if (!inboxPut.isEmpty()) { Table inboxTable = connection.getTable(TableName.valueOf(Constants.INBOX_TABLE)); inboxTable.put(inboxPut); inboxTable.close(); } // 关闭资源 relaTable.close(); contTable.close(); connection.close();
6.3 取关用户
// 3 取关用户 public static void deleteAttends(String uid, String... dels) throws IOException { if (dels.length <= 0) { System.out.println("请传入要取关的用户"); return; } Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); // 更新用户关系表 Table relaTable = connection.getTable(TableName.valueOf(Constants.RELATION_TABLE)); Delete uidDelete = new Delete(Bytes.toBytes(uid)); ArrayList<Delete> attendDeletes = new ArrayList<>(); for (String del : dels) { uidDelete.addColumns(Bytes.toBytes(Constants.RELATION_TABLE_CF1), Bytes.toBytes(del)); Delete attendDelete = new Delete(Bytes.toBytes(del)); attendDelete.addColumns(Bytes.toBytes(Constants.RELATION_TABLE_CF2), Bytes.toBytes(uid)); attendDeletes.add(attendDelete); } relaTable.delete(uidDelete); relaTable.delete(attendDeletes); // 更新微博收件箱表 Table inboxTable = connection.getTable(TableName.valueOf(Constants.INBOX_TABLE)); Delete inboxDelete = new Delete(Bytes.toBytes(uid)); for (String del : dels) { inboxDelete.addColumns(Bytes.toBytes(Constants.INBOX_TABLE_CF), Bytes.toBytes(del)); } inboxTable.delete(inboxDelete); // 关闭资源 inboxTable.close(); relaTable.close(); connection.close(); }
与关注用户逻辑类似,但更简单些,因为只涉及到用户关系表和微博收件箱表两张表。其中,调用了Delete对象的addColumns()方法而不是addColumn()方法,addColumns()方法可以同时删掉多个版本。
6.4 获取某个人的初始化页面数据
// 4 获取某个人的初始化页面数据 public static void getInit(String uid) throws IOException { Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); Table contTable = connection.getTable(TableName.valueOf(Constants.CONTENT_TABLE)); Table inboxTable = connection.getTable(TableName.valueOf(Constants.INBOX_TABLE)); Get inboxGet = new Get(Bytes.toBytes(uid)); inboxGet.setMaxVersions(); // 为Get对象设置最大版本 Result result = inboxTable.get(inboxGet); for (Cell cell : result.rawCells()) { Get contGet = new Get(CellUtil.cloneValue(cell)); Result contResult = contTable.get(contGet); for (Cell contCell : contResult.rawCells()) { System.out.println("RK:" + Bytes.toString(CellUtil.cloneRow(contCell))+ ", ColumnFamily:" + Bytes.toString(CellUtil.cloneFamily(contCell)) + ", ColumnName:" + Bytes.toString(CellUtil.cloneQualifier(contCell)) + ", value:" + Bytes.toString(CellUtil.cloneValue(contCell))); } } // 关闭资源 inboxTable.close(); contTable.close(); connection.close(); }
根据当前uid用户遍历微博收件箱表,找到其关注的所有人,打印出这些人发布的若干条微博数据(由版本数控制)。
创建Get对象,传入uid作为行键,并设置最大版本。传入收件箱表对象,返回一个Result对象,其中包含uid所关注的所有用户的若干条微博的行键。
Table contTable = connection.getTable(TableName.valueOf(Constants.CONTENT_TABLE)); Table inboxTable = connection.getTable(TableName.valueOf(Constants.INBOX_TABLE)); Get inboxGet = new Get(Bytes.toBytes(uid)); inboxGet.setMaxVersions(); // 为Get对象设置最大版本 Result result = inboxTable.get(inboxGet);
获取result对象的所有Cell对象,每一个Cell对象包含一条微博数据的一个行键。遍历所有Cell对象,获取微博数据的行键,之后再微博内容表中根据行键找到相应微博数据,此时返回一个Result对象。这个Result对象只包含一个Cell对象(Cell是HBase中的概念,是由列族、列名、时间戳唯一确定的单元),获取这个Cell对象,进一步地获取行键、列族、列名、值的信息。这里的难点在于对Result和Cell的理解。
for (Cell cell : result.rawCells()) { Get contGet = new Get(CellUtil.cloneValue(cell)); Result contResult = contTable.get(contGet); for (Cell contCell : contResult.rawCells()) { System.out.println("RK:" + Bytes.toString(CellUtil.cloneRow(contCell))+ ", ColumnFamily:" + Bytes.toString(CellUtil.cloneFamily(contCell)) + ", ColumnName:" + Bytes.toString(CellUtil.cloneQualifier(contCell)) + ", value:" + Bytes.toString(CellUtil.cloneValue(contCell))); } } // 关闭资源 inboxTable.close(); contTable.close(); connection.close();
6.5 获取某个人的所有微博详情
// 5 获取某个人的所有微博详情 public static void getWeiBo(String uid) throws IOException { Connection connection = ConnectionFactory.createConnection(Constants.CONFIGURATION); Table contTable = connection.getTable(TableName.valueOf(Constants.CONTENT_TABLE)); // 使用Scan起始终止行键的方法 // Scan scan = new Scan(Bytes.toBytes(uid + "_"), Bytes.toBytes(uid + "|")); Scan scan = new Scan(); // 构建过滤器 RowFilter rowFilter = new RowFilter(CompareFilter.CompareOp.EQUAL, new SubstringComparator(uid + "_")); scan.setFilter(rowFilter); ResultScanner resultScanner = contTable.getScanner(scan); for (Result result : resultScanner) { for (Cell contCell : result.rawCells()) { System.out.println("RK:" + Bytes.toString(CellUtil.cloneRow(contCell))+ ", ColumnFamily:" + Bytes.toString(CellUtil.cloneFamily(contCell)) + ", ColumnName:" + Bytes.toString(CellUtil.cloneQualifier(contCell)) + ", Value:" + Bytes.toString(CellUtil.cloneValue(contCell))); } } // 关闭资源 contTable.close(); connection.close(); }
可以使用Scan起始终止行键的方法,也可以使用过滤器的方法。这里创建一个行键过滤器RowFilter,第一个参数传入比较符,第二个参数传入要比较的对象。之后通过setFilter()方法为Scan对象添加过滤器。
Scan scan = new Scan(); // 构建过滤器 RowFilter rowFilter = new RowFilter(CompareFilter.CompareOp.EQUAL, new SubstringComparator(uid + "_")); scan.setFilter(rowFilter); ResultScanner resultScanner = contTable.getScanner(scan);
7 测试代码
在test包下新建TestWeiBo类,存放程序主函数。
public class TestWeiBo { public static void init() { try { // 创建命名空间 HBaseUtil.createNamespace(Constants.NAMESPACE); // 创建微博内容表 HBaseUtil.createTable(Constants.CONTENT_TABLE, Constants.CONTENT_TABLE_VERSIONS, Constants.CONTENT_TABLE_CF); // 创建用户关系表 HBaseUtil.createTable(Constants.RELATION_TABLE, Constants.RELATION_TABLE_VERSIONS, Constants.RELATION_TABLE_CF1, Constants.RELATION_TABLE_CF2); // 创建微博收件箱表 HBaseUtil.createTable(Constants.INBOX_TABLE, Constants.INBOX_TABLE_VERSIONS, Constants.INBOX_TABLE_CF); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException, InterruptedException { // 初始化 init(); // 1001发布微博 HBaseDao.publishWeiBo("1001", "1001的第一条微博"); // 1002关注1001和1003 HBaseDao.addAttends("1002", "1001", "1003"); // 获取1002初始化页面 HBaseDao.getInit("1002"); System.out.println("------------111------------"); // 1003发布3条微博,同时1001发布2条微博 Thread.sleep(10); HBaseDao.publishWeiBo("1003", "1003的第一条微博"); Thread.sleep(10); HBaseDao.publishWeiBo("1001", "1001的第二条微博"); Thread.sleep(10); HBaseDao.publishWeiBo("1003", "1003的第二条微博"); Thread.sleep(10); HBaseDao.publishWeiBo("1001", "1001的第三条微博"); Thread.sleep(10); HBaseDao.publishWeiBo("1003", "1003的第三条微博"); // 获取1002初始化页面 HBaseDao.getInit("1002"); System.out.println("------------222------------"); // 1002取关1003 HBaseDao.deleteAttends("1002", "1003"); // 获取1002初始化页面 HBaseDao.getInit("1002"); System.out.println("------------333------------"); // 1002再次关注1003 HBaseDao.addAttends("1002", "1003"); // 获取1002初始化页面 HBaseDao.getInit("1002"); System.out.println("------------444------------"); // 获取1001微博详情 HBaseDao.getWeiBo("1001"); System.out.println("------------555------------"); // 获取1003微博详情 HBaseDao.getWeiBo("1003"); } }
程序运行结果如下:(结果有点不对,还没有解决这个问题..)