暂时未有相关云产品技术能力~
前言文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式详细教程秒传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
今天给大家推荐一款一键部署,一键生成全自动化的低代码生成器工具,可以实现前端可视化操作(拖拽形式+配置就可以生成前端页面),后端直接结合前端代码一键生成,数据库(含表字段)可一键生成(拖拽形式+配置),生成完成之后直接部署就可以了,生成的前端代码是Vue,后端代码是springboot。目前还在不断的迭代当中,可以满足在校大学生的毕业设计的烦恼,一键生成完整的Springboot & Vue项目,毕设的功能基本上都够用了,功能是不是很强大了。那么下面给大家介绍下功能吧!再提一句记住,现在是免费的!功能如下:1.数据仓库 新增数据库 新建表 组件拖拽 主键,输入框,密码框,下拉框,单选,多选,开关,滑块,日期,图片上 传,评分,外键,颜色,计数器2.前端页面设计(暂时支持ElementUI)组件拖拽推荐表单,图片上传,复选框,评分,开关,远程下拉,远程标签,打印,链接按 钮,头像,图片,超链接,还包含一些日常表单组件,容器组件,导航组件,高级 组件3.Api列表自定义设置后台接口,支持sql和json形式功能介绍:一、首先下载和安装下载安装代码生成器工具链接地址见文章底部1、下载好直接解压2、直接进入文件夹里面记得文件夹要保持英文路径3、然后等待几分钟后会自动启动系统会出现以下界面二、注册注意:使用手机号注册填写用户名和密码以及邀请码,没有邀请码是不能注册的然后根据方式获取邀请码填写即可三、登录四、数据仓库根据数据仓库设计数据库表字段,用于在页面设计时可以快速配置内置的CRUD代码实现。如下图:1、新建仓库2、按钮功能介绍3、数据库字段设计及生成代码数据仓库这里就成功生成了。页面设计 1、 进入页面设计新建一个页面2、新增完成之后可以自定义修改页面名称3、点击组件按钮查看所有组件官方组件推荐表单,图片上传,复选框,评分,开关,远程下拉,远程标签,打印,链接按 钮,头像,图片,超链接,还包含一些日常表单组件,容器组件,导航组件,高级组件如果以上组件不满足需求可以在组件市场进行下载也可以自行开发组件使用。父子表组件展示:拖拉完成之后,数据仓库中的数据字段就直接在里面呈现,接着点击保存页面,最后预览页面,就可以进行CRUD的操作了。预览页面进行添加进入添加操作以上还可以直接打印、导出等功能。图表展示:一共可以实现几十种统计的图表。代码生成器的大致使用就介绍完了,下面来介绍一下前后端的代码生成到哪里了。打开你安装的代码生成器目录,找到test文件夹就可以找到Java代码了。前端代码可以直接复制本地保存即可,也可以直接下载。最后您还可以把低代码平台以maven的方式集成到自己的springboot项目中,参考project/test项目配置。
对很多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 系列面试题和答案,非常齐全。
秀儿是你吗?本系列 IDEA 版本为: IntelliJ IDEA 2021.1.1 x64Appearance(外观)1. 设置IDEA主题与字体勾选 Sync with OS 会同步系统更改勾选Use custom font 选择代码字体,Size选择字号2. Accessibility(无障碍)辅助功能Support screen readers: 为 IntelliJ IDEA 启用屏幕阅读器支持。User contrast scrollbars: 使编辑器滚动条更加可见。Adjust color for red-green vision deficiecy: 调整 UI 颜色,以更好地感知色盲和弱视的颜色。在这种情况下,代码片段(例如通常以红色突出显示的错误或通常为绿色的字符串)将改变颜色(红色将变为橙色,绿色将变为蓝色)。测试运行器中进度条的颜色也将进行调整,以便可以轻松识别。3. UI Options(界面设置)用户界面选项Show tree indent guides(显示树状缩进级别的垂直线)在树状视图中(例如在“项目”工具窗口中)显示标记缩进级别的垂直线。这些行可以帮助您更好地了解项目中组件的层次结构。开启前效果:开启后效果:Smooth scrcolling(平滑移动)作用: 开启后用鼠标中键在代码区上下滑动更流畅(个人感觉),这个因人而异Use smaller indents in trees(在树状菜单中使用更小的缩进)在树状菜单中使用更小的缩进量开启前效果开启后效果Drag-n-Drop with Alt pressed only(仅按下Alt即可进行拖放)避免意外移动文件,编辑器选项卡,工具窗口按钮和其他UI组件。启用后,按住该Alt键才可移动内容。默认情况下,此选项是禁用的,您可以移动所有内容而无需任何额外的键。Enable mnemonics in menu(在菜单上启用快捷键)按下划线执行菜单操作的热键Merge main menu with window title(合并IDEA主菜单到window标题)将IDEA主菜单合并到window栏,光文字的确不好进开启前效果:开启后效果:Enable mnemonics in controls(在控件中启用助记符)带下划线的热键,您可以按这些热键来使用对话框中的控件Always show full path in window header(始终在窗口标题中显示完整路径)始终在窗口标题中显示完整路径,开启前效果:开启后效果:Display icons in menu items(在菜单项中显示图标)在主菜单和上下文菜单中,在项目左侧显示图标。调整IDEA背景图片4. Antialiasing(抗锯齿)IDE: 选择要应用于IDE的哪种抗锯齿模式(包括菜单,工具窗口等)Subpixel(子像素): 用于LCD显示器,并利用彩色LCD上的每个像素都由红色,绿色和蓝色子像素组成Greyscale(灰度): 建议此选项用于非LCD显示器或垂直放置的显示器。它在像素级别处理文本。No antialiasing(无抗锯齿):此选项可用于高分辨率的显示,其中非抗锯齿的字体渲染速度更快,并且外观可能更好。Editor: 选择要应用于编辑器的抗锯齿模式:Subpixel(子像素): 用于LCD显示器,并利用彩色LCD上的每个像素都由红色,绿色和蓝色子像素组成Greyscale(灰度): 建议此选项用于非LCD显示器或垂直放置的显示器。它在像素级别处理文本。No antialiasing(无抗锯齿):此选项可用于高分辨率的显示,其中非抗锯齿的字体渲染速度更快,并且外观可能更好。5. Tool Windows(工具栏设置)Show tool window bars(显示窗口工具栏)在主窗口的边缘周围显示工具窗口栏开启前效果:开启后效果:Show tool window numbers(工具栏显示数字)开启前效果:开启后效果:并且可以按Alt键加数字键快捷打开菜单,比如:git菜单 可以如图所示按 alt+9即可打开Side-by-side layout on the left(左侧并排布局)被附连到顶部和底部边缘中的两列,而不是堆叠在彼此的顶部上显示垂直工具窗口。比如同时打开三个工具窗口:Project,Faverites,编辑区开启前效果:开启后效果:Side-by-side layou on the right(右侧并排布局)同上反过来Widescreen tool window layout(宽屏工具窗口布局)通过限制水平工具窗口的宽度来最大化垂直工具窗口的高度。开启前效果:开启后效果:6. Presetation Mode(演示模式)选择演示模式的字体大小。更改字体大小后,退出并进入演示模式。2. Menus and Toolbars(菜单和工具栏管理)自定义菜单和工具栏,使其仅包含所需的操作,对其进行重新组合并配置其图标。在可用菜单和工具栏列表中,展开要自定义的节点,然后选择所需的项目。单击+按钮以在所选项目下添加动作或分隔符。单击-按钮以删除所选的项目。单击编辑图标按钮以添加或更改所选操作的图标。您只能将PNG或SVG文件用作图标。单击上移按钮或下移按钮向上或向下移动所选项目。单击恢复按钮以将所选操作或所有操作恢复为默认设置。3. System Settings(系统设置)1. Passwords(密码管理)主要是IntelliJ IDEA来为版本控制存储库,数据库和其他受保护的资源保存您的密码In KeePass: 用来指定KeePass密码数据库文件c.kdbx的位置Protect master password using PGP Key:使用pgp来加密数据库的密码Do not save,forget passwords after restart: 不保存任何密码,重启后需要重新配置2. HTTP Proxy(IDEA代理配置)指定IntelliJ IDEA用于访问Internet的代理设置。HTTP代理适用于HTTP和HTTPS。No proxy 无需代理Auto-detect proxy settings:自动检查代理配置Manual proxy configuration:手动指定代理设置。3. Data Sharing(数据共享)选中这个发送使用情况统计信息复选框后,将会允许JetBrains收集你使用IntelliJ IDEA时最常使用的功能和操作的统计信息。4. Data Formats(设置 IDEA日期格式)设置 IDEA日期格式5. Updates(IDEA更新设置)Check IDE updates for- stable releases:已发行的稳定版本- early access program: 早期发行版本Check for plugin updates: 检查插件更新Show whats new in the editor after an ide update:i当IDEA更新后,在编辑器中显示新功能6. Android SDK配置安卓sdk4. File Colors(文件颜色)使用此页面可以设置不同的背景颜色,以区分特定范围的项目文件。1. Enable file color(启用文件颜色)2. Use in editor tabs(在编辑器标签中使用)3. Use in project view(在项目视图中使用)例如,在“在文件中查找”对话框中Ctrl+Shift+F,开启前效果开启后效果:5. Scopes(IDEA操作作用域)定义各种IntelliJ IDEA操作的范围,例如“查找用法”或“代码检查”。6. Notifications(通知事项)可以启用和禁用有关某些事件的通知,发生的事件的信息。更改其显示方式,并有选择地启用其日志记录。7. Quick Lists(快捷菜单)一组自定义的弹出。可以将其视为自定义菜单或工具栏,您可以为其指定快捷方式以进行快速访问。您可以根据需要创建任意数量的快速列表。快速列表中的每个动作均由0到9之间的数字标识。1.单击添加按钮或Alt+Insert按左窗格以创建新的快速列表。2.将此快捷方式分配一个kyeMap ,在“设置/首选项”对话框中Ctrl+Alt+S,选择“键盘映射”。3.在编辑器中,通过关联的快捷方式访问快速列表。4.如果您不记得该快捷方式,则可以按其名称搜索快速列表。按Shift两次,然后输入快速列表的名称。8. Path Variables(环境变量)1.修改IDEA快捷键类型2.给指定菜单或操作设置快捷键1.选中需要设置的菜单2. 右击出现设置菜单依次为: 添加键盘,添加鼠标,添加缩写,取消快捷操作,重置选择添加键盘
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 的功能。
今天介绍一个 MyBatis - Plus 官方发布的神器:mybatis-mate 为 mp 企业级模块,支持分库分表,数据审计、数据敏感词过滤(AC算法),字段加密,字典回写(数据绑定),数据权限,表结构自动生成 SQL 维护等,旨在更敏捷优雅处理数据。主要功能字典绑定字段加密数据脱敏表结构动态维护数据审计记录数据范围(数据权限)数据库分库分表、动态据源、读写分离、数- - 据库健康检查自动切换。2、使用2.1 依赖导入Spring Boot 引入自动依赖注解包<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-mate-starter</artifactId> <version>1.0.8</version> </dependency>注解(实体分包使用)<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-mate-annotation</artifactId> <version>1.0.8</version> </dependency>2.2 字段数据绑定(字典回写)例如 user_sex 类型 sex 字典结果映射到 sexText 属性@FieldDict(type = "user_sex", target = "sexText") private Integer sex; private String sexText;实现 IDataDict 接口提供字典数据源,注入到 Spring 容器即可。@Component public class DataDict implements IDataDict { /** * 从数据库或缓存中获取 */ private Map<String, String> SEX_MAP = new ConcurrentHashMap<String, String>() {{ put("0", "女"); put("1", "男"); }}; @Override public String getNameByCode(FieldDict fieldDict, String code) { System.err.println("字段类型:" + fieldDict.type() + ",编码:" + code); return SEX_MAP.get(code); } }2.3 字段加密属性 @FieldEncrypt 注解即可加密存储,会自动解密查询结果,支持全局配置加密密钥算法,及注解密钥算法,可以实现 IEncryptor 注入自定义算法。@FieldEncrypt(algorithm = Algorithm.PBEWithMD5AndDES)private String password;2.4 字段脱敏属性 @FieldSensitive 注解即可自动按照预设策略对源数据进行脱敏处理,默认 SensitiveType 内置 9 种常用脱敏策略。例如:中文名、银行卡账号、手机号码等 脱敏策略。也可以自定义策略如下:@FieldSensitive(type = "testStrategy") private String username; @FieldSensitive(type = SensitiveType.mobile) private String mobile;自定义脱敏策略 testStrategy 添加到默认策略中注入 Spring 容器即可。@Configuration public class SensitiveStrategyConfig { /** * 注入脱敏策略 */ @Bean public ISensitiveStrategy sensitiveStrategy() { // 自定义 testStrategy 类型脱敏处理 return new SensitiveStrategy().addStrategy("testStrategy", t -> t + "***test***"); } }例如文章敏感词过滤/** * 演示文章敏感词过滤 */ @RestController public class ArticleController { @Autowired private SensitiveWordsMapper sensitiveWordsMapper; // 测试访问下面地址观察请求地址、界面返回数据及控制台( 普通参数 ) // 无敏感词 http://localhost:8080/info?content=tom&see=1&age=18 // 英文敏感词 http://localhost:8080/info?content=my%20content%20is%20tomcat&see=1&age=18 // 汉字敏感词 http://localhost:8080/info?content=%E7%8E%8B%E5%AE%89%E7%9F%B3%E5%94%90%E5%AE%8B%E5%85%AB%E5%A4%A7%E5%AE%B6&see=1 // 多个敏感词 http://localhost:8080/info?content=%E7%8E%8B%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6 // 插入一个字变成非敏感词 http://localhost:8080/info?content=%E7%8E%8B%E7%8C%AB%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6 @GetMapping("/info") public String info(Article article) throws Exception { return ParamsConfig.toJson(article); } // 添加一个敏感词然后再去观察是否生效 http://localhost:8080/add // 观察【猫】这个词被过滤了 http://localhost:8080/info?content=%E7%8E%8B%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6 // 嵌套敏感词处理 http://localhost:8080/info?content=%E7%8E%8B%E7%8C%AB%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6 // 多层嵌套敏感词 http://localhost:8080/info?content=%E7%8E%8B%E7%8E%8B%E7%8C%AB%E5%AE%89%E7%9F%B3%E5%AE%89%E7%9F%B3%E6%9C%89%E4%B8%80%E5%8F%AA%E7%8C%ABtomcat%E6%B1%A4%E5%A7%86%E5%87%AF%E7%89%B9&see=1&size=6 @GetMapping("/add") public String add() throws Exception { Long id = 3L; if (null == sensitiveWordsMapper.selectById(id)) { System.err.println("插入一个敏感词:" + sensitiveWordsMapper.insert(new SensitiveWords(id, "猫"))); // 插入一个敏感词,刷新算法引擎敏感词 SensitiveWordsProcessor.reloadSensitiveWords(); } return "ok"; } // 测试访问下面地址观察控制台( 请求json参数 ) // idea 执行 resources 目录 TestJson.http 文件测试 @PostMapping("/json") public String json(@RequestBody Article article) throws Exception { return ParamsConfig.toJson(article); } }2.5 DDL 数据结构自动维护解决升级表结构初始化,版本发布更新 SQL 维护问题,目前支持 MySql、PostgreSQL。@Component public class PostgresDdl implements IDdl { /** * 执行 SQL 脚本方式 */ @Override public List<String> getSqlFiles() { return Arrays.asList( // 内置包方式 "db/tag-schema.sql", // 文件绝对路径方式 "D:\\db\\tag-data.sql" ); } }不仅仅可以固定执行,也可以动态执行!!ddlScript.run(new StringReader("DELETE FROM user;\n" + "INSERT INTO user (id, username, password, sex, email) VALUES\n" + "(20, 'Duo', '123456', 0, 'Duo@baomidou.com');"));它还支持多数据源执行!!!@Component public class MysqlDdl implements IDdl { @Override public void sharding(Consumer<IDdl> consumer) { // 多数据源指定,主库初始化从库自动同步 String group = "mysql"; ShardingGroupProperty sgp = ShardingKey.getDbGroupProperty(group); if (null != sgp) { // 主库 sgp.getMasterKeys().forEach(key -> { ShardingKey.change(group + key); consumer.accept(this); }); // 从库 sgp.getSlaveKeys().forEach(key -> { ShardingKey.change(group + key); consumer.accept(this); }); } } /** * 执行 SQL 脚本方式 */ @Override public List<String> getSqlFiles() { return Arrays.asList("db/user-mysql.sql"); } }2.6 动态多数据源主从自由切换@Sharding 注解使数据源不限制随意使用切换,你可以在 mapper 层添加注解,按需求指哪打哪!!@Mapper @Sharding("mysql") public interface UserMapper extends BaseMapper<User> { @Sharding("postgres") Long selectByUsername(String username); }你也可以自定义策略统一调兵遣将@Component public class MyShardingStrategy extends RandomShardingStrategy { /** * 决定切换数据源 key {@link ShardingDatasource} * * @param group 动态数据库组 * @param invocation {@link Invocation} * @param sqlCommandType {@link SqlCommandType} */ @Override public void determineDatasourceKey(String group, Invocation invocation, SqlCommandType sqlCommandType) { // 数据源组 group 自定义选择即可, keys 为数据源组内主从多节点,可随机选择或者自己控制 this.changeDatabaseKey(group, sqlCommandType, keys -> chooseKey(keys, invocation)); } }可以开启主从策略,当然也是可以开启健康检查!具体配置:mybatis-mate: sharding: health: true # 健康检测 primary: mysql # 默认选择数据源 datasource: mysql: # 数据库组 - key: node1 ... - key: node2 cluster: slave # 从库读写分离时候负责 sql 查询操作,主库 master 默认可以不写 ... postgres: - key: node1 # 数据节点 ...2.7 分布式事务日志打印部分配置如下:/** * <p> * 性能分析拦截器,用于输出每条 SQL 语句及其执行时间 * </p> */ @Slf4j @Component @Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})}) public class PerformanceInterceptor implements Interceptor { /** * SQL 执行最大时长,超过自动停止运行,有助于发现问题。 */ private long maxTime = 0; /** * SQL 是否格式化 */ private boolean format = false; /** * 是否写入日志文件<br> * true 写入日志文件,不阻断程序执行!<br> * 超过设定的最大执行时长异常提示! */ private boolean writeInLog = false; @Override public Object intercept(Invocation invocation) throws Throwable { Statement statement; Object firstArg = invocation.getArgs()[0]; if (Proxy.isProxyClass(firstArg.getClass())) { statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement"); } else { statement = (Statement) firstArg; } MetaObject stmtMetaObj = SystemMetaObject.forObject(statement); try { statement = (Statement) stmtMetaObj.getValue("stmt.statement"); } catch (Exception e) { // do nothing } if (stmtMetaObj.hasGetter("delegate")) {//Hikari try { statement = (Statement) stmtMetaObj.getValue("delegate"); } catch (Exception e) { } } String originalSql = null; if (originalSql == null) { originalSql = statement.toString(); } originalSql = originalSql.replaceAll("[\\s]+", " "); int index = indexOfSqlStart(originalSql); if (index > 0) { originalSql = originalSql.substring(index); } // 计算执行 SQL 耗时 long start = SystemClock.now(); Object result = invocation.proceed(); long timing = SystemClock.now() - start; // 格式化 SQL 打印执行结果 Object target = PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(target); MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); StringBuilder formatSql = new StringBuilder(); formatSql.append(" Time:").append(timing); formatSql.append(" ms - ID:").append(ms.getId()); formatSql.append("\n Execute SQL:").append(sqlFormat(originalSql, format)).append("\n"); if (this.isWriteInLog()) { if (this.getMaxTime() >= 1 && timing > this.getMaxTime()) { log.error(formatSql.toString()); } else { log.debug(formatSql.toString()); } } else { System.err.println(formatSql); if (this.getMaxTime() >= 1 && timing > this.getMaxTime()) { throw new RuntimeException(" The SQL execution time is too large, please optimize ! "); } } return result; } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties prop) { String maxTime = prop.getProperty("maxTime"); String format = prop.getProperty("format"); if (StringUtils.isNotEmpty(maxTime)) { this.maxTime = Long.parseLong(maxTime); } if (StringUtils.isNotEmpty(format)) { this.format = Boolean.valueOf(format); } } public long getMaxTime() { return maxTime; } public PerformanceInterceptor setMaxTime(long maxTime) { this.maxTime = maxTime; return this; } public boolean isFormat() { return format; } public PerformanceInterceptor setFormat(boolean format) { this.format = format; return this; } public boolean isWriteInLog() { return writeInLog; } public PerformanceInterceptor setWriteInLog(boolean writeInLog) { this.writeInLog = writeInLog; return this; } public Method getMethodRegular(Class<?> clazz, String methodName) { if (Object.class.equals(clazz)) { return null; } for (Method method : clazz.getDeclaredMethods()) { if (method.getName().equals(methodName)) { return method; } } return getMethodRegular(clazz.getSuperclass(), methodName); } /** * 获取sql语句开头部分 * * @param sql * @return */ private int indexOfSqlStart(String sql) { String upperCaseSql = sql.toUpperCase(); Set<Integer> set = new HashSet<>(); set.add(upperCaseSql.indexOf("SELECT ")); set.add(upperCaseSql.indexOf("UPDATE ")); set.add(upperCaseSql.indexOf("INSERT ")); set.add(upperCaseSql.indexOf("DELETE ")); set.remove(-1); if (CollectionUtils.isEmpty(set)) { return -1; } List<Integer> list = new ArrayList<>(set); Collections.sort(list, Integer::compareTo); return list.get(0); } private final static SqlFormatter sqlFormatter = new SqlFormatter(); /** * 格式sql * * @param boundSql * @param format * @return */ public static String sqlFormat(String boundSql, boolean format) { if (format) { try { return sqlFormatter.format(boundSql); } catch (Exception ignored) { } } return boundSql; } }使用:@RestController @AllArgsConstructor public class TestController { private BuyService buyService; // 数据库 test 表 t_order 在事务一致情况无法插入数据,能够插入说明多数据源事务无效 // 测试访问 http://localhost:8080/test // 制造事务回滚 http://localhost:8080/test?error=true 也可通过修改表结构制造错误 // 注释 ShardingConfig 注入 dataSourceProvider 可测试事务无效情况 @GetMapping("/test") public String test(Boolean error) { return buyService.buy(null != error && error); } }2.8 数据权限mapper 层添加注解:// 测试 test 类型数据权限范围,混合分页模式 @DataScope(type = "test", value = { // 关联表 user 别名 u 指定部门字段权限 @DataColumn(alias = "u", name = "department_id"), // 关联表 user 别名 u 指定手机号字段(自己判断处理) @DataColumn(alias = "u", name = "mobile") }) @Select("select u.* from user u") List<User> selectTestList(IPage<User> page, Long id, @Param("name") String username);模拟业务处理逻辑:@Bean public IDataScopeProvider dataScopeProvider() { return new AbstractDataScopeProvider() { @Override protected void setWhere(PlainSelect plainSelect, Object[] args, DataScopeProperty dataScopeProperty) { // args 中包含 mapper 方法的请求参数,需要使用可以自行获取 /* // 测试数据权限,最终执行 SQL 语句 SELECT u.* FROM user u WHERE (u.department_id IN ('1', '2', '3', '5')) AND u.mobile LIKE '%1533%' */ if ("test".equals(dataScopeProperty.getType())) { // 业务 test 类型 List<DataColumnProperty> dataColumns = dataScopeProperty.getColumns(); for (DataColumnProperty dataColumn : dataColumns) { if ("department_id".equals(dataColumn.getName())) { // 追加部门字段 IN 条件,也可以是 SQL 语句 Set<String> deptIds = new HashSet<>(); deptIds.add("1"); deptIds.add("2"); deptIds.add("3"); deptIds.add("5"); ItemsList itemsList = new ExpressionList(deptIds.stream().map(StringValue::new).collect(Collectors.toList())); InExpression inExpression = new InExpression(new Column(dataColumn.getAliasDotName()), itemsList); if (null == plainSelect.getWhere()) { // 不存在 where 条件 plainSelect.setWhere(new Parenthesis(inExpression)); } else { // 存在 where 条件 and 处理 plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), inExpression)); } } else if ("mobile".equals(dataColumn.getName())) { // 支持一个自定义条件 LikeExpression likeExpression = new LikeExpression(); likeExpression.setLeftExpression(new Column(dataColumn.getAliasDotName())); likeExpression.setRightExpression(new StringValue("%1533%")); plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), likeExpression)); } } } } }; }最终执行 SQL 输出:SELECT u.* FROM user u WHERE (u.department_id IN ('1', '2', '3', '5')) AND u.mobile LIKE '%1533%' LIMIT 1, 10
Spring Cloud Alibaba为分布式应用开发提供了一站式解决方案。它包含开发分布式应用程序所需的所有组件,可以轻松地使用Spring Cloud开发应用程序。最近抽空整理了一份Spring Cloud Alibab学习笔记免费分享给大家,目录如下模块一 微服务架构设计本模块主要介绍了什么是微服务体系结构,以及微服务体系结构设计中的一些常见问题。模块二 Nacos 服务治理Nacos注册中心是整个微服务体系结构的核心。本文将详细介绍Nacos的安装、使用和集群构建过程,并以图文的形式介绍Nacos服务发现的基本原理。模块三 系统保护Sentinel是Alibaba提供的服务保护中间件。使用sentinel可以有效地防止分布式体系结构的系统崩溃。在此阶段,我们将解释Sentinel在限流、熔断、代码控制等方面的最佳实践。模块四 高级特性在这一阶段,我们将介绍SpringCloudAlibaba提供的许多高级功能。例如:配置中心、链路跟踪、性能监控、分布式事务、消息队列等。我们将从应用介绍到原理分析,逐一讲解这些技术。模块五 微服务通信当服务需要相互通信时,springcloudAlibaba支持RPC和restful解决方案。相应的产品是Dubbo和openfeign。在这个阶段,我将给出这些组件的最佳实践和原理分析。模块六 微服务架构最佳实践这阶段,我将拿出自己的私藏干货,为大家讲解微服务架构的综合应用和项目实践。在这里我们将接触到Seata分布式事务架构、多级缓存设计、老项目升级策略!
setnx其实目前通常所说的setnx命令,并非单指redis的setnx key value这条命令。一般代指redis中对set命令加上nx参数进行使用, set这个命令,目前已经支持这么多参数可选:SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]当然了,就不在文章中默写Api了,基础参数还有不清晰的,可以蹦到官网。上图是笔者画的setnx大致原理,主要依托了它的key不存在才能set成功的特性,进程A拿到锁,在没有删除锁的Key时,进程B自然获取锁就失败了。那么为什么要使用PX 30000去设置一个超时时间?是怕进程A不讲道理啊,锁没等释放呢,万一崩了,直接原地把锁带走了,导致系统中谁也拿不到锁。就算这样,还是不能保证万无一失。如果进程A又不讲道理,操作锁内资源超过笔者设置的超时时间,那么就会导致其他进程拿到锁,等进程A回来了,回手就是把其他进程的锁删了,如图:还是刚才那张图,将T5时刻改成了锁超时,被redis释放。进程B在T6开开心心拿到锁不到一会,进程A操作完成,回手一个del,就把锁释放了。当进程B操作完成,去释放锁的时候(图中T8时刻):找不到锁其实还算好的,万一T7时刻有个进程C过来加锁成功,那么进程B就把进程C的锁释放了。以此类推,进程C可能释放进程D的锁,进程D....(禁止套娃),具体什么后果就不得而知了。所以在用setnx的时候,key虽然是主要作用,但是value也不能闲着,可以设置一个唯一的客户端ID,或者用UUID这种随机数。当解锁的时候,先获取value判断是否是当前进程加的锁,再去删除。伪代码:String uuid = xxxx; // 伪代码,具体实现看项目中用的连接工具 // 有的提供的方法名为set 有的叫setIfAbsent set Test uuid NX PX 3000 try{ // biz handle.... } finally { // unlock if(uuid.equals(redisTool.get('Test')){ redisTool.del('Test'); } }这回看起来是不是稳了。相反,这回的问题更明显了,在finally代码块中,get和del并非原子操作,还是有进程安全问题。那么删除锁的正确姿势之一,就是可以使用lua脚本,通过redis的eval/evalsha命令来运行:-- lua删除锁: -- KEYS和ARGV分别是以集合方式传入的参数,对应上文的Test和uuid。 -- 如果对应的value等于传入的uuid。 if redis.call('get', KEYS[1]) == ARGV[1] then -- 执行删除操作 return redis.call('del', KEYS[1]) else -- 不成功,返回0 return 0 end通过lua脚本能保证原子性的原因说的通俗一点:就算你在lua里写出花,执行也是一个命令(eval/evalsha)去执行的,一条命令没执行完,其他客户端是看不到的。那么既然这么麻烦,有没有比较好的工具呢?就要说到redisson了。介绍redisson之前,笔者简单解释一下为什么现在的setnx默认是指set命令带上nx参数,而不是直接说是setnx这个命令。因为redis版本在2.6.12之前,set是不支持nx参数的,如果想要完成一个锁,那么需要两条命令:1. setnx Test uuid 2. expire Test 30即放入Key和设置有效期,是分开的两步,理论上会出现1刚执行完,程序挂掉,无法保证原子性。但是早在2013年,也就是7年前,Redis就发布了2.6.12版本,并且官网(set命令页),也早早就说明了“SETNX, SETEX, PSETEX可能在未来的版本中,会弃用并永久删除”。笔者曾阅读过一位大佬的文章,其中就有一句指导入门者的面试小套路,具体文字忘记了,大概意思如下:说到redis锁的时候,可以先从setnx讲起,最后慢慢引出set命令的可以加参数,可以体现出自己的知识面。如果有缘你也阅读过这篇文章,并且学到了这个套路,作为本文的笔者我要加一句提醒:请注意你的工作年限!首先回答官网表明即将废弃的命令,再引出set命令七年前的“新特性”,如果是刚毕业不久的人这么说,面试官会以为自己穿越了。你套路面试官,面试官也会套路你。 -- vt・沃兹基硕德RedissonRedisson是java的redis客户端之一,提供了一些api方便操作redis。但是redisson这个客户端可有点厉害,笔者在官网截了仅仅是一部分的图:这个特性列表可以说是太多了,是不是还看到了一些JUC包下面的类名,redisson帮我们搞了分布式的版本,比如AtomicLong,直接用RedissonAtomicLong就行了,连类名都不用去新记,很人性化了。锁只是它的冰山一角,并且从它的wiki页面看到,对主从,哨兵,集群等模式都支持,当然了,单节点模式肯定是支持的。本文还是以锁为主,其他的不过多介绍。Redisson普通的锁实现源码主要是RedissonLock这个类,还没有看过它源码的盆友,不妨去瞧一瞧。源码中加锁/释放锁操作都是用lua脚本完成的,封装的非常完善,开箱即用。这里有个小细节,加锁使用setnx就能实现,也采用lua脚本是不是多此一举?笔者也非常严谨的思考了一下:这么厉害的东西哪能写废代码?其实笔者仔细看了一下,加锁解锁的lua脚本考虑的非常全面,其中就包括锁的重入性,这点可以说是考虑非常周全,我也随手写了代码测试一下:的确用起来像jdk的ReentrantLock一样丝滑,那么redisson实现的已经这么完善,redLock又是什么?RedLockredLock的中文是直译过来的,就叫红锁。红锁并非是一个工具,而是redis官方提出的一种分布式锁的算法。就在刚刚介绍完的redisson中,就实现了redLock版本的锁。也就是说除了getLock方法,还有getRedLock方法。笔者大概画了一下对红锁的理解:如果你不熟悉redis高可用部署,那么没关系。redLock算法虽然是需要多个实例,但是这些实例都是独自部署的,没有主从关系。RedLock作者指出,之所以要用独立的,是避免了redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。有些人是不是觉得大佬们都是杠精啊,天天就想着极端情况。其实高可用嘛,拼的就是99.999...% 中小数点后面的位数。回到上面那张简陋的图片,红锁算法认为,只要(N/2) + 1个节点加锁成功,那么就认为获取了锁, 解锁时将所有实例解锁。流程为:顺序向五个节点请求加锁根据一定的超时时间来推断是不是跳过该节点三个节点加锁成功并且花费时间小于锁的有效期认定加锁成功也就是说,假设锁30秒过期,三个节点加锁花了31秒,自然是加锁失败了。这只是举个例子,实际上并不应该等每个节点那么长时间,就像官网所说的那样,假设有效期是10秒,那么单个redis实例操作超时时间,应该在5到50毫秒(注意时间单位)还是假设我们设置有效期是30秒,图中超时了两个redis节点。那么加锁成功的节点总共花费了3秒,所以锁的实际有效期是小于27秒的。即扣除加锁成功三个实例的3秒,还要扣除等待超时redis实例的总共时间。看到这,你有可能对这个算法有一些疑问,那么你不是一个人。回头看看Redis官网关于红锁的描述就在这篇描述页面的最下面,你能看到著名的关于红锁的神仙打架事件。即Martin Kleppmann和antirez的redLock辩论. 一个是很有资历的分布式架构师,一个是redis之父。官方挂人,最为致命。开个玩笑,要是质疑能被官方挂到官网,说明肯定是有价值的。所以说如果项目里要使用红锁,除了红锁的介绍,不妨要多看两篇文章,即:Martin Kleppmann的质疑贴antirez的反击贴
使用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 个框架对同一个功能点的实现, 各位看官肯定会有自己的判断,笔者这里也总结了一份比较。
今天,推荐一个在线IM即时通讯系统项目。我第一次使用就有点上头,爱不释手,必须要推荐给大家。上次是谁要的在线IM即时通讯系统项目啊,我帮你找到了。这是我目前见过的在线IM即时通讯系统项目。功能完整,代码结构清晰。值得推荐。介绍本项目系统是用Java语言,基于t-io开发的轻量、高性能、单机支持几十万至百万在线用户IM,主要目标降低即时通讯门槛,快速打造低成本接入在线IM系统,通过极简洁的消息格式就可以实现多端不同协议间的消息发送如内置(Http、Websocket、Tcp自定义IM协议)等,并提供通过http协议的api接口进行消息发送无需关心接收端属于什么协议,一个消息格式搞定一切!搜索公众号GitHub猿回复“赚钱”,送你一份惊喜礼包。功能特点 1、高性能(单机可支持几十万至百万人同时在线) 2、轻量、可扩展性极强 3、支持集群多机部署 4、支持SSL/TLS加密传输 5、消息格式极其简洁(JSON) 6、一端口支持可插拔多种协议(Socket自定义IM协议、Websocket、Http),各协议可分别独立部署。 7、内置消息持久化(离线、历史、漫游),保证消息可靠性,高性能存储 8、各种丰富的API接口。 9、零成本部署,一键启动。功能演示
近期做了一个前后端合并的spring boot项目,但是要求达成exe文件,提供给不懂电脑的小白安装使用,就去研究了半天,踩了很多坑,写这篇文章,是想看到这篇文章的人,按照我的步骤走,能少踩坑。准备准备工作:一个jar包,没有bug能正常启动的jar包exe4j,一个将jar转换成exe的工具,链接:https://pan.baidu.com/s/1J30uUMJcYnqWCJSr6gkM5w,提取码:6esr,注册码:L-g782dn2d-1f1yqxx1rv1sqdinno setup,一个将依赖和exe一起打成一个安装程序的工具,链接:https://pan.baidu.com/s/1DgFo1ceM_8Bqx_b-veibbQ,提取码:g9jd开始以我为例子,我将jar包放在了桌面打开安装好的exe4j直接下一步进入界面,选择JAVA转EXE然后点下一步,输入名称和输出路径继续点击下一步,选择启动模式下方有个选项,需要设置打包后的程序兼容32和64位系统进来后勾选上然后一直下一步,一直出现如下界面,开始选择jar包以及配置在VM参数配置的地方加上:-Dfile.encoding=utf-8点击下一步,配置JRE下拉框点击后进入如下界面照着这个样子写的目的是,最终会把本地jre目录和exe一起打包,让exe文件自己去根据路径去查找一起打包的jre,可不用再安装jdk接着下一步,选择Client VM然后一直下一步,最终出现如下界面这个时候你会发现桌面多了一个demo.exe文件,这个时候先别着急点开,接下来就是将jre和exe文件再打个包合并,达到在没有jdk电脑环境下也能运行打开inno setup,左上角File - New直接点下一步,填写配置,应用名称,版本等,随意然后点击下一步,这个地方默认就行,直接下一步接着选择生成好的exe文件然后下一步,进入这个界面保持默认,直接下一步依旧下一步,不用管继续下一步,这里是选择语言然后就是选择输出路径和填写安装程序的名字了然后下一步,直接点Next,然后结束配置到最后一步了,脚本文件,到这里会弹出问你是否马上编译,选择否,先把脚本写好再自己编译然后到了最后一步了,把本地的JRE写进脚本Source: "自己本地JRE路径\*"; DestDir: "{app}\{#MyJreName}"; Flags: ignoreversion recursesubdirs createallsubdirs然后直接编译就好了,会提示保存当前脚本,随便起个名字,下个还可以继续用然后等待绿色滚动条结束当绿色滚动条结束后,桌面会多了一个setup.exe文件也同时会跳出一个安装的,因为程序帮你自动启动生成的安装程序了,安装就可以了,安装的时候记得勾选创建快捷方式这个就是最后的程序了,双击运行就可以看到结果了,把setup.exe文件给别人安装,就都可以看到自己的程序了!
需求nginx 可视化管理,例如配置管理性能监控日志监控其他配置方案目前已实现前两条:配置管理,和性能监控日志分析监控这块还需要另找方案实现!目前方案直接套用github大神开发的nginx-guigithub地址:https://github.com/onlyGuo/nginx-gui这个东西真的要吹一波,太好用了而且源码公开,解决了我这种java出身的linux菜鸟的一大难题!界面截图:说明先说明下,我也是刚才现学的,只是写下折腾的过程和碰到的问题详细的用法之类的还是建议访问作者的github和作者的博客查看作者github:https://github.com/onlyGuo/nginx-gui作者博客:http://bl.321aiyi.com/2019/03/18/nginx-gui/折腾一 下载和配置首先到作者github说明页面,下载对应系统版本的安装包需要注意的是linux版本有一段描述不可忽视配置步骤如下:1 下载并解压 Nginx-GUI-For-Linux-1.0.zip略2 修改配置文件文件位置:conf/conf.properties# nginx 安装路径 nginx.path = /usr/local/Cellar/nginx/1.15.12 # nginx 配置文件全路径 nginx.config = /Users/gsk/dev/apps/nginx-1.15.12/conf/nginx.conf # account.admin = admin3 重命名(此步骤仅linux版本需要)根据原作者的描述针对linux 64位版本需要将 lib/bin/下的 java_vms 文件重命名为 java_vms_nginx_gui二 在服务器上运行前面的步骤都完成以后,直接打包发布到服务器# 赋权 sudo chmod -R 777 nginx-gui/ # 后台启动 nohup bash /root/web/nginx-gui/startup.sh > logs/nginx-gui.out &访问默认端口 8889 默认账号密码都是admin遗留问题目前实现的有性能监控可视化配置未能实现的是日志分析访问统计
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)需求,更容易地在不同的项目中复用 提供完善的压力测试方案 提供完善的灰度发布方案 提供完善的微服务部署方案功能截图
前言这套Base Admin是一套简单通用的后台管理系统,主要功能有:权限管理、菜单管理、用户管理,系统设置、实时日志,实时监控,API加密,以及登录用户修改密码、配置个性菜单等。技术栈前端:layuijava后端:SpringBoot + Thymeleaf + WebSocket + Spring Security + SpringData-Jpa + MySql相关后台系统:1、这或许是最美的Vue+Element开源后台管理UI2、带工作流的SpringBoot后台管理项目,一个企业级快速开发解决方案(附源码)工程结构说明java部分、html、js、css部分都是大目录下面按单表一个子目录存放运行预览效果先睹为快,具体介绍在下方,按功能点进行详情介绍功能演示登录(为了方便演示,密码输入框的类型改成text)配置文件分支选择,dev环境无需输入验证码。同时支持多种登录限制。允许/禁止账号多人在线。软删除限制登录IP地址账号过期更多登录限制,还可以继续扩展。系统设置一下简单的系统属性设置,想支持更多的配置可自行扩展(比如这里的:用户管理初始、重置密码)。系统设置新增部分功能,详见文末“补充更新”菜单管理菜单管理是一棵layui的Tree权限管理增删改查动态权限加载权限的加载并不是写死在代码,而是动态从数据库读取,每次调用save方法时更新权限集合。1、妲己是ROLE_USER权限,权限内容为空,无权访问/sys/下面的路径(http://localhost:8888/sys/sysUser/get/1)2、使用sa超级管理员进行权限管理编辑,给ROLE_USER的权限内容添加 /sys/**,妲己立即有权限访问(http://localhost:8888/sys/sysUser/get/1)用户管理主要包括用户信息、登录限制的维护,菜单、权限的分配等。修改用户权限是下一次登录生效。修改用户菜单是刷新系统即可生效。用户管理新增“当前在线用户”管理,详见文末“补充更新”登录用户信息基本信息登录用户只能修改部分信息,例如名称、修改密码修改密码密码使用的是MD5加密并转换为16进制字符串存储,用户除了能主动修改密码外,还能叫管理员重置密码。个性菜单用户可以自行配置自己的个性化快捷菜单。实时日志使用websocket,实时将日志输出到web页面,1秒刷新一次。注意:这里的日志配置只配置了dev环境,prod环境尚未为空,发布生产环境前记得先配置,否则生成的日志文件将不会输入日志内容!实时监控实时监控的是系统硬件环境、以及jvm运行时内存,注:因本人暂无Linux环境,所以只测试了windows环境,有问题请及时反馈,谢谢!使用websocket,实时将数据输出到web页面,1秒刷新一次。API加密请求参数加密响应数据加密1、系统设置新增API加密开关,可一键关闭、开启API加密;开启API加密关闭API加密关键点讲解1、定制url访问权限,动态权限读取,需要自定义配置认证数据源、认证管理器、拦截器,详情步骤请参考:https://www.jianshu.com/p/0a06496e75ea;2、API加密中,由于登录校验是Spring Security做的,因此我们要在UsernamePasswordAuthenticationFilter获取账号、密码之前完成解密操作,正好我们的校验验证码操作就是在它之前,同时要做响应数据的加密操作,所以登录部分的API加密光按照我们之前的博客来还是不够的,需要在CaptchaFilterConfig进行解密操作,解密后new一个自定义RequestWrapper设置Parameter,并将这个新对象传到doFilter交由下一步处理。3、还是API加密问题,我们是在程序启动的时候生成后端RSA秘钥对,正常来说我们在访问登录页面进行登录的时候前端获取一下就可以了,但在开发环境中,我们通常开启热部署功能,改完代码程序可能会自动重启,但登录用户信息仍然保持在本地线程,系统依旧处于登录状态没有跳转到登录页面,导致后端公钥已经改变,但前端依旧用的是旧的后端公钥,所有导致加解密失败;解决:在访问index首页时也获取一下后端公钥,这样在开发的时候idea热部署后刷新页面就可以了(已提交最新代码,解决热部署后刷新页面还是API加解密失败问题;现在热部署后刷新页面即可)4、好多人都不知道,项目有工具类CodeDOM.java可以生成一套单表的完整增删改查后台代码。配置好数据库,指定代码生成父位置。运行main函数即可一键生成一套单表增删改查后台代码。后记这个只是一个比较简单通用的后台系统,如果加入工作流,就可以升级成基础平台,为简化业务开发,将部分通用系统功能整理成独立项目,具体业务功能通过iframe嵌入。1、新增百度富文本的使用。对应字段类型,mysql要改成longtext2、新增“”记住我“”功能,也就是rememberMe,原理以及源码探究请看这位大佬的博客:https://blog.csdn.net/qq_37142346/article/details/80114609需要新增一张表,SQL文件我也以及更新了。4、系统设置新增系统颜色,头部、左侧菜单的颜色可按心情切换(SQL文件已同步更新)5、用户管理模块新增“当前在线用户”管理,可实时查看当前在线用户,以及对当前在线用户进行强制下线操作。
前言在平时的业务开发过程中,后端服务与服务之间的调用往往通过fegin或者RestTemplate两种方式。但是我们在调用服务的时候往往只需要写服务名就可以做到路由到具体的服务,这其中的原理相比大家都知道是SpringCloud的ribbon组件帮我们做了负载均衡的功能。灰度发布的核心就是路由,如果我们能够重写ribbon默认的负载均衡算法是不是就意味着我们能够控制服务的转发呢?是的!调用链分析外部调用请求==>zuul==>服务zuul在转发请求的时候,也会根据 Ribbon从服务实例列表中选择一个对应的服务,然后选择转发.内部调用请求==>zuul==>服务Resttemplate调用==>服务请求==>zuul==>服务Fegin调用==>服务无论是通过 Resttemplate还是 Fegin的方式进行服务间的调用,他们都会从 Ribbon选择一个服务实例返回. 上面几种调用方式应该涵盖了我们平时调用中的场景,无论是通过哪种方式调用(排除直接ip:port调用),最后都会通过Ribbon,然后返回服务实例.预备知识eureka元数据Eureka的元数据有两种,分别为标准元数据和自定义元数据。标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。自定义元数据:自定义元数据可以使用eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不会改变客户端的行为,除非客户端知道该元数据的含义eureka RestFul接口请求名称请求方式HTTP地址请求描述注册新服务POST/eureka/apps/{appID}传递JSON或者XML格式参数内容,HTTP code为204时表示成功取消注册服务DELETE/eureka/apps/{appID}/{instanceID}HTTP code为200时表示成功发送服务心跳PUT/eureka/apps/{appID}/{instanceID}HTTP code为200时表示成功查询所有服务GET/eureka/appsHTTP code为200时表示成功,返回XML/JSON数据内容查询指定appID的服务列表GET/eureka/apps/{appID}HTTP code为200时表示成功,返回XML/JSON数据内容查询指定appID&instanceIDGET/eureka/apps/{appID}/{instanceID}获取指定appID以及InstanceId的服务信息,HTTP code为200时表示成功,返回XML/JSON数据内容查询指定instanceID服务列表GET/eureka/apps/instances/{instanceID}获取指定instanceID的服务列表,HTTP code为200时表示成功,返回XML/JSON数据内容变更服务状态PUT/eureka/apps/{appID}/{instanceID}/status?value=DOWN服务上线、服务下线等状态变动,HTTP code为200时表示成功变更元数据PUT/eureka/apps/{appID}/{instanceID}/metadata?key=valueHTTP code为200时表示成功更改自定义元数据配置文件方式:eureka.instance.metadata-map.version = v1接口请求:PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value实现流程用户请求首先到达Nginx然后转发到网关zuul,此时zuul拦截器会根据用户携带请求token解析出对应的userId网关从Apollo配置中心拉取灰度用户列表,然后根据灰度用户策略判断该用户是否是灰度用户。如是,则给该请求添加请求头及线程变量添加信息version=xxx;若不是,则不做任何处理放行在zuul拦截器执行完毕后,zuul在进行转发请求时会通过负载均衡器Ribbon。负载均衡Ribbon被重写。当请求到达时候,Ribbon会取出zuul存入线程变量值version。于此同时,Ribbon还会取出所有缓存的服务列表(定期从eureka刷新获取最新列表)及其该服务的metadata-map信息。然后取出服务metadata-map的version信息与线程变量version进行判断对比,若值一直则选择该服务作为返回。若所有服务列表的version信息与之不匹配,则返回null,此时Ribbon选取不到对应的服务则会报错!当服务为非灰度服务,即没有version信息时,此时Ribbon会收集所有非灰度服务列表,然后利用Ribbon默认的规则从这些非灰度服务列表中返回一个服务。zuul通过Ribbon将请求转发到consumer服务后,可能还会通过fegin或resttemplate调用其他服务,如provider服务。但是无论是通过fegin还是resttemplate,他们最后在选取服务转发的时候都会通过Ribbon。那么在通过fegin或resttemplate调用另外一个服务的时候需要设置一个拦截器,将请求头version=xxx给带上,然后存入线程变量。在经过fegin或resttemplate 的拦截器后最后会到Ribbon,Ribbon会从线程变量里面取出version信息。然后重复步骤(4)和(5)设计思路首先,我们通过更改服务在eureka的元数据标识该服务为灰度服务,笔者这边用的元数据字段为version。 1.首先更改服务元数据信息,标记其灰度版本。通过eureka RestFul接口或者配置文件添加如下信息eureka.instance.metadata-map.version=v12.自定义zuul拦截器GrayFilter。此处笔者获取的请求头为token,然后将根据JWT的思想获取userId,然后获取灰度用户列表及其灰度版本信息,判断该用户是否为灰度用户。若为灰度用户,则将灰度版本信息version存放在线程变量里面。此处不能用Threadlocal存储线程变量,因为SpringCloud用hystrix做线程池隔离,而线程池是无法获取到ThreadLocal中的信息的! 所以这个时候我们可以参考Sleuth做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal方案。此处使用HystrixRequestVariableDefault实现跨线程池传递线程变量。3.zuul拦截器处理完毕后,会经过ribbon组件从服务实例列表中获取一个实例选择转发。Ribbon默认的Rule为ZoneAvoidanceRule`。而此处我们继承该类,重写了其父类选择服务实例的方法。以下为Ribbon源码:public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule { // 略.... @Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer(); Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); if (server.isPresent()) { return server.get(); } else { return null; } } }以下为自定义实现的伪代码:public class GrayMetadataRule extends ZoneAvoidanceRule { // 略.... @Override public Server choose(Object key) { //1.从线程变量获取version信息 String version = HystrixRequestVariableDefault.get(); //2.获取服务实例列表 List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key); //3.循环serverList,选择version匹配的服务并返回 for (Server server : serverList) { Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata(); String metaVersion = metadata.get("version); if (!StringUtils.isEmpty(metaVersion)) { if (metaVersion.equals(hystrixVer)) { return server; } } } } }4.此时,只是已经完成了 请求==》zuul==》zuul拦截器==》自定义ribbon负载均衡算法==》灰度服务这个流程,并没有涉及到 服务==》服务的调用。服务到服务的调用无论是通过resttemplate还是fegin,最后也会走ribbon的负载均衡算法,即服务==》Ribbon 自定义Rule==》服务。因为此时自定义的GrayMetadataRule并不能从线程变量中取到version,因为已经到了另外一个服务里面了。5.此时依然可以参考Sleuth的源码org.springframework.cloud.sleuth.Span,这里不做赘述只是大致讲一下该类的实现思想。就是在请求里面添加请求头,以便下个服务能够从请求头中获取信息。此处,我们可以通过在 步骤2中,让zuul添加添加线程变量的时候也在请求头中添加信息。然后,再自定义HandlerInterceptorAdapter拦截器,使之在到达服务之前将请求头中的信息存入到线程变量HystrixRequestVariableDefault中。然后服务再调用另外一个服务之前,设置resttemplate和fegin的拦截器,添加头信息。resttemplate拦截器public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request); String hystrixVer = CoreHeaderInterceptor.version.get(); requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer); return execution.execute(requestWrapper, body); } }fegin拦截器public class CoreFeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String hystrixVer = CoreHeaderInterceptor.version.get(); logger.debug("====>fegin version:{} ",hystrixVer); template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer); } }6.到这里基本上整个请求流程就比较完整了,但是我们怎么让Ribbon使用自定义的Rule?这里其实非常简单,只需要在服务的配置文件中配置一下代码即可.yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定义的负载均衡策略类但是这样配置需要指定服务名,意味着需要在每个服务的配置文件中这么配置一次,所以需要对此做一下扩展.打开源码org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration类,该类是Ribbon的默认配置类.可以清楚的发现该类注入了一个PropertiesFactory类型的属性,可以看到PropertiesFactory类的构造方法public PropertiesFactory() { classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName"); classToProperty.put(IPing.class, "NFLoadBalancerPingClassName"); classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName"); classToProperty.put(ServerList.class, "NIWSServerListClassName"); classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName"); }所以,我们可以继承该类从而实现我们的扩展,这样一来就不用配置具体的服务名了.至于Ribbon是如何工作的,这里有一篇方志明的文章(传送门)可以加强对Ribbon工作机制的理解7.到这里基本上整个请求流程就比较完整了,上述例子中是以用户ID作为灰度的维度,当然这里可以实现更多的灰度策略,比如IP等,基本上都可以基于此方式做扩展灰度使用配置文件示例spring.application.name = provide-test server.port = 7770 eureka.client.service-url.defaultZone = http://localhost:1111/eureka/ #启动后直接将该元数据信息注册到eureka #eureka.instance.metadata-map.version = v1测试案例分别启动四个测试实例,有version代表灰度服务,无version则为普通服务。当灰度服务测试没问题的时候,通过PUT请求eureka接口将version信息去除,使其变成普通服务.实例列表:[x] zuul-server[x] provider-testport:7770 version:无port: 7771 version:v1[x] consumer-testport:8880 version:无port: 8881 version:v1修改服务信息服务在eureka的元数据信息可通过接口http://localhost:1111/eureka/apps访问到。服务信息实例:访问接口查看信息http://localhost:1111/eureka/apps/PROVIDE-TEST注意事项通过此种方法更改server的元数据后,由于ribbon会缓存实力列表,所以在测试改变服务信息时,ribbon并不会立马从eureka拉去最新信息m,这个拉取信息的时间可自行配置。同时,当服务重启时服务会重新将配置文件的version信息注册上去。测试演示zuul==>provider服务用户andy为灰度用户。 1.测试灰度用户andy,是否路由到灰度服务 provider-test:7771 2.测试非灰度用户andyaaa(任意用户)是否能被路由到普通服务 provider-test:7770zuul==>consumer服务>provider服务以同样的方式再启动两个consumer-test服务,这里不再截图演示。请求从zuul==>consumer-test==>provider-test,通过fegin和resttemplate两种请求方式测试Resttemplate请求方式fegin请求方式自动化配置与Apollo实现整合,避免手动调用接口。实现配置监听,完成灰度。
需求说明项目中有一个 Excel 导入的需求:缴费记录导入由实施 / 用户 将别的系统的数据填入我们系统中的 Excel 模板,应用将文件内容读取、校对、转换之后产生欠费数据、票据、票据详情并存储到数据库中。在我接手之前可能由于之前导入的数据量并不多没有对效率有过高的追求。但是到了 4.0 版本,我预估导入时Excel 行数会是 10w+ 级别,而往数据库插入的数据量是大于 3n 的,也就是说 10w 行的 Excel,则至少向数据库插入 30w 行数据。因此优化原来的导入代码是势在必行的。我逐步分析和优化了导入的代码,使之在百秒内完成(最终性能瓶颈在数据库的处理速度上,测试服务器 4g 内存不仅放了数据库,还放了很多微服务应用。处理能力不太行)。具体的过程如下,每一步都有列出影响性能的问题和解决的办法。导入 Excel 的需求在系统中还是很常见的,我的优化办法可能不是最优的,欢迎读者在评论区留言交流提供更优的思路声明:本文首发于博客园,作者:后青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 转载请注明,谢谢!一些细节数据导入:导入使用的模板由系统提供,格式是 xlsx (支持 65535+行数据) ,用户按照表头在对应列写入相应的数据数据校验:数据校验有两种:字段长度、字段正则表达式校验等,内存内校验不存在外部数据交互。对性能影响较小数据重复性校验,如票据号是否和系统已存在的票据号重复(需要查询数据库,十分影响性能)数据插入:测试环境数据库使用 MySQL 5.7,未分库分表,连接池使用 Druid迭代记录第一版:POI + 逐行查询校对 + 逐行插入这个版本是最古老的版本,采用原生 POI,手动将 Excel 中的行映射成 ArrayList 对象,然后存储到 List,代码执行的步骤如下:手动读取 Excel 成 List循环遍历,在循环中进行以下步骤检验字段长度一些查询数据库的校验,比如校验当前行欠费对应的房屋是否在系统中存在,需要查询房屋表写入当前行数据返回执行结果,如果出错 / 校验不合格。则返回提示信息并回滚数据显而易见的,这样实现一定是赶工赶出来的,后续可能用的少也没有察觉到性能问题,但是它最多适用于个位数/十位数级别的数据。存在以下明显的问题:查询数据库的校验对每一行数据都要查询一次数据库,应用访问数据库来回的网络IO次数被放大了 n 倍,时间也就放大了 n 倍写入数据也是逐行写入的,问题和上面的一样数据读取使用原生 POI,代码十分冗余,可维护性差。第二版:EasyPOI + 缓存数据库查询操作 + 批量插入针对第一版分析的三个问题,分别采用以下三个方法优化缓存数据,以空间换时间逐行查询数据库校验的时间成本主要在来回的网络IO中,优化方法也很简单。将参加校验的数据全部缓存到 HashMap 中。直接到 HashMap 去命中。例如:校验行中的房屋是否存在,原本是要用 区域 + 楼宇 + 单元 + 房号 去查询房屋表匹配房屋ID,查到则校验通过,生成的欠单中存储房屋ID,校验不通过则返回错误信息给用户。而房屋信息在导入欠费的时候是不会更新的。并且一个小区的房屋信息也不会很多(5000以内)因此我采用一条SQL,将该小区下所有的房屋以 区域/楼宇/单元/房号 作为 key,以 房屋ID 作为 value,存储到 HashMap 中,后续校验只需要在 HashMap 中命中自定义 SessionMapperMybatis 原生是不支持将查询到的结果直接写人一个 HashMap 中的,需要自定义 SessionMapperSessionMapper 中指定使用 MapResultHandler 处理 SQL 查询的结果集@Repository public class SessionMapper extends SqlSessionDaoSupport { @Resource public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { super.setSqlSessionFactory(sqlSessionFactory); } // 区域楼宇单元房号 - 房屋ID @SuppressWarnings("unchecked") public Map<String, Long> getHouseMapByAreaId(Long areaId) { MapResultHandler handler = new MapResultHandler(); this.getSqlSession().select(BaseUnitMapper.class.getName()+".getHouseMapByAreaId", areaId, handler); Map<String, Long> map = handler.getMappedResults(); return map; } } MapResultHandler 处理程序,将结果集放入 HashMappublic class MapResultHandler implements ResultHandler { private final Map mappedResults = new HashMap(); @Override public void handleResult(ResultContext context) { @SuppressWarnings("rawtypes") Map map = (Map)context.getResultObject(); mappedResults.put(map.get("key"), map.get("value")); } public Map getMappedResults() { return mappedResults; } }示例 Mapper@Mapper @Repository public interface BaseUnitMapper { // 收费标准绑定 区域楼宇单元房号 - 房屋ID Map<String, Long> getHouseMapByAreaId(@Param("areaId") Long areaId); } 示例 Mapper.xml<select id="getHouseMapByAreaId" resultMap="mapResultLong"> SELECT CONCAT( h.bulid_area_name, h.build_name, h.unit_name, h.house_num ) k, h.house_id v FROM base_house h WHERE h.area_id = #{areaId} GROUP BY h.house_id </select> <resultMap id="mapResultLong" type="java.util.HashMap"> <result property="key" column="k" javaType="string" jdbcType="VARCHAR"/> <result property="value" column="v" javaType="long" jdbcType="INTEGER"/> </resultMap> 之后在代码中调用 SessionMapper 类对应的方法即可。使用 values 批量插入MySQL insert 语句支持使用 values (),(),() 的方式一次插入多行数据,通过 mybatis foreach 结合 java 集合可以实现批量插入,代码写法如下:<insert id="insertList"> insert into table(colom1, colom2) values <foreach collection="list" item="item" index="index" separator=","> ( #{item.colom1}, #{item.colom2}) </foreach> </insert>使用 EasyPOI 读写 ExcelEasyPOI 采用基于注解的导入导出,修改注解就可以修改Excel,非常方便,代码维护起来也容易。第三版:EasyExcel + 缓存数据库查询操作 + 批量插入第二版采用 EasyPOI 之后,对于几千、几万的 Excel 数据已经可以轻松导入了,不过耗时有点久(5W 数据 10分钟左右写入到数据库)不过由于后来导入的操作基本都是开发在一边看日志一边导入,也就没有进一步优化。但是好景不长,有新小区需要迁入,票据 Excel 有 41w 行,这个时候使用 EasyPOI 在开发环境跑直接就 OOM 了,增大 JVM 内存参数之后,虽然不 OOM 了,但是 CPU 占用 100% 20 分钟仍然未能成功读取全部数据。故在读取大 Excel 时需要再优化速度。莫非要我这个渣渣去深入 POI 优化了吗?别慌,先上 GITHUB 找找别的开源项目。这时阿里 EasyExcel 映入眼帘:emmm,这不是为我量身定制的吗!赶紧拿来试试。EasyExcel 采用和 EasyPOI 类似的注解方式读写 Excel,因此从 EasyPOI 切换过来很方便,分分钟就搞定了。也确实如阿里大神描述的:41w行、25列、45.5m 数据读取平均耗时 50s,因此对于大 Excel 建议使用 EasyExcel 读取。第四版:优化数据插入速度在第二版插入的时候,我使用了 values 批量插入代替逐行插入。每 30000 行拼接一个长 SQL、顺序插入。整个导入方法这块耗时最多,非常拉跨。后来我将每次拼接的行数减少到 10000、5000、3000、1000、500 发现执行最快的是 1000。结合网上一些对 innodb_buffer_pool_size 描述我猜是因为过长的 SQL 在写操作的时候由于超过内存阈值,发生了磁盘交换。限制了速度,另外测试服务器的数据库性能也不怎么样,过多的插入他也处理不过来。所以最终采用每次 1000 条插入。每次 1000 条插入后,为了榨干数据库的 CPU,那么网络IO的等待时间就需要利用起来,这个需要多线程来解决,而最简单的多线程可以使用 并行流 来实现,接着我将代码用并行流来测试了一下:10w行的 excel、42w 欠单、42w记录详情、2w记录、16 线程并行插入数据库、每次 1000 行。插入时间 72s,导入总时间 95 s。并行插入工具类并行插入的代码我封装了一个函数式编程的工具类,也提供给大家/** * 功能:利用并行流快速插入数据 * * @author Keats * @date 2020/7/1 9:25 */ public class InsertConsumer { /** * 每个长 SQL 插入的行数,可以根据数据库性能调整 */ private final static int SIZE = 1000; /** * 如果需要调整并发数目,修改下面方法的第二个参数即可 */ static { System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4"); } /** * 插入方法 * * @param list 插入数据集合 * @param consumer 消费型方法,直接使用 mapper::method 方法引用的方式 * @param <T> 插入的数据类型 */ public static <T> void insertData(List<T> list, Consumer<List<T>> consumer) { if (list == null || list.size() < 1) { return; } List<List<T>> streamList = new ArrayList<>(); for (int i = 0; i < list.size(); i += SIZE) { int j = Math.min((i + SIZE), list.size()); List<T> subList = list.subList(i, j); streamList.add(subList); } // 并行流使用的并发数是 CPU 核心数,不能局部更改。全局更改影响较大,斟酌 streamList.parallelStream().forEach(consumer); } }这里多数使用到很多 Java8 的API,不了解的朋友可以翻看我之前关于 Java 的博客。方法使用起来很简单InsertConsumer.insertData(feeList, arrearageMapper::insertList);其他影响性能的内容日志避免在 for 循环中打印过多的 info 日志在优化的过程中,我还发现了一个特别影响性能的东西:info 日志,还是使用 41w行、25列、45.5m 数据,在 开始-数据读取完毕 之间每 1000 行打印一条 info 日志,缓存校验数据-校验完毕 之间每行打印 3+ 条 info 日志,日志框架使用 Slf4j 。打印并持久化到磁盘。下面是打印日志和不打印日志效率的差别打印日志不打印日志我以为是我选错 Excel 文件了,又重新选了一次,结果依旧缓存校验数据-校验完毕 不打印日志耗时仅仅是打印日志耗时的 1/10 !总结提升Excel导入速度的方法:使用更快的 Excel 读取框架(推荐使用阿里 EasyExcel)对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间使用 values(),(),() 拼接长 SQL 一次插入多行数据使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用)避免在循环中打印无用的日志
项目介绍在线考试系统是一款 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软件架构图系统展示学生考试系统小程序考试系统后台管理系统
推荐一个不错的论坛类开源项目!一款用 Java(spring boot) 实现的现代化社区(论坛 / 问答 / BBS / 社交网络 /博客)系统平台。一个开源的现代化社区平台,它实现了:面向内容讨论的论坛面向知识问答的社区100% 开源功能列表 特性前端:多终端适配(手机端,pc端)自定义主题颜色,方便企业用户自定义主题编辑器支持 control + s 保存编辑器支持 control + v 复制图片上传后端:日志带有调用链,方便排查问题分布式 session,支持集群部署用户角色权限分级,便于用户管理接口权限校验,接口操作更安全可扩展功能接口:文章/问答更新时自带审核,可接入审核中心便于运营管理文件存储抽象接口,可支持自定义接入企业内部文件储存服务缓存服务抽象接口,可支持自定义接入企业内部缓存服务搜索服务抽象接口,可支持自定义接入企业内部搜索服务技术栈后端:数据库:mysql持久层框架:mybatis数据库连接池管理:hikaricp数据库分页插件:github pagehelpermvc框架:spring mvc应用层容器:spring bootjson 序列化工具:fastjson邮件发送 sdk:javax mail七牛云存储 sdk:qiniu java sdk服务端页面渲染:thymeleaf前端:前端 markdown 编辑器:mavon-editor管理后台 js 框架:vue用户端 UI 框架:bootstrap管理后台 UI 框架 iview页面展示
简介大屏设计(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 团队,主要依托在最先进的机器学习技术,致力于服务游戏研发,推广,运营等各个环节。
描述出你想要执行的命令,就能生成相应的代码。现在,GitHub官方和openAI联合为程序员们送上编程神器——GitHub Copilot。AI来给你打工当秘书,从此写代码不用再去Stack Overflow上疯狂搜索了,效率立刻翻倍!这个系统可以像有高手指点一样,配合程序员写代码。甚至程序员只要写下一段注释,Github Copilot就可以补全剩下的代码、提出改进的建议,为程序员省去大量查找的时间,而且可以保持更高的专注力。官网介绍,它已经接受了数十亿行公共代码的训练,并且还在不断学习中。在一次根据函数头补全代码的测试中,Copilot首次测试的正确率可达43%;重复10次测试后,正确率就能提升至57%。有网友就表示,GitHub Copilot能达到的效果令他大吃一惊!使用了两周,Copilot给出的代码和我想写的代码大约有十分之一的重合。这真的很像结对编程,而且可以优化我的代码。Copilot使我成为了更好的程序员!那就话不多说,让我们来看看如何使用GitHub Copilot~AI变成好搭档目前,Github Copilot作为Visual Studio Code插件,支持在本地或GitHub Codespaces上使用。它适用于多种框架和语言,在Python、JavaScript、TypeScript、Rudy、Go几种语言上的表现格外突出。GitHub Copilot的强大之处就是能充当你的“小秘书”。它能够把注释转化成代码,只需描述出你想要执行的命令,GitHub Copilot就能自动为你组装代码。重复的代码打起来太费事了?GitHub Copilot也能帮你做。它能根据你给出的例子,快速生成模板和重复的代码。对于让程序员头疼的测试,它也能提供贴心的服务。只需导入单元测试包,GitHub Copilot就能给出与代码匹配的测试。“能不能多给几种方案?”这话现在也能对Copilot说了,它可以罗列出不同方案任君挑选。甚至,它还能辅导你写代码。遇到不熟悉的语言或者还在学习编程,GitHub Copilot也可以帮助你找出错误、学习新框架,省去了大量查资料的时间。数十亿行代码训练所以,GitHub Copilot是如何做到这样强大的呢?GitHub Copilot由OpenAI Codex提供支持,可以理解为GPT-3的改进版。它由公开源代码和自然语言的训练,因此它可以很好理解编程语言以及人类语言,从而能够把人类语言转化成代码。具体情况中,GitHub Copilot会把程序员给出的命令或代码发送到服务器,然后服务器使用OpenAI Codex来给出代码或建议。据官网介绍,它是用数十亿行的代码训练后,才达到了现在的效果。主要利用了上传到GitHub以及其他网站的源代码,依靠许多编程语言的大量代码和庞大的Azure云计算能力。而且它还会根据程序员在使用过程中的反馈,进一步学习。目前,已经有部分人在测试使用GitHub Copilot;之后,它可能将作为付费产品正式上线。网友:会取代人类吗?u1s1(有一说一),GitHub Copilot能达到的效果还是十分可观的,许多用过的程序员都说好。Copilot在处理React组件时效果格外好,它能做出非常精准的判断。GitHub Copilot会成为程序员不可缺少的一部分,就像很多人用IDE一样。AI写代码如此强悍?那程序员岂不是要危了。有人就调侃道:程序员们写了一个项目来取代程序员。难道有一天,AI写代码真的会超越人类吗?有人就举了一个生动的例子,表达了自己不认同的观点:当电子鼓问世时,音乐界认为鼓手的末日到了。但鼓手才是能在电子鼓上编写绝妙节拍的人啊!也就是说,与人们担心的相反,GitHub Copilot会提高程序员的生产力,可能帮助他们得到更多的报酬。毕竟,程序员总是要把大量时间花在编程以外的事情上……一本书中提到,程序员花费了50%的时间在非编程任务上。所以,GitHub Copilot的效果还是非常值得期待的~现在,它已经可以免费安装了,只需注册账号通过审核,就能体验“AI编程助手”,你要不要来试一下呢?
大家好,我是架构君,今天推荐的这个项目是因为使用手册部署手册非常完善,项目也有开发教程视频对小白非常贴心,接私活可以直接拿去二开非常舒服。该ERP系统基于SpringBoot框架和SaaS模式,立志为中小企业提供开源好用的ERP软件,目前专注进销存+财务+生产功能。主要模块有零售管理、采购管理、销售管理、仓库管理、财务管理、报表查询、系统管理等。支持预付款、收入支出、仓库调拨、组装拆卸、订单等特色功能。拥有库存状况、出入库统计等报表。同时对角色和权限进行了细致全面控制,精确到每个按钮和菜单。# 项目总述很多人说华夏ERP(英文名:jshERP)是目前人气领先的国产ERP系统虽然目前只有进销存+财务+生产的功能,但后面会推出ERP的全部功能演示地址:http://47.116.69.14 演示账号:jsh,密码:123456# 开发初衷华夏ERP立志为中小企业提供开源好用的ERP软件,降低企业的信息化成本个人开发者也可以使用华夏ERP进行二次开发,加快完成开发任务初学JAVA的小伙伴可以下载源代码来进行学习交流感谢热心的小伙伴整理的用户手册 https://kdocs.cn/l/sJaqlO1du# 技术框架核心框架:SpringBoot 2.0.0持久层框架:Mybatis 1.3.2日志管理:Log4j 2.10.0JS框架:Jquery 1.8.0UI框架: EasyUI 1.9.4模板框架: AdminLTE 2.4.0项目管理框架: Maven 3.2.3API接口框架: swagger2.7.0(ip:port/doc.html)# 开发环境建议开发者使用以下环境,可以避免版本不一致带来的问题IDE: IntelliJ IDEA 2017+DB: Mysql5.7+JDK: JDK1.8Maven: Maven3.2.3+# 服务器环境数据库服务器:Mysql5.7+JAVA平台: JRE1.8操作系统:Windows、Linux等# 开源说明本系统100%开源,遵守GPL-3.0协议# 系统美图首页 零售管理 采购管理 销售管理 仓库管理 财务管理 报表查询 商品管理 基本资料 系统管理
后台管理类项目项目名称: JeeSite项目介绍:这是个典型的SSM后台管理项目(不是有很多小伙伴让推荐SSM项目练手嘛),基于经典技术组合(Spring MVC、Shiro、MyBatis、Bootstrap UI等)开发,适合学习练手。而且它作为一个典型的后台管理系统,要素基本都有,包括:组织机构、角色用户、权限授权、数据权限、内容管理、工作流等。尤其要提的就是最后的工作流模块,它可以实现提工单、审核/审批等流程,这个在后台管理类项目里是必备的模块。技术选型主框架:Spring Boot 2.2、Spring Framework 5.2、Apache Shiro 1.6、J2Cache持久层:Apache MyBatis 3.5、Hibernate Validator 6.0、Alibaba Druid 1.1视图层:Spring MVC 5.2、Beetl 3.1(替换JSP)、Bootstrap 3.3、AdminLTE 2.4前端组件:jQuery 3.4、jqGrid 4.7、layer 3.1、zTree 3.5、jquery validation工作流引擎:Flowable 6.5、符合 BPMN 规范、在线流程设计器、中国式工作流技术选型详情:http://jeesite.com/docs/technology/平台介绍JeeSite 快速开发平台,不仅仅是一个后台开发框架,它是一个企业级快速开发解决方案,基于经典技术组合(Spring Boot、Spring MVC、Apache Shiro、MyBatis、Beetl、Bootstrap、AdminLTE)采用经典开发模式,让初学者能够更快的入门并投入到团队开发中去。在线代码生成功能,包括模块如:组织机构、角色用户、菜单及按钮授权、数据权限、系统参数、内容管理、工作流等。采用松耦合设计,模块增减便捷;界面无刷新,一键换肤;众多账号安全设置,密码策略;文件在线预览;消息推送;多元化第三方登录;在线定时任务配置;支持集群,支持SAAS;支持多数据源;支持读写分离、分库分表;支持微服务应用。JeeSite 快速开发平台的主要目的是能够让初级的研发人员快速的开发出复杂的业务功能(经典架构会的人多),让开发者注重专注业务,其余有平台来封装技术细节,降低技术难度,从而节省人力成本,缩短项目周期,提高软件安全质量。JeeSite 自 2013 年发布以来已被广大爱好者用到了企业、政府、医疗、金融、互联网等各个领域中,JeeSite 架构精良、易于扩展、大众思维的设计模式、工匠精神打磨每一个细节,深入开发者的内心,并荣获开源中国《最受欢迎中国开源软件》奖杯,期间也帮助了不少刚毕业的大学生,教师作为入门教材,快速的去实践。JeeSite4 的升级,作者结合了多年总结和经验,以及各方面的应用案例,对架构完成了一次全部重构,也纳入很多新的思想。不管是从开发者模式、底层架构、逻辑处理还是到用户界面,用户交互体验上都有很大的进步,在不忘学习成本、提高开发效率的情况下,安全方面也做和很多工作,包括:身份认证、密码策略、安全审计、日志收集等众多安全选项供你选择。努力为大中小微企业打造全方位企业级快速开发解决方案。平台优势JeeSite 整体架构清晰、稳定技术先进、源代码书写规范、经典技术会的人多、易于维护、易于扩展、安全稳定。JeeSite 功能全,JeeSite 的知识点非常多,也非常少。因为她使用的都是一些通用的技术,通俗的设计风格,大多数基础知识点多数人都能掌握,所以每一个 JeeSite 的功能点都非常容易掌握。只要你学会使用这些功能和组件的应用,就可以顺利的完成系统开发了。JeeSite 是一个低代码开发平台,具有较高的封装度、扩展性,封装不是限制你去做一些事情,而是在便捷的同时,也具有较好的扩展性,在不具备一些功能的情况下,JeeSite 提供了扩展接口,提供了原生调用方法。大家都在用 Spring ,在学习 Spring 架构的优点,Spring 提供了较好的扩展性,可又有多少人去修改它的源代码呢,退一步说,大家去修改了 Spring 的源码,反而会对未来升级造成很大困扰,您说不是呢?这样的例子很多,所以不要纠结,JeeSite 也一样具备强大的扩展性。发展至今 JeeSite 平台架构已经非常稳定,JeeSite 是一个专业的平台,是一个让你使用放心的平台。
今天推介一个CRM项目--悟空软件长期为企业提供企业管理软件(CRM/HRM/OA/ERP等)的研发、实施、营销、咨询、培训、服务于一体的信息化服务。悟空软件以高科技为起点,以技术为核心、以完善的售后服务为后盾,秉承稳固与发展、求实与创新的精神,已为国内外上千家企业提供服务。悟空的发展受益于开源,也会回馈于开源。2019年,悟空CRM会继续秉承“拥抱开放、合作共赢、创造价值”的理念,在开源的道路上继续砥砺前行,和更多的社区开发者一起为国内外开源做出积极贡献。悟空CRM采用全新的前后端分离模式,本仓库代码中已集成前端vue打包后文件,可免去打包操作 Java项目分享如需调整前端代码,请单独下载前端代码,前端代码在根目录的ux文件夹中主要技术栈核心框架:jfinal3.8缓存:redis caffeine数据库连接池:Druid工具类:hutool,fastjson,poi-ooxml定时任务:jfinal-cron项目构建工具:mavenWeb容器:tomcat,undertow(默认)前端MVVM框架:Vue.JS 2.5.x路由:Vue-Router 3.x数据交互:AxiosUI框架:Element-UI 2.6.3安装说明1、配置java运行环境,redis环境,mysql环境。2、将目录doc下的crm9.sql导入到数据库( 初始化安装只需要导入crm9.sql就好了,更新代码导入对应日期的sql文件)。3、修改resources/config/crm9-config.txt下的数据库配置文件。4、修改resources/config/redis.json下的redis连接文件 5、undertow启动端口号在resources/config/undertow.txt下修改。默认账号 admin 默认密码 123456部署说明本项目JDK要求JDK8及以上一、Undertow(默认)<dependency> <groupId>com.jfinal</groupId> <artifactId>jfinal-undertow</artifactId> <version>1.9</version></dependency>取消以上代码的注释,将tomcat的pom依赖javax.servlet.javax.servlet-api注释掉,打包方式改为jar 运行maven package,打包完成后将上述打包命令生成的 crm9-release.zip 文件上传到服务器并解压,运行对应的72crm.sh/72crm.bat即可二、Tomcat部署<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>取消以上代码的注释,将undertow的pom依赖com.jfinal.jfinal-undertow注释掉,并将com.kakarote.crm9.Application的main方法注释掉,打包方式改为war,运行maven package命令,将war包放在tomcat/webapps目录下项目默认是ROOT.war,若需要携带项目名,需要修改 ux/config/prod.env.js的BASE_API为'"/项目名/"',改动完成后需要重新打包替换到webapp下项目webapp下自带打包后的前端代码,如果不需要对前端代码更改,直接访问即可如果更改了前端代码,需要将打包后的dist下static文件夹和index.html替换到webapp下ps:可以使用nginx代理静态文件,后台只做接口响应,项目本身设计是前后端完全分离的前端部署安装node.js 前端部分是基于node.js上运行的,所以必须先安装node.js,版本要求为6.0以上使用npm安装依赖 下载悟空CRM9.0前端代码;可将代码放置在后端同级目录ux,执行命令安装依赖:npm install修改内部配置 修改请求地址或域名:config/dev.env.js里修改BASE_API(开发环境服务端地址,默认localhost) 修改自定义端口:config/index.js里面的dev对象的port参数(默认8090,不建议修改)运行前端 npm run dev注意:前端服务启动,默认会占用8090端口,所以在启动前端服务之前,请确认8090端口没有被占用。程序运行之前需搭建好Server端系统介绍以下为悟空CRM9.0 JAVA版部分功能系统截图
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
简而言之,就是多个人进行编辑。协作有好处也有风险。好处之一是更加全面/协调的方式,更好的利用现有资源和一个更加有力一致的声音。对于我来说,最大的好处是极大的透明度。那是当我需要采纳同事的观点。同事之间来来回回地传文件效率非常低,导致不必要的延误还让人(比如,我)对整个协作这件事都感到不满意。有个好的协作软件,我就能实时地或异步地分享笔记,数据和文件,并用评论来分享自己的想法。这样在文档、图片、视频、演示文稿上协作就不会那么的琐碎而无聊。有很多种方式能在线进行协作,简直不能更简便了。这篇文章展示了我最喜欢的开源的实时文档协作编辑工具。Google Docs 是个非常好的高效应用,有着大部分我所需要的功能。它可以作为一个实时地协作编辑文档的工具提供服务。文档可以被分享、打开并被多位用户同时编辑,用户还能看见其他协作者一个字母一个字母的编辑过程。虽然 Google Docs 对个人是免费的,但并不开源。下面是我带来的最棒的开源协作编辑器,它们能帮你不被打扰的集中精力进行写作,而且是和其他人协同完成。HackpadHackpad 是个开源的基于网页的实时 wiki,基于开源 EtherPad 协作文档编辑器。Hackpad 允许用户实时分享你的文档,它还用彩色编码显示各个作者分别贡献了哪部分。它还允许插入图片、清单,由于提供了语法高亮功能,它还能用来写代码。当2014年4月 Dropbox 收购了 Hackpad 后,就在这个月这款软件以开源的形式发布。让我们经历的等待非常值得。特性:有类似 wiki 所提供的,一套非常完善的功能实时或者异步地记录协作笔记,共享数据和文件,或用评论分享你们的想法细致的隐私许可让你可以邀请单个朋友、一个十几人的团队或者上千的 Twitter 粉丝智能执行直接从流行的视频分享网站上插入视频表格可对使用广泛的包括 C, C#, CSS, CoffeeScript, Java, 以及 HTML 在内的编程语言进行语法高亮网站:hackpad.com源代码:github.com/dropbox/hackpadEtherpadEtherpad 是个基于网页的开源实时协作编辑器,允许多个作者同时编辑一个文本文档,写评论,并与其他作者用群聊方式进行交流。Etherpad 是用 JavaScript 编写的,运行在 AppJet 平台之上,通过 Comet 流实现实时的功能。特性:尽心设计的斯巴达界面简单的格式化文本功能“滑动时间轴”——浏览一个工程历史版本可以下载纯文本、 PDF、微软的 Word 文档、Open Document 和 HTML 格式的文档每隔一段很短的时间就会自动保存可个性化程度高有客户端插件可以扩展编辑的功能几百个支持 Etherpad 的扩展,包括支持 email 提醒,pad 管理,授权可访问性开启可从 Node 里或通过 CLI(命令行界面)和 EtherPad 的内容交互网站:etherpad.org源代码:github.com/ether/etherpad-liteFirepadFirepad 是个开源的协作文本编辑器。它的设计目的是被嵌入到更大的网页应用中对几天内新加入的代码进行批注。Firepad 是个全功能的文本编辑器,有解决冲突,光标同步,用户属性,用户在线状态检测功能。它使用 Firebase 作为后台,而且不需要任何服务器端的代码。他可以被加入到任何网页应用中。Firepad 可以使用 CodeMirror 编辑器或者 Ace 编辑器提交文本,它的操作转换代码是从 ot.js 上借鉴的。如果你想要通过添加简单的文档和代码编辑器来扩展你的网页应用能力,Firepad 最适合不过了。Firepad 已被多个编辑器使用,包括Atlassian Stash Realtime Editor、Nitrous.IO、LiveMinutes 和 Koding。特性:纯正的协作编辑基于 OT 的智能合并及解决冲突支持多种格式的文本和代码的编辑光标位置同步撤销/重做文本高亮用户属性在线检测版本检查点图片通过它的 API 拓展 Firepad支持所有现代浏览器:Chrome、Safari、Opera 11+、IE8+、Firefox 3.6+网站:www.firepad.io源代码:github.com/firebase/firepadOwnCloud DocumentsownCloud Documents 是个可以单独并/或协作进行办公室文档编辑 ownCloud 应用。它允许最多5个人同时在网页浏览器上协作进行编辑 .odt 和 .doc 文件。ownCloud 是个自托管文件同步和分享服务器。他通过网页界面,同步客户端或 WebDAV 提供你数据的使用权,同时提供一个容易在设备间进行浏览、同步和分享的平台。特性:协作编辑,多个用户同时进行文件编辑在 ownCloud 里创建文档上传文档在浏览器里分享和编辑文件,然后在 ownCloud 内部或通过公共链接进行分享这些文件有类似 ownCloud 的功能,如版本管理、本地同步、加密、恢复被删文件通过透明转换文件格式的方式无缝支持微软 Word 文档网站:owncloud.org源代码:github.com/owncloud/documentsGobbyGobby 是个支持在一个会话内进行多个用户聊天并打开多个文档的协作编辑器。所有的用户都能同时在文件上进行工作,无需锁定。不同用户编写的部分用不同颜色高亮显示,它还支持多个编程和标记语言的语法高亮。Gobby 允许多个用户在互联网上实时共同编辑同一个文档。它很好的整合了 GNOME 环境。它拥有一个客户端-服务端结构,这让它能支持一个会话开多个文档,文档同步请求,密码保护和 IRC 式的聊天方式可以在多个频道进行交流。用户可以选择一个颜色对他们在文档中编写的文本进行高亮。还供有一个叫做 infinoted 的专用服务器。特性:成熟的文本编辑能力包括使用 GtkSourceView 的语法高亮功能实时、无需锁定、通过加密(包括PFS)连接的协作文本编辑整合了群聊本地组撤销:撤销不会影响远程用户的修改显示远程用户的光标和选择区域用不同颜色高亮不同用户编写的文本适用于大多数编程语言的语法高亮,自动缩进,可配置 tab 宽度零冲突加密数据传输包括完美的正向加密(PFS)会话可被密码保护通过 Access Control Lists (ACLs) 进行精密的权限保护高度个性化的专用服务器自动保存文档先进的查找和替换功能国际化完整的 Unicode 支持网站:gobby.github.io源代码:github.com/gobbyOnlyOfficeONLYOFFICE(从前叫 Teamlab Office)是个多功能云端在线办公套件,整合了 CRM(客户关系管理)系统、文档和项目管理工具箱、甘特图以及邮件整合器它能让你整理商业任务和时间表,保存并分享你的协作或个人文档,使用网络社交工具如博客和论坛,还可以和你的队员通过团队的即时聊天工具进行交流。能在同一个地方管理文档、项目、团队和顾客关系。OnlyOffice 结合了文本,电子表格和电子幻灯片编辑器,他们的功能跟微软桌面应用(Word、Excel 和 PowerPoint)的功能相同。但是他允许实时进行协作编辑、评论和聊天。OnlyOffice 是用 ASP.NET 编写的,基于 HTML5 Canvas 元素,并且被翻译成21种语言。特性:当在大文档里工作、翻页和缩放时,它能与桌面应用一样强大文档可以在浏览/编辑模式下分享文档嵌入电子表格和电子幻灯片编辑器协作编辑评论群聊移动应用甘特图时间管理权限管理Invoicing 系统日历整合了文件保存系统:Google Drive、Box、OneDrive、Dropbox、OwnCloud整合了 CRM、电子邮件整合器和工程管理模块邮件服务器邮件整合器可以编辑流行格式的文档、电子表格和电子幻灯片:DOC、DOCX、ODT、RTF、TXT、XLS、XLSX、ODS、CSV、PPTX、PPT、ODP
简而言之,就是多个人进行编辑。协作有好处也有风险。好处之一是更加全面/协调的方式,更好的利用现有资源和一个更加有力一致的声音。对于我来说,最大的好处是极大的透明度。那是当我需要采纳同事的观点。同事之间来来回回地传文件效率非常低,导致不必要的延误还让人(比如,我)对整个协作这件事都感到不满意。有个好的协作软件,我就能实时地或异步地分享笔记,数据和文件,并用评论来分享自己的想法。这样在文档、图片、视频、演示文稿上协作就不会那么的琐碎而无聊。有很多种方式能在线进行协作,简直不能更简便了。这篇文章展示了我最喜欢的开源的实时文档协作编辑工具。Google Docs 是个非常好的高效应用,有着大部分我所需要的功能。它可以作为一个实时地协作编辑文档的工具提供服务。文档可以被分享、打开并被多位用户同时编辑,用户还能看见其他协作者一个字母一个字母的编辑过程。虽然 Google Docs 对个人是免费的,但并不开源。下面是我带来的最棒的开源协作编辑器,它们能帮你不被打扰的集中精力进行写作,而且是和其他人协同完成。HackpadHackpad 是个开源的基于网页的实时 wiki,基于开源 EtherPad 协作文档编辑器。Hackpad 允许用户实时分享你的文档,它还用彩色编码显示各个作者分别贡献了哪部分。它还允许插入图片、清单,由于提供了语法高亮功能,它还能用来写代码。当2014年4月 Dropbox 收购了 Hackpad 后,就在这个月这款软件以开源的形式发布。让我们经历的等待非常值得。特性:有类似 wiki 所提供的,一套非常完善的功能实时或者异步地记录协作笔记,共享数据和文件,或用评论分享你们的想法细致的隐私许可让你可以邀请单个朋友、一个十几人的团队或者上千的 Twitter 粉丝智能执行直接从流行的视频分享网站上插入视频表格可对使用广泛的包括 C, C#, CSS, CoffeeScript, Java, 以及 HTML 在内的编程语言进行语法高亮网站:hackpad.com源代码:github.com/dropbox/hackpadEtherpadEtherpad 是个基于网页的开源实时协作编辑器,允许多个作者同时编辑一个文本文档,写评论,并与其他作者用群聊方式进行交流。Etherpad 是用 JavaScript 编写的,运行在 AppJet 平台之上,通过 Comet 流实现实时的功能。特性:尽心设计的斯巴达界面简单的格式化文本功能“滑动时间轴”——浏览一个工程历史版本可以下载纯文本、 PDF、微软的 Word 文档、Open Document 和 HTML 格式的文档每隔一段很短的时间就会自动保存可个性化程度高有客户端插件可以扩展编辑的功能几百个支持 Etherpad 的扩展,包括支持 email 提醒,pad 管理,授权可访问性开启可从 Node 里或通过 CLI(命令行界面)和 EtherPad 的内容交互网站:etherpad.org源代码:github.com/ether/etherpad-liteFirepadFirepad 是个开源的协作文本编辑器。它的设计目的是被嵌入到更大的网页应用中对几天内新加入的代码进行批注。Firepad 是个全功能的文本编辑器,有解决冲突,光标同步,用户属性,用户在线状态检测功能。它使用 Firebase 作为后台,而且不需要任何服务器端的代码。他可以被加入到任何网页应用中。Firepad 可以使用 CodeMirror 编辑器或者 Ace 编辑器提交文本,它的操作转换代码是从 ot.js 上借鉴的。如果你想要通过添加简单的文档和代码编辑器来扩展你的网页应用能力,Firepad 最适合不过了。Firepad 已被多个编辑器使用,包括Atlassian Stash Realtime Editor、Nitrous.IO、LiveMinutes 和 Koding。特性:纯正的协作编辑基于 OT 的智能合并及解决冲突支持多种格式的文本和代码的编辑光标位置同步撤销/重做文本高亮用户属性在线检测版本检查点图片通过它的 API 拓展 Firepad支持所有现代浏览器:Chrome、Safari、Opera 11+、IE8+、Firefox 3.6+网站:www.firepad.io源代码:github.com/firebase/firepadOwnCloud DocumentsownCloud Documents 是个可以单独并/或协作进行办公室文档编辑 ownCloud 应用。它允许最多5个人同时在网页浏览器上协作进行编辑 .odt 和 .doc 文件。ownCloud 是个自托管文件同步和分享服务器。他通过网页界面,同步客户端或 WebDAV 提供你数据的使用权,同时提供一个容易在设备间进行浏览、同步和分享的平台。特性:协作编辑,多个用户同时进行文件编辑在 ownCloud 里创建文档上传文档在浏览器里分享和编辑文件,然后在 ownCloud 内部或通过公共链接进行分享这些文件有类似 ownCloud 的功能,如版本管理、本地同步、加密、恢复被删文件通过透明转换文件格式的方式无缝支持微软 Word 文档网站:owncloud.org源代码:github.com/owncloud/documentsGobbyGobby 是个支持在一个会话内进行多个用户聊天并打开多个文档的协作编辑器。所有的用户都能同时在文件上进行工作,无需锁定。不同用户编写的部分用不同颜色高亮显示,它还支持多个编程和标记语言的语法高亮。Gobby 允许多个用户在互联网上实时共同编辑同一个文档。它很好的整合了 GNOME 环境。它拥有一个客户端-服务端结构,这让它能支持一个会话开多个文档,文档同步请求,密码保护和 IRC 式的聊天方式可以在多个频道进行交流。用户可以选择一个颜色对他们在文档中编写的文本进行高亮。还供有一个叫做 infinoted 的专用服务器。特性:成熟的文本编辑能力包括使用 GtkSourceView 的语法高亮功能实时、无需锁定、通过加密(包括PFS)连接的协作文本编辑整合了群聊本地组撤销:撤销不会影响远程用户的修改显示远程用户的光标和选择区域用不同颜色高亮不同用户编写的文本适用于大多数编程语言的语法高亮,自动缩进,可配置 tab 宽度零冲突加密数据传输包括完美的正向加密(PFS)会话可被密码保护通过 Access Control Lists (ACLs) 进行精密的权限保护高度个性化的专用服务器自动保存文档先进的查找和替换功能国际化完整的 Unicode 支持网站:gobby.github.io源代码:github.com/gobbyOnlyOfficeONLYOFFICE(从前叫 Teamlab Office)是个多功能云端在线办公套件,整合了 CRM(客户关系管理)系统、文档和项目管理工具箱、甘特图以及邮件整合器它能让你整理商业任务和时间表,保存并分享你的协作或个人文档,使用网络社交工具如博客和论坛,还可以和你的队员通过团队的即时聊天工具进行交流。能在同一个地方管理文档、项目、团队和顾客关系。OnlyOffice 结合了文本,电子表格和电子幻灯片编辑器,他们的功能跟微软桌面应用(Word、Excel 和 PowerPoint)的功能相同。但是他允许实时进行协作编辑、评论和聊天。OnlyOffice 是用 ASP.NET 编写的,基于 HTML5 Canvas 元素,并且被翻译成21种语言。特性:当在大文档里工作、翻页和缩放时,它能与桌面应用一样强大文档可以在浏览/编辑模式下分享文档嵌入电子表格和电子幻灯片编辑器协作编辑评论群聊移动应用甘特图时间管理权限管理Invoicing 系统日历整合了文件保存系统:Google Drive、Box、OneDrive、Dropbox、OwnCloud整合了 CRM、电子邮件整合器和工程管理模块邮件服务器邮件整合器可以编辑流行格式的文档、电子表格和电子幻灯片:DOC、DOCX、ODT、RTF、TXT、XLS、XLSX、ODS、CSV、PPTX、PPT、ODP
开源 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
项目介绍行为验证码采用嵌入式集成方式,接入方便,安全,高效。抛弃了传统字符型验证码展示-填写字符-比对答案的流程,采用验证码展示-采集用户行为-分析用户行为流程,用户只需要产生指定的行为轨迹,不需要键盘手动输入,极大优化了传统验证码用户体验不佳的问题;同时,快速、准确的返回人机判定结果。目前对外提供两种类型的验证码,其中包含滑动拼图、文字点选。如图1-1、1-2所示。若希望不影响原UI布局,可采用弹出式交互。概念术语描述术语描述验证码类型1)滑动拼图 blockPuzzle 2)文字点选 clickWord验证用户拖动/点击一次验证码拼图即视为一次“验证”,不论拼图/点击是否正确二次校验验证数据随表单提交到后台后,后台需要调用captchaService.verification做二次校验。目的是核实验证数据的有效性。交互流程① 用户访问应用页面,请求显示行为验证码② 用户按照提示要求完成验证码拼图/点击③ 用户提交表单,前端将第二步的输出一同提交到后台④ 验证数据随表单提交到后台后,后台需要调用captchaService.verification做二次校验。⑤ 第4步返回校验通过/失败到产品应用后端,再返回到前端。如下图所示。目录结构├─core │ ├─captcha java核心源码 │ └─captcha-spring-boot-starter springboot快速启动 ├─images 效果图 ├─service │ ├─springboot 后端为springboot项目示例 │ └─springmvc 后端为springmvc非springboot项目示例 └─view 多语言客户端示例 ├─android 原生android实现示例 ├─flutter flutter实现示例 ├─html 原生html实现示例 ├─ios 原生ios实现示例 ├─uni-app uni-app实现示例 └─vue vue实现示例
项目简介集监控点监控、日志监控、数据可视化以及监控告警为一体的国产开源监控系统,直接部署即可使用。 监控数据类型丰富,提供多种富有表现力的图表,满足对数据可视化的需要,目前支持折线图、饼图、地理位置图,后续会引入 更多富有表现力的图表以加强对数据可视化的支持。相比其它开源监控系统优势支持插件功能, 监控插件无需开发,自由选择监控插件,安装即可使用集成告警功能, 支持多种告警方式 集成分布式日志系统功能 支持多种部署方式 a.集中部署(全部服务部署在一台机器,适合个人或者小团队开发者) b.分布式部署(分布式部署在多台机器,适合小中型企业大规模监控需求)支持自动化配置(机器部署agent后自动注册到监控系统无需在控制台配置、视图根据上报自动绑定相关上报机器) 支持多用户访问(子账号由管理员账号在控制台添加) 上报接口支持主流开发语言,数据上报api 提供类似公共库接口的便捷 特色功能推荐IP地址库: 支持通过IP地址上报时将IP地址转为物理地址,相同物理地址归并展示一个监控API 即可轻松生成监控。 数据的物理地址分布图监控插件市场: 让监控成为可以复用的组件,更多监控插件持续开发中 分布式日志系统: 支持大规模系统日志上报,日志上报支持频率限制、日志染色、自定义字段等高级功能,控制台日志查看支持按关键字、排除关键字、上报时间、上报机器等方式过滤日志,从茫茫日志中轻松找到您需要的日志。视图机制: 监控图表支持视图定制模式,视图可按上报服务器、监控点随意组合,轻松定制您需要的监控视图,并可在监控图表上直接设置告警值 告警集成: 集成告警功能, 支持邮件、短信、微信、PC客户端等告警方式,告警功能无需开发直接可用在线部署在线部署说明: 安装脚本会先检查当前系统是否支持在线安装, 如不支持您可以下载源码后在系统上编译安装。 在线部署目前只支持集中部署方式, 即所有服务部署在一台机器上, 该机器上需要安装 mysql/apache。 安装脚本使用中文 utf8 编码, 安装过程请将您的终端设置为 utf8, 以免出现乱码。 安装脚本同时支持 root 账号和普通账号操作, 使用普通账号执行安装部署要求如下:在线部署使用动态链接库, 需要在指定目录下执行安装脚本, 目录为: /home/mtreport 普通账号某些目录可能无权操作, 需要授权才能正常安装 我们强烈建议您先在本地虚拟机上执行在线安装, 熟悉安装流程后在实际部署到您的服务器上.离线部署(自行编译源码)如果在线安装失败或者需要二次开发, 可以使用源码编译方式安装三部完成部署:执行 make 完成源码编译进入 tools_sh 目录,执行 make_all.sh 生成部署包在安装目录解压部署包,执行 local_install.sh 完成安装使用的技术方案apache + mysql(监控点数据、配置信息使用 mysql 存储, 支持分布式部署) 前端 web 控制台采用 dwz 开源框架前端监控图表采用开源 echarts 绘制后台 cgi 使用开源的cgi模板引擎 - clearsilver, 所有cgi支持以fastcgi方式部署后台服务使用了开源的 socket 开发框架 - C++ Sockets
2022年01月