• [问题求助] Android studio 采用流式方法下载程序崩溃(Java)
    button11.setOnClickListener(v -> { try { ObsClient obsClient = new ObsClient(ak, sk,endPoint); // 流式下载 ObsObject obsObject = obsClient.getObject("namexiao", "Speedtestfile.mp4"); // 读取对象内容 InputStream input = obsObject.getObjectContent(); FileOutputStream fileOutputStream = new FileOutputStream("/Download/myfile.mp4"); byte[] b = new byte[1024]; //ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len; while ((len = input.read(b)) != -1) { fileOutputStream.write(b, 0, len); }// System.out.println("getObjectContent successfully");// System.out.println(new String(bos.toByteArray())); fileOutputStream.close(); input.close(); } catch (ObsException e) { System.out.println("getObjectContent failed"); // 请求失败,打印http状态码 System.out.println("HTTP Code:" + e.getResponseCode()); // 请求失败,打印服务端错误码 System.out.println("Error Code:" + e.getErrorCode()); // 请求失败,打印详细错误信息 System.out.println("Error Message:" + e.getErrorMessage()); // 请求失败,打印请求id System.out.println("Request ID:" + e.getErrorRequestId()); System.out.println("Host ID:" + e.getErrorHostId()); e.printStackTrace(); } catch (Exception e) { System.out.println("getObjectContent failed"); // 其他异常信息打印 e.printStackTrace(); } });点击按键触发事件是程序闪退,求助大佬如何解决
  • [技术干货] Java实现Fast DFS、服务器、OSS上传
    支持Fast DFS、服务器、OSS等上传方式介绍在实际的业务中,可以根据客户的需求设置不同的文件上传需求,支持普通服务器上传+分布式上传(Fast DFS)+云服务上传OSS(OSS)软件架构为了方便演示使用,本项目使用的是前后端不分离的架构前端:Jquery.uploadFile后端:SpringBoot前期准备:FastDFS、OSS(华为)、服务器实现逻辑通过 application 配置对上传文件进行一个自定义配置,从而部署在不同客户环境可以自定义选择方式。优点:一键切换;支持当前主流方式;缺点:迁移数据难度增加:因为表示FileID在对象存储和服务器上传都是生成的UUID,而FastDFS是返回存取ID,当需要迁移的时候,通过脚本可以快速将FastDFS的数据迁移上云,因为存储ID可以共用。但是对象存储和服务器上传的UUID无法被FastDFS使用,增加迁移成本核心代码package com.example.file.util;import com.example.file.common.ResultBean;import com.github.tobato.fastdfs.domain.StorePath;import com.github.tobato.fastdfs.proto.storage.DownloadByteArray;import com.github.tobato.fastdfs.service.FastFileStorageClient;import com.obs.services.ObsClient;import com.obs.services.exception.ObsException;import com.obs.services.model.DeleteObjectRequest;import com.obs.services.model.GetObjectRequest;import com.obs.services.model.ObsObject;import com.obs.services.model.PutObjectResult;import io.micrometer.common.util.StringUtils;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.mock.web.MockMultipartFile;import org.springframework.web.multipart.MultipartFile;import java.io.*;import java.util.Objects;import java.util.UUID;@Slf4jpublic class FileUtil { /** * * @param file 文件 * @param uploadFlag 标识 * @param uploadPath 上传路径 * @param endPoint 域名 * @param ak ak * @param sk sk * @param bucketName 桶名字 * @return fileId 用于下载 */ public static ResultBean uploadFile(MultipartFile file, String uploadFlag, String uploadPath, FastFileStorageClient fastFileStorageClient, String endPoint, String ak, String sk, String bucketName) { if (StringUtils.isBlank(uploadFlag)){ ResultBean.error("uploadFlag is null"); } switch (uploadFlag){ case "fastDFS": return uploadFileByFastDFS(file,fastFileStorageClient); case "huaweiOOS": return uploadFileByHuaweiObject(file, endPoint, ak, ak, bucketName); case "server": default: return uploadFileByOrigin(file,uploadPath); }} /** * 上传文件fastDFS * @param file 文件名 * @return */ private static ResultBean uploadFileByFastDFS(MultipartFile file,FastFileStorageClient fastFileStorageClient){ Long size=file.getSize(); String fileName=file.getOriginalFilename(); String extName=fileName.substring(fileName.lastIndexOf(".")+1); InputStream inputStream=null; try { inputStream=file.getInputStream(); //1-上传的文件流 2-文件的大小 3-文件的后缀 4-可以不管他 StorePath storePath=fastFileStorageClient.uploadFile(inputStream,size,extName,null); log.info("[uploadFileByFastDFS][FullPath]"+storePath.getFullPath()); return ResultBean.success(storePath.getPath()); }catch (Exception e){ log.info("[ERROR][uploadFileByFastDFS]"+e.getMessage()); return ResultBean.error(e.getMessage()); }finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 对象存储上传 * @param file 文件名 * @return */ private static ResultBean uploadFileByHuaweiObject(MultipartFile file, String huaweiEndPoint,String huaweiobsAk,String huaweiobsSk, String bucketName){ String fileName = file.getOriginalFilename(); fileName = renameToUUID(fileName); InputStream inputStream=null; try { inputStream=file.getInputStream(); // 创建ObsClient实例 ObsClient obsClient = new ObsClient(huaweiobsAk, huaweiobsSk, huaweiEndPoint); PutObjectResult result = obsClient.putObject(bucketName, fileName, inputStream); obsClient.close(); return ResultBean.success(fileName); }catch (ObsException e){ log.info("[ERROR][uploadFileByHuaweiObject]"+e.getErrorMessage()); return ResultBean.error(e.getErrorMessage()); }catch (Exception e){ log.info("[ERROR][uploadFileByHuaweiObject]"+e.getMessage()); return ResultBean.error(e.getMessage()); }finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 上传文件原本方法 * @param file 文件名 * @return */ private static ResultBean uploadFileByOrigin(MultipartFile file,String uploadPath){ String fileName = file.getOriginalFilename(); fileName = renameToUUID(fileName); File targetFile = new File(uploadPath); if (!targetFile.exists()) { targetFile.mkdirs(); } FileOutputStream out = null; try { out = new FileOutputStream(uploadPath + fileName); out.write(file.getBytes()); } catch (IOException e) { log.info("[ERROR][uploadFileByOrigin]"+e.getMessage()); return ResultBean.error(e.getMessage()); }finally { try { out.flush(); } catch (IOException e) { e.printStackTrace(); } try { out.close(); } catch (IOException e) { e.printStackTrace(); } } return ResultBean.success(fileName); } /** * 下载 * @return */ public static byte[] downloadFile(String fileId,String uploadFlag,String uploadPath ,FastFileStorageClient fastFileStorageClient, String group, String endPoint,String ak,String sk, String bucketName) { byte[] result=null; switch (uploadFlag){ case "fastDFS": result =downloadFileByFastDFS(fileId,fastFileStorageClient,group); break; case "huaweiOOS": result =downloadFileByHuaweiObject(fileId, endPoint, ak, sk, bucketName); break; case "server": default: String path2 = uploadPath + fileId; path2 = path2.replace("//", "/"); result=downloadFileByOrigin(path2); break; } return result; } /** * 下载文件fastDFS * @param fileId 文件名 * @return */ private static byte[] downloadFileByFastDFS(String fileId,FastFileStorageClient fastFileStorageClient, String group){ DownloadByteArray callback=new DownloadByteArray(); byte[] group1s=null; try { group1s = fastFileStorageClient.downloadFile(group, fileId, callback); }catch (Exception e){ log.info("[ERROR][downloadFileByFastDFS]"+e.getMessage()); } return group1s; } /** * 下载文件对象存储 * @param fileId 文件名 * @return */ private static byte[] downloadFileByHuaweiObject(String fileId, String huaweiEndPoint,String huaweiobsAk,String huaweiobsSk, String bucketName){ byte[] bytes =null; try { // 创建ObsClient实例 ObsClient obsClient = new ObsClient(huaweiobsAk, huaweiobsSk, huaweiEndPoint); // 构造GetObjectRequest请求 GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, fileId); // 执行下载操作 ObsObject obsObject = obsClient.getObject(getObjectRequest); bytes = inputStreamToByteArray(obsObject.getObjectContent()); // 关闭OBS客户端 obsClient.close(); return bytes; }catch (ObsException e){ log.info("[ERROR][downloadFileByHuaweiObject]"+e.getErrorMessage()); }catch (Exception e) { log.info("[ERROR][downloadFileByHuaweiObject]"+e.getMessage()); } return bytes; } /** * * @param input * @return * @throws IOException */ private static byte[] inputStreamToByteArray(InputStream input) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int n = 0; while (-1 != (n = input.read(buffer))) { output.write(buffer, 0, n); } return output.toByteArray(); } /** * 下载文件 * @param fileId 文件名 * @return */ private static byte[] downloadFileByOrigin(String fileId){ File file =new File(fileId); InputStream inputStream=null; byte[] buff = new byte[1024]; byte[] result=null; BufferedInputStream bis = null; ByteArrayOutputStream os = null; try { os=new ByteArrayOutputStream(); bis = new BufferedInputStream(new FileInputStream(file)); int i = bis.read(buff); while (i != -1) { os.write(buff, 0, buff.length); i = bis.read(buff); } result=os.toByteArray(); os.flush(); } catch (Exception e) { log.info("[ERROR][downloadFile]"+e.getMessage()); }finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return result; } /** * 删除文件 * @param fileId 文件ID * @return 删除失败返回-1,否则返回0 */ public static boolean deleteFile(String fileId,String fastDFSFlag,FastFileStorageClient fastFileStorageClient, String group,String uploadPath) { boolean result=false; if (StringUtils.isNotBlank(fastDFSFlag)&& fastDFSFlag.trim().equalsIgnoreCase("true")){ result =deleteFileByFastDFS(fileId,fastFileStorageClient,group); }else { String path2 = uploadPath + fileId; path2 = path2.replace("//", "/"); result=deleteByOrigin(path2); } return result; } private static boolean deleteByOrigin(String fileName) { File file = new File(fileName); // 如果文件路径所对应的文件存在,并且是一个文件,则直接删除 if (file.exists() && file.isFile()) { if (file.delete()) { return true; } else { return false; } } else { return false; } } private static boolean deleteFileByFastDFS(String fileId,FastFileStorageClient fastFileStorageClient, String group) { try { String groupFieId=group+"/"+fileId; StorePath storePath = StorePath.praseFromUrl(groupFieId); fastFileStorageClient.deleteFile(storePath.getGroup(), storePath.getPath()); } catch (Exception e) { log.info("[ERROR][deleteFileByFastDFS]"+e.getMessage()); return false; } return true; } /** * 生成fileId * @param fileName * @return */ private static String renameToUUID(String fileName) { return UUID.randomUUID() + "." + fileName.substring(fileName.lastIndexOf(".") + 1); } /** * 删除文件对象存储 * @param fileId 文件名 * @return */ private static boolean deleteFileByHuaweiObject(String fileId, String huaweiEndPoint,String huaweiobsAk,String huaweiobsSk, String bucketName){ try { // 创建ObsClient实例 ObsClient obsClient = new ObsClient(huaweiobsAk, huaweiobsSk, huaweiEndPoint); // 构造GetObjectRequest请求 DeleteObjectRequest getObjectRequest = new DeleteObjectRequest(bucketName, fileId); // 执行删除操作 obsClient.deleteObject(getObjectRequest); // 关闭OBS客户端 obsClient.close(); }catch (ObsException e){ log.info("[ERROR][deleteFileByHuaweiObject]"+e.getErrorMessage()); }catch (Exception e) { log.info("[ERROR][downloadFileByHuaweiObject]"+e.getMessage()); } return true; } /** * 文件数据输出(image) * @param fileId * @param bytes * @param res */ public static void fileDataOut(String fileId, byte[] bytes, HttpServletResponse res){ String[] prefixArray = fileId.split("\\."); String prefix=prefixArray[1]; File file =new File(fileId); res.reset(); res.setCharacterEncoding("utf-8");// res.setHeader("content-type", ""); res.addHeader("Content-Length", "" + bytes.length); if(prefix.equals("svg")) prefix ="svg+xml"; res.setContentType("image/"+prefix); res.setHeader("Accept-Ranges","bytes"); OutputStream os = null; try {// os = res.getOutputStream();// os.write(bytes);// os.flush(); res.getOutputStream().write(bytes); } catch (IOException e) { System.out.println("not find img.."); } finally { if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } }}参考资料假设个人实战使用可以采用docker快速安装,但是未安装过FastDFS建议普通安装部署,了解一下tracker和storage的使用,以及部署搭建集群。FastDFS普通安装部署:cid:link_2FasdDFS docker安装部署:https://blog.csdn.net/weixin_44621343/article/details/117825755?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-117825755-blog-127896984.235v43pc_blog_bottom_relevance_base6&spm=1001.2101.3001.4242.1&utm_relevant_index=3OpenFeign和FastDFS的类冲突:cid:link_3Gitee地址:cid:link_1
  • [技术干货] Java Gson工具类
    Gson 是 Google 提供的用来在 Java 对象和 JSON 数据之间进行映射的 Java 类库。可以将一个 JSON 字符串转成一个 Java 对象,或者反过来。GsonUtilsimport com.google.gson.*;import com.google.gson.reflect.TypeToken;import lombok.SneakyThrows;import java.lang.reflect.Type;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;import java.time.format.DateTimeFormatter;import java.util.List;import java.util.Map;import java.util.Objects;/** * @Author * @Date 2024/4 * @Des */public class GsonUtils { private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); private static final JsonSerializer<LocalDateTime> dateTimeSerializer = (obj, type, ctx) -> new JsonPrimitive(dateTimeFormatter.format(obj)); private static final JsonSerializer<LocalDate> dateSerializer = (obj, type, ctx) -> new JsonPrimitive(dateFormatter.format(obj)); private static final JsonSerializer<LocalTime> timeSerializer = (obj, type, ctx) -> new JsonPrimitive(timeFormatter.format(obj)); private static final JsonDeserializer<LocalDateTime> dateTimeDeserializer = (json, type, ctx) -> LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(), dateTimeFormatter); private static final JsonDeserializer<LocalDate> dateDeserializer = (json, type, ctx) -> LocalDate.parse(json.getAsJsonPrimitive().getAsString(), dateFormatter); private static final JsonDeserializer<LocalTime> timeDeserializer = (json, type, ctx) -> LocalTime.parse(json.getAsJsonPrimitive().getAsString(), timeFormatter); private static final Gson gson; static { GsonBuilder builder = new GsonBuilder(); builder.disableHtmlEscaping(); builder.enableComplexMapKeySerialization(); // builder.excludeFieldsWithoutExposeAnnotation(); builder.setDateFormat("yyyy-MM-dd HH:mm:ss"); builder.registerTypeAdapter(LocalDateTime.class, dateTimeSerializer); builder.registerTypeAdapter(LocalDate.class, dateSerializer); builder.registerTypeAdapter(LocalTime.class, timeSerializer); builder.registerTypeAdapter(LocalDateTime.class, dateTimeDeserializer); builder.registerTypeAdapter(LocalDate.class, dateDeserializer); builder.registerTypeAdapter(LocalTime.class, timeDeserializer); gson = builder.create(); } public static Type makeJavaType(Type rawType, Type... typeArguments) { return TypeToken.getParameterized(rawType, typeArguments).getType(); } public static String toString(Object value) { if (Objects.isNull(value)) { return null; } if (value instanceof String) { return (String) value; } return toJSONString(value); } public static String toJSONString(Object value) { return gson.toJson(value); } public static String toPrettyString(Object value) { return gson.newBuilder().setPrettyPrinting().create().toJson(value); } public static JsonElement fromJavaObject(Object value) { JsonElement result = null; if (Objects.nonNull(value) && (value instanceof String)) { result = parseObject((String) value); } else { result = gson.toJsonTree(value); } return result; } @SneakyThrows public static JsonElement parseObject(String content) { return JsonParser.parseString(content); } public static JsonElement getJsonElement(JsonObject node, String name) { return node.get(name); } public static JsonElement getJsonElement(JsonArray node, int index) { return node.get(index); } @SneakyThrows public static <T> T toJavaObject(JsonElement node, Class<T> clazz) { return gson.fromJson(node, clazz); } @SneakyThrows public static <T> T toJavaObject(JsonElement node, Type type) { return gson.fromJson(node, type); } public static <T> T toJavaObject(JsonElement node, TypeToken<?> typeToken) { return toJavaObject(node, typeToken.getType()); } public static <E> List<E> toJavaList(JsonElement node, Class<E> clazz) { return toJavaObject(node, makeJavaType(List.class, clazz)); } public static List<Object> toJavaList(JsonElement node) { return toJavaObject(node, new TypeToken<List<Object>>() { }.getType()); } public static <V> Map<String, V> toJavaMap(JsonElement node, Class<V> clazz) { return toJavaObject(node, makeJavaType(Map.class, String.class, clazz)); } public static Map<String, Object> toJavaMap(JsonElement node) { return toJavaObject(node, new TypeToken<Map<String, Object>>() { }.getType()); } @SneakyThrows public static <T> T toJavaObject(String content, Class<T> clazz) { return gson.fromJson(content, clazz); } @SneakyThrows public static <T> T toJavaObject(String content, Type type) { return gson.fromJson(content, type); } public static <T> T toJavaObject(String content, TypeToken<?> typeToken) { return toJavaObject(content, typeToken.getType()); } public static <E> List<E> toJavaList(String content, Class<E> clazz) { return toJavaObject(content, makeJavaType(List.class, clazz)); } public static List<Object> toJavaList(String content) { return toJavaObject(content, new TypeToken<List<Object>>() { }.getType()); } public static <V> Map<String, V> toJavaMap(String content, Class<V> clazz) { return toJavaObject(content, makeJavaType(Map.class, String.class, clazz)); } public static Map<String, Object> toJavaMap(String content) { return toJavaObject(content, new TypeToken<Map<String, Object>>() { }.getType()); }}泛型实体类import com.google.gson.*;import com.google.gson.reflect.TypeToken;import lombok.SneakyThrows;import java.lang.reflect.Type;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;import java.time.format.DateTimeFormatter;import java.util.List;import java.util.Map;import java.util.Objects;/** * @Author * @Date 2024/4 * @Des */public class GsonUtils { private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); private static final JsonSerializer<LocalDateTime> dateTimeSerializer = (obj, type, ctx) -> new JsonPrimitive(dateTimeFormatter.format(obj)); private static final JsonSerializer<LocalDate> dateSerializer = (obj, type, ctx) -> new JsonPrimitive(dateFormatter.format(obj)); private static final JsonSerializer<LocalTime> timeSerializer = (obj, type, ctx) -> new JsonPrimitive(timeFormatter.format(obj)); private static final JsonDeserializer<LocalDateTime> dateTimeDeserializer = (json, type, ctx) -> LocalDateTime.parse(json.getAsJsonPrimitive().getAsString(), dateTimeFormatter); private static final JsonDeserializer<LocalDate> dateDeserializer = (json, type, ctx) -> LocalDate.parse(json.getAsJsonPrimitive().getAsString(), dateFormatter); private static final JsonDeserializer<LocalTime> timeDeserializer = (json, type, ctx) -> LocalTime.parse(json.getAsJsonPrimitive().getAsString(), timeFormatter); private static final Gson gson; static { GsonBuilder builder = new GsonBuilder(); builder.disableHtmlEscaping(); builder.enableComplexMapKeySerialization(); // builder.excludeFieldsWithoutExposeAnnotation(); builder.setDateFormat("yyyy-MM-dd HH:mm:ss"); builder.registerTypeAdapter(LocalDateTime.class, dateTimeSerializer); builder.registerTypeAdapter(LocalDate.class, dateSerializer); builder.registerTypeAdapter(LocalTime.class, timeSerializer); builder.registerTypeAdapter(LocalDateTime.class, dateTimeDeserializer); builder.registerTypeAdapter(LocalDate.class, dateDeserializer); builder.registerTypeAdapter(LocalTime.class, timeDeserializer); gson = builder.create(); } public static Type makeJavaType(Type rawType, Type... typeArguments) { return TypeToken.getParameterized(rawType, typeArguments).getType(); } public static String toString(Object value) { if (Objects.isNull(value)) { return null; } if (value instanceof String) { return (String) value; } return toJSONString(value); } public static String toJSONString(Object value) { return gson.toJson(value); } public static String toPrettyString(Object value) { return gson.newBuilder().setPrettyPrinting().create().toJson(value); } public static JsonElement fromJavaObject(Object value) { JsonElement result = null; if (Objects.nonNull(value) && (value instanceof String)) { result = parseObject((String) value); } else { result = gson.toJsonTree(value); } return result; } @SneakyThrows public static JsonElement parseObject(String content) { return JsonParser.parseString(content); } public static JsonElement getJsonElement(JsonObject node, String name) { return node.get(name); } public static JsonElement getJsonElement(JsonArray node, int index) { return node.get(index); } @SneakyThrows public static <T> T toJavaObject(JsonElement node, Class<T> clazz) { return gson.fromJson(node, clazz); } @SneakyThrows public static <T> T toJavaObject(JsonElement node, Type type) { return gson.fromJson(node, type); } public static <T> T toJavaObject(JsonElement node, TypeToken<?> typeToken) { return toJavaObject(node, typeToken.getType()); } public static <E> List<E> toJavaList(JsonElement node, Class<E> clazz) { return toJavaObject(node, makeJavaType(List.class, clazz)); } public static List<Object> toJavaList(JsonElement node) { return toJavaObject(node, new TypeToken<List<Object>>() { }.getType()); } public static <V> Map<String, V> toJavaMap(JsonElement node, Class<V> clazz) { return toJavaObject(node, makeJavaType(Map.class, String.class, clazz)); } public static Map<String, Object> toJavaMap(JsonElement node) { return toJavaObject(node, new TypeToken<Map<String, Object>>() { }.getType()); } @SneakyThrows public static <T> T toJavaObject(String content, Class<T> clazz) { return gson.fromJson(content, clazz); } @SneakyThrows public static <T> T toJavaObject(String content, Type type) { return gson.fromJson(content, type); } public static <T> T toJavaObject(String content, TypeToken<?> typeToken) { return toJavaObject(content, typeToken.getType()); } public static <E> List<E> toJavaList(String content, Class<E> clazz) { return toJavaObject(content, makeJavaType(List.class, clazz)); } public static List<Object> toJavaList(String content) { return toJavaObject(content, new TypeToken<List<Object>>() { }.getType()); } public static <V> Map<String, V> toJavaMap(String content, Class<V> clazz) { return toJavaObject(content, makeJavaType(Map.class, String.class, clazz)); } public static Map<String, Object> toJavaMap(String content) { return toJavaObject(content, new TypeToken<Map<String, Object>>() { }.getType()); }}示例//Result<String>:Result<String> source = GsonUtils.toJavaObject(c, new TypeToken<Result<String>>() {});String userStatus = Result.getSuccessResult(source, "");//Result<User>:Result<User> source = GsonUtils.toJavaObject(c, new TypeToken<Result<User>>() {});User userInfo = Result.getSuccessResult(source, null);//Result<List<Blog>>:Result<List<Blog>> source = GsonUtils.toJavaObject(c, new TypeToken<Result<List<Blog>>>() {});List<Blog> blogList = Result.getSuccessResult(source, Collections.emptyList());//Result<Map<String, Integer>>:Result<Map<String, Integer>> source = GsonUtils.toJavaObject(c, new TypeToken<Result<Map<String, Integer>>>() { });Map<String, Integer> statistics = Result.getSuccessResult(source, Collections.EMPTY_MAP);
  • [技术干货] SQL PARTITION BY
    前两天看前辈们的老代码,看到了一句神奇的SQL,生平第一次见: select ……………… from     (select xx1,xx2,xx3,……,row_number()      over(partition by xx4,xx5 order by xx6 desc,xx7 desc) rownum      from xxxx_tbl      where xxx8='sdfdsf' ……) where rownum =1 为防止公司说泄露源码,就只能这样表示一下意思了,这句sql的灵魂之处在于row_number() over(partition by xx4,xx5 order by xx6 desc,xx7 desc),你们见没见过我不知道,反正我以前没见过。所以一度没看懂。然后就一探究竟了,为了更加直观,我们还是拿前一篇limit重复问题里的那种表来进行举例。  案例 表结构如下:  字段    类型    注释 id    varchar(20)    主键 col1    varchar(20)    col1 col2    varchar(20)    col2 col3    varchar(20)    col3 全表查询:  SELECT * FROM test1 ORDER BY col1 DESC; 数据为:  id    col1    col2    col3 15    5    9    10 12    2    5    6 14    2    7    8 16    2    4    5 11    1    2    3 其中col1字段不是唯一的,第二第三第四行的col1都是2。 上关键sql:  SELECT id,col1,col2,col3,  row_number() over (PARTITION BY col1 ORDER BY id DESC) AS row_num  FROM test1 ; 返回结果:  id    col1    col2    col3    row_num 11    1    2    3    1 16    2    4    5    1 14    2    7    8    2 12    2    5    6    3 15    5    9    10    1 我们看这个返回结果,其中row_num列即为这段神奇的sql产生的,这一列的序号怎么来的呢:他是按照col1进行分组,然后按照id列进行倒序排序,row_num为分组后组内排序的结果。  分析     看完案例中的sql和执行结果,其实我们就能猜出开头的那个sql的目的了,他是要获取分组后每组的第一个值。其实我们经常会遇到这样的案例,比如:给你一个全年级学生的分数表,我要获取每个班分数最高的前三名。如果说这个年级有多少个班是已知的,我们可以通过union一个一个子查询拼接起来,但是如果班级个数未知,那这时候如果想用一句sql就有点无奈了。     同样,如果我们想要获取的是每个班级分数最高的一个人,我们也可以通过group by加max函数再加子查询解决,但是这里不是一个。  partition by与group by     一开始其实没搞明白,同样是分组partition by和group by有什么区别。从用法上来看: partition by  select xx1,xx2,xx3,……,row_number()      over(partition by xx4,xx5 order by xx6 desc,xx7 desc) rownum      from xxxx_tbl group by  select xx1,max(xx2)     from xxxx_tbl group by xx1 partition by是用在返回参数中的,而group by是用在约束里的。而深层里去理解,partition by是分组后进行组内逐条分析,比如这里的row_number() over,而groupy by则是分组后进行整组的聚合分析,比如上面的max()。  扩展     既然是用于分析的,肯定有一些常用的与之配合的分析函数,比如group常和sum、min、max等组合使用。partition by除了上面的row_number外还有以下一些常用的配合: max:获取组内已排序的最大值 SELECT id,col1,col2,col3, MAX(col3)  over (PARTITION BY col1 ORDER BY id DESC) AS row_num FROM test1 ; id    col1    col2    col3    row_num 11    1    2    3    3 16    2    4    5    5 14    2    7    8    8 12    2    5    6    8 15    5    9    10    10 rank:排名的时候用row_number不是很好,原因是如果有两行order by的id相同,那么row_number就会漏掉其中的一行,而rank则不会漏,同时rank是跳跃排名,比如有两个第二名,那第四个就是第四名,而不是第三名 SELECT id,col1,col2,col3, rank()  over (PARTITION BY col1 ORDER BY id DESC) AS row_num FROM test1 ; id    col1    col2    col3    row_num 11    1    2    3    1 16    2    4    5    1 14    2    7    8    2 12    2    5    6    3 15    5    9    10    1 dense_rank:和rank一样,也是能够查出所有的记录,但是他不是跳跃排名,两个第二名之后是第三名。 SELECT id,col1,col2,col3, dense_rank() over (PARTITION BY col1 ORDER BY id DESC) AS row_num FROM test1 ; id    col1    col2    col3    row_num 11    1    2    3    1 16    2    4    5    1 14    2    7    8    2 12    2    5    6    3 15    5    9    10    1 ————————————————  原文链接:https://blog.csdn.net/qq_30095631/article/details/103558652 
  • [技术干货] partition 子句_SQL PARTITION BY子句概述
    partition 子句This article will cover the SQL PARTITION BY clause and, in particular, the difference with GROUP BY in a select statement. We will also explore various use case of SQL PARTITION BY.本文将介绍SQL PARTITION BY子句,尤其是select语句中与GROUP BY的区别。 我们还将探讨SQL PARTITION BY的各种用例。We use SQL PARTITION BY to divide the result set into partitions and perform computation on each subset of partitioned data.我们使用SQL PARTITION BY将结果集划分为多个分区,并对分区数据的每个子集执行计算。准备样品数据 (Preparing Sample Data )Let us create an Orders table in my sample database SQLShackDemo and insert records to write further queries.让我们在示例数据库SQLShackDemo中创建一个Orders表,并插入记录以编写进一步的查询。Use SQLShackDemoGoCREATE TABLE [dbo].[Orders](    [orderid] INT,    [Orderdate] DATE,    [CustomerName] VARCHAR(100),    [Customercity] VARCHAR(100),     [Orderamount] MONEY)I use ApexSQL Generate to insert sample data into this article. Right click on the Orders table and Generate test data.我使用ApexSQL Generate将示例数据插入本文。 右键单击“订单”表并生成测试数据 。It launches the ApexSQL Generate. I generated a script to insert data into the Orders table. Execute this script to insert 100 records in the Orders table.它启动ApexSQL生成。 我生成了一个脚本,用于将数据插入到Orders表中。 执行此脚本以在Orders表中插入100条记录。USE [SQLShackDemo]GOINSERT [dbo].[Orders]  VALUES (216090, CAST(N'1826-12-19' AS Date), N'Edward', N'Phoenix', 4713.8900)GOINSERT [dbo].[Orders]  VALUES (508220, CAST(N'1826-12-09' AS Date), N'Aria', N'San Francisco', 9832.7200)GO…Once we execute insert statements, we can see the data in the Orders table in the following image.执行插入语句后,我们可以在下图中的Orders表中看到数据。We use SQL GROUP BY clause to group results by specified column and use aggregate functions such as Avg(), Min(), Max() to calculate required values.我们使用SQL GROUP BY子句按指定的列对结果进行分组,并使用诸如Avg(),Min(),Max()之类的聚合函数来计算所需的值。按功能分组 (Group By function syntax)SELECT expression, aggregate function ()FROM tablesWHERE conditionsGROUP BY expressionSuppose we want to find the following values in the Orders table假设我们要在“订单”表中找到以下值Minimum order value in a city一个城市的最小订单价值Maximum order value in a city城市中的最大订单价值Average order value in a city一个城市的平均订单价值Execute the following query with GROUP BY clause to calculate these values.使用GROUP BY子句执行以下查询以计算这些值。SELECT Customercity,        AVG(Orderamount) AS AvgOrderAmount,        MIN(OrderAmount) AS MinOrderAmount,        SUM(Orderamount) TotalOrderAmountFROM [dbo].[Orders]GROUP BY Customercity;In the following screenshot, we can see Average, Minimum and maximum values grouped by CustomerCity.在以下屏幕截图中,我们可以看到按CustomerCity分组的平均值,最小值和最大值。Now, we want to add CustomerName and OrderAmount column as well in the output. Let’s add these columns in the select statement and execute the following code.现在,我们要在输出中也添加CustomerName和OrderAmount列。 让我们将这些列添加到select语句中,并执行以下代码。 SELECT Customercity, CustomerName ,OrderAmount,       AVG(Orderamount) AS AvgOrderAmount,        MIN(OrderAmount) AS MinOrderAmount,        SUM(Orderamount) TotalOrderAmountFROM [dbo].[Orders]GROUP BY Customercity;Once we execute this query, we get an error message. In the SQL GROUP BY clause, we can use a column in the select statement if it is used in Group by clause as well. It does not allow any column in the select clause that is not part of GROUP BY clause.一旦执行此查询,我们将收到一条错误消息。 在SQL GROUP BY子句中,如果同时在Group by子句中使用它,则可以在select语句中使用一列。 它不允许select子句中的任何列不属于GROUP BY子句。We can use the SQL PARTITION BY clause to resolve this issue. Let us explore it further in the next section.我们可以使用SQL PARTITION BY子句解决此问题。 让我们在下一部分中进一步探讨它。SQL分区依据 (SQL PARTITION BY)We can use the SQL PARTITION BY clause with the OVER clause to specify the column on which we need to perform aggregation. In the previous example, we used Group By with CustomerCity column and calculated average, minimum and maximum values.我们可以将SQL PARTITION BY子句与OVER子句一起使用,以指定需要对其进行聚合的列。 在上一个示例中,我们将“分组依据”与“ CustomerCity”一起使用,并计算了平均值,最小值和最大值。Let us rerun this scenario with the SQL PARTITION BY clause using the following query.让我们使用以下查询,使用SQL PARTITION BY子句重新运行此方案。SELECT Customercity,        AVG(Orderamount) OVER(PARTITION BY Customercity) AS AvgOrderAmount,        MIN(OrderAmount) OVER(PARTITION BY Customercity) AS MinOrderAmount,        SUM(Orderamount) OVER(PARTITION BY Customercity) TotalOrderAmountFROM [dbo].[Orders];In the output, we get aggregated values similar to a GROUP By clause. You might notice a difference in output of the SQL PARTITION BY and GROUP BY clause output.在输出中,我们获得类似于GROUP BY子句的聚合值。 您可能会注意到SQL PARTITION BY和GROUP BY子句输出的输出有所不同。Group BySQL PARTITION BYWe get a limited number of records using the Group By clauseWe get all records in a table using the PARTITION BY clause.It gives one row per group in result set. For example, we get a result for each group of CustomerCity in the GROUP BY clause.It gives aggregated columns with each record in the specified table.We have 15 records in the Orders table. In the query output of SQL PARTITION BY, we also get 15 rows along with Min, Max and average values.通过...分组SQL分区依据我们使用Group By子句获得的记录数量有限我们使用PARTITION BY子句获取表中的所有记录。它在结果集中为每组一行。 例如,我们在GROUP BY子句中为CustomerCity的每个组获取结果。它为指定表中的每条记录提供汇总列。我们在订单表中有15条记录。 在SQL PARTITION BY的查询输出中,我们还获得15行以及Min,Max和平均值。In the previous example, we get an error message if we try to add a column that is not a part of the GROUP BY clause.在上一个示例中,如果尝试添加不属于GROUP BY子句的列,则会收到一条错误消息。We can add required columns in a select statement with the SQL PARTITION BY clause. Let us add CustomerName and OrderAmount columns and execute the following query.我们可以使用SQL PARTITION BY子句在select语句中添加必需的列。 让我们添加CustomerName和OrderAmount列并执行以下查询。SELECT Customercity,        CustomerName,        OrderAmount,        AVG(Orderamount) OVER(PARTITION BY Customercity) AS AvgOrderAmount,        MIN(OrderAmount) OVER(PARTITION BY Customercity) AS MinOrderAmount,        SUM(Orderamount) OVER(PARTITION BY Customercity) TotalOrderAmountFROM [dbo].[Orders];We get CustomerName and OrderAmount column along with the output of the aggregated function. We also get all rows available in the Orders table.我们获得CustomerName和OrderAmount列以及聚合函数的输出。 我们还将在Orders表中获得所有可用行。In the following screenshot, you can for CustomerCity Chicago, it performs aggregations (Avg, Min and Max) and gives values in respective columns.在以下屏幕截图中,您可以为CustomerCity Chicago进行聚合(平均,最小和最大)并在相应的列中提供值。Similarly, we can use other aggregate functions such as count to find out total no of orders in a particular city with the SQL PARTITION BY clause.同样,我们可以使用其他聚合函数(例如count)来通过SQL PARTITION BY子句找出特定城市的订单总数。SELECT Customercity,        CustomerName,        OrderAmount,        COUNT(OrderID) OVER(PARTITION BY Customercity) AS CountOfOrders,        AVG(Orderamount) OVER(PARTITION BY Customercity) AS AvgOrderAmount,        MIN(OrderAmount) OVER(PARTITION BY Customercity) AS MinOrderAmount,        SUM(Orderamount) OVER(PARTITION BY Customercity) TotalOrderAmountFROM [dbo].[Orders];We can see order counts for a particular city. For example, we have two orders from Austin city therefore; it shows value 2 in CountofOrders column.我们可以看到特定城市的订单计数。 例如,因此,我们有两个来自奥斯丁市的订单; 它在CountofOrders列中显示值2。带有ROW_NUMBER()的PARTITION BY子句 (PARTITION BY clause with ROW_NUMBER())We can use the SQL PARTITION BY clause with ROW_NUMBER() function to have a row number of each row. We define the following parameters to use ROW_NUMBER with the SQL PARTITION BY clause.我们可以将SQL PARTITION BY子句与ROW_NUMBER()函数一起使用,以获取每行的行号。 我们定义以下参数以将ROW_NUMBER与SQL PARTITION BY子句一起使用。PARTITION BY column – In this example, we want to partition data on PARTITION BY列 –在此示例中,我们要对CustomerCity column CustomerCity列上的数据进行分区Order By: In the ORDER BY column, we define a column or condition that defines row number. In this example, we want to sort data on the OrderAmount column排序依据:在ORDER BY列中,我们定义一列或条件来定义行号。 在此示例中,我们要对OrderAmount列上的数据进行排序SELECT Customercity,        CustomerName,        ROW_NUMBER() OVER(PARTITION BY Customercity       ORDER BY OrderAmount DESC) AS "Row Number",        OrderAmount,        COUNT(OrderID) OVER(PARTITION BY Customercity) AS CountOfOrders,        AVG(Orderamount) OVER(PARTITION BY Customercity) AS AvgOrderAmount,        MIN(OrderAmount) OVER(PARTITION BY Customercity) AS MinOrderAmount,        SUM(Orderamount) OVER(PARTITION BY Customercity) TotalOrderAmountFROM [dbo].[Orders];In the following screenshot, we get see for CustomerCity Chicago, we have Row number 1 for order with highest amount 7577.90. it provides row number with descending OrderAmount.在以下屏幕截图中,我们看到了CustomerCity Chicago ,我们的订单行1为最高金额7577.90。 它为行号提供降序的OrderAmount。具有累积总值的PARTITION BY子句 (PARTITION BY clause with Cumulative total value)Suppose we want to get a cumulative total for the orders in a partition. Cumulative total should be of the current row and the following row in the partition.假设我们要获得分区中订单的累计总数。 累积总数应该是分区中当前行和下一行的总和。For example, in the Chicago city, we have four orders.例如,在芝加哥市,我们有四个订单。CustomerCityCustomerNameRankOrderAmountCumulative Total RowsCumulative TotalChicagoMarvin17577.9Rank 1 +214777.51ChicagoLawrence27199.61Rank 2+314047.21ChicagoAlex36847.66Rank 3+48691.49ChicagoJerome41843.83Rank 41843.83客户城市顾客姓名秩订单金额累积总行累计总数芝加哥Maven1个7577.9等级1 +214777.51芝加哥劳伦斯27199.61等级2 + 314047.21芝加哥亚历克斯36847.66等级3 + 48691.49芝加哥杰罗姆41843.83等级41843.83In the following query, we the specified ROWS clause to select the current row (using CURRENT ROW) and next row (using 1 FOLLOWING). It further calculates sum on those rows using sum(Orderamount) with a partition on CustomerCity ( using OVER(PARTITION BY Customercity ORDER BY OrderAmount DESC).在下面的查询中,我们指定了ROWS子句以选择当前行(使用CURRENT ROW)和下一行(使用1 FOLLOWING)。 它还使用sum(Orderamount)和CustomerCity上的分区(使用OVER(PARTITION BY Customercity或ORDER BY OrderAmount DESC))来计算这些行上的总和。SELECT Customercity,        CustomerName,        OrderAmount,        ROW_NUMBER() OVER(PARTITION BY Customercity       ORDER BY OrderAmount DESC) AS "Row Number",        CONVERT(VARCHAR(20), SUM(orderamount) OVER(PARTITION BY Customercity       ORDER BY OrderAmount DESC ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING), 1) AS CumulativeTotal,Similarly, we can calculate the cumulative average using the following query with the SQL PARTITION BY clause.同样,我们可以使用带有SQL PARTITION BY子句的以下查询来计算累积平均值。 SELECT Customercity,        CustomerName,        OrderAmount,        ROW_NUMBER() OVER(PARTITION BY Customercity       ORDER BY OrderAmount DESC) AS "Row Number",        CONVERT(VARCHAR(20), AVG(orderamount) OVER(PARTITION BY Customercity       ORDER BY OrderAmount DESC ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING), 1) AS CumulativeAVG用PARTITION BY子句限制行的行数 (ROWS UNBOUNDED PRECEDING with the PARTITION BY clause)We can use ROWS UNBOUNDED PRECEDING with the SQL PARTITION BY clause to select a row in a partition before the current row and the highest value row after current row.我们可以将ROWS UNBOUNDED PRECEDING与SQL PARTITION BY子句一起使用,以选择分区中当前行之前的行以及当前行之后的最大值行。In the following table, we can see for row 1; it does not have any row with a high value in this partition. Therefore, Cumulative average value is the same as of row 1 OrderAmount.在下表中,我们可以看到第1行; 该分区中没有任何具有高值的行。 因此,累积平均值与第1行OrderAmount相同。For Row2, It looks for current row value (7199.61) and highest value row 1(7577.9). It calculates the average for these two amounts.对于第2行,它将查找当前行值(7199.61)和最高值行1(7577.9)。 它计算这两个数量的平均值。For Row 3, it looks for current value (6847.66) and higher amount value than this value that is 7199.61 and 7577.90. It calculates the average of these and returns.对于第3行,它将查找当前值(6847.66)和比该值更高的金额值7199.61和7577.90。 它计算这些平均值并返回。CustomerCityCustomerNameRankOrderAmountCumulative Average RowsCumulative AverageChicagoMarvin17577.9Rank 17577.90ChicagoLawrence27199.61Rank 1+27388.76ChicagoAlex36847.66Rank 1+2+37208.39ChicagoJerome41843.83Rank 1+2+3+45867.25客户城市顾客姓名秩订单金额累积平均行累积平均值芝加哥Maven1个7577.9等级17577.90芝加哥劳伦斯27199.61等级1 + 27388.76芝加哥亚历克斯36847.66等级1 + 2 + 37208.39芝加哥杰罗姆41843.83等级1 + 2 + 3 + 45867.25Execute the following query to get this result with our sample data.执行以下查询以通过我们的样本数据获得此结果。 SELECT Customercity,        CustomerName,        OrderAmount,        ROW_NUMBER() OVER(PARTITION BY Customercity       ORDER BY OrderAmount DESC) AS "Row Number",        CONVERT(VARCHAR(20), AVG(orderamount) OVER(PARTITION BY Customercity       ORDER BY OrderAmount DESC ROWS UNBOUNDED PRECEDING), 1) AS CumulativeAvgFROM [dbo].[Orders];结论 (Conclusion)In this article, we explored the SQL PARTIION BY clause and its comparison with GROUP BY clause. We also learned its usage with a few examples. I hope you find this article useful and feel free to ask any questions in the comments below在本文中,我们探讨了SQL PARTIION BY子句及其与GROUP BY子句的比较。 我们还通过一些示例了解了它的用法。 希望本文对您有所帮助,并随时在下面的评论中提问原文链接:https://blog.csdn.net/culuo4781/article/details/107618029
  • [技术干货] mysql表分区
    1.分表与表分区的区别 1.1 关于分表 分表是将一个大表分为几个或是多个小表,例如:table_1每天有1Kw的数据量,table_1随便时间的增长会越来越大,最终达到mysql表的极限,在这种比较极端的情况下 我们可以考虑对table_01进行分表操作,即每天生成与table_1表同样的表,每天一张即table_1_20120920 更多详细:http://blog.51yip.com/mysql/949.html  1.2 关于分区  以myisam为例子,mysql数据库中的数据是以文件的形势存在磁盘上,一张表主要对应着三个文件,一个是frm存放表结构文件,一个存放表数据的,一个是myi存表索引。 也就是将一个表文件分为多个表文件在磁盘上进行存取,提高对io的使用。  1.3 是否支持分区  mysql> show variables like ‘%partition%’; +——————-+——-+ | Variable_name | Value | +——————-+——-+ | have_partitioning | YES | +——————-+——-+ 出现YES表示当前版本支持表分区  1.4 查看分区表信息 select * from INFORMATION_SCHEMA.PARTITIONS where TABLE_SCHEMA=’tablename’ 2.如何分区 2.1 分区方法 分区有二个方法: 水平分区、垂直分区 2.2 分区的类型 === 水平分区的几种模式:=== * Range(范围) – 这种模式允许DBA将数据划分不同范围。例如DBA可以将一个表通过年份划分成三个分区,80年代(1980′s)的数据,90年代(1990′s)的数据以及任何在2000年(包括2000年)后的数据。  * Hash(哈希) – 这中模式允许DBA通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区,。例如DBA可以建立一个对表主键进行分区的表。  * Key(键值) – 上面Hash模式的一种延伸,这里的Hash Key是MySQL系统产生的。  * List(预定义列表) – 这种模式允许系统通过DBA定义的列表的值所对应的行数据进行分割。例如:DBA建立了一个横跨三个分区的表,分别根据2004年2005年和2006年值所对应的数据。  * Composite(复合模式) – 很神秘吧,哈哈,其实是以上模式的组合使用而已,就不解释了。举例:在初始化已经进行了Range范围分区的表上,我们可以对其中一个分区再进行hash哈希分区。  = 垂直分区(按列分)= 举个简单例子:一个包含了大text和BLOB列的表,这些text和BLOB列又不经常被访问,这时候就要把这些不经常使用的text和BLOB了划分到另一个分区,在保证它们数据相关性的同时还能提高访问速度。 2.2 代码演示 range分区如下:  –按天进行划分 错误代码 create table part_range ( id bigint not null auto_increment, ftime date, str text )engine=myisam partition by range (ftime) ( partition p0 values less than (to_days(’2012-09-21′)), partition p1 values less than (to_days(’2012-09-22′)) ) 错误原因: 1. 在partition by range (ftime),ftime需要加to_days转成数字 2.进行分区的字段需要是主键的一部分1 使用以上语句创建时报错 ‘A PRIMARY KEY must include all columns in the table’s partitioning function” 默认分区限制分区字段必须是主键(PRIMARY KEY)的一部分  –按天进行分区 –直接使用时间列不可以,RANGE分区函数返回的列需要是整型。 create table part_range ( id bigint not null auto_increment, ftime date, str text, primary key(id,ftime) )engine=myisam partition by range (to_days(ftime)) ( partition p0 values less than (to_days(’2012-09-21′)), partition p1 values less than (to_days(’2012-09-22′)), PARTITION p3 VALUES LESS THAN MAXVALUE );  –按小时进行分区 create table part_range_day ( id bigint not null auto_increment, ftime datetime, str text, primary key(id,ftime) )engine=myisam partition by range(hour(ftime)) ( partition p0 values less than (1), partition p1 values less than(2), PARTITION p3 VALUES LESS THAN MAXVALUE ) [Err] 1493 – VALUES LESS THAN value must be strictly increasing for each partition  使用id进行划分时  –按id进行划分,id只能是由小到大 create table part_range_id ( id bigint not null auto_increment, ftime datetime, str text, primary key(id,ftime) )engine=myisam partition by range (id) ( partition p0 values less than (10000), partition p1 values less than (20000), partition p2 values less than maxvalue )  –使用list  create table part_range_list ( id bigint not null auto_increment, ftime datetime, str text, primary key(id,ftime) )engine=myisam partition by list (id) ( partition p0 values in (0,1), partition p1 values in (2,4) )  –5.5之后的mysql说可以支持字符目前使用的5.5.24版本,使用字符时依然提示 [Err] 1697 – VALUES value for partition ‘p0′ must have type INT  3.性能测试  硬件:3.6G 内存 cpu Intel(R) Pentiun(R) 软件:win xp2 32位 –没有加分区表part_no_test,myisam引擎  create table part_no_test ( id bigint primary key auto_increment, ftime datetime, str text )engine=myisam  –加入分区的表part_test,myisam引擎,以小时划分四个区 create table part_test ( id bigint auto_increment, ftime datetime, str text, primary key(id,ftime) )engine=myisam partition by range(hour(ftime)) ( partition p0 values less than (6), partition p1 values less than (12), partition p3 values less than (18), partition p4 values less than maxvalue )  –随机数据构造,构造2kw数据量 INSERT INTO part_test(ftime,str)values(FROM_UNIXTIME(unix_timestamp(’2012-09-20 08:00:00′)+FLOOR(7 + (RAND() * 360000))),’sss’); 生成数据存储过程 —生成part_no_test数据 drop procedure if exists part_no_insert_data; create procedure part_insert_data() begin set @id=20000000; while @id>0 do INSERT INTO part_no_test(ftime,str)values(FROM_UNIXTIME(unix_timestamp(’2012-09-20 08:00:00′)+FLOOR(7 + (RAND() * 360000))),RAND()*RAND()*100000000000); set @id=@id-1; end while; end;  —生成part_test数据  drop procedure if exists part_insert_data; create procedure part_insert_data() begin set @id=20000000; while @id>0 do INSERT INTO part_test(ftime,str)values(FROM_UNIXTIME(unix_timestamp(’2012-09-20 08:00:00′)+FLOOR(7 + (RAND() * 360000))),RAND()*RAND()*100000000000); set @id=@id-1; end while; end;  –有分区查询语句 select * from part_test a where a.ftime>’2012-09-20 10:00:00′ and a.ftime<’2012-09-20 12:00:00′;  执行时间:09.906s  –无分区查询语句 select * from part_no_test a where a.ftime>’2012-09-20 10:00:00′ and a.ftime<’2012-09-20 12:00:00′;  执行时间:23.281s  附: Mysql可用的分区函数 DAY() DAYOFMONTH() DAYOFWEEK() DAYOFYEAR() DATEDIFF() EXTRACT() HOUR() MICROSECOND() MINUTE() MOD() MONTH() QUARTER() SECOND() TIME_TO_SEC() TO_DAYS() WEEKDAY() YEAR() YEARWEEK() 等 当然,还有FLOOR(),CEILING() 等,前提是使用这两个分区函数的分区健必须是整型。 要小心使用其中的一些函数,避免犯逻辑性的错误,引起全表扫描。  注: 1.分区的新增、删除每次只能是一个 2.maxvalues 后面不能再加分区 3.分区键必须包含在主键中 ERROR 1503 (HY000): A PRIMARY KEY must include all columns in the table’s partitioning function’ 4.ERROR 1503 (HY000): A UNIQUE INDEX must include all columns in the table’s partitioning function’说明在表上建约束索引(如唯一索引,普通索引可以)会有问题,必须把约束索引列包含在分区健内 5.只有RANGE和LIST分区才能有子分区,每个分区的子分区数量必须相同, 6. MYSQL将NULL值视为0.自动插入最小的分区中。  = 初步结论 = * 分区和未分区占用文件空间大致相同 (数据和索引文件) * 如果查询语句中有未建立索引字段,分区时间远远优于未分区时间 * 如果查询语句中字段建立了索引,分区和未分区的差别缩小,分区略优于未分区。 = 最终结论 = * 对于大数据量,建议使用分区功能。 * 去除不必要的字段 * 根据手册, 增加myisam_max_sort_file_size 会增加分区性能 ————————————————                   原文链接:https://blog.csdn.net/ls3648098/article/details/9353623 
  • [技术干货] SQLServer 窗口函数(OVER、PARTITION BY)
    一、窗口函数的作用窗口函数是对一组值进行操作,不需要使用GROUP BY 子句对数据进行分组,还能够在同一行中同时返回基础行的列和聚合列。窗口函数,基础列和聚合列的查询都非常简单。二、语法格式  窗口函数的语法格式如下:1OVER([PARTITION  BY  value_expression,..[n] ] < ORDER  BY  BY_Clause>)PARTITION:分组;ORDER BY:排序;首先建一张调试表如下:123456CREATE  TABLE  [dbo].[xxx]([Id] [ int ]  NULL ,[ Name ] [nvarchar](50)  NULL ,[Operate] [nvarchar](50)  NULL ,[Score] [ int ]  NULL ,[CreateTime] [datetime]  NULL )  ON  [ PRIMARY ]往里面添加如下数据。三、应用场景1、聚合列与数据列共同显示12--查询姓名、分数、以及全体平均分SELECT  Name , Score,  CAST ( AVG (Score) OVER()  AS  decimal (5,2) )  AS  '平均分'  FROM  xxx  2、分组日期最新1234--对每个人查询日期最新列SELECT  *  FROM  (     SELECT  row_number() OVER(PARTITION  BY  Name  ORDER  BY  CreateTime)  AS  part ,Score,  Name , CreateTime  FROM  xxx)  AS  CWHERE C.part = 13、分页返回结果集内的行号,每个分区从1开始,ORDER BY可确定在特定分区中为行分配唯一 ROW_NUMBER 的顺序。四、排名函数1、ROW_NUMBER()返回结果集内的行号,每个分区从1开始计算,ORDER BY可确定在特定分区中为行分配唯一 ROW_NUMBER 的顺序。1SELECT  row_number() OVER(PARTITION  BY  Name  ORDER  BY  CreateTime) ,Score,  Name , CreateTime  FROM  xxx输出如下:2、RANK()返回结果集的分区内每行的排序。行的排名是从1开始算。如果两个或多个行与一个排名关联,则每个关联行将得到相同的排名。1SELECT  RANK() OVER(PARTITION  BY  Name  ORDER  BY  SCORE) ,Score,  Name , CreateTime  FROM  xxx  下面一张图片很好地说明了Rank与ROW_NUMBER的区别。    3、DENSE_RANK()  返回结果集分区中行的排名,与Rank()类似,只是对并列的处理稍有不同,详见示例。SELECT DENSE_RANK() OVER(PARTITION BY Name ORDER BY SCORE) ,Score, Name, CreateTimeFROM xxx  下面的示例展示了Rank()与Dense_Rank()的区别。    4、NTILE()   NTILE函数把结果中的行关联到组,并为每一行分配一个所属的组的编号,编号从1开始。对于每一个行,NTILE 将返回此行所属的组的编号。  如果分区的行数不能被 integer_expression(就是传入的那个参数,表示分几个组的意思) 整除,则将导致一个成员有两种大小不同的组。按照 OVER 子句指定的顺序,较大的组排在较小的组前面。--每个分区分2个组,该列是改行所属的组名SELECT NTILE(2) OVER(PARTITION BY Name ORDER BY SCORE) ,Score, Name, CreateTimeFROM xxx原文链接:https://blog.csdn.net/xoopx/article/details/51777733
  • [技术干货] sqlserver 分页查询
    1.分页查询  ROW_NUMBER()  OVER(ORDER BY COLUMMS) IF OBJECT_ID('SysLogInfo','u') IS NULL --不存在用户表 BEGIN      CREATE TABLE SysLogInfo(        ID NVARCHAR(50) NOT NULL PRIMARY KEY,        Name NVARCHAR(50) NULL,        OldName NVARCHAR(50) NULL     )     INSERT INTO dbo.SysLogInfo( ID, Name, OldName )     VALUES  ( '1','张三','张二'),             ( '2','李四','张二'),             ( '3','赵武','赵六'),             ( '4','钱7','赵六') END  --分页查询 DECLARE @page INT =1 DECLARE @pageSize INT =2   SELECT ID,Name,OldName  FROM  ( SELECT *,ROW_NUMBER() OVER (ORDER BY ID asc) row FROM  dbo.SysLogInfo) a  WHERE row BETWEEN (@page-1)*@pageSize + 1 AND @page * @pageSize 2.ROW_NUMBER() OVER (PARTITION BY COL1 ORDER BY COL2) 可以返回一个分组的多条记录  SELECT *,ROW_NUMBER() OVER (PARTITION BY OldName ORDER BY  ID ) COUNT  FROM  dbo.SysLogInfo  结果: partition  by关键字是分析性函数的一部分,它和聚合函数(如group by)不同的地方在于它能返回一个分组中的多条记录,而聚合函数一般只有一条反映统计值的记录。 partition  by用于给结果集分组,如果没有指定那么它把整个结果集作为一个分组。 partition by 与group by不同之处在于前者返回的是分组里的每一条数据,并且可以对分组数据进行排序操作。后者只能返回聚合之后的组的数据统计值的记录。 ————————————————                     原文链接:https://blog.csdn.net/wodemingzijiaoke/article/details/90612385 
  • [技术干货] Sqlserver OVER(PARTITION BY)的简单理解
     ROW_NUMBER() OVER(…) 虽然写法很复杂,但这确实只是一个普通函数(就像字符串转数字这样的函数),可以得出一个值 这个函数不会改变数据条数,它的作用是给每个数据记录增加一个字段,这个字段值就是函数得到的值 函数虽然不会改变查询结果条数,但会改变结果的顺序。会按照某个字段a(这个字段会重复)分组,函数的值就是在每个分组内部的排序值(1,2,3,4.。。) 比如:一群人在排队买东西。会自动以家庭为单位分成一段一段的“小组”,然后又在家庭内部按照年龄大小做了排序(比如年龄大人在前面,小孩跟在大人后面),这个函数值就是这个家庭内部排序值 按照上面的比喻,这个函数就是ROW_NUMBER() over (PARTITION BY 家庭编号 order by 年龄) 函数完整的样子:ROW_NUMBER() OVER(PARTITION BY… ORDER BY…)。为什么要很啰嗦的再加一个ROW_NUMBER()? 因为前面这个ROW_NUMBER()会进一步控制函数的特性,后面会讲解【不过我个人认为这个前缀确实很啰嗦。让我设计的话,我会去掉它。因为对于大部分人可能就只会用最普通的函数使用方式,不写时就默认为ROW_NUMBER()多好】 类比一个完整的查询 select *, ROW_NUMBER() over (PARTITION BY 家庭编号 order by 年龄 desc) as “家庭地位” from 人员表 这句话除了得到了所有人员的信息之外,还额外得到了一个字段"家庭地位"(这里就默认:年龄越大,家庭地位越高)  理解了函数的含义和用法,然后我们就可以利用函数,比如,只获取所有家庭地位最高的人,那么就可以在我们再包一层,得到: select * from( select *, ROW_NUMBER() over (PARTITION BY 家庭编号 order by 年龄 desc) as “家庭地位” from 人员表 ) t where t.家庭地位=1  引申 这样就可以解决类似:表T中 字段a会重复,但我们查询的结果又不想要重复的数据,并且要求只要其中最新的那一条 select * from( select *, ROW_NUMBER() over (PARTITION BY a order by create_time desc) as index from T ) t where t.index=1  或者topN问题:分组后,把每组前5个找出来 select * from( select *, ROW_NUMBER() over (PARTITION BY a order by create_time desc) as index from T ) t where t.index < 6  ROW_NUMBER() 这是一个控制 OVER函数特性的参数,而且不能省,over前面必须有一个。但并不是只有ROW_NUMBER()这一种。  假设在一共家庭中,有四个孩子,其中两个是双胞胎,年龄分别是5岁,7岁,7岁,9岁。 两个7岁的双胞胎 谁大谁小是很难说。理论上可以讲,两个孩子年龄是相同的。那么排队时,谁是老大谁是老二呢。 ROW_NUMBER():简单粗暴的做了自己的判断,哪条数据在前面,哪个就是老大(谁先生出来谁就是老大)。即便他们年龄一样(over函数值一样),它给这几个孩子定的家庭地位分别为4,3,2,1 DENSE_RENK():显得更公正一点:既然定好了按年龄排序,那么年龄相同,地位就是相同。它给四个孩子定的家庭地位分别为:3,2,2,1 RENK():比较特殊,它觉得那个5岁的孩子的家庭地位不应该是第3位。因为家里明明有4个孩子,它是最小的,两个并列第二之后,下一个应该是4(所以直接把3给跳过了)。所以它给的家庭地位分别是4,2,2,1  名称 下面这些都是这个函数的名称: 窗口函数,分析函数,分区函数,一般称为窗口函数(window function)。 一般关系型数据库都支持。由于属于比较高级的函数,都是在数据库不断完善的过程中增加的。比如mysql就是8.x之后才有。  和聚合函数对比 聚合函数:sum(), avg() 等统计函数 配合 group by 称为聚合函数 得出的是分组后 每一组等统计数据,改变了数据条数 如果想保持数据原有的样子,则需要使用窗口函数  窗口函数前面除了前面介绍的常用的三种,还可以使用sum() avg()等统计函数,变成: sum() over(partition by ...) x 这里得到的x,就是对这个组内的求和,组内每一条数据都得到一个相同的值  进阶-和排序函数对比 虽然前面说窗口函数的核心是partition by,但实际上partition by也可以去掉。 也就是说:xxx() over(…) 这个函数非常灵活。 其中partition by也可去掉,但此时必须要有order by。 变成:xxx() over(order by …) xxx 此时窗口函数 的窗口就只有一个了:所有数据都在同一个窗口里。 只剩下排序功能,比如 dense_renk() over(over by 成绩) 名次 常见作用:给班级学生成绩排名,成绩一样的,名次就一样 ————————————————    原文链接:https://blog.csdn.net/yunduanyou/article/details/122583303 
  • [技术干货] 分区函数Partition By的基本用法
    1.窗口函数 (1)partition by窗口函数 和 group by分组的区别: partition by关键字是分析性函数的一部分,它和聚合函数(如group by)不同的地方在于它能返回一个分组中的多条记录,而聚合函数一般只有一条反映统计值的记录。 partition by用于给结果集分组,如果没有指定那么它把整个结果集作为一个分组。 partition by与group by不同之处在于前者返回的是分组里的每一条数据,并且可以对分组数据进行排序操作。后者只能返回聚合之后的组的数据统计值的记录。 partition by相比较于group by,能够在保留全部数据的基础上,只对其中某些字段做分组排序(类似excel中的操作),而group by则只保留参与分组的字段和聚合函数的结果; 简单来说窗口函数对部分数据进行排序、计算等操作,group by对一组值进行聚合,即窗口函数是每一行都会保留,group by是从多行浓缩为少数行。 (2)窗口函数基本语法 <窗口函数> over ( partition by<用于分组的列名> order by <用于排序的列名>) (3)窗口函数  专用窗口函数: rank(), dense_rank(), row_number() 聚合函数 : sum(), max(), min(), count(), avg() 等 2.窗口函数的使用 2.1 over函数的写法: over(partition by type order by price desc) --先对 type 中相同的进行分区,在 type 中相同的情况下对 price 进行排序 2.2 专用窗口函数 rank() 和 row_number() 以及 dense_rank() SELECT *,rank() over(partition by type order by price desc) as  mm from commodity; SELECT *,row_number() over(partition by type order by price desc) as  mm from commodity; SELECT *,dense_rank() over(partition by type order by price desc) as  mm from commodity; 从以上结果来看: rank()函数:如果存在并列名次的行,会占用下一个名次的位置,比如苹果的组内排名 1,2,3,4, 但是由于有两个是并列的,所以显示的排名是 1,1,3,4 ,其中 2 的位置还是被占用了 row_number()函数:不考虑并列的情况,此函数即使遇到了price 相同的情况,还是会默认排出一个先后来 dense_rank()函数:如果存在并列名次的行,不会占用下一个名次的位置,例如图片的最后显示的是 1,1,2,3  2.3 聚合函数作为窗口函数 (1) sum() SELECT *,sum(price) over(partition by type order by price) as sum  from commodity; 在进行求和的时候是这样的,当前行的 sum 值是组内当前行与其组内当前行之前所有行的和,例如红色圈出来不的数据,橙子第一行是 6 ,第二行是 两行的和 6 +8 = 14,同样的红色圈出来的 苹果的也是同样的道理。需要注意的是当在排序的时候出现相同的时候,同样的都是 12 或者 同样的都是 5 无法进行区分,所以在计算的时候会把两个或多个值都加进去,这样也就是 橙色圈出来的部分了 从 8 --> 8+10 = 18 --> 18+12+12 = 42 -->18+12+12 = 42 ,大概就是这个意思,下文会告诉大家如何解决这种问题 (rows between unbounded preceding and current row) 我们来多看几种排序的结果是否符合上面的描述:  -- order by type SELECT *,sum(price) over(partition by type order by type) as sum  from commodity; -- order by position SELECT *,sum(price) over(partition by type order by position) as sum  from commodity; -- order by id SELECT *,sum(price) over(partition by type order by id) as sum  from commodity;  (2) max(), min(), avg(), count() SELECT *,sum(price) over(partition by type order by price) as sum,          max(price) over(partition by type order by price) as max,          min(price) over(partition by type order by price) as min,          avg(price) over(partition by type order by price) as avg,          count(price) over(partition by type order by price) as count from commodity; 我们可以看的到, 不管是sum(), avg() 还是min(), max(), count() 他们在窗口函数中,都是对自身记录以及位于自身记录之前的数据进行聚合,求和、求平均、最小值、最大值等。所以,聚合函数作为窗口函数的时候可以在每一行的数据里直观的看到,截止到本行数据统计数据是多少,也可以看出每一行数据对整体的影响。(注意 : 数据重复的除外,有点特殊)也就是说 sum(), max(), min(), avg(), count() 都是类似的。  2.4 rows 与 range rows是物理窗口,即根据order by 子句排序后,取的前N行及后N行的数据计算(与当前行的值无关,只与排序后的行号相关) range是逻辑窗口,是指定当前行对应值的范围取值,列数不固定,只要行值在范围内,对应列都包含在内 通俗点来讲就是说:rows 取的时候是取当前行的前几行以及后几行,包括当前行在内一起进行计算的;而 range 不受行的限制,他跟当前行的值有关,当前行的值减去几,加上几,这个范围内的值都是要进行计算的数据,具体例子如下所示:  --在当前行往前1行,往后2行,一共4行范围内进行计算 rows between 1 preceding and 2 following  --在当前行的数值往前1个数值,往后2个数值,进行计算,范围不一定,因为可能会出现重复值 range between 1 preceding and 2 following --rows  SELECT *,sum(price) over(partition by type order by price rows between 1 preceding and 2 following) as sum from commodity where type = '苹果'; 第一行 8 ,前一行没有,后两行是 10,12 --> 8 + 10 + 12 = 30 第二行是 10 ,前一行 8,后两行 12,12 --> 8 + 10 + 12 + 12 = 42 第三行是 12 ,前一行 10,后两行 12 --> 10 + 12 + 12 = 34 第四行是 12 ,前一行 12,后两行没有 --> 12 + 12 = 24 --range  SELECT *,sum(price) over(partition by type order by price range between 1 preceding and 2 following) as sum from commodity where type = '苹果'; 第一行 8 ,往前一个数值 8-1 = 7,往后两个数值 8+2 = 10 --> 7 <= price <= 10  --> 8 + 10 = 18 第二行 10 ,往前一个数值 10-1 = 9,往后两个数值 10+2 = 12 --> 9 <= price <= 12  --> 10 + 12 + 12 = 34 第三行 12 ,往前一个数值 12-1 = 11,往后两个数值 12+2 = 14 --> 11 <= price <= 14  --> 12 + 12 = 24 第四行 12 ,往前一个数值 12-1 = 11,往后两个数值 12+2 = 14 --> 11 <= price <= 14  --> 12 + 12 = 24  2.5 unbound 和 current row --在当前行往前1行,往后2行,一共4行范围内进行计算 rows between 1 preceding and 2 following  --在当前行的数值往前1个数值,往后2个数值,进行计算,范围不一定,因为可能会出现重复值 range between 1 preceding and 2 following between … and … 后面的数字可以随着需求进行替换,当然也可以使用 unbound 和 current row ; 其中 unbounded 表示不做限制,current row 表示当前行  --按照分组内全部行求和,不做任何限制 rows between unbounded preceding and unbounded following  --从分组内排序的起始行到当前行 rows between unbounded preceding and current row  --按照分组内全部行求和,不做任何限制 range between unbounded preceding and unbounded following   --从分组内排序的起始行的值到当前行的值 range between unbounded preceding and current row   --rows between unbounded preceding and unbounded following SELECT *,sum(price) over(partition by type order by price rows between unbounded preceding and unbounded following) as sum from commodity where type = '苹果';  --rows between unbounded preceding and current row SELECT *,sum(price) over(partition by type order by price rows between unbounded preceding and current row) as sum from commodity where type = '苹果';  --range between unbounded preceding and unbounded following SELECT *,sum(price) over(partition by type order by price range between unbounded preceding and unbounded following) as sum from commodity where type = '苹果';  --range between unbounded preceding and current row SELECT *,sum(price) over(partition by type order by price range between unbounded preceding and current row) as sum from commodity where type = '苹果';  2.6 first_value(), last_valus(), lag(), lead() first_value(字段) over(partition by … order by …) 求分组后的第一个值 last_value(字段) over(partition by … order by …) 求分组后的最后一个值  SELECT *,first_value(price) over(partition by type order by price) as mm  from commodity; SELECT *,last_value(price) over(partition by type order by price) as mm  from commodity;  lag(expresstion,<offset>,<default>) over(partition by … order by …) 取出分组后前n行数据 lead(expresstion,<offset>,<default>) over(partition by … order by …) 取出分组后后n行数据  --取分组后的前两行数据/后两行数据, 默认值设置为 0 SELECT *,lag(price,2,0) over(partition by type order by price) as mm  from commodity; SELECT *,lead(price,2,0) over(partition by type order by price) as mm  from commodity;  SELECT *,lag(price,1,0) over(partition by type order by price) as lag,lead(price,1,0) over(partition by type order by price) as lead  from commodity; --第一个参数:要取的字段 --第二个参数:取排序后的第几条记录 --第三个参数:缺省值,如果后面的记录取不到值就默认取值第三个参数的值,注意参数的类型要与第一个参数所取字段的类型一致哦,话默认为空  注:具体的sql输出结果下文放置了建表语句,可以执行一下,自己体验体验!!! 2.7 preceding 和 following Hive函数, preceding:向前 following:向后,这两个窗口函数不仅可以实现滑窗求和(指定rows范围)或者指定范围内数据求和(指定range范围),也可以用来计算移动平均值:  SELECT *,sum(price) over(partition by type order by price) as sum,avg(price) over(partition by type order by price) as avg,avg(price) over(partition by type order by price rows 2 preceding) as avg2 from commodity where type = '苹果'; 1 3.参考文献 SQL高级功能:窗口函数、存储过程及经典排名问题、topN问题等 分区函数Partition By的用法 SQL:聚合类窗口函数的preceding和following参数用法  4.建表语句 -- ---------------------------- -- Table structure for commodity -- ---------------------------- DROP TABLE IF EXISTS "public"."commodity"; CREATE TABLE "public"."commodity" (   "id" varchar(50) COLLATE "pg_catalog"."default" NOT NULL,   "position" varchar(50) COLLATE "pg_catalog"."default",   "type" varchar(50) COLLATE "pg_catalog"."default",   "price" numeric(10,2) ) ; COMMENT ON COLUMN "public"."commodity"."id" IS '主键'; COMMENT ON COLUMN "public"."commodity"."position" IS '位置(商品放置的货架)'; COMMENT ON COLUMN "public"."commodity"."type" IS '类型'; COMMENT ON COLUMN "public"."commodity"."price" IS '价格';  -- ---------------------------- -- Records of commodity -- ---------------------------- INSERT INTO "public"."commodity" VALUES ('1', '1-001', '苹果', 8.00); INSERT INTO "public"."commodity" VALUES ('2', '2-002', '苹果', 10.00); INSERT INTO "public"."commodity" VALUES ('3', '3-003', '苹果', 12.00); INSERT INTO "public"."commodity" VALUES ('6', '1-001', '橘子', 5.00); INSERT INTO "public"."commodity" VALUES ('7', '1-001', '橙子', 6.00); INSERT INTO "public"."commodity" VALUES ('8', '3-003', '橙子', 8.00); INSERT INTO "public"."commodity" VALUES ('10', '2-002', '菠萝', 10.00); INSERT INTO "public"."commodity" VALUES ('9', '2-002', '香蕉', 5.00); INSERT INTO "public"."commodity" VALUES ('4', '1-001', '苹果', 12.00); INSERT INTO "public"."commodity" VALUES ('5', '1-001', '香蕉', 5.00);  -- ---------------------------- -- Primary Key structure for table commodity -- ---------------------------- ALTER TABLE "public"."commodity" ADD CONSTRAINT "commodity_pkey" PRIMARY KEY ("id"); ————————————————                       原文链接:https://blog.csdn.net/weixin_44711823/article/details/135966741 
  • [技术干货] oracle partition by group by详解
    group by 必须与聚合函数一起使用,最终使用的结果是将多行变成一行,默认取第一行,可能丢失数据partition by 也是分组,不过不会丢失数据,只是把数据做分组1. group by是分组函数,partition by是分析函数(然后像sum()等是聚合函数);2. 在执行顺序上,以下是常用sql关键字的优先级from > where > group by > having > order by而partition by应用在以上关键字之后,实际上就是在执行完select之后,在所得结果集之上进行partition。3.partition by相比较于group by,能够在保留全部数据的基础上,只对其中某些字段做分组排序(类似excel中的操作),而group by则只保留参与分组的字段和聚合函数的结果(类似excel中的pivot)。partition bygroup by4.如果在partition结果上聚合,千万注意聚合函数是逐条累计运行结果的!而在group by后的结果集上使用聚合函数,会作用在分组下的所有记录上。数据如下SQLselect a.cc,a.item,sum(a.num)from table_temp agroup by a.cc,a.itemResult111条记录经group by后为10条,其中cc='cn' and item='8.1.1'对应的两条记录的num汇总成值3.SQL2select a.cc,a.num, min(a.num) over (partition by a.cc order by a.num asc) as amountfrom table_temp agroup by a.cc,a.num;select a.cc,a.num, min(a.num) over (partition by a.cc order by a.num desc) as amountfrom table_temp agroup by a.cc,a.num;Result2两个sql的唯一区别在于a.num的排序上,但从结果红框中的数据对比可以看到amount值并不相同,且第二个结果集amount并不都是最小值1。在这里就是要注意将聚合函数用在partition后的结果集上时,聚合函数是逐条累积计算值的!其实partition by常同row_number() over一起使用,select a.*, row_number() over (partition by a.cc,a.item order by a.num desc) as seqfrom table_temp a两个sql的唯一区别在于a.num的排序上,但从结果红框中的数据对比可以看到amount值并不相同,且第二个结果集amount并不都是最小值1。在这里就是要注意将聚合函数用在partition后的结果集上时,聚合函数是逐条累积计算值的!SQL中只要用到聚合函数就一定要用到group by 吗?答:看情况1、当只做聚集函数查询时候,就不需要进行分组了。2、当聚集函数和非聚集函数出现在一起时,需要将非聚集函数进行group by举例来说:情况一:不需要使用Group by 进行分组,因为其中没有非聚合字段,所以不用Group by 也可以。SELECT SUM(bonus) FROM person情况二:SELECT SUM(bonus),gender FROM person GROUP BY gender由于gender是非聚合字段,Group by 后才可以正常执行。原文链接:https://blog.csdn.net/weixin_44547599/article/details/88764558
  • [其他] HashMap实现原理, 扩容机制
    1.讲下对HashMap的认识 HashMap 存储的是键值对 key - value,key 具有唯一性,采用了链地址法来处理哈希冲突。当往 HashMap 中添加元素时,会计算 key 的 hash 值取余得出元素在数组中的的存放位置。  HashMap底层的数据结构在 JDK1.8 中有了较大的变化,1.8之前采用数组加链表的数据结构,1.8采用数组加链表加红黑树的数据结构。 HashMap 是线程不安全的,线程安全可以使用 HashTable 和 ConcurrentHashMap 。 在 1.8 版本的中 hash() 和 resize( ) 方法也有了很大的改变,提升了性能。 键和值都可存放null,键只能存放一个null,键为null时存放入table[0]。 2.HashMap的一些参数     //HashMap的默认初始长度16     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;           //HashMap的最大长度2的30次幂     static final int MAXIMUM_CAPACITY = 1 << 30;          //HashMap的默认加载因子0.75     static final float DEFAULT_LOAD_FACTOR = 0.75f;          //HashMap链表升级成红黑树的临界值     static final int TREEIFY_THRESHOLD = 8;          //HashMap红黑树退化成链表的临界值     static final int UNTREEIFY_THRESHOLD = 6;          //HashMap链表升级成红黑树第二个条件:HashMap数组(桶)的长度大于等于64     static final int MIN_TREEIFY_CAPACITY = 64;          //HashMap底层Node桶的数组     transient Node<K,V>[] table;          //扩容阈值,当你的hashmap中的元素个数超过这个阈值,便会发生扩容     //threshold = capacity * loadFactor     int threshold;   3.为什么HashMap的长度必须是2的n次幂? 在计算存入结点下标时,会利用 key 的 hsah 值进行取余操作,而计算机计算时,并没有取余等运算,会将取余转化为其他运算。  当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,就可以用位运算代替取余运算,计算更加高效。  4.HashMap 为什么在获取 hash 值时要进行位运算 换种问法:能不能直接使用key的hashcode值计算下标存储?  static final int hash(Object key) {     int h;     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 如果使用直接使用hashCode对数组大小取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让 hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。 (h >>> 16)是无符号右移16位的运算,右边补0,得到 hashCode 的高16位。 (h = key.hashCode()) ^ (h >>> 16) 把 hashCode 和它的高16位进行异或运算,可以使得到的 hash 值更加散列,尽可能减少哈希冲突,提升性能。 而这么来看 hashCode 被散列 (异或) 的是低16位,而 HashMap 数组长度一般不会超过2的16次幂,那么高16位在大多数情况是用不到的,所以只需要拿 key 的 HashCode 和它的低16位做异或即可利用高位的hash值,降低哈希碰撞概率也使数据分布更加均匀。 5.HashMap在JDK1.7和JDK1.8中有哪些不同? HashMap的底层实现 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。  JDK1.8主要解决或优化了以下问题:  resize 扩容和 计算hash 优化 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 6.HashMap的put方法的具体流程? 源码   HashMap是懒加载,只有在第一次put时才会创建数组。 总结 ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值 对,否则转向⑤; ⑤.遍历table[i],并记录遍历长度,如果遍历过程中发现key值相同的,则直接覆盖value,没有相同的key则在链表尾部插入结点,插入后判断该链表长度是否大等于8,大等于则考虑树化,如果数组的元素个数小于64,则只是将数组resize,大等于才树化该链表; ⑥.插入成功后,判断数组中的键值对数量size是否超过了阈值threshold,如果超过,进行扩容。  7.HashMap 的 get 方法的具体流程?   public V get(Object key) {     Node<K,V> e;     return (e = getNode(hash(key), key)) == null ? null : e.value; }  final Node<K,V> getNode(int hash, Object key) {     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;          //Node数组不为空,数组长度大于0,数组对应下标的Node不为空     if ((tab = table) != null && (n = tab.length) > 0 &&         //也是通过 hash & (length - 1) 来替代 hash % length 的         (first = tab[(n - 1) & hash]) != null) {                  //先和第一个结点比,hash值相等且key不为空,key的第一个结点的key的对象地址和值均相等         //则返回第一个结点         if (first.hash == hash && // always check first node             ((k = first.key) == key || (key != null && key.equals(k))))             return first;         //如果key和第一个结点不匹配,则看.next是否为空,不为null则继续,为空则返回null         if ((e = first.next) != null) {             //如果此时是红黑树的结构,则进行处理getTreeNode()方法搜索key             if (first instanceof TreeNode)                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);             //是链表结构的话就一个一个遍历,直到找到key对应的结点,             //或者e的下一个结点为null退出循环             do {                 if (e.hash == hash &&                     ((k = e.key) == key || (key != null && key.equals(k))))                     return e;             } while ((e = e.next) != null);         }     }     return null; } 总结  首先根据 hash 方法获取到 key 的 hash 值 然后通过 hash & (length - 1) 的方式获取到 key 所对应的Node数组下标 ( length对应数组长度 ) 首先判断此结点是否为空,是否就是要找的值,是则返回空,否则判断第二个结点是否为空,是则返回空,不是则判断此时数据结构是链表还是红黑树 链表结构进行顺序遍历查找操作,每次用 == 符号 和 equals( ) 方法来判断 key 是否相同,满足条件则直接返回该结点。链表遍历完都没有找到则返回空。 红黑树结构执行相应的 getTreeNode( ) 查找操作。 8.HashMap的扩容操作是怎么实现的? 不管是JDK1.7或者JDK1.8 当put方法执行的时候,如果table为空,则执行resize()方法扩容。默认长度为16。  JDK1.7扩容 条件:发生扩容的条件必须同时满足两点  当前存储的数量大于等于阈值 发生hash碰撞 因为上面这两个条件,所以存在下面这些情况  就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。 当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。 特点:先扩容,再添加(扩容使用的头插法)  缺点:头插法会使链表发生反转,多线程环境下可能会死循环  扩容之后对table的调整:  table容量变为2倍,所有的元素下标需要重新计算,newIndex = hash (扰动后) & (newLength - 1)  JDK1.8扩容 条件:  当前存储的数量大于等于阈值 当某个链表长度>=8,但是数组存储的结点数size() < 64时 特点:先插后判断是否需要扩容(扩容时是尾插法)  缺点:多线程下,1.8会有数据覆盖  举例: 线程A:往index插,index此时为空,可以插入,但是此时线程A被挂起 线程B:此时,对index写入数据,A恢复后,就把B数据覆盖了  扩容之后对table的调整:  table容量变为2倍,但是不需要像之前一样计算下标,只需要将hash值和旧数组长度相与即可确定位置。  如果 Node 桶的数据结构是链表会生成 low 和 high 两条链表,是红黑树则生成 low 和 high 两颗红黑树 依靠 (hash & oldCap) == 0 判断 Node 中的每个结点归属于 low 还是 high。 把 low 插入到 新数组中 当前数组下标的位置,把 high 链表插入到 新数组中 [当前数组下标 + 旧数组长度] 的位置 如果生成的 low,high 树中元素个数小于等于6退化成链表再插入到新数组的相应下标的位置 9.HashMap 在扩容时为什么通过位运算 (e.hash & oldCap) 得到下标? 从下图中我们可以看出,计算下标通过(n - 1) & hash,旧table的长度为16,hash值只与低四位有关,扩容后,table长度为32(两倍),此时只与低五位有关。  所以此时后几位的结果相同,前后两者之间的差别就差在了第五位上。  同时,扩容的时候会有 low 和 high 两条链表或红黑树来记录原来下标的数据和原来下标 + 旧table下标的数据。  如果第五位 b 是 0,那么只要看低四位 (也就是原来的下标);如果第五位是 1,只要把低四位的二进制数 + 1 0 0 0 0 ,就可以得到新数组下标。前面的部分刚好是原来的下标,后一部分就是旧table的长度 。那么我们就得出来了为什么把 low 插入扩容后 新数组[原来坐标] 的位置,把 high 插入扩容后 新数组[当前坐标 + 旧数组长度] 的位置。  那为什么根据 (e.hash & oldCap) == 0 来做判断条件呢?是因为旧数组的长度 length 的二进制数的第五位刚好是 1,hash & length 就可以计算 hash 值的第五位是 0 还是 1,就可以区别是在哪个位置上。  10.链表升级成红黑树的条件 链表长度大于8时才会考虑升级成红黑树,是有一个条件是 HashMap 的 Node 数组长度大于等于64(不满足则会进行一次扩容替代升级)。  11.红黑树退化成链表的条件 扩容 resize( ) 时,红黑树拆分成的 树的结点数小于等于临界值6个,则退化成链表。 删除元素 remove( ) 时,在 removeTreeNode( ) 方法会检查红黑树是否满足退化条件,与结点数无关。如果红黑树根 root 为空,或者 root 的左子树/右子树为空,root.left.left 根的左子树的左子树为空,都会发生红黑树退化成链表。 12.HashMap是怎么解决哈希冲突的? 使用链地址法(使用散列表)来链接拥有相同下标的数据; 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均; 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快; 13.HaspMap的初始化时数组长度和加载因子的约束范围 可以看到如果初始化数组长度 initialCapacity 小于 0 的话会跑出 IllegalArgumentException 的异常,initialCapacity 大于 MAXIMUM_CAPACITY 即 2 的 30 次幂的时候最大长度也只会固定在 MAXIMUM_CAPACITY ,在扩容的时候,如果数组的长度大等于MAXIMUM_CAPACITY,会将阈值设置为Integer.MAX_VALUE。  加载因子小于等于0时,或者加载因子是NaN时 (NaN 实际上就是 Not a Number的简称) 会抛出 IllegalArgumentException 的异常。 ————————————————                         原文链接:https://blog.csdn.net/qq_49217297/article/details/126304736 
  • [其他] Kafka消息延迟和时序性详解
     一、概括 1.1 介绍 Kafka 消息延迟和时序性 Kafka 消息延迟和时序性对于大多数实时数据流应用程序至关重要。本章将深入介绍这两个核心概念,它们是了解 Kafka 数据流处理的关键要素。  1.1.1 什么是 Kafka 消息延迟? Kafka 消息延迟是指消息从生产者发送到消息被消费者接收之间的时间差。这是一个关键的概念,因为它直接影响到数据流应用程序的实时性和性能。在理想情况下,消息应该以最小的延迟被传递,但在实际情况中,延迟可能会受到多种因素的影响。   消息延迟的因素包括:  网络延迟:消息必须通过网络传输到 Kafka 集群,然后再传输到消费者。网络延迟可能会受到网络拓扑、带宽和路由等因素的影响。  硬件性能:Kafka 集群的硬件性能,包括磁盘、内存和 CPU 的速度,会影响消息的写入和读取速度。  Kafka 内部处理:Kafka 集群的内部处理能力也是一个关键因素。消息必须经过分区、日志段和复制等处理步骤,这可能会引入一些处理延迟。  1.1.2 为什么消息延迟很重要?  消息延迟之所以如此重要,是因为它直接关系到实时数据处理应用程序的可靠性和实时性。在一些应用中,如金融交易处理,甚至毫秒级的延迟都可能导致交易失败或不一致。在监控和日志处理应用中,过高的延迟可能导致数据不准确或失去了时序性。  管理和优化 Kafka 消息延迟是确保应用程序在高负载下仍能快速响应的关键因素。不仅需要了解延迟的来源,还需要采取相应的优化策略。  1.1.3 什么是 Kafka 消息时序性? Kafka 消息时序性是指消息按照它们发送的顺序被接收。这意味着如果消息 A 在消息 B 之前发送,那么消息 A 应该在消息 B 之前被消费。保持消息的时序性对于需要按照时间顺序处理的应用程序至关重要。  维护消息时序性是 Kafka 的一个强大特性。在 Kafka 中,每个分区都可以保证消息的时序性,因为每个分区内的消息是有序的。然而,在多个分区的情况下,时序性可能会受到消费者处理速度不一致的影响,因此需要采取一些策略来维护全局的消息时序性。  1.1.4 消息延迟和时序性的关系 消息延迟和消息时序性之间存在密切的关系。如果消息延迟过大,可能会导致消息失去时序性,因为一条晚到的消息可能会在一条早到的消息之前被处理。因此,了解如何管理消息延迟也包括了维护消息时序性。  在接下来的章节中,我们将深入探讨如何管理和优化 Kafka 消息延迟,以及如何维护消息时序性,以满足实时数据处理应用程序的需求。  1.2 延迟的来源 为了有效地管理和优化 Kafka 消息延迟,我们需要深入了解延迟可能来自哪些方面。下面是一些常见的延迟来源:  1.2.1 Kafka 内部延迟 Kafka 内部延迟是指与 Kafka 内部组件和分区分配相关的延迟。这些因素可能会影响消息在 Kafka 内部的分发、复制和再平衡。  分区分布不均:如果分区分布不均匀,某些分区可能会变得拥挤,而其他分区可能会滞后,导致消息传递延迟。  复制延迟:在 Kafka 中,消息通常会进行复制以确保冗余。复制延迟是指主题的所有副本都能复制消息所需的时间。  再平衡延迟:当 Kafka 集群发生再平衡时,消息的重新分配和复制可能导致消息传递延迟。  二、衡量和监控消息延迟 在本节中,我们将深入探讨如何度量和监控 Kafka 消息延迟,这将帮助你更好地了解问题并采取相应的措施来提高延迟性能。  2.1 延迟的度量 为了有效地管理 Kafka 消息延迟,首先需要能够度量它。下面是一些常见的延迟度量方式:  2.1.1 生产者到 Kafka 延迟 这是指消息从生产者发送到 Kafka 集群之间的延迟。为了度量这一延迟,你可以采取以下方法:  记录发送时间戳:在生产者端,记录每条消息的发送时间戳。一旦消息成功写入 Kafka,记录接收时间戳。然后,通过将这两个时间戳相减,你可以获得消息的生产者到 Kafka 的延迟。  以下是如何记录发送和接收时间戳的代码示例:  // 记录消息发送时间戳 long sendTimestamp = System.currentTimeMillis(); ProducerRecord<String, String> record = new ProducerRecord<>("my_topic", "key", "value"); producer.send(record, (metadata, exception) -> {     if (exception == null) {         long receiveTimestamp = System.currentTimeMillis();         long producerToKafkaLatency = receiveTimestamp - sendTimestamp;         System.out.println("生产者到 Kafka 延迟:" + producerToKafkaLatency + " 毫秒");     } else {         System.err.println("消息发送失败: " + exception.getMessage());     } }); 1 2 3 4 5 6 7 8 9 10 11 12 2.1.2 Kafka 内部延迟 Kafka 内部延迟是指消息在 Kafka 集群内部传递的延迟。你可以使用 Kafka 内置度量来度量它,包括:  Log End-to-End Latency:这是度量消息从生产者发送到消费者接收的总延迟。它包括了网络传输、分区复制、再平衡等各个环节的时间。  以下是一个示例:  // 创建 Kafka 消费者 Properties consumerProps = new Properties(); consumerProps.put("bootstrap.servers", "kafka-broker:9092"); consumerProps.put("group.id", "my-group"); consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);  // 订阅主题 consumer.subscribe(Collections.singletonList("my_topic"));  while (true) {     ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));     for (ConsumerRecord<String, String> record : records) {         long endToEndLatency = record.timestamp() - record.timestampType().createTimestamp();         System.out.println("Log End-to-End 延迟:" + endToEndLatency + " 毫秒");     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 2.1.3 消费者处理延迟 消费者处理延迟是指消息从 Kafka 接收到被消费者实际处理的时间。为了度量这一延迟,你可以采取以下方法:  记录消费时间戳:在消费者端,记录每条消息的接收时间戳和处理时间戳。通过计算这两个时间戳的差值,你可以得到消息的消费者处理延迟。 以下是如何记录消费时间戳的代码示例:  KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps); consumer.subscribe(Collections.singletonList("my_topic"));  while (true) {     ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));     for (ConsumerRecord<String, String> record : records) {         long receiveTimestamp = System.currentTimeMillis();         long consumerProcessingLatency = receiveTimestamp - record.timestamp();         System.out.println("消费者处理延迟:" + consumerProcessingLatency + " 毫秒");     } } 1 2 3 4 5 6 7 8 9 10 11 2.2 监控和度量工具 在度量和监控 Kafka 消息延迟时,使用适当的工具和系统是至关重要的。下面是一些工具和步骤,帮助你有效地监控 Kafka 消息延迟,包括代码示例:  2.2.1 Kafka 内置度量 Kafka 提供了内置度量,可通过多种方式来监控。以下是一些示例,演示如何通过 Kafka 的 JMX 界面访问这些度量:  使用 JConsole 直接连接到 Kafka Broker:  启动 Kafka Broker。 打开 JConsole(Java 监控与管理控制台)。 在 JConsole 中选择 Kafka Broker 进程。 导航到 “kafka.server” 和 “kafka.consumer”,以查看各种度量。 使用 Jolokia(Kafka JMX HTTP Bridge):  启用 Jolokia 作为 Kafka Broker 的 JMX HTTP Bridge。 使用 Web 浏览器或 HTTP 请求访问 Jolokia 接口来获取度量数据。例如,使用 cURL 进行 HTTP GET 请求: curl http://localhost:8778/jolokia/read/kafka.server:name=BrokerTopicMetrics/TotalFetchRequestsPerSec 1 这将返回有关 Kafka Broker 主题度量的信息。  2.2.2 第三方监控工具 除了 Kafka 内置度量,你还可以使用第三方监控工具,如 Prometheus 和 Grafana,来收集、可视化和警报度量数据。以下是一些步骤:  配置 Prometheus:  部署和配置 Prometheus 服务器。 创建用于监控 Kafka 的 Prometheus 配置文件,定义抓取度量数据的频率和目标。 启动 Prometheus 服务器。 设置 Grafana 仪表板:  部署和配置 Grafana 服务器。 在 Grafana 中创建仪表板,使用 Prometheus 作为数据源。 添加度量查询,配置警报规则和可视化图表。 可视化 Kafka 延迟数据:  在 Grafana 仪表板中,你可以设置不同的图表来可视化 Kafka 延迟数据,例如生产者到 Kafka 延迟、消费者处理延迟等。通过设置警报规则,你还可以及时收到通知,以便采取行动。  2.2.3 配置和使用监控工具 为了配置和使用监控工具,你需要执行以下步骤:  定义度量指标:确定你要度量的关键度量指标,如生产者到 Kafka 延迟、消费者处理延迟等。  设置警报规则:为了快速响应问题,设置警报规则,以便在度量数据超出预定阈值时接收通知。  创建可视化仪表板:使用监控工具(如 Grafana)创建可视化仪表板,以集中展示度量数据并实时监测延迟情况。可配置的图表和仪表板有助于更好地理解数据趋势。  以上步骤和工具将帮助你更好地度量和监控 Kafka 消息延迟,以及及时采取行动来维护系统的性能和可靠性。  三、降低消息延迟 既然我们了解了 Kafka 消息延迟的来源以及如何度量和监控它,让我们继续探讨如何降低消息延迟。以下是一些有效的实践方法,可以帮助你减少 Kafka 消息延迟:  3.1 优化 Kafka 配置 3.1.1 Producer 和 Consumer 参数 生产者参数示例: # 生产者参数示例 acks=all compression.type=snappy linger.ms=20 max.in.flight.requests.per.connection=1 1 2 3 4 5 acks 设置为 all,以确保生产者等待来自所有分区副本的确认。这提高了可靠性,但可能增加了延迟。 compression.type 使用 Snappy 压缩消息,减小了网络传输延迟。 linger.ms 设置为 20 毫秒,以允许生产者在发送消息之前等待更多消息。这有助于减少短暂的消息发送延迟。 max.in.flight.requests.per.connection 设置为 1,以确保在收到分区副本的确认之前不会发送新的消息。 消费者参数示例: # 消费者参数示例 max.poll.records=500 fetch.min.bytes=1 fetch.max.wait.ms=100 enable.auto.commit=false 1 2 3 4 5 max.poll.records 设置为 500,以一次性拉取多条消息,提高吞吐量。 fetch.min.bytes 设置为 1,以确保即使没有足够数据,也立即拉取消息。 fetch.max.wait.ms 设置为 100 毫秒,以限制拉取消息的等待时间。 enable.auto.commit 禁用自动提交位移,以确保精确控制消息的确认。 3.1.2 Broker 参数 优化 Kafka broker 参数可以提高整体性能。以下是示例:  # Kafka Broker 参数示例 num.network.threads=3 num.io.threads=8 log.segment.bytes=1073741824 log.retention.check.interval.ms=300000 1 2 3 4 5 num.network.threads 和 num.io.threads 设置为适当的值,以充分利用硬件资源。 log.segment.bytes 设置为 1 GB,以充分利用磁盘性能。 log.retention.check.interval.ms 设置为 300,000 毫秒,以降低清理日志段的频率。 3.1.3 Topic 参数 优化每个主题的参数以满足应用程序需求也很重要。以下是示例:  # 创建 Kafka 主题并设置参数示例 kafka-topics.sh --create --topic my_topic --partitions 8 --replication-factor 2 --config cleanup.policy=compact 1 2 --partitions 8 设置分区数量为 8,以提高并行性。 --replication-factor 2 设置复制因子为 2,以提高可靠性。 --config cleanup.policy=compact 设置清理策略为压缩策略,以减小数据保留成本。 通过适当配置这些参数,你可以有效地优化 Kafka 配置以降低消息延迟并提高性能。请根据你的应用程序需求和硬件资源进行调整。  3.2 编写高效的生产者和消费者 最后,编写高效的 Kafka 生产者和消费者代码对于降低延迟至关重要。以下是一些最佳实践:  3.2.1 生产者最佳实践 使用异步发送:将多个消息批量发送,而不是逐条发送。这可以减少网络通信的次数,提高吞吐量。  使用 Kafka 生产者的缓冲机制:充分利用 Kafka 生产者的缓冲功能,以减少网络通信次数。  使用分区键:通过选择合适的分区键,确保数据均匀分布在不同的分区上,从而提高并行性。  3.2.2 消费者最佳实践 使用多线程消费:启用多个消费者线程,以便并行处理消息。这可以提高处理能力和降低延迟。  调整消费者参数:调整消费者参数,如 fetch.min.bytes 和 fetch.max.wait.ms,以平衡吞吐量和延迟。  使用消息批处理:将一批消息一起处理,以减小处理开销。  3.2.3 数据序列化 选择高效的数据序列化格式对于降低数据传输和存储开销很重要。以下是一些建议的格式:  Avro:Apache Avro 是一种数据序列化框架,具有高度压缩和高性能的特点。它适用于大规模数据处理。  Protocol Buffers:Google Protocol Buffers(ProtoBuf)是一种轻量级的二进制数据格式,具有出色的性能和紧凑的数据表示。  四、Kafka 消息时序性 消息时序性是大多数实时数据流应用程序的核心要求。在本节中,我们将深入探讨消息时序性的概念、为何它如此重要以及如何保障消息时序性。  4.1 什么是消息时序性? 消息时序性是指消息按照它们发送的顺序被接收和处理的特性。在 Kafka 中,每个分区内的消息是有序的,这意味着消息以它们被生产者发送的顺序排列。然而,跨越多个分区的消息需要额外的工作来保持它们的时序性。  4.1.1 为何消息时序性重要? 消息时序性对于许多应用程序至关重要,特别是需要按照时间顺序处理数据的应用。以下是一些应用领域,消息时序性非常关键:  金融领域:在金融交易中,确保交易按照它们发生的确切顺序进行处理至关重要。任何失去时序性的交易可能会导致不一致性或错误的交易。  日志记录:在日志记录和监控应用程序中,事件的时序性对于分析和排查问题非常关键。失去事件的时序性可能会导致混淆和数据不准确。  电商应用:在线商店的订单处理需要确保订单的创建、支付和发货等步骤按照正确的顺序进行,以避免订单混乱和不准确。  4.2 保障消息时序性 在分布式系统中,保障消息时序性可能会面临一些挑战,特别是在跨越多个分区的情况下。以下是一些策略和最佳实践,可帮助你确保消息时序性:  4.2.1 分区和消息排序 使用合适的分区策略对消息进行排序,以确保相关的消息被发送到同一个分区。这样可以维护消息在单个分区内的顺序性。对于需要按照特定键排序的消息,可以使用自定义分区器来实现。  以下是如何使用合适的分区策略对消息进行排序的代码示例:  // 自定义分区器,确保相关消息基于特定键被发送到同一个分区 public class CustomPartitioner implements Partitioner {     @Override     public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {         // 在此处根据 key 的某种规则计算分区编号         // 例如,可以使用哈希函数或其他方法         int numPartitions = cluster.partitionsForTopic(topic).size();         return Math.abs(key.hashCode()) % numPartitions;     }      @Override     public void close() {         // 可选的资源清理     }      @Override     public void configure(Map<String, ?> configs) {         // 可选的配置     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 4.2.2 数据一致性 确保生产者发送的消息是有序的。这可能需要在应用程序层面实施,包括对消息进行缓冲、排序和合并,以确保它们按照正确的顺序发送到 Kafka。  以下是如何确保数据一致性的代码示例:  // 生产者端的消息排序 ProducerRecord<String, String> record1 = new ProducerRecord<>("my-topic", "key1", "message1"); ProducerRecord<String, String> record2 = new ProducerRecord<>("my-topic", "key2", "message2");  // 发送消息 producer.send(record1); producer.send(record2);  // 消费者端保证消息按照键排序 ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) {     // 处理消息,确保按照键的顺序进行 } 1 2 3 4 5 6 7 8 9 10 11 12 13 4.2.3 消费者并行性 在消费者端,使用适当的线程和分区分配来确保消息以正确的顺序处理。这可能涉及消费者线程数量的管理以及确保每个线程只处理一个分区,以避免顺序混乱。  以下是如何确保消费者并行性的代码示例:  // 创建具有多个消费者线程的 Kafka 消费者 Properties consumerProps = new Properties(); consumerProps.put("bootstrap.servers", "kafka-broker:9092"); consumerProps.put("group.id", "my-group"); consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");  // 创建 Kafka 消费者 KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);  // 订阅主题 consumer.subscribe(Collections.singletonList("my-topic"));  // 创建多个消费者线程 int numThreads = 3; for (int i = 0; i < numThreads; i++) {     Runnable consumerThread = new ConsumerThread(consumer);     new Thread(consumerThread).start(); }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 五、总结 在本篇技术博客中,我们深入探讨了 Kafka 消息延迟和时序性的重要性以及如何度量、监控和降低消息延迟。我们还讨论了消息时序性的挑战和如何确保消息时序性。对于构建实时数据流应用程序的开发人员来说,深入理解这些概念是至关重要的。通过合理配置 Kafka、优化网络和硬件、编写高效的生产者和消费者代码,以及维护消息时序性,你可以构建出高性能和可靠的数据流系统。  无论你的应用是金融交易、监控、日志记录还是其他领域,这些建议和最佳实践都将帮助你更好地处理 Kafka 消息延迟和时序性的挑战,确保数据的可靠性和一致性。  六、从零开始学架构:照着做,你也能成为架构师  1、内容介绍 京东购买链接:从零开始学架构:照着做,你也能成为架构师  本书的内容主要包含以下几部分:  架构设计基础,包括架构设计相关概念、历史、原则、基本方法,让架构设计不再神秘; 架构设计流程,通过一个虚拟的案例,描述了一个通用的架构设计流程,让架构设计不再依赖天才的创作,而是有章可循; 架构设计专题:包括高性能架构设计、高可用架构设计、可扩展架构设计,这些模式可以直接参考和应用; 架构设计实战,包括重构、开源方案引入、架构发展路径、互联网架构模板等 ———————————————— 原文链接:https://blog.csdn.net/guorui_java/article/details/135060020 
  • [其他] SimpleDateFormat类为何不是线程安全的?
    提起SimpleDateFormat类,想必做过Java开发的童鞋都不会感到陌生。没错,它就是Java中提供的日期时间的转化类。这里,为什么说SimpleDateFormat类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用SimpleDateFormat类来解析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到SimpleDateFormat类出现问题的并发量,也就是说你们的系统没啥负载!接下来,我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。重现SimpleDateFormat类的线程安全问题为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。有关CountDownLatch类和Semaphore类的具体用法和底层原理与源码解析在【高并发专题】后文会深度分析。这里,大家只需要知道CountDownLatch类可以使一个线程等待其他线程各自执行完毕后再执行。而Semaphore类可以理解为一个计数信号量,必须由获取它的线程释放,经常用来限制访问某些资源的线程数量,例如限流等。好了,先来看下重现SimpleDateFormat类的线程安全问题的代码,如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 测试SimpleDateFormat的线程不安全问题 */ public class SimpleDateFormatTest01 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }可以看到,在SimpleDateFormatTest01类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和CountDownLatch类与Semaphore类来模拟高并发的业务场景。其中,有关日期转化的代码只有如下一行。simpleDateFormat.parse("2020-01-01");当程序捕获到异常时,打印相关的信息,并退出整个程序的运行。当程序正确运行后,会打印“所有线程格式化日期成功”。运行程序输出的结果信息如下所示。Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败 线程:pool-1-thread-9 格式化日期失败 线程:pool-1-thread-10 格式化日期失败 Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败 线程:pool-1-thread-21 格式化日期失败 Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败 线程:pool-1-thread-11 格式化日期失败 java.lang.ArrayIndexOutOfBoundsException 线程:pool-1-thread-27 格式化日期失败 at java.lang.System.arraycopy(Native Method) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597) at java.lang.StringBuffer.append(StringBuffer.java:367) at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败 at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 线程:pool-1-thread-14 格式化日期失败 at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) 线程:pool-1-thread-13 格式化日期失败 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 线程:pool-1-thread-20 格式化日期失败 at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) Process finished with exit code 1说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!接下来,我们就看下,SimpleDateFormat类为何不是线程安全的。SimpleDateFormat类为何不是线程安全的?那么,接下来,我们就一起来看看真正引起SimpleDateFormat类线程不安全的根本原因。通过查看SimpleDateFormat类的源码,我们得知:SimpleDateFormat是继承自DateFormat类,DateFormat类中维护了一个全局的Calendar变量,如下所示。/** * The {@link Calendar} instance used for calculating the date-time fields * and the instant of time. This field is used for both formatting and * parsing. * * <p>Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * <code>DateFormat</code>. * @serial */ protected Calendar calendar;从注释可以看出,这个Calendar对象既用于格式化也用于解析日期时间。接下来,我们再查看parse()方法接近最后的部分。@Override public Date parse(String text, ParsePosition pos){ ################此处省略N行代码################## Date parsedDate; try { parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDate.before(defaultCenturyStart)) { parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } // An IllegalArgumentException will be thrown by Calendar.getTime() // if any fields are out of range, e.g., MONTH == 17. catch (IllegalArgumentException e) { pos.errorIndex = start; pos.index = oldStart; return null; } return parsedDate; }可见,最后的返回值是通过调用CalendarBuilder.establish()方法获得的,而这个方法的参数正好就是前面的Calendar对象。接下来,我们再来看看CalendarBuilder.establish()方法,如下所示。Calendar establish(Calendar cal) { boolean weekDate = isSet(WEEK_YEAR) && field[WEEK_YEAR] > field[YEAR]; if (weekDate && !cal.isWeekDateSupported()) { // Use YEAR instead if (!isSet(YEAR)) { set(YEAR, field[MAX_FIELD + WEEK_YEAR]); } weekDate = false; } cal.clear(); // Set the fields from the min stamp to the max stamp so that // the field resolution works in the Calendar. for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { cal.set(index, field[MAX_FIELD + index]); break; } } } if (weekDate) { int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1; int dayOfWeek = isSet(DAY_OF_WEEK) ? field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek(); if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) { if (dayOfWeek >= 8) { dayOfWeek--; weekOfYear += dayOfWeek / 7; dayOfWeek = (dayOfWeek % 7) + 1; } else { while (dayOfWeek <= 0) { dayOfWeek += 7; weekOfYear--; } } dayOfWeek = toCalendarDayOfWeek(dayOfWeek); } cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek); } return cal; }在CalendarBuilder.establish()方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。类似地, format()方法也存在同样的问题。因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。那么,得知了SimpleDateFormat类不是线程安全的,以及造成SimpleDateFormat类不是线程安全的原因,那么如何解决这个问题呢?接下来,我们就一起探讨下如何解决SimpleDateFormat类在高并发场景下的线程安全问题。解决SimpleDateFormat类的线程安全问题解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。1.局部变量法最简单的一种方式就是将SimpleDateFormat类对象定义成局部变量,如下所示的代码,将SimpleDateFormat类对象定义在parse(String)方法的上面,即可解决问题。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 局部变量法解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest02 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }此时运行修改后的程序,输出结果如下所示。所有线程格式化日期成功至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【JVM专题】的JVM内存模式相关内容中深入剖析,这里不做过多的介绍了。当然,这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。2.synchronized锁方式将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,此时在调用格式化时间的方法时,对SimpleDateFormat对象进行同步即可,代码如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest03 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); } } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }此时,解决问题的关键代码如下所示。synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); }运行程序,输出结果如下所示。所有线程格式化日期成功需要注意的是,虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。3.Lock锁方式Lock锁方式与synchronized锁方式实现原理相同,都是在高并发下通过JVM的锁机制来保证程序的线程安全。通过Lock锁方式解决问题的代码如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author binghe * @version 1.0.0 * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest04 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); //Lock对象 private static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { lock.lock(); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }finally { lock.unlock(); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }通过代码可以得知,首先,定义了一个Lock类型的全局静态变量作为加锁和释放锁的句柄。然后在simpleDateFormat.parse(String)代码之前通过lock.lock()加锁。这里需要注意的一点是:为防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中,如下所示。finally { lock.unlock(); }运行程序,输出结果如下所示。所有线程格式化日期成功此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。4.ThreadLocal方式使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题,使用ThreadLocal解决线程安全问题的代码如下所示。package io.binghe.concurrent.lab06; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest05 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { threadLocal.get().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }通过代码可以得知,将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全问题。运行程序,输出结果如下所示。所有线程格式化日期成功此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。另外,使用ThreadLocal也可以写成如下形式的代码,效果是一样的。package io.binghe.concurrent.lab06; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest06 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); private static DateFormat getDateFormat(){ DateFormat dateFormat = threadLocal.get(); if(dateFormat == null){ dateFormat = new SimpleDateFormat("yyyy-MM-dd"); threadLocal.set(dateFormat); } return dateFormat; } public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { getDateFormat().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }5.DateTimeFormatter方式DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。代码如下所示。package io.binghe.concurrent.lab06; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */ public class SimpleDateFormatTest07 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { LocalDate.parse("2020-01-01", formatter); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }可以看到,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。运行程序,输出结果如下所示。所有线程格式化日期成功使用DateTimeFormatter类来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。6.joda-time方式joda-time是第三方处理日期时间格式化的类库,是线程安全的。如果使用joda-time来处理日期和时间的格式化,则需要引入第三方类库。这里,以Maven为例,如下所示引入joda-time库。<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.9</version> </dependency>引入joda-time库后,实现的程序代码如下所示。package io.binghe.concurrent.lab06; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */ public class SimpleDateFormatTest08 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { DateTime.parse("2020-01-01", dateTimeFormatter).toDate(); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }这里,需要注意的是:DateTime类是org.joda.time包下的类,DateTimeFormat类和DateTimeFormatter类都是org.joda.time.format包下的类,如下所示。import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter;运行程序,输出结果如下所示。所有线程格式化日期成功使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。解决SimpleDateFormat类的线程安全问题的方案总结综上所示:在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用。synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的,经压测,DateTimeFormatter类处理日期和时间的性能效果还不错(后文单独写一篇关于高并发下性能压测的文章)。所以,推荐在高并发场景下的生产环境使用。joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用。来自:https://zhuanlan.zhihu.com/p/395751163
  • [其他] java线程池有哪些类型
    Java中主要有四种类型的线程池,它们分别是:可缓存线程池:通过Executors.newCachedThreadPool()创建,这种线程池会根据需要创建新线程,但同时会重用空闲的线程。如果线程池中的线程超过60秒未被使用,则会被终止并移除,这样可以避免资源浪费。固定线程池:通过Executors.newFixedThreadPool(int nThreads)创建,这种线程池的特点是核心线程数和最大线程数相同,适用于执行长期任务且任务数量固定的情况。定时线程池:通过Executors.newScheduledThreadPool(int corePoolSize)创建,适用于需要周期性执行任务的场景,如定时任务、定时扫描等。单线程化线程池:通过Executors.newSingleThreadExecutor()创建,这种线程池只有一个工作线程,适用于需要保证任务按顺序执行的场景。这些线程池都是ExecutorService接口的实现类,它们各自有不同的特点和适用场景。在实际开发中,选择合适的线程池类型可以提高程序的性能和响应速度。
总条数:696 到第
上滑加载中