暂时未有相关云产品技术能力~
前言文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式详细教程秒传1、什么是秒传通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了.2、本文实现的秒传核心逻辑a、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,b、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径分片上传1.什么是分片上传分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。2.分片上传的场景1.大文件上传2.网络环境环境不好,存在需要重传风险的场景断点续传1、什么是断点续传断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。2、应用场景断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。3、实现断点续传的核心逻辑在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。4、实现流程步骤a、方案一,常规步骤将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;初始化一个分片上传任务,返回本次分片上传唯一标识;按照一定的策略(串行或并行)发送各个分片数据块;发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。b、方案二、本文实现的步骤前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。5、分片上传/断点上传代码实现a、前端采用百度提供的webuploader的插件,进行分片。因本文主要介绍服务端代码实现,webuploader如何进行分片,具体实现可以查看如下链接:http://fex.baidu.com/webuploader/getting-started.htmlb、后端用两种方式实现文件写入,一种是用RandomAccessFile,如果对RandomAccessFile不熟悉的朋友,可以查看如下链接:https://blog.csdn.net/dimudan2015/article/details/81910690另一种是使用MappedByteBuffer,对MappedByteBuffer不熟悉的朋友,可以查看如下链接进行了解:https://www.jianshu.com/p/f90866dcbffc后端进行写入操作的核心代码a、RandomAccessFile实现方式@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS) @Slf4j public class RandomAccessUploadStrategy extends SliceUploadTemplate { @Autowired private FilePathUtil filePathUtil; @Value("${upload.chunkSize}") private long defaultChunkSize; @Override public boolean upload(FileUploadRequestDTO param) { RandomAccessFile accessTmpFile = null; try { String uploadDirPath = filePathUtil.getPath(param); File tmpFile = super.createTmpFile(param); accessTmpFile = new RandomAccessFile(tmpFile, "rw"); //这个必须与前端设定的值一致 long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize(); long offset = chunkSize * param.getChunk(); //定位到该分片的偏移量 accessTmpFile.seek(offset); //写入该分片数据 accessTmpFile.write(param.getFile().getBytes()); boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); return isOk; } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.close(accessTmpFile); } return false; } }b、MappedByteBuffer实现方式@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER) @Slf4j public class MappedByteBufferUploadStrategy extends SliceUploadTemplate { @Autowired private FilePathUtil filePathUtil; @Value("${upload.chunkSize}") private long defaultChunkSize; @Override public boolean upload(FileUploadRequestDTO param) { RandomAccessFile tempRaf = null; FileChannel fileChannel = null; MappedByteBuffer mappedByteBuffer = null; try { String uploadDirPath = filePathUtil.getPath(param); File tmpFile = super.createTmpFile(param); tempRaf = new RandomAccessFile(tmpFile, "rw"); fileChannel = tempRaf.getChannel(); long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize(); //写入该分片数据 long offset = chunkSize * param.getChunk(); byte[] fileData = param.getFile().getBytes(); mappedByteBuffer = fileChannel .map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); mappedByteBuffer.put(fileData); boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); return isOk; } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.freedMappedByteBuffer(mappedByteBuffer); FileUtil.close(fileChannel); FileUtil.close(tempRaf); } return false; } }c、文件操作核心模板类代码@Slf4j public abstract class SliceUploadTemplate implements SliceUploadStrategy { public abstract boolean upload(FileUploadRequestDTO param); protected File createTmpFile(FileUploadRequestDTO param) { FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class); param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath())); String fileName = param.getFile().getOriginalFilename(); String uploadDirPath = filePathUtil.getPath(param); String tempFileName = fileName + "_tmp"; File tmpDir = new File(uploadDirPath); File tmpFile = new File(uploadDirPath, tempFileName); if (!tmpDir.exists()) { tmpDir.mkdirs(); } return tmpFile; } @Override public FileUploadDTO sliceUpload(FileUploadRequestDTO param) { boolean isOk = this.upload(param); if (isOk) { File tmpFile = this.createTmpFile(param); FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile); return fileUploadDTO; } String md5 = FileMD5Util.getFileMD5(param.getFile()); Map<Integer, String> map = new HashMap<>(); map.put(param.getChunk(), md5); return FileUploadDTO.builder().chunkMd5Info(map).build(); } /** * 检查并修改文件上传进度 */ public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) { String fileName = param.getFile().getOriginalFilename(); File confFile = new File(uploadDirPath, fileName + ".conf"); byte isComplete = 0; RandomAccessFile accessConfFile = null; try { accessConfFile = new RandomAccessFile(confFile, "rw"); //把该分段标记为 true 表示完成 System.out.println("set part " + param.getChunk() + " complete"); //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127 accessConfFile.setLength(param.getChunks()); accessConfFile.seek(param.getChunk()); accessConfFile.write(Byte.MAX_VALUE); //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传) byte[] completeList = FileUtils.readFileToByteArray(confFile); isComplete = Byte.MAX_VALUE; for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) { //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE isComplete = (byte) (isComplete & completeList[i]); System.out.println("check part " + i + " complete?:" + completeList[i]); } } catch (IOException e) { log.error(e.getMessage(), e); } finally { FileUtil.close(accessConfFile); } boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete); return isOk; } /** * 把上传进度信息存进redis */ private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath, String fileName, File confFile, byte isComplete) { RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class); if (isComplete == Byte.MAX_VALUE) { redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true"); redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5()); confFile.delete(); return true; } else { if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) { redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false"); redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(), uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf"); } return false; } } /** * 保存文件操作 */ public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) { FileUploadDTO fileUploadDTO = null; try { fileUploadDTO = renameFile(tmpFile, fileName); if (fileUploadDTO.isUploadComplete()) { System.out .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName); //TODO 保存文件信息到数据库 } } catch (Exception e) { log.error(e.getMessage(), e); } finally { } return fileUploadDTO; } /** * 文件重命名 * * @param toBeRenamed 将要修改名字的文件 * @param toFileNewName 新的名字 */ private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) { //检查要重命名的文件是否存在,是否是文件 FileUploadDTO fileUploadDTO = new FileUploadDTO(); if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) { log.info("File does not exist: {}", toBeRenamed.getName()); fileUploadDTO.setUploadComplete(false); return fileUploadDTO; } String ext = FileUtil.getExtension(toFileNewName); String p = toBeRenamed.getParent(); String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName; File newFile = new File(filePath); //修改文件名 boolean uploadFlag = toBeRenamed.renameTo(newFile); fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp()); fileUploadDTO.setUploadComplete(uploadFlag); fileUploadDTO.setPath(filePath); fileUploadDTO.setSize(newFile.length()); fileUploadDTO.setFileExt(ext); fileUploadDTO.setFileId(toFileNewName); return fileUploadDTO; } }总结在实现分片上传的过程,需要前端和后端配合,比如前后端的上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用fastdfs、hdfs等。本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的md5值计算,后端写入的速度还是比较快。如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器,其介绍可以查看官网:https://help.aliyun.com/product/31815.html阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。文末提供一个oss表单上传的链接demo,通过oss表单上传,可以直接从前端把文件上传到oss服务器,把上传的压力都推给oss服务器:https://www.cnblogs.com/ossteam/p/4942227.html
对很多Mac用户来说,想用远程控制请教下大佬,太难了。在Windows上一个QQ就能搞定的事,而Mac用户几乎只能依赖Teamviewer。Teamviewer还遭到不少吐槽:占用高、打开慢,有时还因为被识别为商用而收费……现在,不必再和它较劲了。这款名叫 RustDesk 的远程桌面软件火了!已经在 Github 上获得了13.3k颗星。这个名字已经“暴露”了它,没错,这款软件的开发语言正是Rust。RustDesk支持多个平台,并且“安装包”只有8~9MB,相当轻量了。而且,这款软件属于半便携式,无需安装和配置,开箱即用。用户界面也是非常直观、简单:RustDesk采用的是加密直连,先尝试打洞直连,帮助两者建立连接,如果失败再通过服务器转发。它支持跨平台传输文件。比如,Mac和Windows电脑之间进行文件传输时,界面长这样:Gitee上显示,这位开发者是一位中国程序员,当然软件也支持中文版。苦远程久矣的我,上手试了一下~选择Mac和Android手机客户端,下载安装一气呵成。打开后,界面的确非常清爽,大概是这样:不过,公共服务器目前是不支持修改ID的。接下来,如果想通过手机访问电脑,需要输入对应的ID和密码。接通之后,手机上会显示电脑端的操作界面,并且双指可以调节画面大小:点击下方的工具栏,分别会显示常用的电脑按键,以及一些设置项:此外,如果电脑客户端处于在线状态,在旁边无人的情况下,也可以在手机端输入密码直接访问。有点遗憾的是,RustDesk目前还无法实现对Android设备的控制。其实从去年开始,作者已经开始更新软件版本。不少网友表示:软件体积小、界面简洁,比Teamviewer香~不过也有人基于安全性提出质疑。作者在V2EX上表示,已经在GitHub上开源了90%的代码(算上总代码量),但是保留了服务器代码以及移动端。此外,由于存在内网穿透失败而连接很慢的情况,RustDesk还支持自建中继服务器,并且提供了教程。
一. 简介导出是后台管理系统的常用功能,当数据量特别大的时候会内存溢出和卡顿页面,曾经自己封装过一个导出,采用了分批查询数据来避免内存溢出和使用SXSSFWorkbook方式缓存数据到文件上以解决下载大文件EXCEL卡死页面的问题。不过一是存在封装不太友好使用不方便的问题,二是这些poi的操作方式仍然存在内存占用过大的问题,三是存在空循环和整除的时候数据有缺陷的问题,以及存在内存溢出的隐患。无意间查询到阿里开源的EasyExcel框架,发现可以将解析的EXCEL的内存占用控制在KB级别,并且绝对不会内存溢出(内部实现待研究),还有就是速度极快,大概100W条记录,十几个字段,只需要70秒即可完成下载。遂抛弃自己封装的,转战研究阿里开源的EasyExcel. 不过 说实话,当时自己封装的那个还是有些技术含量的,例如:外观模式,模板方法模式,以及委托思想,组合思想,可以看看。另外,微信搜索关注Java技术栈,发送:设计模式,可以获取我整理的 Java 设计模式实战教程。EasyExcel的github地址是:https://github.com/alibaba/easyexcel二. 案例2.1 POM依赖<!-- 阿里开源EXCEL --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>1.1.1</version> </dependency>2.2 POJO对象package com.authorization.privilege.excel; import java.util.Date; /** * @author qjwyss * @description */ public class User { private String uid; private String name; private Integer age; private Date birthday; public User() { } public User(String uid, String name, Integer age, Date birthday) { this.uid = uid; this.name = name; this.age = age; this.birthday = birthday; } public String getUid() { return uid; } public void setUid(String uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } }2.3 测试环境2.3.1.数据量少的(20W以内吧):一个SHEET一次查询导出/** * 针对较少的记录数(20W以内大概)可以调用该方法一次性查出然后写入到EXCEL的一个SHEET中 * 注意: 一次性查询出来的记录数量不宜过大,不会内存溢出即可。 * * @throws IOException */ @Test public void writeExcelOneSheetOnceWrite() throws IOException { // 生成EXCEL并指定输出路径 OutputStream out = new FileOutputStream("E:\\temp\\withoutHead1.xlsx"); ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX); // 设置SHEET Sheet sheet = new Sheet(1, 0); sheet.setSheetName("sheet1"); // 设置标题 Table table = new Table(1); List<List<String>> titles = new ArrayList<List<String>>(); titles.add(Arrays.asList("用户ID")); titles.add(Arrays.asList("名称")); titles.add(Arrays.asList("年龄")); titles.add(Arrays.asList("生日")); table.setHead(titles); // 查询数据导出即可 比如说一次性总共查询出100条数据 List<List<String>> userList = new ArrayList<>(); for (int i = 0; i < 100; i++) { userList.add(Arrays.asList("ID_" + i, "小明" + i, String.valueOf(i), new Date().toString())); } writer.write0(userList, sheet, table); writer.finish(); }2.3.2.数据量适中(100W以内):一个SHEET分批查询导出/** * 针对105W以内的记录数可以调用该方法分多批次查出然后写入到EXCEL的一个SHEET中 * 注意: * 每次查询出来的记录数量不宜过大,根据内存大小设置合理的每次查询记录数,不会内存溢出即可。 * 数据量不能超过一个SHEET存储的最大数据量105W * * @throws IOException */ @Test public void writeExcelOneSheetMoreWrite() throws IOException { // 生成EXCEL并指定输出路径 OutputStream out = new FileOutputStream("E:\\temp\\withoutHead2.xlsx"); ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX); // 设置SHEET Sheet sheet = new Sheet(1, 0); sheet.setSheetName("sheet1"); // 设置标题 Table table = new Table(1); List<List<String>> titles = new ArrayList<List<String>>(); titles.add(Arrays.asList("用户ID")); titles.add(Arrays.asList("名称")); titles.add(Arrays.asList("年龄")); titles.add(Arrays.asList("生日")); table.setHead(titles); // 模拟分批查询:总记录数50条,每次查询20条, 分三次查询 最后一次查询记录数是10 Integer totalRowCount = 50; Integer pageSize = 20; Integer writeCount = totalRowCount % pageSize == 0 ? (totalRowCount / pageSize) : (totalRowCount / pageSize + 1); // 注: 此处仅仅为了模拟数据,实用环境不需要将最后一次分开,合成一个即可, 参数为:currentPage = i+1; pageSize = pageSize for (int i = 0; i < writeCount; i++) { // 前两次查询 每次查20条数据 if (i < writeCount - 1) { List<List<String>> userList = new ArrayList<>(); for (int j = 0; j < pageSize; j++) { userList.add(Arrays.asList("ID_" + Math.random(), "小明", String.valueOf(Math.random()), new Date().toString())); } writer.write0(userList, sheet, table); } else if (i == writeCount - 1) { // 最后一次查询 查多余的10条记录 List<List<String>> userList = new ArrayList<>(); Integer lastWriteRowCount = totalRowCount - (writeCount - 1) * pageSize; for (int j = 0; j < lastWriteRowCount; j++) { userList.add(Arrays.asList("ID_" + Math.random(), "小明", String.valueOf(Math.random()), new Date().toString())); } writer.write0(userList, sheet, table); } } writer.finish(); }2.3.3.数据量很大(几百万都行):多个SHEET分批查询导出/** * 针对几百万的记录数可以调用该方法分多批次查出然后写入到EXCEL的多个SHEET中 * 注意: * perSheetRowCount % pageSize要能整除 为了简洁,非整除这块不做处理 * 每次查询出来的记录数量不宜过大,根据内存大小设置合理的每次查询记录数,不会内存溢出即可。 * * @throws IOException */ @Test public void writeExcelMoreSheetMoreWrite() throws IOException { // 生成EXCEL并指定输出路径 OutputStream out = new FileOutputStream("E:\\temp\\withoutHead3.xlsx"); ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX); // 设置SHEET名称 String sheetName = "测试SHEET"; // 设置标题 Table table = new Table(1); List<List<String>> titles = new ArrayList<List<String>>(); titles.add(Arrays.asList("用户ID")); titles.add(Arrays.asList("名称")); titles.add(Arrays.asList("年龄")); titles.add(Arrays.asList("生日")); table.setHead(titles); // 模拟分批查询:总记录数250条,每个SHEET存100条,每次查询20条 则生成3个SHEET,前俩个SHEET查询次数为5, 最后一个SHEET查询次数为3 最后一次写的记录数是10 // 注:该版本为了较少数据判断的复杂度,暂时perSheetRowCount要能够整除pageSize, 不去做过多处理 合理分配查询数据量大小不会内存溢出即可。 Integer totalRowCount = 250; Integer perSheetRowCount = 100; Integer pageSize = 20; Integer sheetCount = totalRowCount % perSheetRowCount == 0 ? (totalRowCount / perSheetRowCount) : (totalRowCount / perSheetRowCount + 1); Integer previousSheetWriteCount = perSheetRowCount / pageSize; Integer lastSheetWriteCount = totalRowCount % perSheetRowCount == 0 ? previousSheetWriteCount : (totalRowCount % perSheetRowCount % pageSize == 0 ? totalRowCount % perSheetRowCount / pageSize : (totalRowCount % perSheetRowCount / pageSize + 1)); for (int i = 0; i < sheetCount; i++) { // 创建SHEET Sheet sheet = new Sheet(i, 0); sheet.setSheetName(sheetName + i); if (i < sheetCount - 1) { // 前2个SHEET, 每个SHEET查5次 每次查20条 每个SHEET写满100行 2个SHEET合计200行 实用环境:参数:currentPage: j+1 + previousSheetWriteCount*i, pageSize: pageSize for (int j = 0; j < previousSheetWriteCount; j++) { List<List<String>> userList = new ArrayList<>(); for (int k = 0; k < 20; k++) { userList.add(Arrays.asList("ID_" + Math.random(), "小明", String.valueOf(Math.random()), new Date().toString())); } writer.write0(userList, sheet, table); } } else if (i == sheetCount - 1) { // 最后一个SHEET 实用环境不需要将最后一次分开,合成一个即可, 参数为:currentPage = i+1; pageSize = pageSize for (int j = 0; j < lastSheetWriteCount; j++) { // 前俩次查询 每次查询20条 if (j < lastSheetWriteCount - 1) { List<List<String>> userList = new ArrayList<>(); for (int k = 0; k < 20; k++) { userList.add(Arrays.asList("ID_" + Math.random(), "小明", String.valueOf(Math.random()), new Date().toString())); } writer.write0(userList, sheet, table); } else if (j == lastSheetWriteCount - 1) { // 最后一次查询 将剩余的10条查询出来 List<List<String>> userList = new ArrayList<>(); Integer lastWriteRowCount = totalRowCount - (sheetCount - 1) * perSheetRowCount - (lastSheetWriteCount - 1) * pageSize; for (int k = 0; k < lastWriteRowCount; k++) { userList.add(Arrays.asList("ID_" + Math.random(), "小明1", String.valueOf(Math.random()), new Date().toString())); } writer.write0(userList, sheet, table); } } } } writer.finish(); }2.4 生产环境2.4.0.Excel常量类package com.authorization.privilege.constant; /** * @author qjwyss * @description EXCEL常量类 */ public class ExcelConstant { /** * 每个sheet存储的记录数 100W */ public static final Integer PER_SHEET_ROW_COUNT = 1000000; /** * 每次向EXCEL写入的记录数(查询每页数据大小) 20W */ public static final Integer PER_WRITE_ROW_COUNT = 200000; }注:为了书写方便,此处俩个必须要整除,可以省去很多不必要的判断。另外如果自己测试,可以改为100,20。分享给你:Spring Boot 学习笔记。2.4.1.数据量少的(20W以内吧):一个SHEET一次查询导出@Override public ResultVO<Void> exportSysSystemExcel(SysSystemVO sysSystemVO, HttpServletResponse response) throws Exception { ServletOutputStream out = null; try { out = response.getOutputStream(); ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX); // 设置EXCEL名称 String fileName = new String(("SystemExcel").getBytes(), "UTF-8"); // 设置SHEET名称 Sheet sheet = new Sheet(1, 0); sheet.setSheetName("系统列表sheet1"); // 设置标题 Table table = new Table(1); List<List<String>> titles = new ArrayList<List<String>>(); titles.add(Arrays.asList("系统名称")); titles.add(Arrays.asList("系统标识")); titles.add(Arrays.asList("描述")); titles.add(Arrays.asList("状态")); titles.add(Arrays.asList("创建人")); titles.add(Arrays.asList("创建时间")); table.setHead(titles); // 查数据写EXCEL List<List<String>> dataList = new ArrayList<>(); List<SysSystemVO> sysSystemVOList = this.sysSystemReadMapper.selectSysSystemVOList(sysSystemVO); if (!CollectionUtils.isEmpty(sysSystemVOList)) { sysSystemVOList.forEach(eachSysSystemVO -> { dataList.add(Arrays.asList( eachSysSystemVO.getSystemName(), eachSysSystemVO.getSystemKey(), eachSysSystemVO.getDescription(), eachSysSystemVO.getState().toString(), eachSysSystemVO.getCreateUid(), eachSysSystemVO.getCreateTime().toString() )); }); } writer.write0(dataList, sheet, table); // 下载EXCEL response.setHeader("Content-Disposition", "attachment;filename=" + new String((fileName).getBytes("gb2312"), "ISO-8859-1") + ".xls"); response.setContentType("multipart/form-data"); response.setCharacterEncoding("utf-8"); writer.finish(); out.flush(); } finally { if (out != null) { try { out.close(); } catch (Exception e) { e.printStackTrace(); } } } return ResultVO.getSuccess("导出系统列表EXCEL成功"); }2.4.2.数据量适中(100W以内):一个SHEET分批查询导出@Override public ResultVO<Void> exportSysSystemExcel(SysSystemVO sysSystemVO, HttpServletResponse response) throws Exception { ServletOutputStream out = null; try { out = response.getOutputStream(); ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX); // 设置EXCEL名称 String fileName = new String(("SystemExcel").getBytes(), "UTF-8"); // 设置SHEET名称 Sheet sheet = new Sheet(1, 0); sheet.setSheetName("系统列表sheet1"); // 设置标题 Table table = new Table(1); List<List<String>> titles = new ArrayList<List<String>>(); titles.add(Arrays.asList("系统名称")); titles.add(Arrays.asList("系统标识")); titles.add(Arrays.asList("描述")); titles.add(Arrays.asList("状态")); titles.add(Arrays.asList("创建人")); titles.add(Arrays.asList("创建时间")); table.setHead(titles); // 查询总数并 【封装相关变量 这块直接拷贝就行 不要改动】 Integer totalRowCount = this.sysSystemReadMapper.selectCountSysSystemVOList(sysSystemVO); Integer pageSize = ExcelConstant.PER_WRITE_ROW_COUNT; Integer writeCount = totalRowCount % pageSize == 0 ? (totalRowCount / pageSize) : (totalRowCount / pageSize + 1); // 写数据 这个i的最大值直接拷贝就行了 不要改 for (int i = 0; i < writeCount; i++) { List<List<String>> dataList = new ArrayList<>(); // 此处查询并封装数据即可 currentPage, pageSize这个变量封装好的 不要改动 PageHelper.startPage(i + 1, pageSize); List<SysSystemVO> sysSystemVOList = this.sysSystemReadMapper.selectSysSystemVOList(sysSystemVO); if (!CollectionUtils.isEmpty(sysSystemVOList)) { sysSystemVOList.forEach(eachSysSystemVO -> { dataList.add(Arrays.asList( eachSysSystemVO.getSystemName(), eachSysSystemVO.getSystemKey(), eachSysSystemVO.getDescription(), eachSysSystemVO.getState().toString(), eachSysSystemVO.getCreateUid(), eachSysSystemVO.getCreateTime().toString() )); }); } writer.write0(dataList, sheet, table); } // 下载EXCEL response.setHeader("Content-Disposition", "attachment;filename=" + new String((fileName).getBytes("gb2312"), "ISO-8859-1") + ".xls"); response.setContentType("multipart/form-data"); response.setCharacterEncoding("utf-8"); writer.finish(); out.flush(); } finally { if (out != null) { try { out.close(); } catch (Exception e) { e.printStackTrace(); } } } return ResultVO.getSuccess("导出系统列表EXCEL成功"); }2.4.3.数据里很大(几百万都行):多个SHEET分批查询导出@Override public ResultVO<Void> exportSysSystemExcel(SysSystemVO sysSystemVO, HttpServletResponse response) throws Exception { ServletOutputStream out = null; try { out = response.getOutputStream(); ExcelWriter writer = new ExcelWriter(out, ExcelTypeEnum.XLSX); // 设置EXCEL名称 String fileName = new String(("SystemExcel").getBytes(), "UTF-8"); // 设置SHEET名称 String sheetName = "系统列表sheet"; // 设置标题 Table table = new Table(1); List<List<String>> titles = new ArrayList<List<String>>(); titles.add(Arrays.asList("系统名称")); titles.add(Arrays.asList("系统标识")); titles.add(Arrays.asList("描述")); titles.add(Arrays.asList("状态")); titles.add(Arrays.asList("创建人")); titles.add(Arrays.asList("创建时间")); table.setHead(titles); // 查询总数并封装相关变量(这块直接拷贝就行了不要改) Integer totalRowCount = this.sysSystemReadMapper.selectCountSysSystemVOList(sysSystemVO); Integer perSheetRowCount = ExcelConstant.PER_SHEET_ROW_COUNT; Integer pageSize = ExcelConstant.PER_WRITE_ROW_COUNT; Integer sheetCount = totalRowCount % perSheetRowCount == 0 ? (totalRowCount / perSheetRowCount) : (totalRowCount / perSheetRowCount + 1); Integer previousSheetWriteCount = perSheetRowCount / pageSize; Integer lastSheetWriteCount = totalRowCount % perSheetRowCount == 0 ? previousSheetWriteCount : (totalRowCount % perSheetRowCount % pageSize == 0 ? totalRowCount % perSheetRowCount / pageSize : (totalRowCount % perSheetRowCount / pageSize + 1)); for (int i = 0; i < sheetCount; i++) { // 创建SHEET Sheet sheet = new Sheet(i, 0); sheet.setSheetName(sheetName + i); // 写数据 这个j的最大值判断直接拷贝就行了,不要改动 for (int j = 0; j < (i != sheetCount - 1 ? previousSheetWriteCount : lastSheetWriteCount); j++) { List<List<String>> dataList = new ArrayList<>(); // 此处查询并封装数据即可 currentPage, pageSize这俩个变量封装好的 不要改动 PageHelper.startPage(j + 1 + previousSheetWriteCount * i, pageSize); List<SysSystemVO> sysSystemVOList = this.sysSystemReadMapper.selectSysSystemVOList(sysSystemVO); if (!CollectionUtils.isEmpty(sysSystemVOList)) { sysSystemVOList.forEach(eachSysSystemVO -> { dataList.add(Arrays.asList( eachSysSystemVO.getSystemName(), eachSysSystemVO.getSystemKey(), eachSysSystemVO.getDescription(), eachSysSystemVO.getState().toString(), eachSysSystemVO.getCreateUid(), eachSysSystemVO.getCreateTime().toString() )); }); } writer.write0(dataList, sheet, table); } } // 下载EXCEL response.setHeader("Content-Disposition", "attachment;filename=" + new String((fileName).getBytes("gb2312"), "ISO-8859-1") + ".xls"); response.setContentType("multipart/form-data"); response.setCharacterEncoding("utf-8"); writer.finish(); out.flush(); } finally { if (out != null) { try { out.close(); } catch (Exception e) { e.printStackTrace(); } } } return ResultVO.getSuccess("导出系统列表EXCEL成功"); }三、总结造的假数据,100W条记录,18个字段,测试导出是70s。在实际上产环境使用的时候,具体的还是要看自己写的sql的性能。sql性能快的话,会很快。有一点推荐一下:在做分页的时候使用单表查询, 对于所需要处理的外键对应的冗余字段,在外面一次性查出来放到map里面(推荐使用@MapKey注解),然后遍历list的时候根据外键从map中获取对应的名称。一个宗旨:少发查询sql, 才能更快的导出。题外话:如果数据量过大,在使用count(1)查询总数的时候会很慢,可以通过调整mysql的缓冲池参数来加快查询。还有就是遇到了一个问题,使用pagehelper的时候,数据量大的时候,limit 0,20W, limit 20W,40W, limit 40W,60W, limit 60W,80W 查询有的时候会很快,有的时候会很慢,待研究。另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 Java 系列面试题和答案,非常齐全。
Apache Kafka 是一个分布式开源流平台,被广泛应用于各大互联网公司。Kafka 设计之初被用于消息队列,自 2011 年由 LinkedIn 开源以来,Kafka 迅速从消息队列演变为成熟的事件流处理平台。Kafka 具有四个核心 API,借助这些 API,Kafka 可以用于以下两大类应用:建立实时流数据管道,可靠地进行数据传输,在系统或应用程序之间获取数据。构建实时流媒体应用程序,以改变系统或应用程序之间的数据或对数据流做出反应。近日,Apache Kafka 3.0.0 正式发布,这是一个重要的版本更新,其中包括许多新的功能。例如:已弃用对 Java 8 和 Scala 2.12 的支持,对它们的支持将在 4.0 版本中彻底移除,以让开发者有时间进行调整。Kafka Raft 支持元数据主题的快照,以及 self-managed quorum 方面的其他改进。废弃了消息格式 v0 和 v1。默认情况下为 Kafka Producer 启用更强的交付保证。优化了 OffsetFetch 和 FindCoordinator 请求。更灵活的 MirrorMaker 2 配置和 MirrorMaker 1 的弃用。能够在 Kafka Connect 的一次调用中重新启动连接器的任务。连接器日志上下文和连接器客户端覆盖现在是默认启用的。增强了 Kafka Streams 中时间戳同步的语义。修改了 Stream 的 TaskId 的公共 API。在 Kafka Streams 中,默认的 serde 变成了 null,还有一些其他的配置变化。接下来,我们来看看新版本具体在哪些地方进行了更新。根据官方资料介绍,Apache Kafka 3.0 引入了各种新功能、突破性的 API 更改以及对 KRaft 的改进——Apache Kafka 的内置共识机制将取代 Apache ZooKeeper™。虽然 KRaft 尚未被推荐用于生产(已知差距列表),但对 KRaft 元数据和 API 进行了许多改进。Exactly-once 和分区重新分配支持值得强调。鼓励大家查看 KRaft 的新功能并在开发环境中试用它。从 Apache Kafka 3.0 开始,生产者默认启用最强的交付保证(acks=all, enable.idempotence=true)。这意味着用户现在默认获得排序和持久性。此外,不要错过 Kafka Connect 任务重启增强、KStreams 基于时间戳同步的改进以及 MirrorMaker2 更灵活的配置选项。常规变化①KIP-750(第一部分):弃用 Kafka 中对 Java 8 的支持在 3.0 中,Apache Kafka 项目的所有组件都已弃用对 Java 8 的支持。这将使用户有时间在下一个主要版本(4.0)之前进行调整,届时 Java 8 支持将被取消。②KIP-751(第一部分):弃用 Kafka 中对 Scala 2.12 的支持对 Scala 2.12 的支持在 Apache Kafka 3.0 中也已弃用。与 Java 8 一样,我们给用户时间来适应,因为计划在下一个主要版本(4.0)中删除对 Scala 2.12 的支持。Kafka 代理、生产者、消费者和管理客户端①KIP-630:Kafka Raft 快照我们在 3.0 中引入的一个主要功能是 KRaft 控制器和 KRaft 代理能够为名为 __cluster_metadata 的元数据主题分区生成、复制和加载快照。Kafka 集群使用此主题来存储和复制有关集群的元数据信息,如代理配置、主题分区分配、领导等。随着此状态的增长,Kafka Raft Snapshot 提供了一种有效的方式来存储、加载和复制此信息。②KIP-746:修改 KRaft 元数据记录自第一版 Kafka Raft 控制器以来的经验和持续开发表明,需要修改一些元数据记录类型,当 Kafka 被配置为在没有 ZooKeeper(ZK)的情况下运行时使用这些记录类型。③KIP-730:KRaft 模式下的生产者 ID 生成在 3.0 和 KIP-730 中,Kafka 控制器现在完全接管了生成 Kafka 生产者 ID 的责任。控制器在 ZK 和 KRaft 模式下都这样做。这让我们更接近桥接版本,这将允许用户从使用 ZK 的 Kafka 部署过渡到使用 KRaft 的新部署。④KIP-679:Producer 将默认启用最强的交付保证从 3.0 开始,Kafka 生产者默认开启幂等性和所有副本的交付确认。这使得默认情况下记录交付保证更强。⑤KIP-735:增加默认消费者会话超时Kafka Consumer 的配置属性的默认值 session.timeout.ms 从 10 秒增加到 45 秒。这将允许消费者在默认情况下更好地适应暂时的网络故障,并在消费者似乎只是暂时离开组时避免连续重新平衡。⑥KIP-709:扩展 OffsetFetch 请求以接受多个组 ID请求 Kafka 消费者组的当前偏移量已经有一段时间了。但是获取多个消费者组的偏移量需要对每个组进行单独的请求。在 3.0 和 KIP-709 中,fetch 和 AdminClient API 被扩展为支持在单个请求/响应中同时读取多个消费者组的偏移量。⑦KIP-699:更新 FindCoordinator 以一次解析多个 Coordinator支持可以以有效方式同时应用于多个消费者组的操作在很大程度上取决于客户端有效发现这些组的协调者的能力。这通过 KIP-699 成为可能,它增加了对通过一个请求发现多个组的协调器的支持。Kafka 客户端已更新为在与支持此请求的新 Kafka 代理交谈时使用此优化。⑧KIP-724:删除对消息格式 v0 和 v1 的支持自 2017 年 6 月随 Kafka 0.11.0 推出四年以来,消息格式 v2 一直是默认消息格式。因此,在桥下流过足够多的水(或溪流)后,3.0 的主要版本为我们提供了弃用旧消息格式(即 v0 和 v1)的好机会。这些格式今天很少使用。在 3.0 中,如果用户将代理配置为使用消息格式 v0 或 v1,他们将收到警告。此选项将在 Kafka 4.0 中删除(有关详细信息和弃用 v0 和 v1 消息格式的影响,请参阅 KIP-724)。⑨KIP-707:KafkaFuture 的未来当 KafkaFuture 引入该类型以促进 Kafka AdminClient 的实现时,Java 8 之前的版本仍在广泛使用,并且 Kafka 正式支持 Java 7。快进几年后,现在 Kafka 运行在支持CompletionStage和 CompletableFuture 类类型的 Java 版本上。使用 KIP-707,KafkaFuture 添加了一种返回 CompletionStage 对象的方法,并以 KafkaFuture 向后兼容的方式增强了可用性。⑩KIP-466:添加对 List<T> 序列化和反序列化的支持KIP-466为泛型列表的序列化和反序列化添加了新的类和方法——这一特性对 Kafka 客户端和 Kafka Streams 都非常有用。⑪KIP-734:改进 AdminClient.listOffsets 以返回时间戳和具有最大时间戳的记录的偏移量用户列出 Kafka 主题/分区偏移量的功能已得到扩展。使用 KIP-734,用户现在可以要求 AdminClient 返回主题/分区中具有最高时间戳的记录的偏移量和时间戳。这是不是与什么的 AdminClient 收益已经为最新的偏移,这是下一个记录的偏移,在主题/分区写入混淆。这个扩展现有 ListOffsets API 允许用户探测生动活泼的通过询问哪个是最近写入的记录的偏移量以及它的时间戳是什么来分区。Kafka Connect①KIP-745:连接 API 以重新启动连接器和任务在 Kafka Connect 中,连接器在运行时表示为一组Connector类实例和一个或多个Task类实例,并且通过 Connect REST API 可用的连接器上的大多数操作都可以应用于整个组。从一开始,一个值得注意的例外 restart 是 Connector 和 Task 实例的端点。要重新启动整个连接器,用户必须单独调用以重新启动连接器实例和任务实例。在 3.0 中,KIP-745 使用户能够通过一次调用重新启动所有或仅失败的连接器 Connector 和 Task 实例。此功能是附加功能,restartREST API 的先前行为保持不变。②KIP-738:删除 Connect 的内部转换器属性在之前的主版本(Apache Kafka 2.0)中弃用它们之后,internal.key.converter 并 internal.value.converter 在 Connect 工作器的配置中作为配置属性和前缀被删除。展望未来,内部 Connect 主题将专门使用 JsonConverter 来存储没有嵌入模式的记录。任何使用不同转换器的现有 Connect 集群都必须将其内部主题移植到新格式(有关升级路径的详细信息,请参阅 KIP-738)。③KIP-722:默认启用连接器客户端覆盖从 Apache Kafka 2.3.0 开始,可以配置连接器工作器以允许连接器配置覆盖连接器使用的 Kafka 客户端属性。这是一个广泛使用的功能,现在有机会发布一个主要版本,默认启用覆盖连接器客户端属性的功能(默认 connector.client.config.override.policy 设置为 All)。④KIP-721:在连接 Log4j 配置中启用连接器日志上下文另一个在 2.3.0 中引入但到目前为止尚未默认启用的功能是连接器日志上下文。这在 3.0 中发生了变化,连接器上下文默认添加 log4j 到 Connect 工作器的日志模式中。从以前的版本升级到 3.0 将 log4j 通过在适当的情况下添加连接器上下文来更改导出的日志行的格式。Kafka Streams①KIP-695:进一步改进 Kafka Streams 时间戳同步KIP-695 增强了 Streams 任务如何选择获取记录的语义,并扩展了配置属性的含义和可用值 max.task.idle.ms。此更改需要 Kafka 消费者 API 中的一种新方法,currentLag 如果本地已知且无需联系 Kafka Broker,则能够返回特定分区的消费者滞后。②KIP-715:在流中公开提交的偏移量3.0 开始,三个新的方法添加到 TaskMetadata 接口:committedOffsets,endOffsets 和 timeCurrentIdlingStarted。这些方法可以允许 Streams 应用程序跟踪其任务的进度和运行状况。③KIP-740:清理公共 API TaskIdKIP-740 代表了 TaskId 该类的重大革新。有几种方法和所有内部字段已被弃用,新的 subtopology() 和 partition() 干将替换旧 topicGroupId 和 partition 字段(参见 KIP-744 的相关变化和修正 KIP-740)。④KIP-744:迁移 TaskMetadata,并 ThreadMetadata 与内部实现的接口KIP-744 将 KIP-740 提出的更改更进一步,并将实现与许多类的公共 API 分开。为了实现这一点,引入了新的接口 TaskMetadata、ThreadMetadata 和 StreamsMetadata,而弃用了具有相同名称的现有类。⑤KIP-666:添加 Instant 基于方法到 ReadOnlySessionStore交互式查询 API 扩展了 ReadOnlySessionStore 和 SessionStore 接口中的一组新方法,这些方法接受 Instant 数据类型的参数。此更改将影响需要实现新方法的任何自定义只读交互式查询会话存储实现。⑥KIP-622:添加 currentSystemTimeMs 和 currentStreamTimeMs 到 ProcessorContext该 ProcessorContext 增加在 3.0 两个新的方法,currentSystemTimeMs 和 currentStreamTimeMs。新方法使用户能够分别查询缓存的系统时间和流时间,并且可以在生产和测试代码中以统一的方式使用它们。⑦KIP-743:删除 0.10.0-2.4Streams 内置指标版本配置的配置值3.0 中取消了对 Streams 中内置指标的旧指标结构的支持。KIP-743 正在 0.10.0-2.4 从配置属性中删除该值 built.in.metrics.version。这 latest 是目前此属性的唯一有效值(自 2.5 以来一直是默认值)。⑧KIP-741:将默认 SerDe 更改为 null删除了默认 SerDe 属性的先前默认值。流过去默认为 ByteArraySerde。用 3.0 开始,没有缺省,和用户需要任一组其的 SerDes 根据需要在 API 中或通过设置默认 DEFAULT_KEY_SERDE_CLASS_CONFIG 和 DEFAULT_VALUE_SERDE_CLASS_CONFIG 在它们的流配置。先前的默认值几乎总是不适用于实际应用程序,并且造成的混乱多于方便。⑨KIP-733:更改 Kafka Streams 默认复制因子配置有了主要版本的机会,Streams 配置属性的默认值replication.factor会从 1 更改为 -1。这将允许新的 Streams 应用程序使用在 Kafka 代理中定义的默认复制因子,因此在它们转移到生产时不需要设置此配置值。请注意,新的默认值需要 Kafka Brokers 2.5 或更高版本。⑩KIP-732:弃用 eos-alpha 并用 eos-v2 替换 eos-beta在 3.0 中不推荐使用的另一个 Streams 配置值是 exactly_once 作为属性的值 processing.guarantee。该值 exactly_once 对应于 Exactly Once Semantics (EOS) 的原始实现,可用于连接到 Kafka 集群版本 0.11.0 或更高版本的任何 Streams 应用程序。此 EOS 的第一实现已经通过流第二实施 EOS 的,这是由值表示取代 exactly_once_beta 在 processing.guarantee 性质。展望未来,该名称 exactly_once_beta 也已弃用并替换为新名称 exactly_once_v2。在下一个主要版本(4.0)中,exactly_once 和 exactly_once_beta 都将被删除,exactly_once_v2 作为 EOS 交付保证的唯一选项。⑪KIP-725:优化 WindowedSerializer 和 WindowedDeserializer 的配置配置属性 default.windowed.key.serde.inner 和 default.windowed.value.serde.inner 已弃用。取而代之的是 windowed.inner.class.serde 供消费者客户端使用的单个新属性。建议 Kafka Streams 用户通过将其传递到 SerDe 构造函数来配置他们的窗口化 SerDe,然后在拓扑中使用它的任何地方提供 SerDe。⑫KIP-633:弃用 Streams 中宽限期的 24 小时默认值在 Kafka Streams 中,允许窗口操作根据称为宽限期的配置属性处理窗口外的记录。以前,这个配置是可选的,很容易错过,导致默认为 24 小时。这是 Suppression 运营商用户经常感到困惑的原因,因为它会缓冲记录直到宽限期结束,因此会增加 24 小时的延迟。在 3.0 中,Windows 类通过工厂方法得到增强,这些工厂方法要求它们使用自定义宽限期或根本没有宽限期来构造。已弃用默认宽限期为 24 小时的旧工厂方法,以及与 grace() 已设置此配置的新工厂方法不兼容的相应 API。⑬KIP-623:internal-topics 为流应用程序重置工具添加“ ”选项通过 kafka-streams-application-reset 添加新的命令行参数,应用程序重置工具的 Streams 使用变得更加灵活:--internal-topics。新参数接受逗号分隔的主题名称列表,这些名称对应于可以使用此应用程序工具安排删除的内部主题。将此新参数与现有参数相结合,--dry-run 允许用户在实际执行删除操作之前确认将删除哪些主题并在必要时指定它们的子集。MirrorMaker①KIP-720:弃用 MirrorMaker v1在 3.0 中,不推荐使用 MirrorMaker 的第一个版本。展望未来,新功能的开发和重大改进将集中在 MirrorMaker 2(MM2)上。②KIP-716:允许使用 MirrorMaker2 配置偏移同步主题的位置在 3.0 中,用户现在可以配置 MirrorMaker2 创建和存储用于转换消费者组偏移量的内部主题的位置。这将允许 MirrorMaker2 的用户将源 Kafka 集群维护为严格只读的集群,并使用不同的 Kafka 集群来存储偏移记录(即目标 Kafka 集群,甚至是源和目标集群之外的第三个集群)。Apache Kafka 3.0 是 Apache Kafka 项目向前迈出的重要一步。
Facebook 称,他们最近的一次大版本升级到 MySQL 5.6 花了一年多时间才完成,还在 5.6 版上开发 LSM 树存储引擎,MyRocks。在升级到 5.7 的同时构建一个新的存储引擎,会大大减慢 MyRocks 的进度,因此我们选择继续使用 5.6,直到 MyRocks 完成,MySQL 5.6 的寿命也即将结束,决定升级到 MySQL 8.0 。官博介绍说,此次过程比之前的升级更具挑战。MySQL 是由 Oracle 公司开发的一个开源数据库,它为 Facebook 的一些最重要的工作负载提供了动力。我们积极开发 MySQL 中的新特性,以支持不断演化的需求。这些特性对MySQL的许多方面进行了修改,包括客户机连接器、存储引擎、优化器以及复制。为了迁移工作负载,对于每个新的 MySQL 主版本,我们都需要投入大量的时间和精力。其中的挑战包括:将自定义功能移植到新版本确保主要版本之间的复制兼容最小化现有应用程序查询所需的更改对阻碍服务器支持我们工作负载的性能退化进行修复。我们最近一次的主版本升级是到 MySQL 5.6,它花了一年多的时间才推出。当5.7 版发布时,我们还在 5.6 版上开发 LSM 树存储引擎和 MyRocks。在升级到 5.7 的同时构建一个新的存储引擎,会大大减慢 MyRocks 的进度,因此我们选择继续使用 5.6,直到 MyRocks 完成。MySQL 8.0 发布之际,我们正在做 MyRocks 向用户数据库(UDB)服务层推出的收尾。该版本包括一些引人注目的特性,如基于写集的并行复制和提供原子 DDL 支持的事务数据字典等。对我们来说,迁移到 8.0 还将带来包括文档存储在内的,我们已经错过的 5.7 特性。版本 5.6 的使命即将结束,我们希望在 MySQL 社区中保持活跃,尤其是在 MyRocks 存储引擎上的工作。8.0 中的增强功能,比如即时 DDL,可以加快 MyRocks 的模式更改,但是我们需要在 8.0 的代码库中使用它。考虑到更新代码的好处,我们决定迁移到 8.0。下面将分享我们如何解决 8.0 迁移项目的难题,以及在这个过程中发现的一些惊喜。当最初确定项目范围时,可以明确的是,迁移到 8.0 会比迁移到 5.6 或 MyRocks 更困难。当时,我们定制的 5.6 分支有 1700 多个代码补丁需要移植到 8.0。在我们移植这些更改时,新的 Facebook 的 MySQL 特性和修复已被添加到5.6 的代码库中,从而使目标变得更加遥不可及。我们有许多 MySQL 服务器在生产环境中运行,为大量截然不同的应用程序提供服务。我们还有众多管理 MySQL 实例的软件架构。这些应用执行诸如收集统计数据或管理服务器备份之类的操作。从 5.6 升级到 8.0 完全跳过了 5.7。在 5.6 中处于活动状态的 API 在 5.7中可能被弃用,而在 8.0 中可能会被移除,这要求我们必须更新所有使用了现已删除API的应用程序。许多 Facebook 功能与 8.0 中的类似功能并不向前兼容,需要一种弃用或迁移途径。MyRocks 的增强功能需要在 8.0 中运行,包括本地化分区和崩溃恢复。1、代码补丁首先我们建立了 8.0 分支,用于在开发环境中进行构建和测试。然后,我们开始从 5.6 分支移植补丁的漫长过程。开始的时候有 1700 多个补丁,但我们能将其组织成几个主要类别。我们的大多数自定义代码都有很好的注释和描述,因此可以很容易地确定应用程序是否仍然需要它,或者是否可以将它删除。通过特殊关键字或唯一变量名所启用的功能,也使得确定关联变得很容易,因为我们可以搜索应用程序代码库来找到它们的用例。有些补丁非常晦涩难懂,需要做调查工作 — 挖掘旧的设计文档、邮件或代码评审注释,以了解它们的历史。我们将每个补丁分入四类之一:Drop:不再使用,或在8.0中具有同等功能的特性,不需要移植。Build/Client:支持我们构建环境的非服务器特性,修改过的 MySQL 工具,比如 mysqlbinlog,或者增加的功能,如异步客户端 API 等,需要移植。非 MyRocks 服务器:mysqld 服务器中与 MyRocks 存储引擎无关的特性,需要移植。MyRocks 服务器:支持 MyRocks 存储引擎的特性,需要移植。我们使用电子表格跟踪每个补丁的状态和相关历史信息,并且在删除补丁时记录理由。更新相同特性的多个补丁被组在一起进行移植。移植并提交到 8.0 分支的补丁,用 5.6 提交信息进行了注释。由于我们需要筛选大量的补丁,将不可避免地出现移植状态上的差异,这些注释帮助我们解决了此类问题。客户端和服务器类别中的每个补丁都自然而然地成为一个软件发布里程碑。随着所有与客户端相关的更改的移植,我们能够将客户端工具和连接器代码更新到8.0。一旦所有非 MyRocks 服务器特性都被移植,我们就可以为 InnoDB 服务器部署8.0 mysqld了。完成 MyRocks 服务器特性移植使我们能够更新 MyRocks 安装。有些复杂特性需要对 8.0 进行重大更改,一些方面存在很大的兼容性问题。例如,上游 8.0 binlog 事件格式与我们一些对 5.6 的定制修改不兼容。Facebook 5.6 特性使用的错误代码与上游 8.0 分配给新特性的错误代码冲突。我们最终需要修补 5.6 服务器,以使其与 8.0 向前兼容。完成所有这些特性的移植花了几年时间。到最终结束时,我们已经评估了 2300 多个补丁,并将其中 1500 个移植到了 8.0 版本。另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 MySQL 系列面试题和答案,非常齐全。2、迁移途径我们将多个 mysqld 实例组合到一个 MySQL 副本集中。副本集中的每个实例都包含相同的数据,但在地理上分布到不同的数据中心,以提供数据可用性和故障切换支持。每个副本集都有一个主实例。其余的实例都是从实例。主实例处理所有写流量,并将数据异步复制到所有从实例。由 5.6 主/5.6 从所组成的副本集开始,最终目标是包含 8.0 主/ 8.0 从的副本集。我们遵循一个类似于 UDB MyRocks migration plan 的迁移规划。对于每个副本集,通过一个使用 mysqldump 生成的逻辑备份,创建并添加到 8.0 的从实例。这些从实例不提供任何应用程序读取流量;在 8.0 从实例上开启读取流量;允许将 8.0 从实例升级为主实例;禁用 5.6 实例的读取流量;移除所有 5.6 实例。每个副本集可以独立地通过上述步骤进行迁移,并可根据需要停留在一个步骤上。我们将副本集分成更小的组,在组中进行每一次迁移。如果发现问题,我们可以回滚到上一步。在某些情况下,副本集能够在其它副本集开始之前到达最后一步。为了自动化迁移大量副本集,我们需要构建新的软件架构。可以通过简单地更改配置文件中的一行,将副本集组合并在每个阶段中移动它们。任何遇到问题的副本集都能单独回滚。3、基于行的复制作为 8.0 迁移工作的一部分,我们决定将使用基于行的复制(row-based replication,RBR)作为标准。一些 8.0 特性需要 RBR,并且它简化了 MyRocks 的移植工作。我们的大多数 MySQL 副本集已经在使用 RBR,而那些仍然运行基于语句的复制(statement-based replication,SBR)的副本集不容易迁移。这些副本集通常有不含任何高基数键的表。完全转向 RBR 是一个目标,但添加主键所需的长尾工作的优先级往往低于其它项目。因此,我们将 RBR 作为 8.0 的要求。在评估并向每个表添加主键之后,我们今年切换了最后一个 SBR 副本集。使用 RBR 还为我们提供了一个解决应用程序问题的替代解决方案,我们在将一些副本集移动到 8.0 主实例时遇到了这个问题,将在后面讨论。4、自动化验证大多数 8.0 迁移过程都涉及使用我们的自动化架构和应用查询来测试和验证 mysqld 服务器。我们用来管理服务器的自动化基础架构在随着 MySQL 服务器的增长而增长。为了确保所有 MySQL 自动化组件都与 8.0 版本兼容,我们投资构建了一个测试环境,该环境利用虚拟机上的测试副本集来验证行为。我们为 canary 编写了在 5.6 版本和 8.0 版本上运行的每个自动化组件的集成测试,并验证了它们的正确性。在进行此演练时,我们发现了几个错误和行为差异。当 MySQL 架构的每一部分都在我们的 8.0 服务器上进行验证时,我们发现并修复了(或解决了)一些有趣的问题:解析错误日志、mysqldump 输出或服务器 show 命令的文本输出的软件很容易损坏。服务器输出的细微变化常常会暴露出工具解析逻辑中的错误。8.0 的默认 utf8mb4 排序规则设置导致 5.6 和 8.0 实例之间的排序规则不匹配。8.0 表可能会使用新的 utf8mb4_0900 排序规则,即使对于由 5.6 的show create table生成的create语句也是如此,因为使用utf8mb4_general_ci 的 5.6 模式没有显式指定排序规则。这些表差异通常会导致复制和模式验证工具出现问题;某些复制失败的错误代码发生了变化,我们必须修复我们的自动化程序来正确处理它们;8.0 版本的数据字典废弃了 table.frm 文件,但是我们的一些自动化系统使用它们来检测表模式的修改;我们必须更新自动化系统,以支持 8.0 中引入的动态权限。5、应用程序验证我们希望迁移对应用程序尽可能透明,但是有些应用程序的查询会出现性能退化,或者在8.0 上会失败。对于 MyRocks 迁移,我们构建了一个 MySQL 影子测试框架,该框架捕获生产流量并将其重放到测试实例中。对于每个应用程序工作负载,我们在 8.0 上创建了测试实例,并向它们回放影子流量的查询。我们捕获并记录了从 8.0 服务器返回的错误,并发现了一些有趣的问题。不幸的是,并非所有这些问题都是在测试过程中发现的。例如,事务死锁是应用程序在迁移过程中发现的。在研究不同的解决方案时,我们可以暂时将这些应用程序回滚到 5.6 版本。8.0 引入了新的保留关键字,其中一些关键字,如 groups 和 rank,与应用程序查询中常用的表列名或别名相冲突。这些查询没有通过反引号转义名称,导致解析错误。使用了自动转义查询中列名的软件库的应用程序没有遇到这些问题,但并非所有应用程序都使用这些软件库。解决这个问题很简单,但是需要时间来跟踪生成这些查询的应用程序属主和代码库。在 5.6 和 8.0 之间还发现了有些 REGEXP 不兼容。一些包含在 InnoDB 上的 insert ... on duplicate key 查询的应用程序遇到了 repeatable-read 事务死锁。5.6 有一个 bug,在 8.0 中得到了修复,但是修复增加了事务死锁的可能性。在分析了查询之后,我们能够通过降低隔离级别来解决该问题。这个选项对我们来说是可用的,因为我们已经切换到基于行的复制。我们自定义的 5.6 文档存储和 JSON 函数与 8.0 不兼容。使用文档存储的应用程序需要将文档类型转换为文本以进行迁移。对于 JSON 函数,我们向 8.0 服务器中添加了兼容 5.6 的版本,以便应用程序以后可以迁移到 8.0 API。我们对 8.0 服务器的查询和性能测试发现了一些需要立即解决的问题。我们发现在 ACL 缓存部分出现了新的互斥争用热点。当大量连接同时打开时,它们都会阻塞 ACL 检查;当存在大量 binlog 文件并且 binlog 的高速写入导致频繁轮换文件时,binlog 索引访问也发现了类似的争用;几个涉及临时表的查询被中断。这些查询会返回意外错误,或者运行时间太长以致超时。内存使用量与 5.6 相比有所增加,特别是对于 MyRocks 实例,因为必须加载 8.0 中的 InnoDB 。默认的 performance_schema 设置启用了所有工具集并消耗了大量内存。我们限制了内存使用,只启用了少量的工具,并对代码进行了更改,以禁用无法手动关闭的表。然而,并不是所有增加的内存都是分配给 performance_schema 的。我们需要检查和修改各种 InnoDB 内部数据结构,以进一步减少内存占用。这一努力使 8.0 的内存使用率降到了可以接受的水平。6、接下来的工作到目前为止,8.0 的移植已经花了几年时间。我们已将许多 InnoDB 副本集转换为完全在 8.0 上运行。剩下的大部分都处于迁移途径的不同阶段。现在,我们的大多数定制功能都已移植到 8.0,更新到 Oracle 的次版本相对容易些,我们计划跟上最新版本的步伐。跳过 5.7 这样的主版本会带来一些问题,我们的迁移需要解决这些问题。首先,我们无法就地升级服务器,需要使用逻辑转储和还原来构建新服务器。但是,对于非常大的 mysqld 实例,这可能需要在活跃生产服务器上运行很多天,而且这个脆弱的过程可能会在完成之前被中断。对于这些大型实例,我们必须修改备份和恢复系统来应对重建。其次,检测 API 更改要困难得多,因为 5.7 可能会向我们的应用程序客户端发出不推荐警告,以提示修复潜在的问题。而我们需要在迁移生产工作负载之前,运行额外的影子测试来查找失败。使用自动转义模式对象名称的 mysql 客户端软件,有助于减少兼容性问题的数量。在一个副本集中支持两个主版本非常困难。一旦副本集将其主实例升级为 8.0,最好尽快禁用并移除 5.6 实例。应用程序用户往往会发现只有 8.0 支持的新特性,比如 utf8mb4_0900 排序规则,使用这些排序规则可能中断 8.0 和 5.6 实例之间的复制流。尽管我们在迁移过程中遇到了种种障碍,但我们已经看到了运行 8.0 带来的好处。一些应用程序选择了提早迁移到 8.0,以利用诸如文档存储和改进的日期时间支持等功能。我们一直在考虑如何在 MyRocks 上支持像即时DDL这样的存储引擎特性。总的来说,新版本大大扩展了 MySQL@Facebook 的功能。
Spring Cloud Alibaba为分布式应用开发提供了一站式解决方案。它包含开发分布式应用程序所需的所有组件,可以轻松地使用Spring Cloud开发应用程序。最近抽空整理了一份Spring Cloud Alibab学习笔记免费分享给大家,目录如下模块一 微服务架构设计本模块主要介绍了什么是微服务体系结构,以及微服务体系结构设计中的一些常见问题。模块二 Nacos 服务治理Nacos注册中心是整个微服务体系结构的核心。本文将详细介绍Nacos的安装、使用和集群构建过程,并以图文的形式介绍Nacos服务发现的基本原理。模块三 系统保护Sentinel是Alibaba提供的服务保护中间件。使用sentinel可以有效地防止分布式体系结构的系统崩溃。在此阶段,我们将解释Sentinel在限流、熔断、代码控制等方面的最佳实践。模块四 高级特性在这一阶段,我们将介绍SpringCloudAlibaba提供的许多高级功能。例如:配置中心、链路跟踪、性能监控、分布式事务、消息队列等。我们将从应用介绍到原理分析,逐一讲解这些技术。模块五 微服务通信当服务需要相互通信时,springcloudAlibaba支持RPC和restful解决方案。相应的产品是Dubbo和openfeign。在这个阶段,我将给出这些组件的最佳实践和原理分析。模块六 微服务架构最佳实践这阶段,我将拿出自己的私藏干货,为大家讲解微服务架构的综合应用和项目实践。在这里我们将接触到Seata分布式事务架构、多级缓存设计、老项目升级策略!
使用fluent mybatis也可以不用写具体的 xml 文件,通过 java api 可以构造出比较复杂的业务 sql 语句,做到代码逻辑和 sql 逻辑的合一。不再需要在 Dao 中组装查询或更新操作,或在 xml 与 mapper 中再组装参数。那对比原生 Mybatis,Mybatis Plus 或者其他框架,FluentMybatis提供了哪些便利呢?需求场景设置我们通过一个比较典型的业务需求来具体实现和对比下,假如有学生成绩表结构如下:create table `student_score` ( id bigint auto_increment comment '主键ID' primary key, student_id bigint not null comment '学号', gender_man tinyint default 0 not null comment '性别, 0:女; 1:男', school_term int null comment '学期', subject varchar(30) null comment '学科', score int null comment '成绩', gmt_create datetime not null comment '记录创建时间', gmt_modified datetime not null comment '记录最后修改时间', is_deleted tinyint default 0 not null comment '逻辑删除标识' ) engine = InnoDB default charset=utf8;现在有需求:「统计 2000 年三门学科('英语', '数学', '语文')及格分数按学期,学科统计最低分,最高分和平均分, 且样本数需要大于 1 条,统计结果按学期和学科排序」我们可以写 SQL 语句如下select school_term, subject, count(score) as count, min(score) as min_score, max(score) as max_score, avg(score) as max_score from student_score where school_term >= 2000 and subject in ('英语', '数学', '语文') and score >= 60 and is_deleted = 0 group by school_term, subject having count(score) > 1 order by school_term, subject;那上面的需求,分别用fluent mybatis, 原生mybatis和Mybatis plus来实现一番。三者实现对比使用fluent mybatis 来实现上面的功能需要本文具体演示代码可加我微信:codedq,免费获取!我们可以看到fluent api的能力,以及 IDE 对代码的渲染效果。换成mybatis原生实现效果定义Mapper接口public interface MyStudentScoreMapper { List<Map<String, Object>> summaryScore(SummaryQuery paras); }定义接口需要用到的参数实体 SummaryQuery@Data @Accessors(chain = true) public class SummaryQuery { private Integer schoolTerm; private List<String> subjects; private Integer score; private Integer minCount; }定义实现业务逻辑的mapper xml文件<select id="summaryScore" resultType="map" parameterType="cn.org.fluent.mybatis.springboot.demo.mapper.SummaryQuery"> select school_term, subject, count(score) as count, min(score) as min_score, max(score) as max_score, avg(score) as max_score from student_score where school_term >= #{schoolTerm} and subject in <foreach collection="subjects" item="item" open="(" close=")" separator=","> #{item} </foreach> and score >= #{score} and is_deleted = 0 group by school_term, subject having count(score) > #{minCount} order by school_term, subject </select>实现业务接口(这里是测试类,实际应用中应该对应 Dao 类)@RunWith(SpringRunner.class) @SpringBootTest(classes = QuickStartApplication.class) public class MybatisDemo { @Autowired private MyStudentScoreMapper mapper; @Test public void mybatis_demo() { // 构造查询参数 SummaryQuery paras = new SummaryQuery() .setSchoolTerm(2000) .setSubjects(Arrays.asList("英语", "数学", "语文")) .setScore(60) .setMinCount(1); List<Map<String, Object>> summary = mapper.summaryScore(paras); System.out.println(summary); } }总之,直接使用 mybatis,实现步骤还是相当的繁琐,效率太低。那换成mybatis plus的效果怎样呢?换成mybatis plus实现效果mybatis plus的实现比mybatis会简单比较多,实现效果如下如红框圈出的,写mybatis plus实现用到了比较多字符串的硬编码(可以用 Entity 的 get lambda 方法部分代替字符串编码)。字符串的硬编码,会给开发同学造成不小的使用门槛,个人觉的主要有 2 点:字段名称的记忆和敲码困难Entity 属性跟随数据库字段发生变更后的运行时错误其他框架,比如TkMybatis在封装和易用性上比mybatis plus要弱,就不再比较了。生成代码编码比较fluent mybatis生成代码设置public class AppEntityGenerator { static final String url = "jdbc:mysql://localhost:3306/fluent_mybatis_demo?useSSL=false&useUnicode=true&characterEncoding=utf-8"; public static void main(String[] args) { FileGenerator.build(Abc.class); } @Tables( /** 数据库连接信息 **/ url = url, username = "root", password = "password", /** Entity类parent package路径 **/ basePack = "cn.org.fluent.mybatis.springboot.demo", /** Entity代码源目录 **/ srcDir = "spring-boot-demo/src/main/java", /** Dao代码源目录 **/ daoDir = "spring-boot-demo/src/main/java", /** 如果表定义记录创建,记录修改,逻辑删除字段 **/ gmtCreated = "gmt_create", gmtModified = "gmt_modified", logicDeleted = "is_deleted", /** 需要生成文件的表 ( 表名称:对应的Entity名称 ) **/ tables = @Table(value = {"student_score"}) ) static class Abc { } }mybatis plus代码生成设置public class CodeGenerator { static String dbUrl = "jdbc:mysql://localhost:3306/fluent_mybatis_demo?useSSL=false&useUnicode=true&characterEncoding=utf-8"; @Test public void generateCode() { GlobalConfig config = new GlobalConfig(); DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setDbType(DbType.MYSQL) .setUrl(dbUrl) .setUsername("root") .setPassword("password") .setDriverName(Driver.class.getName()); StrategyConfig strategyConfig = new StrategyConfig(); strategyConfig .setCapitalMode(true) .setEntityLombokModel(false) .setNaming(NamingStrategy.underline_to_camel) .setColumnNaming(NamingStrategy.underline_to_camel) .setEntityTableFieldAnnotationEnable(true) .setFieldPrefix(new String[]{"test_"}) .setInclude(new String[]{"student_score"}) .setLogicDeleteFieldName("is_deleted") .setTableFillList(Arrays.asList( new TableFill("gmt_create", FieldFill.INSERT), new TableFill("gmt_modified", FieldFill.INSERT_UPDATE))); config .setActiveRecord(false) .setIdType(IdType.AUTO) .setOutputDir(System.getProperty("user.dir") + "/src/main/java/") .setFileOverride(true); new AutoGenerator().setGlobalConfig(config) .setDataSource(dataSourceConfig) .setStrategy(strategyConfig) .setPackageInfo( new PackageConfig() .setParent("com.mp.demo") .setController("controller") .setEntity("entity") ).execute(); } }FluentMybatis特性一览三者对比总结看完 3 个框架对同一个功能点的实现, 各位看官肯定会有自己的判断,笔者这里也总结了一份比较。
1. 引言读写分离要做的事情就是对于一条SQL该选择哪个数据库去执行,至于谁来做选择数据库这件事儿,无非两个,要么中间件帮我们做,要么程序自己做。因此,一般来讲,读写分离有两种实现方式。第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;第二种是应用程序自己去做分离。这里我们选择程序自己来做,主要是利用Spring提供的路由数据源,以及AOP。然而,应用程序层面去做读写分离最大的弱点(不足之处)在于无法动态增加数据库节点,因为数据源配置都是写在配置中的,新增数据库意味着新加一个数据源,必然改配置,并重启应用。当然,好处就是相对简单。2. AbstractRoutingDataSource基于特定的查找key路由到特定的数据源。它内部维护了一组目标数据源,并且做了路由key与目标数据源之间的映射,提供基于key查找数据源的方法。3. 实践关于配置请参考《MySQL主从复制配置》地址:www.cnblogs.com/cjsblog/p/9706370.html3.1. maven依赖<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cjs.example</groupId> <artifactId>cjs-datasource-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>cjs-datasource-demo</name> <description></description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!--<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.5</version> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> </dependencies> <configuration> <configurationFile>${basedir}/src/main/resources/myBatisGeneratorConfig.xml</configurationFile> <overwrite>true</overwrite> </configuration> <executions> <execution> <id>Generate MyBatis Artifacts</id> <goals> <goal>generate</goal> </goals> </execution> </executions> </plugin>--> </plugins> </build> </project>3.2. 数据源配置application.ymlspring: datasource: master: jdbc-url: jdbc:mysql://192.168.102.31:3306/test username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver slave1: jdbc-url: jdbc:mysql://192.168.102.56:3306/test username: pig # 只读账户 password: 123456 driver-class-name: com.mysql.jdbc.Driver slave2: jdbc-url: jdbc:mysql://192.168.102.36:3306/test username: pig # 只读账户 password: 123456 driver-class-name: com.mysql.jdbc.Driver多数据源配置/** * 关于数据源配置,参考SpringBoot官方文档第79章《Data Access》 * 79. Data Access * 79.1 Configure a Custom DataSource * 79.2 Configure Two DataSources */ @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave1") public DataSource slave1DataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave2") public DataSource slave2DataSource() { return DataSourceBuilder.create().build(); } @Bean public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource, @Qualifier("slave2DataSource") DataSource slave2DataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DBTypeEnum.MASTER, masterDataSource); targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource); targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource); MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource(); myRoutingDataSource.setDefaultTargetDataSource(masterDataSource); myRoutingDataSource.setTargetDataSources(targetDataSources); return myRoutingDataSource; } }这里,我们配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源。MyBatis配置@EnableTransactionManagement @Configuration public class MyBatisConfig { @Resource(name = "myRoutingDataSource") private DataSource myRoutingDataSource; @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(myRoutingDataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); return sqlSessionFactoryBean.getObject(); } @Bean public PlatformTransactionManager platformTransactionManager() { return new DataSourceTransactionManager(myRoutingDataSource); } }由于 Spring 容器中现在有4个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。3.3. 设置路由key / 查找数据源目标数据源就是那前3个这个我们是知道的,但是使用的时候是如果查找数据源的呢?首先,我们定义一个枚举来代表这三个数据源package com.cjs.example.enums; public enum DBTypeEnum { MASTER, SLAVE1, SLAVE2; }接下来,通过ThreadLocal将数据源设置到每个线程上下文中public class DBContextHolder { private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>(); private static final AtomicInteger counter = new AtomicInteger(-1); public static void set(DBTypeEnum dbType) { contextHolder.set(dbType); } public static DBTypeEnum get() { return contextHolder.get(); } public static void master() { set(DBTypeEnum.MASTER); System.out.println("切换到master"); } public static void slave() { // 轮询 int index = counter.getAndIncrement() % 2; if (counter.get() > 9999) { counter.set(-1); } if (index == 0) { set(DBTypeEnum.SLAVE1); System.out.println("切换到slave1"); }else { set(DBTypeEnum.SLAVE2); System.out.println("切换到slave2"); } } }获取路由keypackage com.cjs.example.bean; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.lang.Nullable; public class MyRoutingDataSource extends AbstractRoutingDataSource { @Nullable @Override protected Object determineCurrentLookupKey() { return DBContextHolder.get(); } }设置路由key默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)@Aspect @Component public class DataSourceAop { @Pointcut("!@annotation(com.cjs.example.annotation.Master) " + "&& (execution(* com.cjs.example.service..*.select*(..)) " + "|| execution(* com.cjs.example.service..*.get*(..)))") public void readPointcut() { } @Pointcut("@annotation(com.cjs.example.annotation.Master) " + "|| execution(* com.cjs.example.service..*.insert*(..)) " + "|| execution(* com.cjs.example.service..*.add*(..)) " + "|| execution(* com.cjs.example.service..*.update*(..)) " + "|| execution(* com.cjs.example.service..*.edit*(..)) " + "|| execution(* com.cjs.example.service..*.delete*(..)) " + "|| execution(* com.cjs.example.service..*.remove*(..))") public void writePointcut() { } @Before("readPointcut()") public void read() { DBContextHolder.slave(); } @Before("writePointcut()") public void write() { DBContextHolder.master(); } /** * 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库 */ // @Before("execution(* com.cjs.example.service.impl.*.*(..))") // public void before(JoinPoint jp) { // String methodName = jp.getSignature().getName(); // // if (StringUtils.startsWithAny(methodName, "get", "select", "find")) { // DBContextHolder.slave(); // }else { // DBContextHolder.master(); // } // } }有一般情况就有特殊情况,特殊情况是某些情况下我们需要强制读主库,针对这种情况,我们定义一个主键,用该注解标注的就读主库package com.cjs.example.annotation; public @interface Master { }例如,假设我们有一张表member@Service public class MemberServiceImpl implements MemberService { @Autowired private MemberMapper memberMapper; @Transactional @Override public int insert(Member member) { return memberMapper.insert(member); } @Master @Override public int save(Member member) { return memberMapper.insert(member); } @Override public List<Member> selectAll() { return memberMapper.selectByExample(new MemberExample()); } @Master @Override public String getToken(String appId) { // 有些读操作必须读主数据库 // 比如,获取微信access_token,因为高峰时期主从同步可能延迟 // 这种情况下就必须强制从主数据读 return null; } }4. 测试@RunWith(SpringRunner.class) @SpringBootTest public class CjsDatasourceDemoApplicationTests { @Autowired private MemberService memberService; @Test public void testWrite() { Member member = new Member(); member.setName("zhangsan"); memberService.insert(member); } @Test public void testRead() { for (int i = 0; i < 4; i++) { memberService.selectAll(); } } @Test public void testSave() { Member member = new Member(); member.setName("wangwu"); memberService.save(member); } @Test public void testReadFromMaster() { memberService.getToken("1234"); } }查看控制台5. 工程结构
1能否详细给我们介绍一下快手大数据架构的发展历程,目前在各个关键部位的技术选型是什么?出于什么样的考虑?快手大数据架构团队是在 2017 年开始组建的,整个大数据架构服务也是从那时开始演进发展的。出于目标与成本上的考虑,快手的大数据架构服务大部分都是基于开源系统构建的。截止到目前为止,快手的大数据架构的发展大概经历 3 个阶段:1. 大数据架构基础服务从 0 到 1 建设2017 年左右,快手大数据架构服务主要以支持离线分析场景为主,服务建设首先从离线存储计算服务起步,行业内已经公认 Hadoop 是解决这类场景的最佳方案,考虑到后续我们会基于这个版本之上进行持续开发迭代,所以我们选择下载 CDH Hadoop 2.6 的源代码(选择 CDH 版本是因为比 apache 版本要更加稳定,且没有选择更高版本,也是基于稳定性的考虑,不过现在想起来,其实当时可以换到最新版本),并编译源码进行部署。主要以推广应用、解决业务问题,以及做一些轻量型的改进为主。此外,快手在一开始就重度依赖 Kafka 服务,除了离线日志收集传输场景外,还包括在线服务消息队列以及 cache 同步场景。选择 Kafka 主要是因为其性能好且有大厂(LinkedIn)最佳实践。我们对 Kafka 系统的建设起步还算比较早的,那时主要在解决 Kafka 集群扩展性与可用性的问题,在 2017 年 11 月,我们就解决了 Kafka 集群的扩容问题,能够做到集群扩容对业务基本无影响,且整个扩容流程自动化完成,有兴趣的同学可参考 2019 年北京 QCon 上分享的议题:《快手万亿级别 Kafka 集群应用实践与技术演进之路》。https://www.infoq.cn/article/Q0o*QzLQiay31MWiOBJH?utm_source=tuicool&utm_medium=referral大概在 2018 上半年,大数据架构服务开始延伸到实时计算以及交互式分析场景。对于实时计算场景,我们采用了 Flink 引擎,主要是因为 Flink 完全是原生的实时计算引擎,相比于 Storm,有着丰富实时语义的实现、窗口抽象、状态存储等能力,为开发者提供了非常多便利的工具。相比于 Spark,Flink 的实时性更好。对于 OLAP 分析需求,我们采用 Druid 引擎,并同时提供了配套的 Superset 交互分析可视化平台。该系统一经落地,受到了广泛研发同学的喜爱,并得到了快速的发展。在 OlAP 引擎上,我们最终没有选择 Kylin,主要的原因是 Druid 有着更好的实时性、更多查询能力以及更大的查询灵活性。Kylin 虽然有数据立方体建模加速查询能力,但是 Druid 的物化能力也可以做到类似的效果(不过当时物化功能开源版本没有实现,后来我们自己实现了)。快手的大数据架构存储服务除了支持离线存储场景之外,还同时在支持快手短视频在线存储场景。快手短视频存储平台(对象存储平台),内部代号 blobstore,采用 HDFS 服务存储对象的数据本身,同时采用 HBase 存储对象的索引,整体由 gateway 服务进行对象存储逻辑层的实现。作为对象存储服务而言,这套架构设计的简单且实用。截止到目前,快手的对象存储服务仍沿用着这套基础架构,并进行了功能与能力上的大范围增强。前期我们主要做了一些 HDFS、HBase 服务面向在线场景下的可用性改进,如单点宕机快速恢复等。还有一个方面是运维,我一直坚持大数据架构服务运维和研发要在同一个团队,主要是因为大数据架构服务众多,导致运维流程繁多,且非常复杂。整体运维和研发的沟通协作非常频繁。同一个团队可以更加密切配合。在运维上,我们一开始就主张尽可能通过平台化的方式提效整个运维工作,所以在开始阶段,我们就强调运维操作的复用性,运维以自动化,工具化构建为主,并引入 ambari 作为大数据服务运维的基础平台,逐渐开始接管各类大数据架构服务,并鼓励运维同学多总结目前没有被平台化的操作流程,提炼流程的通用性,为下一步全面平台化运维做好准备工作。截止到 2018 年 6 月左右,整个大数据架构系统已经初步完成存储、调度、计算层服务与运维的从 0 到 1 的建设,并已经在快手大范围应用起来了。2、大数据架构服务深度定制阶段公司业务进一步扩大与高速增长,给整个大数据架构服务的稳定性、扩展性、性能都带来了巨大的冲击。作为应对,在大数据架构服务从 0 到 1 的构建之后,我们开始夯实各层服务的现有能力,对现有的开源服务进行大量的深度开发与定制。受限于篇幅限制,这里主要会挑一些重点的改进简单介绍下。在存储服务上,HDFS/HBase 服务主要的改进包括:单点故障快速恢复、读写性能优化、服务分级保障与回退等待、服务柔性可用、fastfail、QPS 限流、扩展性改进等,此外我们还自研了位图数据库 BitBase,用于 UV 计算,留存计算等场景。简单介绍下 HDFS 服务分级保障能力,这个功能主要是面对离线场景的。在离线计算的场景下,集群整体业务负载基本上没有固定规律,因为个别大作业启动起来后,会直接造成 HDFS 主节点的满载,满载的主要表现是服务延迟升高,QPS 打平,RPC 服务请求堆积。一旦该现象出现在凌晨时段,将会影响核心链路数据的产出,造成故障。为了保障核心链路的生产的稳定,我们引入了优先级的概念,并连同计算资源调度服务一起,给核心作业(高优先级)提供计算与存储资源的整体保障。在实现上,HDFS 主节点 RPC 服务采用多队列设计,将不同优先级的作业请求路由到不同队列,处理线程池线程按照不同比例从不同队列取请求进行处理,一旦高优先级队列请求出现高时延情况,则直接降低低优先级队列请求处理比例,将资源向高优先级队列倾斜,从而保障高优先级作业请求的延迟稳定。如果低优先级队列满,则反馈给客户端特殊信号,客户端进行 backoff 等待重试。由于核心作业相对稳定,负载也相对稳定,基本不会出现由于核心作业导致服务过载的情况。通过这个能力的控制,可以保障核心作业的数据产出延迟稳定,不受低优异常作业流量徒增的影响。在 Kafka 消息服务上,主要改进包括单点故障快速恢复、平滑扩容、Mirror 服务集群化、资源隔离、Cache 改造、智能限速、QPS 限流、柔性可用、Kafka On HDFS 存储计算分离等。简单介绍下 Kafka On HDFS 这个能力,Kafka 服务的性能主要依赖内存 cache 层,一旦读数据 miss cache,会产生磁盘读操作(lag 读),由于目前磁盘主要还是机械盘,因此一旦 lag 读,性能会急剧下降。此外,如果 topic 的 consumer group 很多的话,非常容易造成磁盘单盘热点,使得性能进一步恶化,甚至影响 broker 上的其他 topic 的读写操作。另一方面,随着业务快速发展,Kafka 集群规模也越来越大,原有 Kafka 的架构模式在超大规模下会造成大量的运维成本。为了解决上述两个问题,我们开发了 Kafka On HDFS 方案,并控制一旦 group 的读操作产生了 cache miss,broker 会直接从 HDFS 读取数据返回给消费者,且由于 HDFS 的数据是按照块打散的,所以在消费者 lag 读的时候,能够充分利用多盘的能力支持读,进而提升 lag 读性能。另一方面,由于 Kafka On HDFS 方案可实现存储与计算的分离,这样 broker 变成了无状态的服务,在单点宕机的时候,可以直接从 HDFS 上恢复,感兴趣的同学可以关注 2020 年 Qcon 北京的议题:《快手实时处理系统存储架构演进之路》https://qcon.infoq.cn/2020/beijing/presentation/2344在调度引擎上,主要改进点是资源超配功能以及自研了 YARN 的新版本调度器 KwaiScheduler,改进调度性能,并定制了大量的调度策略、例如标签调度、作业分级阻断、基于用户的公平调度、基于优先级的调度等,此外还重构了资源抢占模块。其中 KwaiScheduler 采用了分批与并行混合调度方案,SLS 工具评测出来的调度性能相比于 apache hadoop3.0 的版本有 20~30 倍提升,调度性能可达:2.5w/s~3.5w/s。 YARN 的新版本调度器 KwaiScheduler:https://www.infoq.cn/article/vkH8pdfqAZFh3YXaSSsG在计算引擎上,自研了智能 SQL 引擎 Beacon,可以自动路由 Presto、Spark、MR 引擎,整个引擎路由完全对用户透明,提升了性能并降低了使用成本。OLAP 引擎上,深度定制 Druid 系统,开发了物化视图功能、精准去重功能、中心节点优化改进单集群扩展性、资源隔离以及可用性改进点等。此外,我们还引入了 Clickhouse 引擎,并同时自研了 Clickhouse On HDFS 服务 KwaiCH,彻底解决了 Clickhouse 服务运维难,扩容难的问题。实时引擎上,增加新的状态存储引擎 SlimBase,Source 同步消费功能,JobManger 高可用、实时 SQL 建模等。智能 SQL 引擎 Beacon:https://www.infoq.cn/article/BN9cJjg1t-QSWE6fqkoR?utm_source=related_readClickhouse On HDFS 服务 KwaiCHhttps://www.infoq.cn/article/vGabIOdeUM87hv6X8qlL?from=singlemessage状态存储引擎 SlimBasehttps://open.mi.com/conference/hbasecon-asia-2019在运维上,基于 ambari 自研了可以管理 10w+ 机器规模的服务管理平台 Kalaxy,基本囊括了大数据服务运维、基础运维的全部场景。极大提升了运维效率。3、大数据架构服务整合统一、云化阶段从 2020 年开始,快手大数据架构整体上会做进一步整合,并朝向云化服务发展,为公司各业务线提供一体式的大数据基础服务。具体详情,敬请期待。2在春晚红包活动中,快手的大数据架构面临了哪些问题,做了哪些针对性的调整优化?当听到快手成为 2020 年春节联欢晚会独家互动合作伙伴时,我是非常兴奋的,同时也是压力巨大的。和春晚活动相关的大数据服务包括:在线短视频上传服务、在线消息中间件服务、实时计算、日志上传与离线计算。在线场景下,主要是要能扛住极端并发下的峰值流量,保障活动期间服务整体稳定运行。原有的 HDFS、HBase、Kafka 服务在面对超高并发请求的压力下,可能会出现服务雪崩以及大规模的节点不可用的情况,将会造成重大事故。于是,为了应对可能的极限峰值压力,我们在三个月的时间内开发并上线了过载保护、服务限流与快速 failover、分级保障等能力,实现了 HDFS、HBase、Kafka 三类服务的柔性可用,以及灵活的服务请求控制能力,使得 HDFS、HBase、Kafka 服务在极端压力下,也可以保持峰值吞吐提供服务。当然也不能只依赖服务管控的方式,在服务容量规划与评估上,我们也做了大量的工作,最终也是比较精准的预测了春晚的流量。在整个三个月里,我们也进行了非常多次的全链路压测与故障演练,以便确认系统在超高压力下的能够提供高稳定性的服务。实时计算场景下,主要是保障活动实时大盘的高稳定性运行。为了保障实时服务整体稳定,我们除了开发并上线服务柔性可用的能力以及进行压测之外,还针对活动以及核心数据的实时生产,建设了多条互备的物理链路,一旦单条物理链路出现问题,可以随时切换到另外一条上,保障了活动期间实时数据的产出的高稳定性。离线场景下,主要面临的问题是日志服务可能会被降级,会导致生产 ODS 层数据延迟,进而导致公司级别的离线核心数据的产出出现延迟。此外,活动当天的数据规模以及日志服务恢复时可能带来的峰值不好预期,所以数据真实的恢复时间也不好评估,给离线链路的核心数据产出的预案设计带来了非常大的困难。为了应对这种情况,我们首先把作业按照重要性进行了分级,并制定了多种情况下的数据恢复以及数据的分级产出方案。在资源保障上,YARN 提供了按照优先级进行作业阻断提交与回收的能力,以及按照作业优先级进行资源调度的能力,保障了离线链路上核心数据的及时产出。通过对大数据架构服务,面向以上几种场景下能力的改造,我们顺利完成了整个春晚活动的稳定性保障任务。整体活动期间,各类大数据架构服务整体稳定平稳,全部达成了之前稳定性预期的目标。3快手在调度系统方面有哪些值得业界借鉴的经验?大数据架构团队针对资源调度系统 YARN 做了很多非常好的改进以及资源上的规划。在资源管理上,1)采用了”队列隔离 + 优先级调度能力”的双层保障。规划了生产队列、adhoc 队列、回溯队列、test 队列,以便做到不同大类别作业的资源消耗的隔离,不同类型的作业相互之间不受影响。此外,每个作业都要设置优先级,并在队列内部提供了按照作业优先级进行资源调度与隔离的能力,可细粒度控制不同等级作业资源消耗,最终实现分级保障的目标;2)给每个业务线设置 quota(quota 等于 minshare),并保障在任何情况下,每个业务线都可以使用到 quota 数量的资源。在调度策略上,1)针对同一集群不同性质的作业利用标签调度进行物理隔离,例如离线作业、实时作业之间的物理隔离;2)针对 adhoc 场景,提供按照个人用户公平的资源的调度策略,以便保障每个人都能获得相同资源,避免一个人由于提交作业过多而占用大量资源的情况;3)重构并开启了资源抢占功能,解决由于大作业长时间占用资源不释放导致 quota 资源量不能被快速满足的情况。4)开发上线 App Slot 抢占能力,避免高优先级受最大 APP 限制不能快速执行的问题。4能否详细介绍一下快手在 Hadoop 方面的应用实践?Hadoop 对快手而言重点解决了什么问题?Hadoop 是快手大数据架构体系的一部分,通常说的 Hadoop 指的是 HDFS、YARN、MR 这三个服务。目前 Hadoop 主要应用在离线数据分析场景。HDFS 是海量离线数据存储底层基础服务,快手所有离线的数据都存储在 HDFS 上,其规模达 EB 级别,为了降低成本,我们还采用 EC 技术进一步降低副本空间,目前快手 EC 的数据规模达数百 PB。YARN 系统为各种计算类型作业(MR、Spark、FLink 等)进行资源的分配与调度。我们自研了 YARN 的新型调度器 KwaiScheduler,评测的调度性能可达:2.5w/s~3.5w/s,相比于 Apache Hadoop 3.0 的版本有 20~30 倍提升。此外,kwaiScheduler 提供了可插拔的调度策略,增加调度策略变得极其容易,目前借助该功能提供了混合式的调度策略:针对 adhoc 场景,提供面向个人公平的资源调度策略;针对数据例行生产,提供面向作业优先级的调度策略;面向实时场景,提供资源均衡的调度策略等等。此外,为了进一步改进性能,我们在进行 Spark 引擎替换 MR 引擎的工作,快手的 MR 引擎的作业占比在逐渐减低中,但是由于 MR 引擎出色的稳定性表现,在部分核心 ETL 场景下,MR 引擎可能会被保留。综上所述,Hadoop 是非常核心的底层基础服务,在快手大数据架构体系中占据着核心地位。5关于国内外唱衰 Hadoop 的言论,您怎么看?Hadoop 如何摆脱目前的尴尬现状?Hadoop(狭义 Hadoop)主要指的是 HDFS、YARN、MR 这三个服务,主要解决了企业离线数据分析的场景需求。近几年新型开源分析系统 Spark、Flink、Druid、Clickhouse 等在实时性,性能上比 Hadoop 有很大程度的提升与补充。但是这些系统也仅仅是对 MR 这个计算引擎的替换,目前在大数据场景下,主流的存储与资源管理系统仍然是 HDFS 和 YARN,且短期内不会出现什么变化。之所以存储会选择 HDFS,一方面是因为 HDFS 系统的稳定性、可靠性以及扩展性非常出色,另一方面,企业现有的数据已经存储到 HDFS 系统上,迁移本身成本也是比较大的。整体上没有什么强烈理由需要替换。对于 YARN 系统而言,虽然 K8s 目前发展势头强劲,但是大部分企业仍然应用 K8s 在在线服务领域,离线数据分析领域实践少之又少。主要是因为 K8s 系统在调度能力上,以及对现有分析引擎的支持上还不是特别完善。而 YARN 本身也在快速迭代,且各大互联网公司对 YARN 的调度能力以及扩展性、甚至资源超配都做到很大的改进。整体上替换 YARN 的动力也不强。未来一个可能的方向是要考虑如何将 YARN 和 K8S 服务整体一起,提供统一的调度服务。所以,我觉得对于自己组建大数据架构服务的企业来说,要对整个大数据生态的基础服务有比较深入的了解。从应用上看,Hadoop 仍然是一个很好的大数据基础服务,只不过要因地制宜,且把更多能力更强的新型系统引入到企业中,帮助解决不同场景下的需求。最后再做到服务整合,在提升性能的前提下,减少业务的使用成本。6如何看待大数据架构与云架构之间的关系?类似 Hadoop 的大数据技术会在云服务的冲击下逐渐没落吗?首先再明确下这两个概念:大数据架构:是为解决大数据业务场景需求的分布式基础服务,其定位可以认为是大数据方向的基础架构。整体上可划分为三个层次分布式存储层:主要包括各类大数据场景的存储服务,如分布式文件系统(HDFS)、分布式 KV 系统(HBase)以及分布式消息缓存(Kafka)等。主要解决的是海量数据的存储问题(也有相当多的互联网公司利 Kafka 系统进行数据传输接入)分布式调度层:资源调度层目前主要使用的 YARN,提供了一个资源池抽象层,把各类计算引擎作业统一管理与调度。计算引擎层:是面向各类场景的计算引擎,包括解决实时计算场景需求的 Flink 系统,解决离线计算场景的 SQL 类引擎,以及解决交互式分析场景下的 adhoc 以及 olap 引擎。云架构:是实现云计算能力的底层基础服务与设施,行业内公认的云架构分为三大层次:基础设施层(IaaS,基础设施即服务):将基础设施,例如网络,机器等硬件资源抽象成服务,提供给客户使用,解决了客户在如何采购机器,建设机房,以及构建网络等基础类工作的问题。平台层(PaaS,平台即服务):将平台,例如开发、存储、调度与计算平台等,做整合抽象成服务,为用户提供了开发环境,解决用户快速构建业务服务的能力。软件服务层(SaaS,软件即服务):将软件,例如推送、反作弊等应用,作为服务提供。客户可以根据需求通过互联网向厂商订购,并使用应用软件服务。从这两个概念上看,大数据架构可以认为是云架构中 PaaS 层的一部分。专注于为客户提供在大数据场景下的业务快速构建能力。大数据架构服务,连同数据生产开发套件一起为面向数据分析的客户提供一体式的 PaaS 层解决方案。从这个方面上看,即使在云架构中,依然会保留大数据架构技术。从各自发展上来看,创业企业、小型企业以及一部分中型企业,需求相对来说可能会比较简单,出于成本、人力的考虑,会投向云架构提供的服务上,以便快速实现业务逻辑提供产品获取利润。对于大型企业以及一部分中型企业而言,业务体量很大,面临的需求也会变得丰富、个性化且复杂,云架构所提供的服务,不一定能够完全满足具体的场景与需求,此外,如果放到云上,数据本身也存在安全层面的隐患,所以除了成本因素外,还会考虑快速支持、安全等因素,通常会自建大数据架构服务,以便有效支撑企业发展。当然在这些企业中的大数据架构技术并不是简单的拿来应用,与此同时,还会对其进行大量深度定制开发,以便满足企业发展需求。整个大数据架构技术会也会向着服务能力整合统一,以及企业内部云化的方向发展。所以从上述两个方面看,大数据架构技术并不会没落,且会和云架构一样继续蓬勃发展。
一、Cloud-Platform介绍Cloud-Platform是国内首个基于Spring Cloud微服务化开发平台,具有统一授权、认证后台管理系统,其中包含具备用户管理、资源权限管理、网关API 管理等多个模块,支持多业务系统并行开发,可以作为后端服务的开发脚手架。代码简洁,架构清晰,适合学习和直接项目中使用。核心技术采用Spring Boot 2.4.1、Spring Cloud (2020.0.0)以及Spring Cloud Alibaba 2.2.4 相关核心组件,采用Nacos注册和配置中心,集成流量卫兵Sentinel,前端采用vue-element-admin组件,Elastic Search自行集成。功能截图详细了解https://gitee.com/geek_qi/cloud-platform二、pig介绍基于 Spring Cloud Hoxton 、Spring Boot 2.4、 OAuth2 的 RBAC 权限管理系统基于数据驱动视图的理念封装 element-ui,即使没有 vue 的使用经验也能快速上手提供对常见容器化支持 Docker、Kubernetes、Rancher2 支持提供 lambda 、stream api 、webflux 的生产实践功能截图详细了解https://gitee.com/log4j/pig三、microservices-platform介绍基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务多租户系统架构。并引入组件化的思想实现高内聚低耦合并且高度可配置化,适合学习和企业中使用。真正实现了基于RBAC、jwt和oauth2的无状态统一权限认证的解决方案,面向互联网设计同时适合B端和C端用户,支持CI/CD多环境部署,并提供应用管理方便第三方系统接入;同时还集合各种微服务治理功能和监控功能。推荐:Java面试练题宝典模块包括:企业级的认证系统、开发平台、应用监控、慢sql监控、统一日志、单点登录、Redis分布式高速缓存、配置中心、分布式任务调度、接口文档、代码生成等等。功能截图详细了解https://gitee.com/zlt2000/microservices-platform四、SpringBlade介绍SpringBlade 是一个由商业级项目升级优化而来的SpringCloud分布式微服务架构、SpringBoot单体式微服务架构并存的综合型项目,采用Java8 API重构了业务代码,完全遵循阿里巴巴编码规范。采用Spring Boot 2.4 、Spring Cloud 2020 、Mybatis 等核心技术,同时提供基于React和Vue的两个前端框架用于快速搭建企业级的SaaS多租户微服务平台。功能截图详细了解https://gitee.com/smallc/SpringBlade五、RuoYi-Cloud介绍基于Spring Boot、Spring Cloud & Alibaba的分布式微服务架构权限管理系统。推荐:Java面试练题宝典采用前后端分离的模式,微服务版本前端。后端采用Spring Boot、Spring Cloud & Alibaba。注册中心、配置中心选型Nacos,权限认证使用Redis。流量控制框架选型Sentinel,分布式事务选型Seata。功能截图详细了解https://gitee.com/y_project/RuoYi-Cloud六、open-capacity-platform介绍简称ocp是基于layui+springcloud的企业级微服务框架(用户权限管理,配置中心管理,应用管理,....),其核心的设计目标是分离前后端,快速开发部署,学习简单,功能强大,提供快速接入核心接口能力,其目标是帮助企业搭建一套类似百度能力开放平台的框架;基于layui前后端分离的企业级微服务架构 兼容spring cloud netflix & spring cloud alibaba 优化Spring Security内部实现,实现API调用的统一出口和权限认证授权中心 提供完善的企业微服务流量监控,日志监控能力 通用的微服务架构应用非功能性(NFR)需求,更容易地在不同的项目中复用 提供完善的压力测试方案 提供完善的灰度发布方案 提供完善的微服务部署方案功能截图
项目介绍在线考试系统是一款 java + vue 的前后端分离的考试系统。主要优点是开发、部署简单快捷、界面设计友好、代码结构清晰。目前支持web端和微信小程序,能覆盖到pc机和手机等设备。开源版使用须知仅用个人学习,商用请购买授权 http://www.mindskip.net/buy.html禁止将本项目的代码和资源进行任何形式的出售,产生的一切任何后果责任由侵权者自负参考地址:https://github.com/mindskip/xzs学生系统功能登录、注册:注册时要选年级,过滤不同年级的试卷, 账号为student/123456首页:任务中心、固定试卷、时段试卷、可以能做的一部分试卷试卷中心:包含了所有能做的试卷,按学科来过滤和分页考试记录:所有的试卷考试记录在此处分页,可以查看试卷结果、用时、得分、自行批改等错题本:所有做错的题目,可以看到做题的结果、分数、难度、解析、正确答案等个人中心:个人日志记录消息:消息通知试卷答题和试卷查看:展示出题目的基本信息和需要填写的内容管理系统功能登录:账号为 admin/123456主页:包含了试卷、题目、做卷数、做题数、用户活跃度的统计功能,活跃度和做题数是按月统计用户管理:对不同角色 学生、教师、管理员 的增删改查管理功能卷题管理:试卷列表:试卷的增删改查,新增包含选择学科、试卷类型、试卷名称、考试时间,试卷内容包含添加大标题,然后添加题目到此试卷中,组成一套完整的试卷题目列表:题目的增删改查,目前题型包含单选题、多选题、判断题、填空题、简单题,支持图片、公式等。教育管理:对不同年级的学科进行增删改查消息中心:可以对多个用户进行消息发送日志中心:用户的基本操作进行日志记录,了解用户使用过情况小程序功能用户登录登出功能,登录会自动绑定微信账号,登出会解绑首页包含任务中心、固定试卷、时段试卷,和web端保持一致试卷模块,固定试卷和时段试卷的分页查询,下拉加载更多,上拉刷新当前数据记录模块,考试结果的分页,包含了试卷基本信息我的模块,包含个人资料的修改,个人动态,消息中心模块技术栈列表后台系统:spring-boot 2.1.6.RELEASEspring-boot-security 用户登录验证undertow web容器postgresql/mysql 优秀的开源数据库redis 缓存,提升系统性能mybatis 数据库中间件hikari 速度最快的数据库连接池七牛云存储 目前10G内免费前台系统:Vue.js 采用新版,使用了vue-cli3搭建的系统,减少大量配置文件element-ui 最流行的vue组件,采用的最新版vue-element-admin 最新版,对该系统做了大量精简,只保留了部分样式和控件echarts 图表统计ueditor 填空题扩展插件微信小程序:iView 主题样式使用教程redis 安装进群获取到数据库脚本,创建表初始化数据/uexam/source/xzs为后台代码,建议使用IntelliJ IDEA打开,在application-dev.yml文件中,配置好postgesql/mysql、redis的服务地址,打开XzsApplication文件编译运行,默认端口为8000。学生系统地址:http://localhost:8000/student管理端地址:http://localhost:8000/admin软件架构图系统展示学生考试系统小程序考试系统后台管理系统
简介大屏设计(AJ-Report)是一个可视化拖拽编辑的全开源项目,直观,酷炫,具有科技感的图表工具。内置的基础功能包括数据源,数据集,报表管理。多数据源支持,内置mysql、elasticsearch、kudu驱动,支持自定义数据集省去数据接口开发,支持17种大屏组件,不会开发,照着设计稿也可以制作大屏。三步轻松完成大屏设计:配置数据源—->写SQL配置数据集—->拖拽配置大屏—->保存发布。欢迎体验。数据流程图核心技术后端Spring Boot2.3.5.RELEASE: Spring Boot是一款开箱即用框架,让我们的Spring应用变的更轻量化、更快的入门。在主程序执行main函数就可以运行。你也可以打包你的应用为jar并通过使用java -jar来运行你的Web应用;Mybatis-plus3.3.2: MyBatis-plus(简称 MP)是一个 MyBatis (opens new window) 的增强工具。flyway5.2.1: 主要用于在你的应用版本不断升级的同时,升级你的数据库结构和里面的数据前端npm:node.js的包管理工具,用于统一管理我们前端项目中需要用到的包、插件、工具、命令等,便于开发和维护。webpack:用于现代 JavaScript 应用程序的_静态模块打包工具ES6:Javascript的新版本,ECMAScript6的简称。利用ES6我们可以简化我们的JS代码,同时利用其提供的强大功能来快速实现JS逻辑。vue-cli:Vue的脚手架工具,用于自动生成Vue项目的目录及文件。vue-router:Vue提供的前端路由工具,利用其我们实现页面的路由控制,局部刷新及按需加载,构建单页应用,实现前后端分离。element-ui:基于MVVM框架Vue开源出来的一套前端ui组件。avue: 用该组件包裹后可以变成拖拽组件,采用相对于父类绝对定位;用键盘的上下左右也可以控制移动vue-echarts: vue-echarts是封装后的vue插件,基于 ECharts v4.0.1+ 开发vue-superslide: Vue-SuperSlide(Github) 是 SuperSlide 的 Vue 封装版本vuedraggable: 是一款基于Sortable.js实现的vue拖拽插件。截图
众所周知,AI 在围棋上的实力是人类所不能及的。不过斗地主还不一定。在 2017 年 AlphaGo 3 比 0 战胜中国棋手,被授予职业九段之后,柯洁决定参加斗地主比赛,并获得了冠军。在当时的赛后采访中,柯洁表示,「很欢乐,希望以后再多拿一些冠军,无论什么样的冠军都想拿!」但是好景不长,在这种随机性更高的游戏上, AI 紧随而至。近日,快手 AI 平台部的研究者用非常简单的方法在斗地主游戏中取得了突破,几天内就战胜了所有已知的斗地主打牌机器人,并达到了人类玩家水平。而且,复现这个研究只需要一个普通的四卡 GPU 服务器。随着斗地主 AI 的不断进化,人(Ke)类(Jie)的斗地主冠军宝座不知还能否保住。人工智能在很多棋牌类游戏中取得了很大的成功,例如阿尔法狗(围棋)、冷扑大师(德州扑克)、Suphx(麻将)。但斗地主却因其极大的状态空间、丰富的隐含信息、复杂的牌型和并存的合作与竞技,一直以来被认为是一个极具挑战的领域。近日,快手 AI 平台部在斗地主上取得了突破,提出了首个从零开始的斗地主人工智能系统 —— 斗零(DouZero)。比较有趣的是,该系统所使用的算法极其简单却非常有效。团队创新性地将传统的蒙特卡罗方法(即我们初高中课本中常说的「用频率估计概率」)与深度学习相结合,并提出了动作编码机制来应付斗地主复杂的牌型组合。该算法在不借助任何人类知识的情况下,通过自我博弈学习,在几天内战胜了所有已知的斗地主打牌机器人,并达到了人类玩家水平。相关论文已被国际机器学习顶级会议 ICML 2021 接收,论文代码也已开源。同时,论文作者开放了在线演示平台供研究者和斗地主爱好者体验。论文链接:https://arxiv.org/abs/2106.06135GitHub 链接:https://github.com/kwai/DouZero在线演示:(电脑打开效果更佳;如果访问太慢,可从 GitHub 上下载并离线安装:https://github.com/datamllab/rlcard-showdown)在线演示支持中文和英文。使用者可以选择明牌 / 暗牌,并可以调节 AI 出牌速度。在明牌模式下,用户可以看到 AI 预测出的最好的三个牌型和预计胜率。让 AI 玩斗地主难在哪儿?一直以来,斗地主都被视为一个极具挑战性的领域。首先,与许多扑克游戏和麻将一样,斗地主属于非完美信息游戏(玩家不能看到其他玩家的手牌),且包含很多「运气」成分。因此,斗地主有非常复杂的博弈树,以及非常大的状态空间(每个状态代表一种可能遇到的情况)。除此之外,相较于德州扑克和麻将,斗地主还有两个独特的挑战:合作与竞争并存:无论是德州扑克还是麻将,玩家之间都是竞争关系。然而,在斗地主中,两个农民玩家要相互配合对抗地主。虽然过去有论文研究过游戏中的合作关系 [1],但是同时考虑合作和竞争仍然是一个很大的挑战。庞大而复杂的牌型:斗地主有复杂的牌型结构,例如单张、对子、三带一、顺子、炸弹等等。它们的组合衍生出了 27,472 种牌型 [2]:在强化学习里,这些牌型被称为动作空间。作为对比,这里列举出了常见强化学习环境及棋牌类游戏的动作空间大小:虽然无限注德州扑克本身有与斗地主有相同数量级的动作空间,但是其动作空间很容易通过抽象的方式缩小,即把类似的动作合并成一个。例如,加注 100 和加注 101 没有很大的区别,可以合并成一个。然而,斗地主中一个动作中的每张牌都很重要,且很难进行抽象。例如,三带一中带的单张可以是任意手牌。选错一次(比如拆掉了一个顺子)就很可能导致输掉整局游戏。几乎所有的强化学习论文都只考虑了很小动作集的情况,例如最常用的环境雅达利只有十几个动作。有部分论文考虑了较大动作集的环境,但一般也只有几百个。斗地主却有上万个可能的动作,并且不同状态有不同的合法动作子集,这无疑给设计强化学习算法带来了很大挑战。之前的研究表明,常用的强化学习算法,如 DQN 和 A3C,在斗地主上仅仅略微好于随机策略 [2][3]。「斗零」是怎么斗地主的?比较有趣的是,斗零的核心算法极其简单。斗零的设计受启发于蒙特卡罗方法(Monte-Carlo Methods)[4]。具体来说,算法的目标是学习一个价值网路。网络的输入是当前状态和一个动作,输出是在当前状态做这个动作的期望收益(比如胜率)。简单来说,价值网络在每一步计算出哪种牌型赢的概率最大,然后选择最有可能赢的牌型。蒙特卡罗方法不断重复以下步骤来优化价值网络:用价值网络生成一场对局记录下该对局中所有的状态、动作和最后的收益(胜率)将每一对状态和动作作为网络输入,收益作为网络输出,用梯度下降对价值网络进行一次更新其实,所谓的蒙特卡罗方法就是一种随机模拟,即通过不断的重复实验来估计真实价值。在初高中课本中,我们学过「用频率估计概率」,这就是典型的蒙特卡罗方法。以上所述是蒙特卡罗方法在强化学习中的简单应用。然而,蒙特卡罗方法在强化学习领域中被大多数研究者忽视。学界普遍认为蒙特卡罗方法存在两个缺点:1. 蒙特卡罗方法不能处理不完整的状态序列。2. 蒙特卡罗方法有很大的方差,导致采样效率很低。然而,作者却惊讶地发现蒙特卡罗方法非常适合斗地主。首先,斗地主可以很容易产生完整的对局,所以不存在不完整的状态序列。其次,作者发现蒙特卡罗方法的效率其实并没有很低。因为蒙特卡罗方法实现起来极其简单,我们可以很容易通过并行化来采集大量的样本以降低方差。与之相反,很多最先进的强化学习算法虽然有更好的采样效率,但是算法本身就很复杂,因此需要很多计算资源。综合来看,蒙特卡罗方法在斗地主上运行时间(wall-clock time)并不一定弱于最先进的方法。除此之外,作者认为蒙特卡罗方法还有以下优点:很容易对动作进行编码。斗地主的动作与动作之前是有内在联系的。以三带一为例:如果智能体打出 KKK 带 3,并因为带牌带得好得到了奖励,那么其他的牌型的价值,例如 JJJ 带 3,也能得到一定的提高。这是由于神经网络对相似的输入会预测出相似的输出。动作编码对处理斗地主庞大而复杂的动作空间非常有帮助。智能体即使没有见过某个动作,也能通过其他动作对价值作出估计。不受过度估计(over-estimation)的影响。最常用的基于价值的强化学习方法是 DQN。但众所周知,DQN 会受过度估计的影响,即 DQN 会倾向于将价值估计得偏高,并且这个问题在动作空间很大时会尤为明显。不同于 DQN,蒙特卡罗方法直接估计价值,因此不受过度估计影响。这一点在斗地主庞大的动作空间中非常适用。蒙特卡罗方法在稀疏奖励的情况下可能具备一定优势。在斗地主中,奖励是稀疏的,玩家需要打完整场游戏才能知道输赢。DQN 的方法通过下一个状态的价值估计当前状态的价值。这意味着奖励需要一点一点地从最后一个状态向前传播,这可能导致 DQN 更慢收敛。与之相反,蒙特卡罗方法直接预测最后一个状态的奖励,不受稀疏奖励的影响。「斗零」系统如何实现?斗零系统的实现也并不复杂,主要包含三个部分:动作 / 状态编码、神经网络和并行训练。动作 / 状态编码如下图所示,斗零将所有的牌型编码成 15x4 的由 0/1 组成的矩阵。其中每一列代表一种牌,每一行代表对应牌的数量。例如,对于 4 个 10,第 8 列每一行都是 1;而对于一个 4,第一行只有最后一行是 1。这种编码方式可适用于斗地主中所有的牌型。斗零提取了多个这样的矩阵来表示状态,包括当前手牌,其他玩家手牌之和等等。同时,斗零提取了一些其他 0/1 向量来编码其他玩家手牌的数量、以及当前打出的炸弹数量。动作可以用同样的方式进行编码。神经网络如下图所示,斗零采用一个价值神经网络,其输入是状态和动作,输出是价值。首先,过去的出牌用 LSTM 神经网络进行编码。然后 LSTM 的输出以及其他的表征被送入了 6 层全连接网络,最后输出价值。并行训练系统训练的主要瓶颈在于模拟数据的生成,因为每一步出牌都要对神经网络做一次前向传播。斗零采用多演员(actor)的架构,在单个 GPU 服务器上,用了 45 个演员同时产生数据,最终数据被汇集到一个中央训练器进行训练。比较有趣的是,斗零并不需要太多的计算资源,仅仅需要一个普通的四卡 GPU 服务器就能达到不错的效果。这可以让大多数实验室轻松基于作者的代码做更多的尝试。实验为验证斗零系统的有效性,作者做了大量的实验。这里我们选取部分实验结果。作者将斗零和多个已有的斗地主 AI 系统进行了对比,具体包括:DeltaDou [5] 是首个达到人类玩家水平的 AI。算法主要基于贝叶斯推理和蒙特卡罗树搜索,但缺点是需要依赖很多人类经验,并且训练时间非常长。即使在用规则初始化的情况下,也需要训练长达两个月。CQN [3] 是一个基于牌型分解和 DQN 的一种方法。虽然牌型分解被证明有一定效果,但是该方法依然不能打败简单规则。SL (supervised learning,监督学习)是基于内部搜集的顶级玩家的对战数据,用同样的神经网络结构训练出来的模型。除此之外,作者尽可能搜集了所有已知的规则模型,包括 RHCP、RHCP-v2、RLCard 中的规则模型 [2],以及一个随机出牌策略。斗地主中玩家分为地主和农民两个阵营。作者使用了两个评估指标来比较算法之间的性能:WP (Winning Percentage) 代表了地主或农民阵营的胜率。算法 A 对算法 B 的 WP 指标大于 0.5 代表算法 A 强于算法 B。ADP (Average Difference in Points) 表示地主或农民的得分情况。每有一个炸弹 ADP 都会翻倍。算法 A 对算法 B 的 ADP 指标大于 0 代表算法 A 强于算法 B。实验 1:与已知斗地主 AI 系统的对比作者比较胜率(WP)和分值(ADP)。如下表所示,斗零(DouZero)在两项指标上都明显好于已知方法。值得一提的是,因为斗地主本身有很大的「运气」成分,高几个百分点的胜率就代表很大的提高了。实验 2:在 Botzone 平台上的对比Botzone(https://www.botzone.org.cn/)是由北京大学 AI 实验室开发的在线对战平台,支持多种游戏的在线评测,并举办过多场棋牌类 AI 比赛。作者将斗零上传到了斗地主对战的系统。Botzone 计分结果表明,斗零在 344 个对战机器人中脱颖而出,在 2020 年 10 月 30 日排名第一。实验 3:斗零的训练效率作者用 DeltaDou 和 SL 作为对手,测量斗零的训练效率。所有的实验都在一个服务器上进行,该服务器包括 4 个 1080Ti GPU 和 48 核处理器。如下图所示,斗零在两天内超过了 SL,在 10 天内超过了 DeltaDou。实验 4:与人类数据的比较斗零究竟学出了什么样的策略呢?作者将人类数据作为测试数据,计算不同阶段的模型在人类数据上的准确率,如下图所示。我们可以发现两个有趣的现象。首先,斗零在前五天的训练中准确率不断提高。这表明斗零通过自我博弈的方式学到了类似于人类的出牌方式。其次,在五天以后,准确率反而下降,这说明斗零可能学到了一些超出人类知识的出牌方式。实验 5:案例分析上文提到,斗地主游戏中两个农民需要配合才能战胜地主。作者为此做了一个案例分析,如下图所示。图中下方农民出一个小牌就能帮助右方农民获胜。图中显示了预测出的最优的三个牌型和预测的价值。我们可以看到预测结果基本符合预期。下方农民「认为」出 3 有非常高的获胜概率,而出 4 或 5 的预期价值会明显变低,因为右方农民的手牌很有可能是 4。结果表明斗零确实学到了一定的合作策略。总结斗零的成功表明简单的蒙特卡罗算法经过一些加强(神经网络和动作编码)就可以在复杂的斗地主环境上有着非常好的效果。作者希望这个结果能启发未来强化学习的研究,特别是在稀疏奖励、复杂动作空间的任务中。蒙特卡罗算法在强化学习领域一直不受重视,作者也希望斗零的成功能启发其他研究者对蒙特卡罗方法做更深入的研究,更好地理解在什么情况下蒙特卡罗方法适用,什么情况下不适用。为推动后续研究,作者开源了斗地主的模拟环境和所有的训练代码。值得一提的是,斗零可以在普通的服务器上训练,并不需要云计算的支持。作者同时开源了在线演示平台和分析平台,以帮助研究者和斗地主爱好者更好地理解和分析 AI 的出牌行为。鉴于当前的算法极其简单,作者认为未来还有很大的改进空间,比如引入经验回放机制来提高效率、显性建模农民之间的合作关系等等。作者也希望未来能将斗零的技术应用到其他扑克游戏以及更加复杂的问题中。研发团队介绍:这项工作是由 Texas A&M University 的 DATA 实验室和快手 AI 平台部的游戏 AI 团队合作而成。DATA 实验室主要从事数据挖掘和机器学习算法等方面的研究,以更好地从大规模、网络化、动态和稀疏数据中发现可操作的模式。快手游戏 AI 团队,主要依托在最先进的机器学习技术,致力于服务游戏研发,推广,运营等各个环节。
PDMan是由国内知名金融IT上市公司,内部研发团队设计的一款面向数据库模型建模的软件,是PowerDesigner的一个优秀的替代方案.特点如下:免费使用功能简洁,去除晦涩难懂的设置,化繁为简,实用为上,上手非常容易。Windows,Mac,Linux三个平台均可以使用(敲黑板,重点)。自带参考案例,学习容易。新建一个项目,完全不需要做任何配置。对开发极其友好,可生成各种数据库以及编程语言的模型类。目前系统默认实现了MySQL,Oracle,Java的代码自动生成,并且带注释。其他类型的数据库或语言,只需要添加相应的“数据库”并设置好相应的doT模板就可以了。一键自动生成数据表结构文档,方便客户交付。数据库 版本管理 以及 数据库同步 功能,解决数据库版本管理的一大痛点。生成数据库脚本以及提供导出功能。启动画面主工作界面代码模板编辑器数据类型以及数据域脚本导出导出特定类型的脚本数据库版本管理生成文档:
MrDoc 是基于Python开发的在线文档系统,适合作为个人和小型团队的文档、知识和笔记管理工具。致力于成为优秀的私有化在线文档部署方案,支持Markdown和富文本编辑模式 。功能特性站点管理用户注册、用户登录、用户管理、注册邀请码配置、全站关闭注册开关、全站强制登录开关;广告代码配置、统计代码配置、站点信息配置、备案号配置;附件格式配置、附件大小配置、图片大小配置;个人管理文集管理:新建、删除、权限控制、转让、协作、导出、生成电子书格式文件文档管理:新建、删除、回收站、历史版本文档模板管理:新建、删除图片管理:上传、分组、删除附件管理:上传、删除Token管理:借助Token高效新建和获取文档;个人信息管理:修改昵称、修改电子邮箱、切换文档编辑器;文档书写Markdown 、富文本两种编辑模式,Editor.md、Vditor、iceEditor三种编辑器加持,自由选择、自由切换;图片、附件、科学公式、音视频、思维导图、流程图、Echart图表;文档排序、文档上级设置、文档模板插入;标签设置;文档阅读两栏式布局,三级目录层级显示,左侧文集大纲,右侧文档正文;文档阅读字体缩放、字体类型切换、页面社交分享、移动端阅读优化;文集EPUB、PDF文件下载,文档Markdown文件下载;标签关系网络图;文档全文搜索;简明安装教程1、安装依赖库pip install -r requirements.txt2、初始化数据库在安装完所需的第三方库并配置好数据库信息之后,我们需要对数据库进行初始化。在项目路径下打开命令行界面,运行如下命令生成数据库迁移:python manage.py makemigrations 运行如下命令执行数据库迁移:python manage.py migrate执行完毕之后,数据库就初始化完成了。3、创建管理员账户在初始化完数据库之后,需要创建一个管理员账户来管理整个MrDoc,在项目路径下打开命令行终端,运行如下命令:python manage.py createsuperuser按照提示输入用户名、电子邮箱地址和密码即可。4、测试运行在完成上述步骤之后,即可运行使用MrDoc。在测试环境中,可以使用Django自带的服务器运行MrDoc,其命令为:python manage.py runserver
开源 OA PlatformO2OA是基于J2EE架构,集成移动办公、智能办公,支持私有化部署,自适应负载能力的,能够很大程度上节约企业软件开发成本的基于AGPL协议开放源代码的企业信息化系统需求定制开发解决方案,对外提供专业的开发运维等技术服务。O2OA平台拥有流程管理、门户管理、信息管理、数据管理和服务管理五大核心能力。用户可以直接使用平台已有功能进行信息信息化建设,平台提供了完整的用户管理,权限管理,流程和信息管理体系,并且提供了大量的开发组件和开箱即用的应用,可以大幅度减化企业信息化建设成本和业务应用开发难度。其主要能力如下:流程管理:全功能流程引擎。基于任务驱动,开放式服务驱动,高灵活性、扩展性 ,应用场景丰富,可轻松实现公文、合同、项目管理等复杂工作流应用。信息管理:具有权限控制能力的内容管理平台,能轻松实现知识管理、通知公司、规章制度、文件管理等内容发布系统。门户管理:具体可视化表单编辑的,支持HTML直接导入的,支持各类数据源,外部应用集成能力的。服务管理:可以在前端脚本的形式,开发和自定义web服务,实现与后端服务数据交互的能力。数据中心:可以通过配置轻松实现数据透视图展示,数据统计、数据可视化图表开发等等功能。智能办公:拥有语音办公、人脸识别、指纹认证、智能文档纠错、智能填表推荐等智能办公特色移动办公:支持安卓\IOS手机APP办公,支持与企业微信和钉钉集成,支持企业私有化微信部署开箱即用:O2OA还提供如考勤管理、日程管理、会议管理、脑图管理、便签、云文件、企业社区、执行力管理等开箱即用的应用供企业选择产品特点:代码全部开源,开发者可以下载源码进行任意,编译成自己的信息化平台。平台全功能免费,无任何功能和人数限制。支持私有化部署,下载软件安装包后可以安装在自己的服务器上,数据更安全。随时随地办公,平台支持兼容HTML5的浏览器,并且提供了原生的IOS/Android应用,并且支持钉钉和企业微信集成。高可扩展性,用户通过简单的学习后,可以自定义配置门户、流程应用、内容管理应用全功能全平台化办公平台解决方案功能全:O2OA内置数十种开箱即用的应用以及开发组件,您可以免费获取全功能的O2OA平台来搭建符合企业要求的各种应用终端全:O2OA覆盖了几乎全部终端。您可以使用IE10+、Chrome、Firefox、Safari、Opera等主流浏览器(包括使用相同内核的其他浏览器如:360浏览器、百度浏览器、猎豹浏览器等等)。除此之外,您还可以使用安卓、IOS移动APP进行流程处理等办公,也可以将O2OA与企业钉钉、微信集成,实现真正的随时随地办公。平台全:O2OA支持在windows,linux(redhat\centos\ubantu)、aix、solaris、Kylin(国产)等主流操作系统上进行安装部署,平台适应性强数据库全:O2OA支持几乎市面上所有的数据为软件,如:Oracle, SQLServer, Mysql, DB2, Infomix, PostgreSQL等主流数据库,并且还支持达梦等国产数据库系统。版式文件:版式文件是O2OA在公文业务上的一个显著的特色,公司自研了完全符合GB9704-2012标准的版式公文组件, 支持所有终端,无需加载任何插件,展现效率更高,展现效果更好。产品图片image.png
2022年01月