Hbase 实现微博好友关注功能

前言

在社交网站中,比如像QQ,微信,新浪微博之类的,像这种社交属性强的app,里面必然会有好友关注、好友推荐、好友动态等这样的功能;

拿好友功能来说,一般来讲,当用户量还不够大的时候,mysql就可以解决,当用户量继续增长的时候,mysql配合redis也可以解决,当数据继续增长,上升到诸如像微博,QQ这样的体量时,可能需要考虑存储效率更好、承载容量更大的大数据存储引擎了,比如像 hbase这种轻松可以容纳TB甚至PB级别数据的大数据库引擎;

业务需求分析

本例需实现2个功能

  • 好友关注与取关注;
  • 发布微博;

这是两个几乎所有社交类APP都会涉及到的功能,拿出来探讨并通过代码进行实现;

实现过分析

从业务实现的需求出发,既然是使用Habse来存储数据,肯定是先分析表结构,把需要创建的表规划清楚;

1、好友关注功能设计思路

对于好友关注与取关注这个功能,首先想到的,肯定是用户关系表,通过这个表,保存好友之间的关系,具体该如何设计呢?

在这里插入图片描述

以上面这张图为例进行说明:

  • 创建一张保存用户关系的表,cf有两个,分别为关注人列表和粉丝列表,这里简化操作,忽略了用户的其他字段,仅保存了用户的主键ID,而rk为每个用户,以user1用户来说,关注人列表中有 user2,user3和user4,而粉丝列表中有 user3,user4和user5;
  • 对于user1来讲,每次关注一个新用户,则往rk为user1的这一栏的关注人列表中,添加新的关注人即可;
  • 同样,当有人关注了user1时,就在rk为user1的这一栏的粉丝列簇中,添加一条数据;
  • 再则,我们看到,当user1关注了user2的时候,对usr1做了操作之后,同样需要对user2这个数据做同样的操作;

上面的分析即为编写代码时候的指导思路,下面就开始编码过程实现吧;

2、发布微博功能设计思路

设想一下,当我们发布一条微博动态的时候,最直接的就是我们的粉丝,粉丝在进入首页刷新页面的时候,我们的粉丝将会接收到我们发布最新的内容,因此,很明显这里需要一张微博内容表;

在这里插入图片描述

内容表比较简单,cf为微博内容,rk以user_id即可,当某个用户发布一条微博内容时,往该表插入一条数据即可,问题是,用户会发布多个动态,于是以时间戳(版本)来区分,实际在操作的时候,可以以时间错倒叙进行存储;

3、获取微博动态内容功能设计思路

当我们打开为微博或微信的时候,就能收到我们关注好友的最新的发布内容,这个是怎么做到的呢?

从上面的两张表分析来看,好像是各自独立的,因为好友关注和用户发布微博写到内容表确实是两个不相干的操作,而对于某个具体的用户来讲,刷微博的时候,我们能够联想到,自然是从粉丝表中拿到粉丝,再把这些粉丝最近发布的微博拿出来进行展现不就好了吗?

这个思路其实也不是不可以,问题是,操作繁琐,有没有什么办法可以解决呢?于是,我们想到可以使用一张中间关联表;

在这里插入图片描述

从这个表的存储结构来看,这张表冗余了某个用户的关注人的发布的微博内容,以user1用户为例做说明,列簇中,维护了一个个关注人以及关注人对应的内容rowkey,实际中,根据实际微博内容展示的数量,可以冗余510条,而这510条内容的,正好按照时间倒叙取过来即可;

那么这个表的数据什么时候存进去呢?

很明显可以想到,这是在某个用户发布一条微博内容的时候,那么梳理下这个过程,大概如下:

  • user1有个粉丝user2,user1发布一条微博动态;
  • usr1发布的微博内容将会存储到微博内容表;
  • 由于user2是user1的粉丝,对于user2来说,将会在上面的内容关系表中,将user1发表的内容rowkey拿过来存储到这个表中

环境准备

1、基于centos7或者windows环境,搭建一台单机版的hbase服务环境,我这边已提前准备好并开启了hbase 的shell客户端;

在这里插入图片描述

2、添加基础maven依赖

		<dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase</artifactId>
            <version>1.3.1</version>
        </dependency>

3、定义一个常量类,用于保存3张表名称以及相关的列簇信息

public class WeiboConstants {
    
    

    /**
     * 微博命名空间
     */
    public static final String NMAE_SPACE = "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;

}

4、基础工具类

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;

import java.io.IOException;

public class CommonUtils {
    
    

    public static Connection connection = null;
    public static Admin admin = null;

