说明: Hadoop一些版本内部的变化还是挺多的,大家在上网找教程的时候也要注意教程版本与生产环境的版本
本示例Hadoop的版本为3.3.6,SpringBoot版本为2.7.0,Java17,以下内容根据实际生产环境情况总结而来,如果有不对的地方请大佬指出
包含案例(电商平台):记录用户画像进行推送商品,根据商品描述分词匹配最适合的商品
Hadoop概念
在开发中,我们经常说到两个和大数据有关的框架——Hadoop和Spark,那么相信有很多人跟我刚开始一样,认为这两个框架是对立的关系,但实际上这两个框架更像是在不同层次上的不同解决方案,如果硬要类比的话,Hadoop更多解决的是对于性能要求不是特别高并且又要求较高的安全性的场景,Sprak解决的是对于性能和效率要求较高的应用场景
我们可以把Hadoop分为两个东西: HDFS和MapReduce,以下教程也将会分开来叙述
HDFS:
HDFS是分布式文件存储系统的缩写,简单来讲,就是一个文件管理器,而在Hadoop的HDFS中,对于文件的任何操作,都是有权限的,并且这个权限还比较复杂,等下会详细来说明
MapReduce:
MapReduce其实也可以分为两个阶段:
- Map阶段:
对于文本数据进行统计,并传入Reduce阶段
- Reduce阶段:
对于Map阶段的数据进行统计并输出
在MapReduce中,所有操作和阶段之间的数据通信是在磁盘上(以文件的方式)进行的,所以进行MapReduce计算的时候我们一般不是每次相同请求都进行一次匹配(这样对服务器的IO是个比较大的考验),我们会将请求的关键字的hash作为一个逻辑缓存存入HDFS,每次请求的时候先从逻辑缓存处判断是否需要运行MapReduce运算
HDFS:
HDFS中的权限管理
HDFS中和权限有关的关键词有: 租约,用户,以及文件权限(包含读写等)
- 租约
租约是针对于某一个文件而言,就是类似于操作系统中的文件的写权限只能由一个应用独占这样(租约只会针对于文件的写操作,不会影响文件的读操作)
注意:这里的租约也是有一个心跳包策略,如果在xxx时间内,没有响应租约,那么该租约作废,可以被其他"应用"占用
- 用户
用户是指的是对于某个目录,当前执行操作的用户名字符串(有点不懂,但大部分情况下是用户名),每次涉及到创建文件就会指定该文件/目录属于某个用户,其他用户无权限操作(root除外)
- 文件权限
类比于操作系统中的文件权限:读/写,可读/不可写等,HDFS中新增了一个追加权限
注意: 这里的文件权限是在文件创建的时候就指定了的,不可以在文件创建后进行更改,如果想要更改只能将文件删除 后再进行更改
HDFS开发的一些前置操作和坑
在win下开发需要准备下载为win开发的bin包
以下是找到的最全的地址: github.com/kontext-tec…
只需要找个文件夹把文件中的bin放入充当HadoopHome就行
HDFS操作封装代码
封装核心操作包含:读取,删除,追加文件,增加和下载文件
配置Bean:创建fileSystem(即创建和Hadoop的HDFS连接,类比于MySQL的connection,但这个是建议持久化作为Bean容器的)
@Value("${hadoop.fs.defaultFS}")
private String defaultFS;//这里是你Hadoop集群的地址(以hdfs://开头的那个)
@Value("${hadoop.binSrc}")
private String hadoopHome;//这里是你本地开发中的Hadoop文件夹放的路径
@Bean
public FileSystem fileSystemInit() throws IOException, URISyntaxException, InterruptedException {
String hadoop = System.getenv("HADOOP_HOME");//获取环境变量,判断当前环境是否有Hadoop配置,有的话就直接用HADOOP_HOME的变量,否则就是配置文件中的HADOOP_HOME(要求服务器也要有Hadoop的)
if (StringUtil.isNotEmpty(hadoop)) System.setProperty("hadoop.home.dir", hadoop);
else System.setProperty("hadoop.home.dir", hadoopHome);
Configuration conf = new Configuration();
conf.setBoolean("dfs.support.append", true);//支持文件追加权限
conf.set("dfs.replication", "1");
//↑副本数量,设置为1,因为公司用的单机部署,所以这里要加,大家看生产环境选择性加(一般节点数低于3的都需要加这个,value值按照有几个节点就写几个节点)
URI uri=new URI(defaultFS);
conf.set("fs.defaultFS", defaultFS);//设置HDFS的地址
FileSystem fileSystem = FileSystem.get(uri,conf,"hadoop");//第三个参数是用户/用户名
log.info("fileSystem创建成功");
return fileSystem;
// return null;
}
应用关闭前的操作(释放租约)
@Component
@Log4j2
public class PreCloseDoing {
@Autowired
private HadoopUtil hadoopUtil;
@PreDestroy
public void CloseFile(){
hadoopUtil.CloseFileSystem();
log.info("已关闭FileSystem,释放租约");
}
}
核心操作封装:
import com.alibaba.fastjson2.JSON;
import com.github.pagehelper.util.StringUtil;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.hadoop.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.*;
import java.util.List;
@Component
public class HadoopUtil {
@Value("${hadoop.fs.defaultFS}")
private String defaultFS;
@Autowired
private FileSystem fileSystem;
/**
* @param srcPath:本地文件路径
* @param dstPath:HDFS中的文件路径
* @return 上传结果
*/
public boolean uploadFile(String srcPath, String dstPath) {
Path src = new Path(srcPath);
Path dst = new Path(defaultFS + dstPath);
try {
fileSystem.copyFromLocalFile(false, true, src, dst);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* @param FileName:上传到Hadoop的HDFS文件路径名字(包含路径)
* @param data:byte流数据
*/
public void uploadFile(String FileName, byte[] data) {
Path dstPath = new Path(FileName);
FSDataOutputStream out = null;
try {
out = fileSystem.create(dstPath, true);
out.write(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
CloseStream(out);
}
}
public void CloseFileSystem() {
try {
if (fileSystem != null) {
fileSystem.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @param srcPath:远程文件路径
* @param dstPath:本地文件路径路径
* @return 下载结果
*/
public boolean downloadFile(String srcPath, String dstPath) {
Path src = new Path(srcPath);
Path dst = new Path(dstPath);
try {
fileSystem.copyToLocalFile(false, src, dst, true);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
public boolean deleteFile(String dstPath) {
if (!fileExists(dstPath)) return false;
try {
fileSystem.delete(new Path(dstPath), true);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* @param outputStream:要下载到的输出流
* @param dstPath:在服务器中的地址,前提是文件要存在
*/
public void downloadFile(OutputStream outputStream, String dstPath) {
Path downLoadPath = new Path(dstPath);
FSDataInputStream in = null;
try {
in = fileSystem.open(downLoadPath);
IOUtils.copyBytes(in, outputStream, 4096, false);
} catch (IOException e) {
e.printStackTrace();
} finally {
CloseStream(in);
}
}
public boolean fileExists(String path) {
Path file = new Path(defaultFS + path);
try {
return fileSystem.exists(file);
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/**
* @param distPath:文件上传的路径
* @param text:上传的文本内容
*/
public void appendStringToFile(String distPath, String text) {
Path appendPath = new Path(distPath);
FSDataOutputStream out = null;
try {
out = fileSystem.append(appendPath);
String appendStr = text + "\r\n";
out.write(appendStr.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
CloseStream(out);
}
}
public HadoopResult readResult(String path) {
HadoopResult result = new HadoopResult();
// 检查文件是否存在
Path filePath = new Path(path);
try {
if (!fileSystem.exists(filePath)) {
throw new IOException("文件不存在:" + path);
}
} catch (IOException e) {
e.printStackTrace();
}
// 读取文件内容
try {
BufferedReader br = new BufferedReader(new InputStreamReader(fileSystem.open(filePath)));
String line;
while (StringUtil.isNotEmpty(line = br.readLine())) {
result.addResult(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
*
* @param path:文件在HDFS中的路径
* @param maxTime: 最大存活时间
* @param containReadTime: 是否需要考虑上次读的时间
* @return
*/
public boolean FileIsInTime(String path,long maxTime,boolean containReadTime){
try {
boolean exists = fileExists(path);
if (!exists) return false;
FileStatus status = fileSystem.getFileStatus(new Path(path));
long createTime = containReadTime? Math.max(status.getModificationTime(), status.getAccessTime()):status.getModificationTime();
long currentTimeMillis = System.currentTimeMillis();
return (currentTimeMillis-createTime)< maxTime;
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
private void CloseStream(Closeable stream) {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
注: 如果大家生产环境正常的话,应该只需要上面的那一堆
但如果很不幸,碰到了和我一样的情况:Hadoop只支持在localHost进行操作,即做一些操作会将网络重定向到本地xxx端口的话,那你应该需要下面的代理服务器
前提:Hadoop重定向到的那个端口你也要暴露出来(把防火墙开放端口)
特殊情况下你可能需要代理服务器:
其中的log可以自行替换或者直接System.out
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import lombok.extern.log4j.Log4j2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class PortForwardServer {
private static final String TARGET_HOST = "";//服务器地址
private static final int TARGET_PORT = 0000;//重定向的端口
private boolean flag = true;
private ServerSocket serverSocket = null;
private final Log log = LogFactory.get(Log4j2.class);
public void run() throws IOException {
try {
// 创建 ServerSocket 对象,并绑定到本地地址 127.0.0.1:TARGET_PORT
serverSocket = new ServerSocket(TARGET_PORT);
log.info("代理服务器已启动");
while (flag) {
// 接受客户端连接请求
Socket clientSocket = null;
try {
clientSocket = serverSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
// 创建代理线程,并启动代理线程
new ProxyThread(clientSocket, TARGET_HOST, TARGET_PORT).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void Close() {
flag = false;
if (serverSocket!=null){
if (!serverSocket.isClosed()){
try {
serverSocket.close();
log.info("代理服务器已关闭");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
监听线程:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ProxyThread extends Thread {
private Socket clientSocket;
private final String TARGET_HOST;
private final int TARGET_PORT;
public ProxyThread(Socket clientSocket,String TARGET_HOST,int TARGET_PORT) {
this.clientSocket = clientSocket;
this.TARGET_HOST=TARGET_HOST;
this.TARGET_PORT=TARGET_PORT;
}
@Override
public void run() {
try {
// 连接目标服务器
Socket targetSocket = new Socket(TARGET_HOST, TARGET_PORT);
// 创建代理线程,并启动代理线程
new TargetThread(clientSocket.getInputStream(), targetSocket.getOutputStream()).start();
new TargetThread(targetSocket.getInputStream(), clientSocket.getOutputStream()).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class TargetThread extends Thread {
private InputStream in;
private OutputStream out;
public TargetThread(InputStream in, OutputStream out) {
this.in = in;
this.out = out;
}
@Override
public void run() {
try {
// 读取输入流中的数据,并写入输出流中
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
如下是starter的代码:(有些注解没写)
注意:这里的portForwardServer.run();必须在SpringApplication.run(Starter.class);之后,因为启动代理服务器会阻塞掉代码的执行,不会执行到启动SpringBoot,当然的也可以大佬来优化
public class Starter {
private static PortForwardServer portForwardServer=new PortForwardServer();
public static void main(String[] args) {
SpringApplication.run(Starter.class);
try {
portForwardServer.run();
} catch (IOException e) {
e.printStackTrace();
}
}
@PreDestroy
public void CloseServer(){
portForwardServer.Close();
}
}