    static {
    
    
        Configuration conf = HBaseConfiguration.create();
        //使用 HBaseConfiguration 的单例方法实例化
        conf = HBaseConfiguration.create();
        conf.set("hbase.zookeeper.quorum", "127.0.0.1");
        conf.set("hbase.zookeeper.property.clientPort", "2181");
        try {
    
    
            connection = ConnectionFactory.createConnection(conf);
            admin = connection.getAdmin();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    public static Connection getConnection(){
    
    
        return connection;
    }

    /**
     * 创建表
     * @param tableName 表名
     * @param columnFamily 列簇名
     * @throws Exception
     */
    public static void createTable(String tableName, int versions,String... columnFamily) throws Exception {
    
    

        if (columnFamily.length <= 0) {
    
    
            System.out.println("请传入列簇信息");
        }

        //判断表是否存在
        if (isTableExists(tableName)) {
    
    
            System.out.println("表" + tableName + "已存在");
            close();
            return;
        }

        //创建表属性对象,表名需要转字节
        HTableDescriptor descriptor = new HTableDescriptor(TableName.valueOf(tableName));

        //循环创建多个列族
        for (String cf : columnFamily) {
    
    
            HColumnDescriptor columnDescriptor = new HColumnDescriptor(cf);
            columnDescriptor.setMaxVersions(versions);
            descriptor.addFamily(columnDescriptor);
        }

        //根据对表的配置,创建表
        admin.createTable(descriptor);
        System.out.println("表" + tableName + "创建成功!");

        close();
    }

    /**
     * 判断表是否存在
     * @param tableName
     * @return
     * @throws Exception
     */
    public static boolean isTableExists(String tableName) throws Exception {
    
    
        boolean result = admin.tableExists(TableName.valueOf(tableName));
        return result;
    }

    /**
     * 创建命名空间
     * @param nameSpace
     */
    public static void createNameSpace(String nameSpace){
    
    
        if(nameSpace == null){
    
    
            System.out.println(nameSpace + ": 不存在 !" );
            return;
        }
        NamespaceDescriptor namespaceDescriptor = NamespaceDescriptor.create(nameSpace).build();
        try {
    
    
            admin.createNamespace(namespaceDescriptor);
        } catch (NamespaceExistException e){
    
    
            System.out.println("命名空间已存在");
        }
        catch (IOException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(nameSpace + ": 命名空间创建成功");
    }

    /**
     * 关闭连接
     */
    public static void close() {
    
    
        if (admin != null) {
    
    
            try {
    
    
                admin.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
        if (connection != null) {
    
    
            try {
    
    
                connection.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

}

一、发布微博代码实现

以用户1为例,用户1将要发布一条微博,那么要进行什么样的操作呢?

代码逻辑

  • 操作主表:微博内容表,用户1发布的内容插入到微博内容表
  • 在用户关系表中,找到用户1的粉丝列表;
  • 拿到用户1的粉丝列表后,再在内容关系表中,依次将本次的内容插入到内容关系表

具体代码实现过程可参考代码中的注释

public class WeiBoService {
    
    

    private static CommonUtils commonUtils = new CommonUtils();

    /**
     * 发布微博
     *
     * @param uid
     * @param content
     */
    public static void publish(String uid, String content) throws Exception {
    
    

        //1、操作微博内容表
        Connection connection = commonUtils.getConnection();
        Table contentTable = connection.getTable(TableName.valueOf(WeiboConstants.CONTENT_TABLE));

        //2、操作微博内容表

        long ts = System.currentTimeMillis();
        String rowKey = uid + "_" + ts;

        Put contentPut = new Put(Bytes.toBytes(rowKey));
        contentPut.addColumn(
                Bytes.toBytes(WeiboConstants.CONTENT_TABLE_CF),
                Bytes.toBytes("content"),
                Bytes.toBytes(content)
        );
        contentTable.put(contentPut);

        //3、操作微博内容关联表
        Table relationTable = connection.getTable(TableName.valueOf(WeiboConstants.RELATION_TABLE));
        //获取当前发布人的粉丝列表
        Get get = new Get(Bytes.toBytes(uid));
        get.addFamily(Bytes.toBytes(WeiboConstants.RELATION_TABLE_CF2));
        Result result = relationTable.get(get);

        //4、遍历这个人的粉丝,循环构建微博内容关系列表,往该表插入数据
        List<Put> inboxPuts = new ArrayList<>();
        for (Cell cell : result.rawCells()) {
    
    

            Put inboxPut = new Put(CellUtil.cloneQualifier(cell));
            //将当前用户发布的内容放进去
            inboxPut.addColumn(
                    Bytes.toBytes(WeiboConstants.INBOX_TABLE_CF),
                    Bytes.toBytes(uid),
                    Bytes.toBytes(rowKey));
            inboxPuts.add(inboxPut);
        }

        //5、将内容关系数据入库
        if (inboxPuts != null && inboxPuts.size() > 0) {
    
    
            Table inboxTable = connection.getTable(TableName.valueOf(WeiboConstants.INBOX_TABLE));
            inboxTable.put(inboxPuts);
        }
        //6、关闭资源
        commonUtils.close();
    }

}

二、关注好友功能代码实现

场景,现在用户1要关注用户2,用户3,用户4…

代码逻辑分析:

  • 操作主表,用户关系表;
  • 用户关系表有2个列簇,rw为当前操作人即user1的ID,关注人和粉丝各自一个cf;
  • 首先,给用户1的关注的cf插入数据;
  • 用户1关注了2,3,4,那么,1将作为2,3,4的粉丝,需要将1分别添加到2,3,4用户的粉丝列表对应的cf中;
  • 需要在内容关系表中,新增rk为user1,指定列簇下,2,3,4用户的最新发布的微博内容rk的数据,便于后续获取微博动态时使用;
import com.congge.constants.WeiboConstants;
import com.congge.utils.CommonUtils;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

import java.util.ArrayList;
import java.util.List;

public class WeiBoService {
    
    

    private static CommonUtils commonUtils = new CommonUtils();

    /**
     * 关注好友
     *
     * @param uid
     * @param attends
     */
    public static void attendUser(String uid, String... attends) throws Exception {
    
    

        if (attends.length <= 0) {
    
    
            System.out.println("请选择关注人");
            return;
        }
        Connection connection = commonUtils.getConnection();

        //1、关系表添加数据
        Table relationTable = connection.getTable(TableName.valueOf(WeiboConstants.RELATION_TABLE));
        List<Put> allPuts = new ArrayList<>();
        Put uidPut = new Put(Bytes.toBytes(uid));

        for (String attend : attends) {
    
    
            //2、给当前操作人循环添加粉丝
            uidPut.addColumn(Bytes.toBytes(WeiboConstants.RELATION_TABLE_CF1),
                    Bytes.toBytes(attend), Bytes.toBytes(attend));


            //3、同时,对于被关注的人来说,他们的粉丝列簇里面需要把当前操作人放进去
            Put attendPut = new Put(Bytes.toBytes(attend));
            attendPut.addColumn(Bytes.toBytes(WeiboConstants.RELATION_TABLE_CF2),
                    Bytes.toBytes(uid), Bytes.toBytes(uid));
            allPuts.add(attendPut);

        }

        //将操作人自身的put对象也添加进去
        allPuts.add(uidPut);
        //数据写入关系表
        relationTable.put(allPuts);

        //2、操作内容表
        Table contentTable = connection.getTable(TableName.valueOf(WeiboConstants.CONTENT_TABLE));

        //创建关系内容表的PUT对象
        Put inboxPut = new Put(Bytes.toBytes(uid));

        long ts = System.currentTimeMillis();
        // 获取微博内容表数据,然后依次添加到关系表
        for (String attend : attends) {
    
    
            //获取这个人近期发布的微博内容
            Scan scan = new Scan(Bytes.toBytes(attend + "_"), Bytes.toBytes(attend + "|"));
            ResultScanner resultScanner = contentTable.getScanner(scan);
            for (Result result : resultScanner) {
    
    
                inboxPut.addColumn(Bytes.toBytes(WeiboConstants.INBOX_TABLE_CF),
                        Bytes.toBytes(attend), ts++, result.getRow());
            }
        }
        //插入数据
        if (!inboxPut.isEmpty()) {
    
    
            Table inboxTable = connection.getTable(TableName.valueOf(WeiboConstants.INBOX_TABLE));
            inboxTable.put(inboxPut);
        }
    }
}

二、获取某用户微博动态代码实现

场景:假设用户1打开微博,这时候将会出现用户1的好友的最新的微博内容

实现逻辑分析:

  • 以用户1的rk查询微博内容关系表的数据;
  • 第一步中查到的,由于只是其关注用户的rk,需要拿到r再去内容表中回查;
  • 再去内容表中,查到各个关注人的rk对应的微博内容即可;
import com.congge.constants.WeiboConstants;
import com.congge.utils.CommonUtils;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

import java.util.ArrayList;
import java.util.List;

public class WeiBoService {
    
    

    private static CommonUtils commonUtils = new CommonUtils();

    /**
     * 获取某用户的初始化页面
     *
     * @param uid
     */
    public static void getInit(String uid) throws Exception {
    
    
        Connection connection = commonUtils.getConnection();
        Table inboxTable = connection.getTable(TableName.valueOf(WeiboConstants.INBOX_TABLE));
        Table contentTable = connection.getTable(TableName.valueOf(WeiboConstants.CONTENT_TABLE));

        //创建内容关联表对象
        Get inboxGet = new Get(Bytes.toBytes(uid));
        inboxGet.setMaxVersions();
        Result result = inboxTable.get(inboxGet);

        for (Cell cell : result.rawCells()) {
    
    
            //构建微博内容GET对象
            Get contentGet = new Get(CellUtil.cloneQualifier(cell));
            Result contentResult = contentTable.get(contentGet);

            for (Cell contentCell : contentResult.rawCells()) {
    
    
                System.out.println(
                        "RK:" + Bytes.toString(CellUtil.cloneRow(contentCell)) +
                                ",CF:" + Bytes.toString(CellUtil.cloneFamily(contentCell)) +
                                ",CN :" + Bytes.toString(CellUtil.cloneQualifier(contentCell)) +
                                ",Value :" + Bytes.toString(CellUtil.cloneValue(contentCell))
                );
            }

        }

        commonUtils.close();
    }

}

猜你喜欢

转载自blog.csdn.net/zhangcongyi420/article/details/124208472
今日推荐