工作中常见错误清单
1、springboot实现无数据库启动
问题
springboot往往是作为b/s系统的server端的架子来使用,但是有些时候,是作为静默的server,并没有界面和数据库,但是springboot默认是链接数据库的,如何解决这个问题呢?使用springboot,不连接数据库来启动项目。
解决方案
能百度到的解决方案,往往是在启动类上增加注解,如下:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class})
但是很多时候,加了这个注解,还是不能解决自动寻找配置文件中url进行初始化数据库连接的异常。
原因在于,在pom文件中,使用跟数据库相关的依赖,如spring-data,druid等,需要把数据库相关的依赖去掉,然后再加上注解,就能实现无数据库启动springboot了。
2、SpringBoot集成钉钉报警sdk
(解决Failed to introspect Class异常)
1. pom文件配置
在resources/lib
目录下加入钉钉的sdk的jar包。
链接: https://pan.baidu.com/s/11gor6cfrHPBkQcWSvJOYvQ 密码: 0kd4
<dependency>
<groupId>com.dingtalk.api</groupId>
<artifactId>dingtalk</artifactId>
<version>3.0.12</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/taobao-sdk-java-auto_1479188381469-20191122.jar
</systemPath>
</dependency>
maven插件配置:
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
// ++++++++ 添加这部分配置
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
// ++++++++ 添加这部分配置
</plugin>
</plugins>
注意:如果部署到服务器上,但是没有配置maven插件,在Spring Bean中使引用sdk文件,就会导致
Failed to introspect Class [xxx] from ClassLoader [org.springframework.boot.loader.LaunchedURLClassLoader@492691d7]
而实际上,这个异常出现的原因就是:Spring在加载bean时,找不到对应的Class文件。作者:小胖学编程链接:https://www.jianshu.com/p/83b82b4de2de来源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2. 代码编写
@Slf4j
public class DingtalkUtils {
/**
* 钉钉群消息推送
*
* @param webHook 钉钉生成的访问地址
* @param content 要通知的内容
*/
public static void dingtalk(String webHook, String content, String atMobiles) {
try{
DingTalkClient client = new DefaultDingTalkClient(webHook);
OapiRobotSendRequest request = new OapiRobotSendRequest();
request.setMsgtype("text");
OapiRobotSendRequest.Text text = new OapiRobotSendRequest.Text();
text.setContent(content);
request.setText(text);
if(atMobiles!=null) {
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
at.setAtMobiles(Arrays.asList(atMobiles.split(",")));
request.setAt(at);
}
client.execute(request);
} catch (Exception e) {
log.error("Alter to DingTalk error.", e);
}
}
}
3.推荐阅读
钉钉开放平台
Springboot Maven项目引入钉钉机器人jar包(SDK)遇到的问题
3.解决es查询只能查一万条数据问题
方法一:
如果需要搜索分页,可以通过from size组合来进行。from表示从第几行开始,size表示查询多少条文档。from默认为0,size默认为10,
如果搜索size大于10000,需要设置index.max_result_window参数
注意:size的大小不能超过index.max_result_window这个参数的设置,默认为10,000。
PUT _settings
{
"index": {
"max_result_window": "10000000"
}
}
方式二:
使用 scroll 代替,官方推荐方式。
优缺点:
方式一,当结果足够大的时候,会大大加大内存和CPU的消耗。使用非常方便。
方式二: 当结果足够大的时候, scroll 性能更加。但是不灵活和 scroll_id 难管理问题存在。
个人测试:当 结果足够大的时候 产生 scroll_id 性能也不低。如果只是一页页按照顺序,scroll是极好的,但是如果是无规则的翻页,那也是性能消耗极大的。
这里有两个步骤处理:
1.修改索引配置中的max_result_window
由于在logstash中配置索引的时候使用es的默认的索引模板,
默认的索引模板配置信息中的from+size最大值为10000,
原因见官网描述:
因此可以首先修改索引配置文件中max_result_window的值:
通过请求:
put http://ip:9200/index/_settings
{ “index” : { “max_result_window” : 10000000}}
得到结果:
{
“acknowledged”: true
}
说明配置成功.
查看索引信息:
http://ip:9200/index
可以在结果中看到该参数已经变成了一千万
2.修改
我发现改了上述的配置之后查询还是只有一万条, 经过查询发现还需要配置track_total_hits=true才能返回真实数据.
http://ip:port/index/_search?track_total_hits=true
4、关于OpenFeign使用后出现A bean with that name has already been defined and overriding is disabled.
OpenFeign可能出现的The bean ‘XXX.FeignClientSpecification’ could not be registered.
问题
原因
解决办法
添加springboot的配置
测试
问题
当我们使用OpenFeign启动项目以后可能会碰到如下问题
原因
存在一个以上的Feign接口指向同一个微服务
解决办法
控制台已经说明得很明白了,请看箭头所指。
添加springboot的配置
spring:
main:
spring:
main:
allow-bean-definition-overriding: true
#allow-bean-definition-overriding: true #允许多个Feign接口都指向一个服务
测试
成功启动
5.Consider defining a bean of type org.springframework.data.redis.connection.RedisConnectionFactory
Consider defining a bean of type ‘org.springframework.data.redis.core.RedisTemplate’ in your configuration
代码如下:
springboot 整合redis是爆出的错误,找不到RedisTemplate 配置
解决方法:
1、检查依赖是否导入
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2.检查是否配置了属性文件
host: 127.0.01
port: 6379
password:
database: 0
如果没有配置redis会调用默认的配置
3.如果以上两步还没有解决问题
尝试把注解修改成@Resource
@Resource
private RedisTemplate<String, Object> redisTemplate;
4.如果还没有解决问题
尝试修改下pom.xml 中 springboot的版本
org.springframework.boot
spring-boot-starter-parent
1.5.17.RELEASE
6.MySQL8.0 root 密码忘记修改(centos)
壹:修改MySQL配置文件可免密码登录
1、进入文件:vi /etc/my.cnf
2、在文件最后添加:skip-grant-tables;
3、重启systemctl restart mysqld
贰:登录MySQL数据库
1、登录:mysql -u root
2、提示输入密码按回车进入
3、进入数据库,切换用户:use mysql;
4、更新root用户信息,把密码置空:
update user set authentication_string=’’ where user=‘root’;
5、刷新表:flush privileges;
6、退出MySQL,注释掉/etc/my.cnf文件最后的 skip-grant-tables ;
7、重启MySQL数据库:systemctl restart mysqld
叁:设置密码
1、修改root用户密码:
ALTER user ‘root’@’%’ IDENTIFIED BY ‘yourPassw0rd’;
2、flush privileges;
3、退出MySQL数据库,重新登录即可。
pip install mysql-connector==2.2.9 -i https://pypi.tuna.tsinghua.edu.cn/simple
java诊断工具-Arthas
查看追踪日志命令
trace com.rlcloud.workerbee.controller.WorkflowController submit
JSON字符串转实体
String userString = "{"id":1,"name","xiaoming"}";
JSONObject userJson = JSONObject.parseObject(userString);
User user = JSON.toJavaObject(userJson,User.class);
Mysql数据入库es步骤:
-
通过ES提供的 构造器 来建立起和ES之间的远程连接
-
创建高层对象准备操作ES创建的连接
-
查询数据库表数据
-
循环遍历,构建索引数据
-
将数据通过bulk操作进入es
参考代码:
package com.rlcloud.log.util;
import com.alibaba.fastjson.JSON;
import org.apache.http.HttpHost;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* @author likk
* @create 2021-11-23 17:11
*/
@Component
@EnableScheduling
public class EsSyncGoodsDataService {
//通过ES提供的 构造器 来建立起和ES之间的远程连接
private static RestClientBuilder restClientBuilder = RestClient.builder(new HttpHost("192.168.22.131", 19200, "http"));
//创建高层对象准备操作ES创建的连接
private static RestHighLevelClient restHighLevelClient = new RestHighLevelClient(restClientBuilder);
@Resource
EsSyncGoodsDataMapper esSyncGoodsDataMapper;
@Scheduled(cron = "* * 1 * * ?")
//或直接指定时间间隔,这里是1小时
public void queryEsSyncGoodsData(){
//查询修改或创建的时间在一小时内的数据添加到ES中
List<EsSyncGoodsEntity> list= esSyncGoodsDataMapper.queryEsSyncGoodsData();
//循环 新增
list.forEach(a->{
try {
//创建批量请求
BulkRequest bulkRequest = new BulkRequest();
//创建索引:
IndexRequest indexRequest = new IndexRequest("goods_spu");
//放入数据json字符串 类型 json
indexRequest.source(JSON.toJSONString(a), XContentType.JSON);
//esId
indexRequest.id(a.getSpuId().toString());
//新增索引
bulkRequest.add(indexRequest);
//将数据通过bulk操作进入es
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println("新增成功");
}catch (Exception e){
e.printStackTrace();
}
});
System.out.println(list);
}
}
es数据入库mysql 完成
根据ip地址获取位置经纬度
1、maven依赖:
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.13.1</version>
</dependency>
2、需要下载GeoLite2-City.mmdb文件
3、样例代码:
package com.rlcloud.log.util;
import com.alibaba.fastjson.JSON;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.record.Location;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
@Slf4j
public class GeoIp2Demo {
public static void main(String[] args) {
// IP V4
String ip = "58.210.98.38";
// IP V6 也是可以的
DatabaseReader reader = null;
CityResponse response = null;
try {
File database = new File("D:\\software\\GeoLite2-City_20220719\\GeoLite2-City.mmdb");
// 读取数据库内容
reader = new DatabaseReader.Builder(database).build();
InetAddress ipAddress = InetAddress.getByName(ip);
// 获取查询结果
response = reader.city(ipAddress);
Location location = response.getLocation();
Double latitude = location.getLatitude();//维度
Double longitude = location.getLongitude();//经度
log.info("经度:{},维度:{}",longitude,latitude);
System.out.println(JSON.toJSONString(response));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
启动的项目为什么过一段时间服务就关了
SpringBoot项目运行一段时间后自动关闭的坑
经过一番查找才发现是由于自己启动方式不正确导致的,我在linux上运行jar包是通过 java -jar XXX.jar >/dev/null 2>&1 & 方式运行的,我一直以为&和nohup一样,后来才知道&运行的程序在SSH连接断开后就会退出。最后通过 nohup java -jar XXX.jar >/dev/null 2>&1 & 方式运行项目就可以了。
Lambda表达式
代码1:
/**
* 分页查询
* @param pageParam 分页参数
* @param qo 查询条件
* @return PageResult<DataSourceConfigVO> 分页结果数据
*/
default PageResult<DataSourceConfigPageVO> queryPage(PageParam pageParam, DataSourceConfigQO qo) {
IPage<DataSourceConfig> page = this.prodPage(pageParam);
LambdaQueryWrapperX<DataSourceConfig> wrapperX = WrappersX.lambdaQueryX(DataSourceConfig.class)
.likeIfPresent(DataSourceConfig::getTitle, qo.getTitle());
this.selectPage(page, wrapperX);
IPage<DataSourceConfigPageVO> voPage = page.convert(DataSourceConfigConverter.INSTANCE::poToPageVo);
return new PageResult<>(voPage.getRecords(), voPage.getTotal());
}
- 模糊查询
LambdaQueryWrapperX<DataSourceConfig> wrapperX = WrappersX.lambdaQueryX(DataSourceConfig.class).likeIfPresent(DataSourceConfig::getTitle, qo.getTitle());
/**
* 根据模板组ID查询模板文件目录项集合
* @param templateGroupId 模板组ID
* @return List<TemplateDirectoryEntry>
*/
default List<TemplateEntry> listByTemplateGroupId(Integer templateGroupId) {
return this.selectList(Wrappers.<TemplateEntry>lambdaQuery().eq(TemplateEntry::getGroupId, templateGroupId));
}
/**
* 检测是否在指定目录下存在指定名称的文件
* @param entryId 目录项ID
* @param name 文件名称
* @param groupId 组id
* @return 是否存在
*/
default boolean existSameName(Integer entryId, String name, Integer groupId) {
Long count = this.selectCount(Wrappers.<TemplateEntry>lambdaQuery().eq(TemplateEntry::getParentId, entryId)
.eq(TemplateEntry::getFilename, name).eq(TemplateEntry::getGroupId, groupId));
return count != null && count > 0;
}
this.selectList(Wrappers.<TemplateEntry>lambdaQuery().eq(TemplateEntry::getGroupId, templateGroupId));
LambdaQueryWrapperX<DataSourceConfig> wrapperX = WrappersX.lambdaQueryX(DataSourceConfig.class)
.likeIfPresent(DataSourceConfig::getTitle, qo.getTitle());
- 排序
//摘要信息查询
LambdaQueryWrapper<SysAnnouncementPO> qw = Wrappers.lambdaQuery(SysAnnouncementPO.class).orderByDesc(SysAnnouncementPO::getUpdateTime);
if (org.springframework.util.StringUtils.hasLength(queryDTO.getSearchWord())) {
qw.like(SysAnnouncementPO::getMsgAbstract, queryDTO.getSearchWord());
}
Page<SysAnnouncementPO> poPage = iAnnouncementService.page(MybatisPlusUtil.toPage(pageDTO), qw);
- 校验
//校验邮箱是否重复
List<UserPO> emailList = userService.list(Wrappers.lambdaQuery(UserPO.class).eq(UserPO::getEmail, user.getEmail()).notIn(UserPO::getId,user.getId()));
if (!CollectionUtils.isEmpty(emailList)) {
throw new BusinessException(ErrorCode.BUSINESS_USER_EMAIL_REPEAT_FAILED);
}
//校验手机号是否重复
List<UserPO> phoneList = userService.list(Wrappers.lambdaQuery(UserPO.class).eq(UserPO::getPhone, user.getPhone()).notIn(UserPO::getId,user.getId()));
if (!CollectionUtils.isEmpty(phoneList)) {
throw new BusinessException(ErrorCode.BUSINESS_USER_PHONE_REPEAT_FAILED);
}
获取文件真实格式
String originalFilename = multipartFile.getOriginalFilename();
String extension = "";
int i = originalFilename.lastIndexOf('.');
if (i > 0) {
extension = originalFilename.substring(i+1);
}
//MultipartFile转换成File
File file = ImageUtil.transferToFile(multipartFile);
//获取原始文件后缀
String fileType = CheckFileTypeUtil.getFileType(file.getAbsolutePath().toString());
//校验文件是否修改过原属性
if(!fileType.equals(extension)){
//检查是否是正确的图⽚格式
BufferedImage bufferedImage = ImageIO.read(multipartFile.getInputStream());
if(bufferedImage == null){
System.out.println("上传图片格式错误");
throw new BusinessException(ErrorCode.BUSINESS_BIDDING_UPLOAD_IMAGE_FAILED);
}
}
System.out.println("check :{}" + ImageUtil.isImage(multipartFile.getResource().getFile().getAbsoluteFile()));
//检验上传图片格式
if(!ImageUtil.isImage(multipartFile.getResource().getFile())){
System.out.println("上传图片格式错误");
}
MultipartFile 转File的几种方式
一、MultipartFile转File
二、代码示例
1.第一种方式
2.第二种方式
3.第三种方式
总结
前言
最近写项目有个需求是上传Excel文件并读取Excel文件中的内容,项目采用的是前后端分离的模式,前端采用FormData形式提交后台,后台接收类型是MultipartFile,但是我读取文件的时候类型是File,因为直接从MultipartFile里面获取流进行处理,文件过大时会造成内存溢出,所以需MultipartFile转File
一、MultipartFile转File
在将 MultipartFile 类型转换为file类型 时,一般都是新建临时文件夹,然后将其转换,可以指定路径新建,也可以建在项目根目录
二、代码示例
1.第一种方式
创建一个临时路径,转换之后得到File,然后再将其删除
File file = new File(path);
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file);
2.第二种方式
此方法我在尝试的过程中一直报错,没成功,百度看到有这种方式,所以记录一下
public File transferToFile(MultipartFile multipartFile) {
// 选择用缓冲区来实现这个转换即使用java 创建的临时文件 使用 MultipartFile.transferto()方法 。
File file = null;
try {
String originalFilename = multipartFile.getOriginalFilename();
String[] filename = originalFilename.split(“\.”);
file=File.createTempFile(filename[0], filename[1]);
multipartFile.transferTo(file);
file.deleteOnExit();
} catch (IOException e) {
e.printStackTrace();
}
return file;
}
3.第三种方式
这种方式会把上传的文件放到项目的根目录下,也要记得删啊
public File multipartFileToFile(MultipartFile file) throws Exception {
File toFile = null;
if (file.equals(“”) || file.getSize() <= 0) {
file = null;
} else {
InputStream ins = null;
ins = file.getInputStream();
toFile = new File(file.getOriginalFilename());
inputStreamToFile(ins, toFile);
ins.close();
}
return toFile;
}
private static void inputStreamToFile(InputStream ins, File file) {
try {
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
} catch (Exception e) {
throw new ToLogException(“读取文件错误”, e);
}
}
Java获取文件的真实格式
这个方法只能在有限的范围内有效。比如图片类型判断,音频文件格式判断,视频文件格式判断等这种肯定是2进制且专业性很强的文件类型判断。
1、文件类型枚取类
/**
* 文件类型枚取
*/
public enum FileType {
/**
* JEPG.
*/
JPEG("FFD8FF"),
/**
* PNG.
*/
PNG("89504E47"),
/**
* GIF.
*/
GIF("47494638"),
/**
* TIFF.
*/
TIFF("49492A00"),
/**
* Windows Bitmap.
*/
BMP("424D"),
/**
* CAD.
*/
DWG("41433130"),
/**
* Adobe Photoshop.
*/
PSD("38425053"),
/**
* Rich Text Format.
*/
RTF("7B5C727466"),
/**
* XML.
*/
XML("3C3F786D6C"),
/**
* HTML.
*/
HTML("68746D6C3E"),
/**
* Email [thorough only].
*/
EML("44656C69766572792D646174653A"),
/**
* Outlook Express.
*/
DBX("CFAD12FEC5FD746F"),
/**
* Outlook (pst).
*/
PST("2142444E"),
/**
* MS Word/Excel.
*/
XLS_DOC("D0CF11E0"),
/**
* MS Access.
*/
MDB("5374616E64617264204A"),
/**
* WordPerfect.
*/
WPD("FF575043"),
/**
* Postscript.
*/
EPS("252150532D41646F6265"),
/**
* Adobe Acrobat.
*/
PDF("255044462D312E"),
/**
* Quicken.
*/
QDF("AC9EBD8F"),
/**
* Windows Password.
*/
PWL("E3828596"),
/**
* ZIP Archive.
*/
ZIP("504B0304"),
/**
* RAR Archive.
*/
RAR("52617221"),
/**
* Wave.
*/
WAV("57415645"),
/**
* AVI.
*/
AVI("41564920"),
/**
* Real Audio.
*/
RAM("2E7261FD"),
/**
* Real Media.
*/
RM("2E524D46"),
/**
* MPEG (mpg).
*/
MPG("000001BA"),
/**
* Quicktime.
*/
MOV("6D6F6F76"),
/**
* Windows Media.
*/
ASF("3026B2758E66CF11"),
/**
* MIDI.
*/
MID("4D546864");
private String value = "";
/**
* Constructor.
*
* @param type
*/
private FileType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
2、文件类型判断类
/**
* 文件类型判断类
*/
public final class FileTypeJudge {
/**
* Constructor
*/
private FileTypeJudge() {}
/**
* 将文件头转换成16进制字符串
*
* @param 原生byte
* @return 16进制字符串
*/
private static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder();
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
/**
* 得到文件头
*
* @param filePath 文件路径
* @return 文件头
* @throws IOException
*/
private static String getFileContent(String filePath) throws IOException {
byte[] b = new byte[28];
InputStream inputStream = null;
try {
inputStream = new FileInputStream(filePath);
inputStream.read(b, 0, 28);
} catch (IOException e) {
e.printStackTrace();
throw e;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
throw e;
}
}
}
return bytesToHexString(b);
}
/**
* 判断文件类型
*
* @param filePath 文件路径
* @return 文件类型
*/
public static FileType getType(String filePath) throws IOException {
String fileHead = getFileContent(filePath);
if (fileHead == null || fileHead.length() == 0) {
return null;
}
fileHead = fileHead.toUpperCase();
FileType[] fileTypes = FileType.values();
for (FileType type : fileTypes) {
if (fileHead.startsWith(type.getValue())) {
return type;
}
}
return null;
}
}
3、测试类
public class Test {
/**
* @param args
*/
public static void main(String args[]) throws Exception {
System.out.println(FileTypeJudge.getType("D:\\test.zip"));
}
}
Java使用Tess4J 实现简单的图像识别(Maven版)
一、前言
最近有个朋友需要用Java做一个图像识别的东西,因此帮忙参考了网上资料写了一个基于Tess4J简单版的图像识别demo,供参考。
二、简单实例
1、首先创建一个新的maven项目(创建教程在此省略,自行百度),将所需jar包引入pom.xml
<dependencies>
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
2、在任意地方创建一个文件夹tessdata,将下载的chi_sim.traineddata 和 eng.traineddata语言包存放在该目录下,也可以直接存放到自己项目的resources/tessdata目录下。
语言库下载地址:https://github.com/tesseract-ocr/tessdata
语言库地址
3、编写代码
// 识别图片的路径(修改为自己的图片路径)
String path = "D:\\test.jpg";
// 语言库位置(修改为跟自己语言库文件夹的路径)
String lagnguagePath = "D:\\tessdata";
File file = new File(path);
ITesseract instance = new Tesseract();
//设置训练库的位置
instance.setDatapath(lagnguagePath);
//chi_sim :简体中文, eng 根据需求选择语言库
instance.setLanguage("eng");
String result = null;
try {
long startTime = System.currentTimeMillis();
result = instance.doOCR(file);
long endTime = System.currentTimeMillis();
System.out.println("Time is:" + (endTime - startTime) + " 毫秒");
} catch (TesseractException e) {
e.printStackTrace();
}
System.out.println("result: ");
System.out.println(result);
4、测试
本文以一张简单的图片为例:图片放置在D:\test.png根目录下,因此使用代码测试前需要修改代码中指定的两个路径!!!
识别图片:
测试
运行代码后:
识别后
5、可选步骤:配置环境变量(TESSDATA_PREFIX)
环境变量地址指向你存放语言包的文件夹路径,如:我的语言包路径在 D:\tessdata
@Scheduled 注解 用于定时循环执行任务
fixedDelay控制方法执行的间隔时间(毫秒),是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次。上个过程结束后,等待300ms,执行下个过程
fixedRate是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行。以固定300ms的频率执行某个过程,不管前面的过程是否还在进行,一般用于可以独立、并行的执行过程
cron表达式可以定制化执行任务,但是执行的方式是与fixedDelay相近的,也是会按照上一次方法结束时间开始算起。
initialDelay 如: @Scheduled(initialDelay = 10000,fixedRate = 15000,这个定时器就是在上一个的基础上加了一个initialDelay = 10000 意思就是在容器启动后,延迟10秒后再执行一次定时器,以后每15秒再执行一次该定时器。
例如:
@Scheduled(cron=“0 /10 * * * ?“) 表示每隔十分钟执行一次
每隔5秒执行一次:”/5 * * * * ?”
每隔1分钟执行一次:“0 */1 * * * ?”
每天23点执行一次:“0 0 23 * * ?”
每天凌晨1点执行一次:“0 0 1 * * ?”
每月1号凌晨1点执行一次:“0 0 1 1 * ?”
每月最后一天23点执行一次:“0 0 23 L * ?”
每周星期天凌晨1点实行一次:“0 0 1 ? * L”
在26分、29分、33分执行一次:“0 26,29,33 * * * ?”
每天的0点、13点、18点、21点都执行一次:“0 0 0,13,18,21 * * ?”
表示在每月的1日的凌晨2点调度任务:“0 0 2 1 * ? *”
表示周一到周五每天上午10:15执行作业:“0 15 10 ? * MON-FRI”
表示2002-2006年的每个月的最后一个星期五上午10:15执行:"0 15 10 ? 6L 2002-2006
//fixedRate 定义一个按一定频率执行的定时任务
@Scheduled(fixedRate = 5000)
public void fixedRate(){
System.out.println(“每5毫秒执行fixedRate一次:”+ DateUtils.dateToString(new Date(),“yyyy-MM-dd HH:mm:ss”));
}
//fixedDelay 定义一个按一定频率执行的定时任务,与上面不同的是,改属性可以配合initialDelay, 定义该任务延迟执行时间。
@Scheduled(fixedDelay = 5000)
public void fixedDelay(){
System.out.println("每5毫秒执行fixedDelay一次:"+ DateUtils.dateToString(new Date(),"yyyy-MM-dd HH:mm:ss"));
}
@Scheduled(initialDelay = 1000,fixedRate = 5000)
public void initialDelay(){
System.out.println("华丽的分割符----"+DateUtils.dateToString(new Date(),"yyyy-MM-dd HH:mm:ss"));
}
Springboot 2.x 如何解决重复提交 (本地锁的实践)
程序员内点事 2020-02-05 原文
有没有遇到过这种情况:网页响应很慢,提交一次表单后发现没反应,然后你就疯狂点击提交按钮(12306就经常被这样怒怼),如果做过防重复提交还好,否则那是什么级别的灾难就不好说了。。。
本文主要是应用 自定义注解、
spring AOP、
· Guava Cache
生成一种本地锁,来达到的防重复提交效果,由于是基于内存的缓存,所以这种实现方式并不适用于分布式服务
Guava是什么?
guava包是google嫌弃JAVA自带的类库不好用,自行研发的一套工具包,对JDK工具做了很好的拓展。例如:并发[Concurrency]、缓存[Caches]、 函数式风格[Functional idioms]、 字符串处理[Strings]等等。
一、引入Guava包依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
二、自定义LocalLock注解
自定义一个LocalLock注解用于需要防止重复提交的方法上
/**
* 锁的注解
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock { /**
* @author fly
*/
String key() default "";
}
注解定义好以后就需要做AOP拦截器切面的具体实现,在 interceptor()
方法上采用的是 Around
(环绕增强) ,所有带 LocalLock
注解的都将被切面处理;
既然是缓存,那紧跟的属性一定要有过期时间,通过expireAfterWrite
设置缓存的过期时间,maximumSize
设置缓存的个数。
通过在内存中查询key是否存在来判断是否让再次提交,和Redis
的setNX
方法是一个原理。
那么这个注解该怎么用呢?
@Aspect
@Configuration
public class LockMethodInterceptor { private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
// 最大缓存 100 个
.maximumSize(1000)
// 设置写缓存后 5 秒钟过期
.expireAfterWrite(5, TimeUnit.SECONDS)
.build(); @Around("execution(public * *(..)) && @annotation(com.battcn.annotation.LocalLock)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
LocalLock localLock = method.getAnnotation(LocalLock.class);
String key = getKey(localLock.key(), pjp.getArgs());
if (!StringUtils.isEmpty(key)) {
if (CACHES.getIfPresent(key) != null) {
throw new RuntimeException("请勿重复请求");
}
// 如果是第一次请求,就将 key 当前对象压入缓存中
CACHES.put(key, key);
}
try {
return pjp.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("服务器异常");
} finally {
// TODO 为了演示效果,这里就不调用 CACHES.invalidate(key); 代码了
}
} /**
* key 的生成策略,如果想灵活可以写成接口与实现类的方式(TODO 后续讲解)
*
* @param keyExpress 表达式
* @param args 参数
* @return 生成的key
*/
private String getKey(String keyExpress, Object[] args) {
for (int i = 0; i < args.length; i++) {
keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());
}
return keyExpress;
}
}
控制层的实现
我们将注解加在控制层方法上,key = "city:arg[0]
key自己定义,arg[0]
这个匹配规则表示替换成第一个参数。那么就实现city:token
在一定时间内不可以重复提交了
@RestController
@RequestMapping("/city")
public class BookController {
@LocalLock(key = "city:arg[0]")
@GetMapping
public String query(@RequestParam String token) {
return "ok- " + token;
}
}
测试
接下来我们就测试一下,我用的是postman
第一请求正常响应
紧接着请求第二次,返回结果“重复提交”,显然我们实现成功了
很多时候我们都被一些技术高大上又抽象的专业名称所迷惑,看似遥不可及晦涩难懂,但事实上动手实践一下,你会发现简单得很!
小数点后保留2位小数的正则表达式
今天同事问我,这个正则表达式是什么意思?(如下所示)
^(([1-9]{1}\\d*)|([0]{1}))(\\.(\\d){0,2})?$
我说目前我也不知道它代表什么,那要看你的使用场景了,一时半会也看不出来,我得分析一下。
他说,要求保留两位小数。这是我网上百度的用法,你看一下对不对?
于是,我就去求证了。
分析
要看明白正则表达式,首先就要从语法层面进行分析,把每一部分都了解清楚,弄明白,必须知道每一部分匹配的是什么,随后整个表达式的意思也就迎刃而解了。
^(([1-9]{1}\\d*)|([0]{1}))(\\.(\\d){0,2})?$
(1)首先从写法上可以看出,使用了显式定义正则表达式的语法,因为其中存在对字符串""(反斜杠)的转义。
普及一下:正则表达式的定义共有2种方式:显示定义和隐式定义。
/\d是正则表达式中的元字符,用于匹配数字,相当于[0-9],所以[0-9]也可以写成\d/
var myregex = new RegExp(“[0-9]”); //显式定义
var myregex = /[0-9]/; //隐式定义
对两种定义方式的解释说明:
(2)其次,我们把正则表达式拆分开,以分组的形式来观察,问题就简单多了。因为复杂的正则表达式就是由许多子表达式构成的。
此处涉及到正则表达的3个知识点:定位符、限定符和分组
何谓定位符?即限定某些字符出现的位置。
说明:^表示必须以什么字符开头;$表示必须以什么字符结尾。
何谓限定符?即限定某个字符或某类字符出现的次数。
说明:
- 表示重复0次或更多次(任意次数);
?表示重复0次或1次(最多1次);
{n}表示重复n次;
{n,m}表示重复n-m次;
何谓分组?分组又称为子表达式,即把一个正则表达式的全部或部分分成一个或多个组。
语法:分组使用的字符为“(”和“)”,即左括号和右括号。每一个子表达式都可以当做一个整体来处理。
拆分的步骤:
//初始
^(([1-9]{1}\d*)|([0]{1}))(\.(\d){0,2})?$
//为了简化操作,方便观察,在这里把字符串的转义都去掉。
^(([1-9]{1}\d*)|([0]{1}))(.(\d){0,2})?$
即:(([1-9]{1}\d*)|([0]{1}))(.(\d){0,2})
//第一步:先分组,不管定位符和限定符。总共可分为两大组。
(([1-9]{1}\d*)|([0]{1}))//第一大组:整数部分
(.(\d){0,2})//第二大组:小数部分
//第二步:继续分组,将第一大组(整数部分)继续拆分,可分为3部分。
([1-9]{1}\d*)//第一部分
| //第二部分
([0]{1})//第三部分
知识点说明:
[…]是正则表达式中的元字符。它会匹配方括号中的所有字符。
|是正则表达式中的选择符。简单来说就是:用于二选一。即选择2个选项之中的任意一个,选他或选她。
第二步分析:
因此,在第二步中,第一部分和第三部分的子表达式,这两部分只要满足任何一个部分都可以匹配的上。
注:这也就说明了整数部分的两种情况:首位是0和首位不是0。即0.12、13.14、5.21
先来看第一部分中的内容,即第一种情况,首位不为0:([1-9]{1}\d*)
可以看出,第一部分由两个模块组成,即[1-9]{1}和\d*。
(1)[1-9]{1}表示1-9之间的数字只出现一次,即保证了该数的首位不是0,总之是大于0的数。
(2)\d*表示0-9之间的数字可以出现任意次,即0次或更多次。出现0次说明是一位整数。出现更多次就是多位整数。
再来看第三部分中的内容,即第二种情况,首位为0:([0]{1})【一个字符没必要使用[元字符,直接写0就行,即(0{1})】
这个就比较简单了,表示数字0只能出现一次,即首位只能是0,而且只能有一个0。
总结:第一大组匹配了整数部分的情况,大于0的数还是小于1的数。
(.(\d){0,2})//第二大组:小数部分
//第三步:继续分组,将第二大组(小数部分)继续拆分,可分为3部分。
. //第一部分
(\d) //第二部分
{0,2} //第三部分
.(点)是正则表达式中的元字符。它会匹配除了换行符以外的任意字符。
注:由于.(点)是元字符,所以,如果想要匹配字面意义上的点时(此处需要匹配小数点),需要使用转义字符\(反斜杠)将它进行转义,即.。
第三步分析:
第一部分:.表示匹配小数点。
第二部分:(\d)表示匹配数字0-9。【此处可以不用分组符,用了多此一举】
第三部分:{0,2}表示将前面的字符重复0-2次,即重复的次数不确定,可能重复0次,可能重复1次,也可能重复2次。
其实,二三部分应该放在一块分析,即\d{0,2},表示:0个数字、1个数字或者2个数字。
总结:第二大组匹配了小数点后所保留的位数,分别是小数点后保留0位小数(即正整数)、小数点后保留1位小数或者小数点后保留2位小数。
注:如果要匹配小数点后保留2位小数,则只需要改一下限定符即可。.\d{2}
至此为止,我们已经将正则表达式拆分完毕,现在就需要将它们完整的分析一下。
(([1-9]{1}\d*)|(0{1}))(.\d{0,2})
完整分析:
/1、先加上限定符?(问号)【它表示前面紧跟的字符,即(.\d{0,2})这个整体,重复0次或1次(最多1次)】/
(([1-9]{1}\d*)|(0{1}))(.\d{0,2})?
//如果(.\d{0,2})不存在,表示该数为自然数(指非负整数,即正整数与0的集合),无小数部分。
//如果(.\d{0,2})存在,表示该数为小数。
/*如果是小数的话,按照大小来分,存在两种情况:
*(1)可能是大于0的小数;即([1-9]{1}\d)
**(2)也可能是小于1的小数;即(0{1})
*/
下载
/*如果是小数的话,按照位数来分,存在3种情况:
**(1)无小数位数,为整数;即(.\d{0,2})重复0次,相当于没有它
**(2)有小数位数,为小数;即(.\d{0,2})重复1次
** 小数的位数又分为三种情况:{0,2}相当于是{0},{1},{2}的集合,它们之间是“或”的关系
** 小数点后保留0位小数;即.\d{0}
** 小数点后保留1位小数;即.\d{1}
** 小数点后保留2位小数;即.\d{2}
*/
在这里普及一下整数的知识:
我们以0为界限,将整数分为3大类:
正整数。即大于0的整数,如:1,2,3,…
0。 0既不是正整数,也不是负整数(0是整数)。
负整数。即小于0的整数,如:-1,-2,-3,…
/*2、最后再加上限定开始和结束位置的限定符,^和KaTeX parse error: Undefined control sequence: \d at position 16: 。*/ ^(([1-9]{1}\̲d̲*)|(0{1}))(\.\d…
//这就表示了,它必须以数字开头和结尾。限定了它必须是一个数字,而不能包含其他的字符。
结果
^(([1-9]{1}\d*)|(0{1}))(.\d{0,2})?$就表示了小数点后可以保留0位、1位、或2位小数。
如果我们要求小数点后只能保留2位小数,则修改表示小数点后面数字的重复次数(即位数)的限定符{},直接将{0,2}改为{2},然后去掉限定符?即可。去掉了 ? 就代表该数不可能是整数,一定是小数。
最终的正则表达式:^(([1-9]{1}\d*)|(0{1}))(.\d{2})$
push to origin/master was rejected错误解决方案
idea中,发布项目到OSChina的Git中,当时按照这样的流程添加Git,然后push,提示:push to origin/master war rejected"。
大概原因是:初始化项目时,远程仓库我建了README.md文件,而本地仓库与远程仓库尚未进行文件关联,因此需要将两个仓库的文件进行关联后提交。
解决方案如下:
1.切换到自己项目所在的目录,右键选择GIT BASH Here,Idea中可使用Alt+F12
2.在terminl窗口中依次输入命令:
git pull
git pull origin master
git pull origin master --allow-unrelated-histories
3.在idea中重新push自己的项目
git push -u origin master -f
注:
如果git仓库有README文件,而本地没有,可以将远程仓库的README先删除掉;
有部分朋友如果没有成功的话,请删除自己本地项目下.git的隐藏目录,重新尝试关联项目推送即可!
$ cd 当前项目目录
$ git init
$ git remote add origin [git仓库地址]
$ git add .
$ git commit -m "Initial commit"
$ git push -u origin master -f
linux上传文件
rz -E
JAVA多线程编程之异步
日常开发中我们在一个接口中需要处理多个任务,通常都是串行的,这样导致接口的响应时间是每个任务的执行时间的总和。为了缩短响应时间,通常会使用异步处理多任务。
需求举例:查询书籍基本信息,书籍详细信息,作者信息并将结果数据返回。
假设查询书籍基本信息花费500毫秒,查询书籍详细信息花费500毫秒,查询作者信息花费500毫秒,共计1500毫秒,使用异步处理时间一般都是远小于1500毫秒的。
下面使用异步调用方式优化接口
1、异步任务类
实现 Callable 接口,用来处理带返回结果的任务。taskId 用来区别返回结果集数据
package com.example.demo.task;
import java.util.concurrent.Callable;
/**
-
异步任务
-
@param
*/
public class AsynTaskCallable implements Callable{private String taskId;
private Callable task;
public AsynTaskCallable(String taskId, Callable task) {
this.taskId = taskId;
this.task = task;
}@Override
public T call() throws Exception {
T callResult = task.call();
TaskResult result = new TaskResult();
result.setTaskId(taskId);
result.setData(callResult);
return (T) result;
}
}
2、异步任务调用类
用来调用异步任务辅助类,completionService 用来指定线程池执行异步任务,tasks 为带返回结果的任务,可以实现多场景复用,减少重复编写相似的代码。
package com.example.demo.task;
import com.sun.istack.internal.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
/**
-
异步任务调用
*/
public class AsynTaskHelper {/**
- 使用指定线程池执行异步任务
*/
private CompletionService<TaskResult> completionService = null;
/**
- 任务集合
*/
private List tasks = null;
/**
- 设置线程池
- @param executorService 线程池
- @return
*/
public AsynTaskHelper setExecutorService(ExecutorService executorService){
completionService = new ExecutorCompletionService(executorService);
return this;
}
/**
- 添加任务,返回结果
- @param taskId
- @param task
- @return
*/
public AsynTaskHelper addTask(String taskId, Callable task) {
AsynTaskCallable callProxy = new AsynTaskCallable(taskId, task);
if(null == tasks || tasks.isEmpty()){
tasks = new ArrayList<>();
}
tasks.add(callProxy);
return this;
}
/**
- 提交任务
- @return
*/
public AsynTaskHelper submit(){
if(null != tasks && !tasks.isEmpty()){
for (Callable callResult : tasks) {
completionService.submit(callResult);
}
}
return this;
}
/**
- 获取返回结果
- @return Map<K, V> K为任务Id
- @throws ExecutionException
- @throws InterruptedException
*/
public Map<String, T> getResult() throws ExecutionException, InterruptedException {
return getResult(2, TimeUnit.SECONDS);
}
/**
- 获取返回结果
- @param timeout
- @param unit
- @return Map<K, V> K为任务Id
- @throws InterruptedException
- @throws ExecutionException
*/
public Map<String, T> getResult(long timeout,@NotNull TimeUnit unit) throws InterruptedException, ExecutionException {
Map<String, T> result = new HashMap<>();
if(null == tasks){
return result;
}
for (int i = 0; i < tasks.size(); i++) {
Future<TaskResult> poll = completionService.poll(timeout, unit);
if(null != poll){
TaskResult task = poll.get();
if(null != poll && null != task){
result.put(task.getTaskId(), task.getData());
}
}
}
return result;
}
- 使用指定线程池执行异步任务
}
3、任务结果类
用来接收异步任务返回结果数据
package com.example.demo.task;
/**
-
任务结果数据
-
@param
*/
public class TaskResult {/**
- 任务Id
*/
private String taskId;
/**
- 返回数据
*/
private T data;
public String getTaskId() {
return taskId;
}public void setTaskId(String taskId) {
this.taskId = taskId;
}public T getData() {
return data;
}public void setData(T data) {
this.data = data;
}@Override
public String toString() {
return “TaskResult{” +
“taskId='” + taskId + ‘’’ +
“, data=” + data +
‘}’;
}
}
4、异步调用
指定线程池执行任务 - 任务Id
ExecutorService executor = Executors.newFixedThreadPool(500);
正常业务操作
//查询Book信息
Callable bookCall = () -> bookService.get(bookId);
//查询BookDetail信息
Callable bookDetailCall = () -> bookDetailService.get(bookId);
//查询Author信息
Callable auhtorCall = () -> authorService.get(bookId);
创建异步任务
//创建异步任务
AsynTaskHelper taskCallors = new AsynTaskHelper()
.setExecutorService(executor)
.addTask(“book”, bookCall)
.addTask(“bookDetail”, bookDetailCall)
.addTask(“author”, auhtorCall)
.submit();
获取结果,因为任务是异步的,可能第一时间拿不到结果,这里使用自旋的方式获取结果,如果3秒后还是没有结果则直接返回。
do{
Map map = taskCallors.getResult();
book = (Book) map.get(“book”);
bookDetail = (BookDetail) map.get(“bookDetail”);
author = (Author) map.get(“author”);
runTime = System.currentTimeMillis() - beginTime;
} while ((null == book || null == bookDetail || null == author) && runTime < 3000);
完整示例调用代码
package com.example.demo.controller;
import com.example.demo.domain.Author;
import com.example.demo.domain.Book;
import com.example.demo.domain.BookDetail;
import com.example.demo.service.AuthorService;
import com.example.demo.service.BookDetailService;
import com.example.demo.service.BookService;
import com.example.demo.task.AsynTaskHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
@RestController
public class BookController {
@Autowired
private BookService bookService;
@Autowired
private BookDetailService bookDetailService;
@Autowired
private AuthorService authorService;
private ExecutorService executor = Executors.newFixedThreadPool(500);
@GetMapping("books5/{bookId}")
public Map find5(@PathVariable String bookId) throws ExecutionException, InterruptedException {
Map<String, Object> result = new HashMap<>();
Long beginTime = System.currentTimeMillis();
System.out.println("开始并行查询,开始时间:" + beginTime);
//查询Book信息
Callable<Book> bookCall = () -> bookService.get(bookId);
//查询BookDetail信息
Callable<BookDetail> bookDetailCall = () -> bookDetailService.get(bookId);
//查询Author信息
Callable<Author> auhtorCall = () -> authorService.get(bookId);
//创建异步任务
AsynTaskHelper taskCallors = new AsynTaskHelper()
.setExecutorService(executor)
.addTask("book", bookCall)
.addTask("bookDetail", bookDetailCall)
.addTask("author", auhtorCall)
.submit();
Book book = null;
BookDetail bookDetail = null;
Author author = null;
long runTime;
do{
Map map = taskCallors.getResult();
book = (Book) map.get("book");
bookDetail = (BookDetail) map.get("bookDetail");
author = (Author) map.get("author");
runTime = System.currentTimeMillis() - beginTime;
} while ((null == book || null == bookDetail || null == author) && runTime < 3000);
System.out.println("结束并行查询,总耗时:" + (System.currentTimeMillis() - beginTime));
result.put("book", book);
result.put("detail", bookDetail);
result.put("author", author);
return result;
}
}
通过 AsynTaskHelper 调用异步任务能缩短接口响应时间,进而提升系统并发能力,后续有类似的使用场景也支持复用,减少重复编码工作。
代码生成
freemarker 格式化输出之首字母小写
AdminUser:uncap_first:${'AdminUser'?uncap_first} //首字母小写
uncap_first: adminUser
输出如下:
uncap_first: adminUser
使用exclusions标签排除jar
<!-- shiro-freemarker-tags -->
<dependency>
<groupId>net.mingsoft</groupId>
<artifactId>shiro-freemarker-tags</artifactId>
<version>0.1</version>
<!-- 排除掉里面的quartz包 -->
<exclusions>
<exclusion>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</exclusion>
</exclusions>
</dependency>
mybatis 时间范围查询
mybatis框架支持直接用>=或<=查询日期范围,如:
<if test="startTime != null and startTime != ''">
and timer.create_time >= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
and timer.create_time <= #{endTime}
</if>
但这种方式,如果开始时间和结束时间是同一个日期,就查不出来了,一种解决方法是用DATE_FORMAT转一下日期,如:
<if test="startTime != null and startTime !='' ">
<![CDATA[ and DATE_FORMAT(create_time, '%Y-%m-%d') >= #{startTime} ]]>
</if>
<if test="endTime != null and endTime !='' ">
<![CDATA[ and DATE_FORMAT(create_time, '%Y-%m-%d') <= #{endTime} ]]>
</if>
mysql怎样查询日期范围
MySQL 提供了 BETWEEN AND 关键字,用来判断字段的数值是否在指定范围内。
BETWEEN AND 需要两个参数,即范围的起始值和终止值。如果字段值在指定的范围内,则这些记录被返回。如果不在指定范围内,则不会被返回。
使用 BETWEEN AND 的基本语法格式如下:
其中:
- NOT:可选参数,表示指定范围之外的值。如果字段值不满足指定范围内的值,则这些记录被返回。
- 取值1:表示范围的起始值。
- 取值2:表示范围的终止值。
BETWEEN AND 和 NOT BETWEEN AND 关键字在查询指定范围内的记录时很有用。例如,查询学生的年龄段、出生日期,员工的工资水平等。
示例如下:
如下表,查询,create_time为datetime类型,查询两个日期范围内的数据。
方式一、between…and(推荐)
SELECT * FROM k_student WHERE create_time between '2019-07-25 00:00:33' and '2019-07-25 00:54:33'
方式二、大小于号
SELECT * FROM k_student WHERE create_time >= '2019-07-25 00:00:33' AND create_time <= '2019-07-25 00:54:32'
方式三、转换为UNIX_TIMESTAMP比较,create_time若加了索引,不走索引
SELECT * FROM k_student WHERE UNIX_TIMESTAMP(create_time) between UNIX_TIMESTAMP('2019-07-25 00:00:33') and UNIX_TIMESTAMP('2019-07-25 00:54:33')
EasyExcel
请求参数时间格式
@DateTimeFormat 只会在GET请求中生效,对于请求体中的转换无能为力,这个时候需要@JsonFormat
@ApiModelProperty(value="实名认证提交时间")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private java.util.Date submitTime;
Docker常用命令
docker run -d nginx:alpine # 后台运行Nginx
docker run -d --name red_srv redis # 后台运行Redis
docker run -it --name ubuntu 2e6 sh # 使用IMAGE ID,登录Ubuntu18.04
对于正在运行中的容器,我们可以使用 docker exec 命令在里面执行另一个程序,效果和 docker run 很类似,但因为容器已经存在,所以不会创建新的容器。它最常见的用法是使用 -it 参数打开一个 Shell,从而进入容器内部,例如:
docker exec -it red_srv sh
办法当然有,就是在执行 docker run 命令的时候加上一个 --rm 参数,这就会告诉 Docker 不保存容器,只要运行完毕就自动清除,省去了我们手工管理容器的麻烦。我们还是用刚才的 Nginx、Redis 和 Ubuntu 这三个容器来试验一下,加上 --rm 参数(省略了 name 参数):
docker run -d --rm nginx:alpine
docker run -d --rm redis
docker run -it --rm 2e6 sh
这里有两个 COPY 命令示例,你可以看一下:
COPY ./a.txt /tmp/a.txt # 把构建上下文里的a.txt拷贝到镜像的/tmp目录
COPY /etc/hosts /tmp # 错误!不能使用构建上下文之外的文件
有的时候在 Dockerfile 里写这种超长的 RUN 指令很不美观,而且一旦写错了,每次调试都要重新构建也很麻烦,所以你可以采用一种变通的技巧:把这些 Shell 命令集中到一个脚本文件里,用 COPY 命令拷贝进去再用 RUN 来执行:
COPY setup.sh /tmp/ # 拷贝脚本到/tmp目录
RUN cd /tmp && chmod +x setup.sh \ # 添加执行权限
&& ./setup.sh && rm setup.sh # 运行脚本然后再删除
RUN 指令实际上就是 Shell 编程,如果你对它有所了解,就应该知道它有变量的概念,可以实现参数化运行,这在 Dockerfile 里也可以做到,需要使用两个指令 ARG 和 ENV。
它们区别在于 ARG 创建的变量只在镜像构建过程中可见,容器运行时不可见,而 ENV 创建的变量不仅能够在构建镜像的过程中使用,在容器运行时也能够以环境变量的形式被应用程序使用。
下面是一个简单的例子,使用 ARG 定义了基础镜像的名字(可以用在“FROM”指令里),使用 ENV 定义了两个环境变量:
ARG IMAGE_BASE="node"
ARG IMAGE_TAG="alpine"
ENV PATH=$PATH:/tmp
ENV DEBUG=OFF
还有一个重要的指令是 EXPOSE,它用来声明容器对外服务的端口号,对现在基于 Node.js、Tomcat、Nginx、Go 等开发的微服务系统来说非常有用:
EXPOSE 443 # 默认是tcp协议
EXPOSE 53/udp # 可以指定udp协议
如何编写 Dockerfile 内容稍微多一点,我再简单做个小结:
- 创建镜像需要编写 Dockerfile,写清楚创建镜像的步骤,每个指令都会生成一个 Layer。
- Dockerfile 里,第一个指令必须是 FROM,用来选择基础镜像,常用的有 Alpine、Ubuntu 等。其他常用的指令有:COPY、RUN、EXPOSE,分别是拷贝文件,运行 Shell 命令,声明服务端口号。
- docker build 需要用 -f 来指定 Dockerfile,如果不指定就使用当前目录下名字是“Dockerfile”的文件。
- docker build 需要指定“构建上下文”,其中的文件会打包上传到 Docker daemon,所以尽量不要在“构建上下文”中存放多余的文件。
- 创建镜像的时候应当尽量使用 -t 参数,为镜像起一个有意义的名字,方便管理。
最后是课下作业时间,这里有一个完整的 Dockerfile 示例,你可以尝试着去解释一下它的含义,然后再自己构建一下:
# Dockerfile
# docker build -t ngx-app .
# docker build -t ngx-app:1.0 .
ARG IMAGE_BASE="nginx"
ARG IMAGE_TAG="1.21-alpine"
FROM ${IMAGE_BASE}:${IMAGE_TAG}
COPY ./default.conf /etc/nginx/conf.d/
RUN cd /usr/share/nginx/html \
&& echo "hello nginx" > a.txt
EXPOSE 8081 8082 8083
该怎么上传自己的镜像
第一步,你需要在 Docker Hub 上注册一个用户,这个就不必再多说了。
第二步,你需要在本机上使用 docker login 命令,用刚才注册的用户名和密码认证身份登录,像这里就用了我的用户名“chronolaw”:
第三步很关键,需要使用 docker tag 命令,给镜像改成带用户名的完整名字,表示镜像是属于这个用户的。或者简单一点,直接用 docker build -t 在创建镜像的时候就起好名字。
这里我就用上次课里的镜像“ngx-app”作为例子,给它改名成 chronolaw/ngx-app:1.0:
docker tag ngx-app chronolaw/ngx-app:1.0
第四步,用 docker push 把这个镜像推上去,我们的镜像发布工作就大功告成了:
docker push chronolaw/ngx-app:1.0
好了,今天我们一起学习了镜像仓库,了解了 Docker Hub 的使用方法,整理一下要点方便你加深理解:
- 镜像仓库(Registry)是一个提供综合镜像服务的网站,最基本的功能是上传和下载。
- Docker Hub 是目前最大的镜像仓库,拥有许多高质量的镜像。上面的镜像非常多,选择的标准有官方认证、下载量、星数等,需要综合评估。
- 镜像也有很多版本,应该根据版本号和操作系统仔细确认合适的标签。在 Docker Hub 注册之后就可以上传自己的镜像,用 docker tag 打上标签再用 docker push 推送。
- 离线环境可以自己搭建私有镜像仓库,或者使用 docker save 把镜像存成压缩包,再用 docker load 从压缩包恢复成镜像。
Docker如何共享主机上的文件
我还是以 Redis 为例,启动容器,使用 -v 参数把本机的“/tmp”目录挂载到容器里的“/tmp”目录,也就是说让容器共享宿主机的“/tmp”目录:
docker run -d --rm -v /tmp:/tmp redis
然后我们再用 docker exec 进入容器,查看一下容器内的“/tmp”目录,应该就可以看到文件与宿主机是完全一致的。
docker exec -it b5a sh # b5a是容器ID
你也可以在容器里的“/tmp”目录下随便做一些操作,比如删除文件、建立新目录等等,再回头观察一下宿主机,会发现修改会即时同步,这就表明容器和宿主机确实已经共享了这个目录。
-v 参数挂载宿主机目录的这个功能,对于我们日常开发测试工作来说非常有用,我们可以在不变动本机环境的前提下,使用镜像安装任意的应用,然后直接以容器来运行我们本地的源码、脚本,非常方便。
这里我举一个简单的例子。比如我本机上只有 Python 2.7,但我想用 Python 3 开发,如果同时安装 Python 2 和 Python 3 很容易就会把系统搞乱,所以我就可以这么做:
1、先使用 docker pull 拉取一个 Python 3 的镜像,因为它打包了完整的运行环境,运行时有隔离,所以不会对现2、有系统的 Python 2.7 产生任何影响。在本地的某个目录编写 Python 代码,然后用 -v 参数让容器共享这个目录。
3、现在就可以在容器里以 Python 3 来安装各种包,再运行脚本做开发了。
docker pull python:alpine
docker run -it --rm -v `pwd`:/tmp python:alpine sh
显然,这种方式比把文件打包到镜像或者 docker cp 会更加灵活,非常适合有频繁修改的开发测试工作。
Docker 如何实现网络互通
网络互通的关键在于“打通”容器内外的网络,而处理网络通信无疑是计算机系统里最棘手的工作之一,有许许多多的名词、协议、工具,在这里我也没有办法一下子就把它都完全说清楚,所以只能从“宏观”层面讲个大概,帮助你快速理解。
Docker 提供了三种网络模式,分别是 null、host 和 bridge。
- null 是最简单的模式,也就是没有网络,但允许其他的网络插件来自定义网络连接,这里就不多做介绍了。
- host 的意思是直接使用宿主机网络,相当于去掉了容器的网络隔离(其他隔离依然保留),所有的容器会共享宿主机的 IP 地址和网卡。这种模式没有中间层,自然通信效率高,但缺少了隔离,运行太多的容器也容易导致端口冲突。
host 模式需要在 docker run 时使用 --net=host 参数,下面我就用这个参数启动 Nginx:
docker run -d --rm --net=host nginx:alpine
为了验证效果,我们可以在本机和容器里分别执行 ip addr 命令,查看网卡信息:
ip addr # 本机查看网卡
docker exec xxx ip addr # 容器查看网卡
本机查看网卡
容器查看网卡
可以看到这两个 ip addr 命令的输出信息是完全一样的,比如都是一个网卡 ens160,IP 地址是“192.168.10.208”,这就证明 Nginx 容器确实与本机共享了网络栈。
-
第三种 bridge,也就是桥接模式,它有点类似现实世界里的交换机、路由器,只不过是由软件虚拟出来的,容器和宿主机再通过虚拟网卡接入这个网桥(图中的 docker0),那么它们之间也就可以正常的收发网络数据包了。不过和 host 模式相比,bridge 模式多了虚拟网桥和网卡,通信效率会低一些。
和 host 模式一样,我们也可以用 --net=bridge 来启用桥接模式,但其实并没有这个必要,因为 Docker 默认的网络模式就是 bridge,所以一般不需要显式指定。
下面我们启动两个容器 Nginx 和 Redis,就像刚才说的,没有特殊指定就会使用 bridge 模式:
docker run -d --rm nginx:alpine # 默认使用桥接模式
docker run -d --rm redis # 默认使用桥接模式
然后我们还是在本机和容器里执行 ip addr 命令(Redis 容器里没有 ip 命令,所以只能在 Nginx 容器里执行):
对比一下刚才 host 模式的输出,就可以发现容器里的网卡设置与宿主机完全不同,eth0 是一个虚拟网卡,IP 地址是 B 类私有地址“172.17.0.2”。
我们还可以用 docker inspect 直接查看容器的 ip 地址:
docker inspect xxx |grep IPAddress
这显示出两个容器的 IP 地址分别是“172.17.0.2”和“172.17.0.3”,而宿主机的 IP 地址则是“172.17.0.1”,所以它们都在“172.17.0.0/16”这个 Docker 的默认网段,彼此之间就能够使用 IP 地址来实现网络通信了。
Docker 如何分配服务端口号
你一定知道,服务器应用都必须要有端口号才能对外提供服务,比如 HTTP 协议用 80、HTTPS 用 443、Redis 是 6379、MySQL 是 3306。第 4 讲我们在学习编写 Dockerfile 的时候也看到过,可以用 EXPOSE 指令声明容器对外的端口号。
一台主机上的端口号数量是有限的,而且多个服务之间还不能够冲突,但我们打包镜像应用的时候通常都使用的是默认端口,容器实际运行起来就很容易因为端口号被占用而无法启动。
解决这个问题的方法就是加入一个“中间层”,由容器环境例如 Docker 来统一管理分配端口号,在本机端口和容器端口之间做一个“映射”操作,容器内部还是用自己的端口号,但外界看到的却是另外一个端口号,这样就很好地避免了冲突。
**端口号映射需要使用 bridge 模式,并且在 docker run 启动容器时使用 -p 参数,形式和共享目录的 -v 参数很类似,用 : 分隔本机端口和容器端口。**比如,如果要启动两个 Nginx 容器,分别跑在 80 和 8080 端口上:
docker run -d -p 80:80 --rm nginx:alpine
docker run -d -p 8080:80 --rm nginx:alpine
这样就把本机的 80 和 8080 端口分别“映射”到了两个容器里的 80 端口,不会发生冲突,我们可以用 curl 再验证一下:
使用 docker ps 命令能够在“PORTS”栏里更直观地看到端口的映射情况:
照例简单小结一下这次的要点:
- docker cp 命令可以在容器和主机之间互相拷贝文件,适合简单的数据交换。
- docker run -v 命令可以让容器和主机共享本地目录,免去了拷贝操作,提升工作效率。
- host 网络模式让容器与主机共享网络栈,效率高但容易导致端口冲突。
- bridge 网络模式实现了一个虚拟网桥,容器和主机都在一个私有网段内互联互通。
- docker run -p 命令可以把主机的端口号映射到容器的内部端口号,解决了潜在的端口冲突问题。
Kubernetes 是什么
Kubernetes 就是一个生产级别的容器编排平台和集群管理系统,不仅能够创建、调度容器,还能够监控、管理服务器,它凝聚了 Google 等大公司和开源社区的集体智慧,从而让中小型公司也可以具备轻松运维海量计算节点——也就是“云计算”的能力。
什么是 minikube
Kubernetes 一般都运行在大规模的计算集群上,管理很严格,这就对我们个人来说造成了一定的障碍,没有实际操作环境怎么能够学好用好呢?
好在 Kubernetes 充分考虑到了这方面的需求,提供了一些快速搭建 Kubernetes 环境的工具,在官网(https://kubernetes.io/zh/docs/tasks/tools/)上推荐的有两个:kind 和 minikube,它们都可以在本机上运行完整的 Kubernetes 环境。
我说一下对这两个工具的个人看法,供你参考。
kind 基于 Docker,意思是“Kubernetes in Docker”。它功能少,用法简单,也因此运行速度快,容易上手。不过它缺少很多 Kubernetes 的标准功能,例如仪表盘、网络插件,也很难定制化,所以我认为它比较适合有经验的 Kubernetes 用户做快速开发测试,不太适合学习研究。
不选 kind 还有一个原因,它的名字与 Kubernetes YAML 配置里的字段 kind 重名,会对初学者造成误解,干扰学习。
再来看 minikube,从名字就能够看出来,它是一个“迷你”版本的 Kubernetes,自从 2016 年发布以来一直在积极地开发维护,紧跟 Kubernetes 的版本更新,同时也兼容较旧的版本(最多只到之前的 6 个小版本)。
minikube 最大特点就是“小而美”,可执行文件仅有不到 100MB,运行镜像也不过 1GB,但就在这么小的空间里却集成了 Kubernetes 的绝大多数功能特性,不仅有核心的容器编排功能,还有丰富的插件,例如 Dashboard、GPU、Ingress、Istio、Kong、Registry 等等,综合来看非常完善。
所以,我建议你在这个专栏里选择 minikube 来学习 Kubernetes。
Springboot 后台运行服务命令
nohup java -jar xxx.jar &
最终命令的一般形式为nohup command >out.file 2>&1 &。这里面,“1”表示文件描述符 1,表示标准输出,“2”表示文件描述符 2,意思是标准错误输出,“2>&1”表示标准输出和错误输出合并了。合并到哪里去呢?到 out.file 里。那这个进程如何关闭呢?我们假设启动的程序包含某个关键字,那就可以使用下面的命令。
ps -ef |grep 关键字 |awk '{print $2}'|xargs kill -9
Linux命令
CPU 其实也不是单纯的一块,它包括三个部分,运算单元、数据单元和控制单元。
运算单元只管算,例如做加法、做位移等等。但是,它不知道应该算哪些数据,运算结果应该放在哪里。
运算单元计算的数据如果每次都要经过总线,到内存里面现拿,这样就太慢了,所以就有了数据单元。数据单元包括 CPU 内部的缓存和寄存器组,空间很小,但是速度飞快,可以暂时存放数据和运算结果。
有了放数据的地方,也有了算的地方,还需要有个指挥到底做什么运算的地方,这就是控制单元。控制单元是一个统一的指挥中心,它可以获得下一条指令,然后执行这条指令。这个指令会指导运算单元取出数据单元中的某几个数据,计算出个结果,然后放在数据单元的某个地方。
move a b :把b值赋给a,使a=b
call和ret :call调用子程序,子程序以ret结尾
jmp :无条件跳
int :中断指令
add a b : 加法,a=a+b
or :或运算
xor :异或运算
shl :算术左移
ahr :算术右移
push xxx :压xxx入栈
pop xxx: xxx出栈
inc: 加1
dec: 减1
sub a b : a=a-b
cmp: 减法比较,修改标志位
内核的启动从入口函数 start_kernel() 开始。在 init/main.c 文件中,start_kernel 相当于内核的 main 函数。打开这个函数,你会发现,里面是各种各样初始化函数 XXXX_init。
银行卡号脱敏
package com.basic.testdemo;
/**
* @author summer
* @date 2021-11-03 15:52
*/
public class Demo1 {
public static void main(String[] args) {
String conceal = toConceal("370983199007233221");
System.out.println(conceal);
}
private static final int SIZE = 7;// 控制输出 SYMBOL 的个数
private static final String SYMBOL = "*";
public static String toConceal(String value) {
if (null == value || "".equals(value)) {
return value;
}
int len = value.length();
int pamaone = len / 2;
int pamatwo = pamaone - 1;
int pamathree = len % 2;
StringBuilder stringBuilder = new StringBuilder();
if (len <= 2) {
if (pamathree == 1) {
return SYMBOL;
}
stringBuilder.append(SYMBOL);
stringBuilder.append(value.charAt(len - 1));
} else {
if (pamatwo <= 0) {
stringBuilder.append(value.substring(0, 1));
stringBuilder.append(SYMBOL);
stringBuilder.append(value.substring(len - 1, len));
} else if (pamatwo >= SIZE / 2 && SIZE + 1 != len) {
int pamafive = (len - SIZE) / 2;
stringBuilder.append(value.substring(0, pamafive));
for (int i = 0; i < SIZE; i++) {
stringBuilder.append(SYMBOL);
}
if ((pamathree == 0 && SIZE / 2 == 0) || (pamathree != 0 && SIZE % 2 != 0)) {
stringBuilder.append(value.substring(len - pamafive, len));
} else {
stringBuilder.append(value.substring(len - (pamafive + 1), len));
}
} else {
int pamafour = len - 2;
stringBuilder.append(value.substring(0, 1));
for (int i = 0; i < pamafour; i++) {
stringBuilder.append(SYMBOL);
}
stringBuilder.append(value.substring(len - 1, len));
}
}
return stringBuilder.toString();
}
}
Java实现排行榜功能
前言
最近项目需要开发一个排行榜功能,根据订单金额进行排名,同金额排名相同,不同则跳过,序列递增。
技术实现
- MySQL
通过SQL语句也能实现,不过SQL过于复杂,也不好维护。
SELECT
CASE
WHEN
@pre = final_score THEN
@pic + 0
WHEN @pre := final_score THEN
@pic := @pic + 1 ELSE @pic := @pic + 1
END AS rank,
rr.id registrationRecordId,
rr.final_score AS number,
us.userName,
us.id userId,
us.avatarImageURL headPortrait,
CAST( p.`projectName` AS CHAR CHARSET UTF8 ) AS projectName,
p.thumbnailURL,
p.id projectId
FROM
registration_record rr
INNER JOIN `user` us ON rr.user_id = us.id
INNER JOIN project p ON p.id = rr.project_id,(
SELECT
@pre := NULL,
@pic := 0
) AS init
WHERE
rr.final_score IS NOT NULL
AND rr.competition_id = 41
ORDER BY
rr.final_score DESC
SQL类似这样,光从可读性就能劝退很多同学了。
- Redis
Redis也能实现排行榜功能,主要通过zset中的分数特性来实现,不过对于我的业务不太适合 - SQL+Java代码
通过SQL中的order by将查询的List根据某字段进行排好序,再将此List通过Java代码实现最终排行榜功能(推荐使用)
//伪SQL
select
a.schoolId
a.schoolName,
sum(a.amount)as count
from a
group by a.schoolId
order by count desc
将需要排序的集合通过某字段排好序
Java代码
/**
* 成交金额排名
*
* @param amountRankList
* @return
*/
private static List<AmountRankVO> amountRank(List<AmountRankVO> amountRankList) {
amountRankList.sort((s1, s2) -> -Float.compare(s1.getCount(), s2.getCount()));
int index = 0;
int count = 0;
int tmpSize = 0;
Float lastCount = -1.00f;
List<AmountRankVO> tmpAmountRankList = new ArrayList<>();
for (int i = 0; i < amountRankList.size(); i++) {
AmountRankVO amountRankVO = amountRankList.get(i);
if (Double.compare(lastCount, amountRankVO.getCount()) != 0) {
lastCount = amountRankVO.getCount();
index = index + 1 + count;
count = 0;
}
amountRankVO.setSequence(index);
//相同并列,不同则跳过,序号一次递增
if (tmpSize > 0) {
if (amountRankVO.getCount() < amountRankList.get(tmpSize - 1).getCount()){
amountRankVO.setSequence(tmpSize + 1);
index = tmpSize + 1;
}
}
tmpAmountRankList.add(amountRankVO);
tmpSize = tmpAmountRankList.size();
}
return tmpAmountRankList;
}
如果想实现同金额相同排名,不出现序列自增,只需注释下面代码:
//相同并列,不同则跳过,序号一次递增
if (tmpSize > 0) {
if (amountRankVO.getCount() < amountRankList.get(tmpSize - 1).getCount()){
amountRankVO.setSequence(tmpSize + 1);
index = tmpSize + 1;
}
}
最后
最后我们看一下具体实现的效果,类似功能比如成绩排名,销量排名等都能适用
四个Java死锁检测工具
在 Java 中,死锁(Deadlock)情况是指:两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。
线程竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西。
在程序执行的时候,难免会遇到死锁的情况。
下面介绍一下如何排查Java中的死锁线程。
先来个死锁的例子:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDeadLock {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
thread1.start();
thread2.start();
}
static class DeadLockDemo implements Runnable {
Lock lockA;
Lock lockB;
public DeadLockDemo(Lock lockA, Lock lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
try {
lockA.lock();
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
TimeUnit.SECONDS.sleep(2);
lockB.lock();
System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock();
lockB.unlock();
System.out.println(Thread.currentThread().getName() + "正常结束!");
}
}
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.
执行该类,可以明显看到,程序不会自动结束,说明还有线程占用资源或者等待资源。
首先使用 jps 命令列出当前的Java进程:
下面使用一些工具进行抓取死锁的线程。
1、jstack
找到疑似死锁的例子,找到 PID,上图中可以看到 20148 线程是我上面执行死锁的例子:
> jstack -l 20148
20148 com.yudianxx.basic.线程.ReentrantLock.ReentrantLockDeadLock1.2.
jps -l ; -l 参数可以显示完整的启动类
执行 jstack -l 20148
往下找,会显示一段 deadlock 的关键字:
再看到下面,提示:
at com.yudianxx.basic.线程.ReentrantLock.ReentrantLockDeadLock$DeadLockDemo.run(ReentrantLockDeadLock.java:39)1.
也就是 ReentrantLockDeadLock 类下的 lockB.lock() 这一行。
即可定位到死锁的类和行数。
2、jconsole
jconsole 位于 JDK 的 bin 目录,双击即可运行。
如下,选择需要建立连接的进程。
切换到 线程,再点击下方的 检测死锁 ,即可查看死锁的情况:
除此之外,jconsole 还可以查看堆内存、CPU、线程数 等其他信息。
3、jvisualvm
jvisualvm 也在 JDK 的 bin 目录。
选择本地的进程,上方切换至 线程 ,再点击一下 线程Dump 即可。
点击后可以看到线程的状态日志,可以看到死锁的信息:
4、jmc
同样位于 JDK 的 bin 目录。
打开你需要监测的进程:
下方切换到 线程
图中看到的就是死锁的标识。
以上就是定位java线程死锁的工具,推荐使用 jstack 命令,毕竟后三个工具在Linux中是没有的。
jstack 通过找到类入口,再找出当前线程正在等待哪个线程,然后再定位到死锁的行数,即可定位引起死锁的原因。
Mysql查询报错Subquery returns more than 1 row **
解决方法:在子查询的条件语句末尾加 limit 1 。
elasticsearch场景的三中分页方式
elasticsearch分页方式(浅分页)
第一种:浅分页
浅分页是简单意义上的分页(from+size)。就是查询前50条数据,然后截断前10条,只返回10-20的数据。这样其实白白浪费了前10条的查询。
其中:from定义了目标数据的偏移值,size定义当前返回的数目。
实现原理:
因为es是基于分片的,假设有5个分片,from=100,size=10.则会根据排序规则从5个分片中各取回100条数据,然后汇总成500条数据然后再选择最后的10条数据。
明显缺点:
假设当前请求1000页的结果,从10001到10010,那么所有的分片(假设当前有5个分片)就需要返回前10010*5条数据到协调节点,然后在协调节点需要对50050结果排序,并最终丢弃到前50040条数据,可以看出页码数越大,
第二种:深度分页:
scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景
优点:
适合处理大量数据的情况,要比千分也的性能高
缺点:
基于生成的历史快照,对于数据变更的部分不会体现到快照上
第三种:search_after
search_after 分根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页。
优点;无状态查询,可以防止查询过程中,数据的变更无法反应到查询中
不需要维护scroll_id,不需要维护快照,避免大量的资源损耗
缺点:由于无状态查询,可能导致跨页面查询的数据不一致
综合对比:
分页方式 | 性能 | 优点 | 缺点 | 应用场景 |
---|---|---|---|---|
from+size | 低 | 灵活性好,实现简单 | 深度分页问题 | 数据量比较小,能容忍深度分页问题 |
scroll | 中 | 解决了深度分页问题 | 无法反应数据的实时性 | 海量数据的导出需要查询海量结果集的数据 |
search_after | 高 | 性能最好,不存在深度分页问题,能够反应数据的实时变化 | 实现复杂,需要有一个全局唯一的字段连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果,它不适用于大幅度跳页查询 | 海量数据的分页 |
讲讲mysql中的日志binlog、redo log和undo log
日志是mysql数据库的重要组成部分,记录着数据库运行期间各种状态信息。mysql日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。作为开发,重点需要关注的是二进制日志(binlog)和事务日志(包括redo log和undo log)。
binlog
binlog用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。binlog是mysql的逻辑日志,由Server层进行记录,一般使用任何存储引擎的mysql数据库都会记录binlog日志。
逻辑日志:可以简单理解为记录的就是sql语句。
物理日志:因为mysql数据最终是保存在数据页中的,物理日志记录的就是数据页变更。
binlog是通过追加的方式进行写入的,可以通过max_binlog_size参数设置每个binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。
binlog使用场景
在实际应用中,binlog的主要使用场景有两个,分别是主从复制和数据恢复。
1主从复制:在Master端开启binlog,然后将binlog发送到各个Slave端,Slave端重放binlog从而达到主从数据一致。
2数据恢复:通过使用mysqlbinlog工具来恢复数据。
binlog刷盘时机
对于InnoDB存储引擎而言,只有在事务提交时才会记录biglog,此时记录还在内存中,mysql通过sync_binlog参数控制biglog的刷盘时机,取值范围是0-N:
●0:不去强制要求,由系统自行判断何时写入磁盘;
●1:每次commit的时候都要将binlog写入磁盘;
●N:每N个事务,才会将binlog写入磁盘。
从上面可以看出,sync_binlog最安全的是设置是1,这也是MySQL 5.7.7之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。
binlog日志格式
binlog日志有三种格式,分别为STATMENT、ROW和MIXED。
在 MySQL 5.7.7之前,默认的格式是STATEMENT,MySQL 5.7.7之后,默认值是ROW。日志格式通过binlog-format指定。
●STATMENT 基于SQL语句的复制(statement-based replication, SBR),每一条会修改数据的sql语句会记录到binlog中。优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO, 从而提高了性能;缺点:在某些情况下会导致主从数据不一致,比如执行sysdate()、slepp()等。
●ROW 基于行的复制(row-based replication, RBR),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了。优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题;缺点:会产生大量的日志,尤其是alter table的时候会让日志暴涨
●MIXED 基于STATMENT和ROW两种模式的混合复制(mixed-based replication, MBR),一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog
redo log
redo log的作用
背景:事务的四大特性里面有一个是持久性,具体来说就是只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态。
mysql的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:
1因为Innodb是以页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
2一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机IO写入性能太差!
因此mysql设计了redo log,具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了(相对而言文件更小并且是顺序IO)。
redo log基本概念
redo log包括两部分:一个是内存中的日志缓冲(redo log buffer),另一个是磁盘上的日志文件(redo log file)。mysql每执行一条DML语句,先将记录写入redo log buffer,后续某个时间点再一次性将多个操作记录写到redo log file。这种先写日志,再写磁盘的技术就是MySQL里经常说到的WAL(Write-Ahead Logging) 技术。
在计算机操作系统中,用户空间(user space)下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间(kernel space)缓冲区(OS Buffer)。因此,redo log buffer写入redo log file实际上是先写入OS Buffer,然后再通过系统调用fsync()将其刷到redo log file中,过程如下:
mysql支持三种将redo log buffer写入redo log file的时机,可以通过innodb_flush_log_at_trx_commit参数配置,各参数值含义如下:
参数值
含义
0(延迟写)
事务提交时不会将redo log buffer
中日志写入到os buffer
,而是每秒写入os buffer
并调用fsync()
写入到redo log file
中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
1(实时写,实时刷)
事务每次提交都会将redo log buffer
中的日志写入os buffer
并调用fsync()
刷到redo log file
中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
2(实时写,延迟刷)
每次提交都仅写入到os buffer
,然后是每秒调用fsync()
将os buffer
中的日志写入到redo log file
。
redo log记录形式
前面说过,redo log实际上记录数据页的变更,而这种变更记录是没必要全部保存,因此redo log实现上采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。如下图:
在innodb中,既有redo log需要刷盘,还有数据页也需要刷盘,redo log存在的意义主要就是降低对数据页刷盘的要求。在上图中,write pos表示redo log当前记录的LSN(逻辑序列号)位置,check point表示数据页更改记录刷盘后对应redo log所处的LSN(逻辑序列号)位置。write pos到check point之间的部分是redo log空着的部分,用于记录新的记录;check point到write pos之间是redo log待落盘的数据页更改记录。当write pos追上check point时,会先推动check point向前移动,空出位置再记录新的日志。
启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如binlog)要快很多。重启innodb时,首先会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从checkpoint开始恢复。还有一种情况,在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度,此时会出现数据页中记录的LSN大于日志中的LSN,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。
redo log与binlog区别
由binlog和redo log的区别可知:binlog日志只用于归档,只依靠binlog是没有crash-safe能力的。但只有redo log也不行,因为redo log是InnoDB特有的,且日志上的记录落盘后会被覆盖掉。因此需要binlog和redo log二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。
undo log
数据库事务四大特性中有一个是原子性,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。实际上,原子性底层就是通过undo log实现的。undo log主要记录了数据的逻辑变化,比如一条INSERT语句,对应一条DELETE的undo log,对于每个UPDATE语句,对应一条相反的UPDATE的undo log,这样在发生错误时,就能回滚到事务之前的数据状态。同时,undo log也是MVCC(多版本并发控制)实现的关键。
DDD领域模型设计实战
在笔者学习 DDD 的过程中,大部分文章通常都是在谈 DDD 的概念,理论,诚然这些很重要,但 DDD 的读者大多还是习惯与传统开发的方式,而 DDD 的思想与传统开发模式大为不同,当大量的理论铺面而来的时候,难免觉得无从着力,本系列文章希望通过一个实际系统的 DDD 案例,让读者对 DDD 的落地有一定的认识,认识的同时也会产生新的疑问,带着这些疑问在回头去学习 DDD 的系统理论,相信能够对读者起到帮助。
DDD概览
此章节希望读者对DDD有一些基本概念,在本章中不会深入到具体概念的细节,在《实现领域驱动设计》一书中DDD每个概念背后都有一套详细的设计原则,后续文章中我们将结合编码的同时将一些概念与读者一起描述。
什么是领域驱动设计?
领域驱动设计目前被大量的提及,那么什么是领域驱动设计呢?笔者在刚开始接触时被这个问题纠结了很久,随着持续的学习,搜索大家对DDD的总结,发现DDD很难用一句话简单的描述清楚,让读者可以理解其含义。因此关于这个问题的解释我们就稍微繁琐一点,在领域驱动设计中,领域可以理解为业务,领域专家就是对业务很了解的人,比如你想要做一个在线车票的售票系统,那么平时我们看到的售票员可能就是领域专家,在比如你已经在一个业务上做了5年研发了,经历了各种需求的迭代,讨论,你懂得比新来的产品,业务还多,那么你有可能就是你们公司的领域专家。领域驱动设计的核心就是与领域专家一起通过领域建模的方式去设计的我们的软件程序。
- 那么领域如何驱动设计?或者说业务如何驱动软件设计?
单纯聊这个问题很奇怪,我们平时开发不都是业务驱动的吗?是的,但仔细的琢磨一下我们的开发过程,你会发现其中的问题。我们在和业务(领域)专家讨论时,我们是想着将需求如何映射到代码上,还是想着应该创建那些表,改那些表字段才能满足需求呢?我们在拿到一个产品原型,需求清单第一步是写代码还是创建数据表呢?大多数时候答案是后者,因此我们实际是将面向业务开发转换为了面向数据开发。
那么DDD如何解决这个问题呢,答案是领域模型,我个人认为领域模型的核心是通过模型承载和保存领域知识,并通过模型与代码的映射将这些领域知识保存在程序代码中。在传统的开发中,当业务被转换为一张张数据表时,丢失最多的就是领域知识。
DDD可以做什么
DDD主要分为两个部分,战略设计与战术设计,战略设计围绕微服务拆分,战术设计围绕微服务构建
DDD怎么做
- 领域专家与研发人员一起(研发人员可能就是领域专家),通过一系列的方式方法(DDD并没有明确说明用什么方法),划分出业务的边界,这个边界就是限界上下文,微服务可以以限界上下文指定微服务的拆分,但是微服务的拆分并不是说一定以限界上下文为边界,这里面还需要考虑其它因数,比如3个火枪手原则、两个披萨原则以及组织架构对微服务拆分的影响等。
- 研发人员通过领域模型,领域模型就是DDD中用于指定微服务实现的模型,保存领域知识,通过这种方式DDD通过领域模型围绕业务进⾏建模,并将模型与代码进⾏映射,业务调整影响代码的同时,代码也能直接的反映业务。
按照常规的编码⽅式,代码就不能直接反映业务了吗? 请参考贫血模型与充血模型
充血模型编码实践
DDD领域模型
实体与值对象
- 实体的特征
- 唯一标识,对唯一性事物进行建模
- 包含了业务的关键行为,可以随着业务持续变化
- 修改时,因为有唯一标识,所以还是同一个实体
在上图中,订单就是一个实体,因为他有订单的唯一ID,通过它可以表示订单这个事务的唯一性,并且在订单的整个生命周期,随着业务订单也在不断的变化,创建订单到订单完成,订单状态在不断的变化,但是因为它们有唯一的订单ID,所以它们就是同一个实体。
- 值对象的特征
- 描述事物的某个特征,通常作为实体属性存在
- 创建后即不可变
- 修改时,用另一个值对象予以替换
在上图中,订单商品就是一个值对象,因为在订单语境下,商品就是订单的一个特征,同时订单中的商品在订单创建的那一刻就会被"快照"下来,如果商品的发生变化,比如价格从100元涨价到10000元,订单中的商品也不会同步去修改。
在此种业务语境下,订单商品就符合对值对象的描述,那么如果卖家修改订单中商品的价格怎么办呢,在DDD中通过覆盖的方式进行修改,而不是只修改一个价格属性。
除了订单商品外,收获地址也是一个值对象,那么收获地址可以是一个实体吗? 答案是可以的,当业务在收获地址管理的上下文语境里的时候,收获地址就是一个实体。
更多对实体特征的描述,可以参考《实现领域驱动设计》一书
领域服务
领域服务可以帮助我们分担实体的功能,承接部分业务逻辑,做一些实体不变处理的业务流程,它不是必须的。
在上图中,描述的是一个创建消息的领域服务,因为消息的实体中有用户的值对象,但是用户的信息通常在另一个限界上下文,也就是另一个微服务中,因此需要通过一些facade接口获取,如果把这些接口的调用防在领域实体
中就会导致实体过于臃肿,且也不必保持其独立性,因为它需要被类似于Spring这样的框架进行管理,依赖注入一些接口,因此通过领域服务进行辅助是一种很好的方式。
聚合
将实体和值对象在一致性边界之内组成聚合,使用聚合划分限界上下文(微服务)内部的边界,聚合根做为一种特殊的实体,用于管理聚合内部的实体与值对象,并将自身暴露给外部进行引用。
比如在上图中描述的是一个订单聚合,在这个聚合中,它里面有两个实体,一个是订单一个是退货退款协议,显然退货退款协议应该依托于订单,但是它也符合实体的特征,因此被定义为实体。在此情况下,订单实体就是此聚合的聚合根。
聚合的一致性边界
生命周期一致性
生命周期的一致性,聚合对外的生命周期保持一致,聚合根生命周期结束,聚合的内部所有对象的生命周期也都应该结束。
事务的一致性
事务的一致性,这里的事务指的是数据库事务,每个数据库事务指包含一个聚合,不应该有垮聚合的事务
领域事件
领域事件表示领域中所发生的事情,通过领域事件可以实现微服务内的信息同步,同时也可以实现对外部系统的解耦。
如上图所示,聚合变更后创建领域事件,领域事件有两种方式进行发布。
- 与聚合事务一起进行存储,比如存储进一个本地事件表,在由事件转发器转发到消息队列,这样保证的事件不会丢失。
- 直接进行转发到消息队列,但是此时因为事件还未入口,因此需要在聚合事务与消息队列发布事件之间做XA的2PC事务提交,因为有2PC存在,通常性能不会太好。
除了向外部系统发布事件,限界上下文内部的多个聚合也可以通过一些本地事务发布器来进行事务的发布,比如Spring Event 或 EventBus等
资源库
资源库是保存聚合的地方,将聚合实例存放在资源库(Repository)中,之后再通过该资源库来获取相同的实例。
- Save: 聚合对象由Repository的实现,转换为存储所支持的数据结构进行持久化
- Find: 根据存储所支持的数据结构,由Repository的实现转换为聚合对象
应用服务
应用服务负责流程编排,它将要实现的功能委托给一个或多个领域对象来实现,本身只负责处理业务用例的执行顺序以及结果的拼装同时也可以在应用服务做些权限验证等工作。
DDD推荐的架构模式
本章我们来聊一聊DDD推荐的架构模式,这些架构模式用于指导服务内的具体实现,对于服务内的逻辑分层,职能角色,依赖关系都有现实的指导意义。
DDD分层
在一个典型的DDD分层架构中,分为用户界面层(Interfacce) , 应用层(Application), 领域层(Domain) ,基础设施层 (Infrastructure), 其中领域层是DDD分层架构中的核心,它是保存领域知识的地方。
分层架构的一个重要原则是:每层只能与位于其下方的层发生耦合。
在传统的DDD分层中,下图是他们的依赖关系。
如果读者没有使用过DDD可能对此理解不是很直观,可以将用户界面层想象为Controller,应用层与领域层想象为Service,基础设施层想象为Repository或者DAO,可能会好理解一些
可以看到,在传统的DDD分层架构中,基础层是被其它层所共同依赖的,它处于最底层,这可能导致重心偏移(想象一下在Service依赖DAO的场景),然而在DDD中领域层才是核心,因此要改变这种依赖。
如何改变这种依赖关系呢,在面向对象设计中有一种设计原则叫做依赖导致原则( Dependence Inversion Principle,DIP)。
DIP的定义为:
高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
根据DIP改进以后的架构如下图所示。
改进后的DDD分层,将整个依赖过程反过来了,但实际上仅仅是反过来了这么简单吗?在DIP的理论中,高层模块与低层模块是不相互依赖的,他们都依赖于一个抽象,那么这么看来,模块之间就不在是一种强耦合的关系了。
比如,在DIP之前,领域层直接依赖于基础设施层。
改进后,他们依赖于IUserRepository的抽象,抽象由基础层去实现,领域层并不关心如何实现。
由此各模块可以对内实现强内聚对外提供松耦合依赖。
六边形架构(端口适配器架构)
六边形架构,对于每种外界类型,都有一个适配器与之相对应。业务核心逻辑被包裹在内部,外界通过应用层API与内部进行交互,内部的实现无须关注外部的变化,更加聚焦。在这种架构下还可以轻易地开发用于测试的适配器。
同时六边形架构又名“端口适配器架构”, 这里的端口不一定指传统意义上的服务端口,可以理解为一种通讯方式,比如在一个服务中,我们可能会提供给用户浏览器的基于HTTP的通讯方式,提供给服务内部的基于RPC的通讯方式,以及基于MQ的通讯方式等,适配器指的是用于将端口输入转换为服务内部接口可以理解的输入。
刚才我们讨论的是外部向领域服务内部输入部分的端口+适配器模式,同时输出时也同样,比如当我们的要将领域对象进行存储时,我们知道有各种各样的存储系统,比如Mysql、ES、Mongo等,假如说我们可以抽象出一个适配器,用于适配不同的存储系统,那么我们就可以灵活的切换不同的存储介质,这对于我们开发测试,以及重构都是很有帮助的,而在DDD中这个抽象的适配器就资源库。
理解到这些以后,我们来看下六边形架构的整体架构。
在此中架构下,业务层被聚焦在内部的六边形,内部的六边形不关心外部如何运作,只关注与内部的业务实现,这也是DDD推崇的方式,研发人员应该更关注于业务的实现也就是领域层的工作,而不是
聚焦在技术的实现。结合分层架构的思想,外部的六边形可以理解为接口层与基础层,内部理解为应用层与领域层,内部通过DIP与外部解耦。
在《实现领域驱动设计》一书中,作者认为它是一种具有持久生命力的架构。
充血模型编码实践
本章我们将对通过《重构》一书中的案例,回顾贫血模型与充血模型,为后面的编码做知识储备,在DDD实践中,我们将大量用到充血模型的编码方式,如果你对贫血模型与充血模型已经了解了,可以跳过本章。
什么是贫血模型与充血模型?
回答这个问题,我们从《重构》一书中的一个影片租赁的案例,以及一个订单的开发场景,分别使用贫血模型与充血模型来实现,读者可以从中感受其差别理解它们的不同。
影片租赁场景
需要说明的是下面的代码基本与《重构》一书中的代码相同,但笔者省略了重构的各个代码优化环节,只展示了贫血模型与充血模型代码的不同。书中源代码,笔者也手写了一份实现,感兴趣可以通过以下链接点击查看。
https://gitee.com/izhengyin/some-code/tree/master/refactor/src/main/java/com/izhengyin/somecode/refactor/movierental/version
需求描述
根据顾客租聘的影片打印出顾客消费金额与积分
-
积分规则
-
- 默认租聘积一分,如果是新片且租聘大于1天,在加一分
-
-
费用规则
-
- 普通片 ,租聘起始价2元,如果租聘时间大于2天,每天增加1.5元
- 新片 ,租聘价格等于租聘的天数
-
- 儿童片 ,租聘起始价1.5元,如果租聘时间大于3天,每天增加1.5元
基于贫血模型的实现
下面是影片 Movie 、租赁 Rental 两个贫血模型类,下面这样的代码在我们日常开发中是比较常见,简单来说它们就是只包含数据,不包含业务逻辑的类,从面向对象角度来说也违背了面向对象里面封装的设计原则。
面向对象封装:隐藏信息、保护数据,只暴露少量接口,提高代码的可维护性与易用性;
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title;
private Integer priceCode;
public Movie(String title, Integer priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Integer getPriceCode() {
return priceCode;
}
public void setPriceCode(Integer priceCode) {
this.priceCode = priceCode;
}
}
plain
public class Rental {
/**
* 租的电影
*/
private Movie movie;
/**
* 已租天数
*/
private int daysRented;
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return movie;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
public int getDaysRented() {
return daysRented;
}
public void setDaysRented(int daysRented) {
this.daysRented = daysRented;
}
}
接着是我们的Customer类,Customer类的问题是里面包含了原本应该是Movie与Reatal的业务逻辑,给人感觉很重,Customer可以类别我们日常开发的XxxService,想想我们是不是在Service层中不断的堆砌业务逻辑。
plain
private String name;
private List<Rental> rentals = new ArrayList<>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
this.rentals.add(rental);
}
public String getName() {
return name;
}
/**
* 根据顾客租聘的影片打印出顾客消费金额与积分
* @return
*/
public String statement(){
double totalAmount = 0;
String result = getName()+"的租聘记录 \n";
for (Rental each : rentals){
double thisAmount = getAmount(each);
result += "\t" + each.getMovie().getTitle() + " \t" + thisAmount +" \n";
totalAmount += thisAmount;
}
int frequentRenterPoints = getFrequentRenterPoints(rentals);
result += "租聘总价 : "+ totalAmount + "\n";
result += "获得积分 : "+ frequentRenterPoints;
return result;
}
/**
* 获取积分总额
* @param rentals
* @return
*/
private int getFrequentRenterPoints(List<Rental> rentals){
return rentals.stream()
.mapToInt(rental -> {
//默认租聘积一分,如果是 Movie.NEW_RELEASE 且租聘大于1天,在加一分
int point = 1;
if(rental.getMovie().getPriceCode().equals(Movie.NEW_RELEASE) && rental.getDaysRented() > 1){
point ++;
}
return point;
})
.sum();
}
/**
* 获取单个影片租聘的价格
* 1. 普通片 ,租聘起始价2元,如果租聘时间大于2天,每天增加1.5元
* 2. 新片 ,租聘价格等于租聘的天数
* 3. 儿童片 ,租聘起始价1.5元,如果租聘时间大于3天,每天增加1.5元
* @param rental
* @return
*/
private double getAmount(Rental rental){
double thisAmount = 0;
switch (rental.getMovie().getPriceCode()){
case Movie.REGULAR:
thisAmount += 2;
if(rental.getDaysRented() > 2){
thisAmount += (rental.getDaysRented() - 2) * 1.5;
}
break;
case Movie.NEW_RELEASE:
thisAmount += rental.getDaysRented();
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if(rental.getDaysRented() > 3){
thisAmount += (rental.getDaysRented() - 3) * 1.5;
}
break;
default:
//nothings todo
break;
}
return thisAmount;
}
}
最后我们运行主程序类,进行输出,得到下面结果,记住这个结果,我们会通过重新模型重构后,保证同样的输出。
plain
张三的租聘记录
儿童片 1.5
普通片 3.5
新片 5.0
租聘总价 : 10.
获得积分 :
主程序类
plain
public class Main {
public static void main(String[] args) {
Movie movie1 = new Movie(“儿童片”, Movie.CHILDRENS);
Movie movie2 = new Movie(“普通片”, Movie.REGULAR);
Movie movie3 = new Movie(“新片”, Movie.NEW_RELEASE);
Customer customer = new Customer(“张三”);
customer.addRental(new Rental(movie1,1));
customer.addRental(new Rental(movie2,3));
customer.addRental(new Rental(movie3,5));
System.out.println(customer.statement())
}
}
基于充血模型的实现
我们的类没有变化,只是类里面的实现发生了变化,接下来就逐一看看类的实现都发生了那些改变。
重构后影片 Movie 类
- 删除了不必要setXXX方法
- 增加了 getCharge 获取费用电影费用的方法,将原本 Customer 的逻辑交由Movie类实现。
注:Movie类还有优化空间,但不是本文的重点,读者感兴趣可以查看此链接
https://gitee.com/izhengyin/some-code/tree/master/refactor/src/main/java/com/izhengyin/somecode/refactor/movierental/version
plain
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title;
private Integer priceCode;
public Movie(String title, Integer priceCode) {
this.title = title;
this.priceCode = priceCode;
}
public String getTitle() {
return title;
}
public Integer getPriceCode() {
return priceCode;
}
/**
*获取单个影片租聘的价格
* 1. 普通片 ,租聘起始价2元,如果租聘时间大于2天,每天增加1.5元
* 2. 新片 ,租聘价格等于租聘的天数
* 3. 儿童片 ,租聘起始价1.5元,如果租聘时间大于3天,每天增加1.5元
* @param daysRented
* @return
*/
public double getCharge(int daysRented){
double thisAmount = 0;
switch (this.priceCode){
case REGULAR:
thisAmount += 2;
if(daysRented > 2){
thisAmount += (daysRented - 2) * 1.5;
}
break;
case NEW_RELEASE:
thisAmount += daysRented;
break;
case CHILDRENS:
thisAmount += 1.5;
if(daysRented > 3){
thisAmount += (daysRented - 3) * 1.5;
}
break;
default:
//nothings todo
break;
}
return thisAmount;
}
}
重构后租赁 Rental 类
- 移除了部分不必要的 get / set 方法
- 增加一个 getPoint 方法,计算租赁积分,将原本 Customer 的逻辑交由获取积分的业务交由getPoint实现,但总积分的计算还是在Customer。
- 增加一个 getCharge 方法,具体调用Movie::getCharge传入租赁天数得到租赁的费用,因为在这个需求中主体是租赁
plain
/**
* 租的电影
*/
private Movie movie;
/**
* 已租天数
*/
private int daysRented;
public Rental(Movie movie, int daysRented) {
this.movie = movie;
this.daysRented = daysRented;
}
public Movie getMovie() {
return movie;
}
/**
* 默认租聘积一分,如果是新片且租聘大于1天,在加一分
* @return
*/
public int getPoint(){
int point = 1;
if(this.movie.getPriceCode().equals(Movie.NEW_RELEASE) && this.daysRented > 1){
point ++;
}
return point;
}
/**
* 获取费用
* @return
*/
public double getCharge(){
return this.movie.getCharge(this.daysRented);
}
}
瘦身后的Customer
plain
public class Customer {
private String name;
private List<Rental> rentals = new ArrayList<>();
public Customer(String name) {
this.name = name;
}
public void addRental(Rental rental) {
this.rentals.add(rental);
}
public String getName() {
return name;
}
/**
* 根据顾客租聘的影片打印出顾客消费金额与积分
* @return
*/
public String statement(){
double totalAmount = 0;
String result = getName()+"的租聘记录 \n";
for (Rental each : rentals){
double thisAmount = each.getCharge();
result += "\t" + each.getMovie().getTitle() + " \t" + thisAmount +" \n";
totalAmount += thisAmount;
}
int frequentRenterPoints = getFrequentRenterPoints(rentals);
result += "租聘总价 : "+ totalAmount + "\n";
result += "获得积分 : "+ frequentRenterPoints;
return result;
}
/**
* 获取积分总额
* @param rentals
* @return
*/
private int getFrequentRenterPoints(List<Rental> rentals){
return rentals.stream()
.mapToInt(Rental::getPoint)
.sum();
}
}
最后我们运行主程序类,得到同样的输出。
源码地址: https://gitee.com/izhengyin/ddd-message/tree/master/src/main/java/democode/movierental
订单的场景
需求描述
- 创建订单
- 设置订单优惠
订单场景贫血模型实现
Order 类 , 只包含了属性的Getter,Setter方法
plain
public class Order {
private long orderId;
private int buyerId;
private int sellerId;
private BigDecimal amount;
private BigDecimal shippingFee;
private BigDecimal discountAmount;
private BigDecimal payAmount;
private String address;
}
OrderService ,根据订单创建中的业务逻辑,组装order数据对象,最后进行持久化
plain
* 创建订单
* @param buyerId
* @param sellerId
* @param orderItems
*/
public void createOrder(int buyerId,int sellerId,List<OrderItem> orderItems){
//新建一个Order数据对象
Order order = new Order();
order.setOrderId(1L);
//算订单总金额
BigDecimal amount = orderItems.stream()
.map(OrderItem::getPrice)
.reduce(BigDecimal.ZERO,BigDecimal::add);
order.setAmount(amount);
//运费
order.setShippingFee(BigDecimal.TEN);
//优惠金额
order.setDiscountAmount(BigDecimal.ZERO);
//支付总额 = 订单总额 + 运费 - 优惠金额
BigDecimal payAmount = order.getAmount().add(order.getShippingFee()).subtract(order.getDiscountAmount());
order.setPayAmount(payAmount);
//设置买卖家
order.setBuyerId(buyerId);
order.setSellerId(sellerId);
//设置收获地址
order.setAddress(JSON.toJSONString(new Address()));
//写库
orderDao.insert(order);
orderItems.forEach(orderItemDao::insert);
}
在此种方式下,核心业务逻辑散落在OrderService中,比如获取订单总额与订单可支付金额是非常重要的业务逻辑,同时对象数据逻辑一同混编,在此种模式下,代码不能够直接反映业务,也违背了面向对象的SRP原则。
设置优惠
plain
* 设置优惠
* @param orderId
* @param discountAmount
*/
public void setDiscount(long orderId, BigDecimal discountAmount){
Order order = orderDao.find(orderId);
order.setDiscountAmount(discountAmount);
//从新计算支付金额
BigDecimal payAmount = order.getAmount().add(order.getShippingFee()).subtract(discountAmount);
order.setPayAmount(payAmount);
//orderDao => 通过主键更新订单信息
orderDao.updateByPrimaryKey(order);
}
贫血模型在设置折扣时因为需要考虑到折扣引发的支付总额的变化,因此还需要在从新的有意识的计算支付总额,因为面向数据开发需要时刻考虑数据的联动关系,在这种模式下忘记了修改某项关联数据的情况可能是时有发生的。
订单场景充血模型实现
Order 类,包含了业务关键属于以及行为,同时具有良好的封装性
/**
* @author zhengyin
* Created on 2021/10/
*/
@Getter
public class Order {
private long orderId;
private int buyerId;
private int sellerId;
private BigDecimal shippingFee;
private BigDecimal discountAmount;
private Address address;
private Set<OrderItem> orderItems;
//空构造,只是为了方便演示
public Order(){}
public Order(long orderId,int buyerId ,int sellerId,Address address, Set<OrderItem> orderItems){
this.orderId = orderId;
this.buyerId = buyerId;
this.sellerId = sellerId;
this.address = address;
this.orderItems = orderItems;
}
/**
* 更新收货地址
* @param address
*/
public void updateAddress(Address address){
this.address = address;
}
/**
* 支付总额等于订单总额 + 运费 - 优惠金额
* @return
*/
public BigDecimal getPayAmount(){
BigDecimal amount = getAmount();
BigDecimal payAmount = amount.add(shippingFee);
if(Objects.nonNull(this.discountAmount)){
payAmount = payAmount.subtract(discountAmount);
}
return payAmount;
}
/**
* 订单总价 = 订单商品的价格之和
* amount 可否设置为一个实体属性?
*/
public BigDecimal getAmount(){
return orderItems.stream()
.map(OrderItem::getPrice)
.reduce(BigDecimal.ZERO,BigDecimal::add);
}
/**
* 运费不能为负
* @param shippingFee
*/
public void setShippingFee(BigDecimal shippingFee){
Preconditions.checkArgument(shippingFee.compareTo(BigDecimal.ZERO) >= 0, "运费不能为负");
this.shippingFee = shippingFee;
}
/**
* 设置优惠
* @param discountAmount
*/
public void setDiscount(BigDecimal discountAmount){
Preconditions.checkArgument(discountAmount.compareTo(BigDecimal.ZERO) >= 0, "折扣金额不能为负");
this.discountAmount = discountAmount;
}
/**
* 原则上,返回给外部的引用,都应该防止间接被修改
* @return
*/
public Set<OrderItem> getOrderItems() {
return Collections.unmodifiableSet(orderItems);
}
}
OrderService , 仅仅负责流程的调度
plain
* 创建订单
* @param buyerId
* @param sellerId
* @param orderItems
*/
public void createOrder(int buyerId, int sellerId, Set<OrderItem> orderItems){
Order order = new Order(1L,buyerId,sellerId,new Address(),orderItems);
//运费不随订单其它信息一同构造,因为运费可能在后期会进行修改,因此提供一个设置运费的方法
order.setShippingFee(BigDecimal.TEN);
orderRepository.save(order);
}
在此种模式下,Order类完成了业务逻辑的封装,OrderService仅负责业务逻辑与存储之间的流程编排,并不参与任何的业务逻辑,各模块间职责更明确。
设置优惠
plain
* 设置优惠
* @param orderId
* @param discountAmount
*/
public void setDiscount(long orderId, BigDecimal discountAmount){
Order order = orderRepository.find(orderId);
order.setDiscount(discountAmount);
orderRepository.save(order);
}
在充血模型的模式下,只需设置具体的优惠金额,因为在Order类中已经封装了相关的计算逻辑,比如获取支付总额时,是实时通过优惠金额来计算的。
plain
* 支付总额等于订单总额 + 运费 - 优惠金额
* @return
*/
public BigDecimal getPayAmount(){
BigDecimal amount = getAmount();
BigDecimal payAmount = amount.add(shippingFee);
if(Objects.nonNull(this.discountAmount)){
payAmount = payAmount.subtract(discountAmount);
}
return payAmount;
}
写到这里,可能读者会有疑问,文章都在讲充血模型的业务,那数据怎么进行持久化?
数据持久化时我们通过封装的 OrderRepository 来进行持久化操作,根据存储方式的不同提供不同的实现,以数据库举例,那么我们需要将Order转换为PO对象,也就是持久化对象,这时的持久化对象就是面向数据表的贫血模型对象。
比如下面的伪代码
plain
private final OrderDao orderDao;
private final OrderItemDao orderItemDao;
public OrderRepository(OrderDao orderDao, OrderItemDao orderItemDao) {
this.orderDao = orderDao;
this.orderItemDao = orderItemDao;
}
public void save(Order order){
// 在此处通过Order实体,创建数据对象 new OrderPO() ; new OrderItemPO();
// orderDao => 存储订单数据
// orderItemDao => 存储订单商品数据
}
public Order find(long orderId){
//找到数据对象,OrderPO
//找到数据对象,OrderItemPO
//组合返回,order实体
return new Order();
}
}
通过上面两种实现方式的对比,相信读者对两种模型已经有了明确的认识了,在贫血模型中,数据和业务逻辑是割裂的,而在充血模型中数据和业务是内聚的。
Spring框架下@value注解属性static无法获取值问题
这篇文章主要介绍了spring框架下@value注解属性static无法获取值问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
目录
@value注解属性static无法获取值
@Value("${appId}")
private static String appid;
这样是无法直接获得值的
解决办法
需要这样写
private static String appid;
@Value("${appId}")
public void setAppid(String appId) {
this.appid = appid;
}
@Value注解取不到值的几种情况
在spring的框架中,我们经常使用**@Value注解来获取定义在application.properties**的属性值,正常情况下是可以轻松的获取到值的,但是有几种特殊情况下是获取不到值的,在这记录下,以防止以后犯错误。正常获取的用法如下:
在application.properties中定义一个属性值:
正常情况下在代码里面这样获取:
@Value("${ftpIP}")
private String ftpIP;
几种获取不到值的特殊情况如下
情况一:使用static或者final修饰了tagValue
@Value("${ftpIP}")
private static String ftpIP;
@Value("${ftpUserName}")
private final String ftpUserName;
情况二:用该注解的类上面没有加注解,即不能被spring管理
public class FTPManagerService {
@Value("${ftpIP}")
private static String ftpIP;
}
情况三:类被new新建了实例,而没有使用@Autowired
public class FTPManagerService {
@Value("${ftpIP}")
private static String ftpIP;
}
public class Test{
/*错误用法*/
FTPManagerService f = new FTPManagerService ();
/*正确用法*/
@Autowired
FTPManagerService f2;
}
Node问题
npm ERR Cannot read properties of null (reading ‘pickAlgorithm‘)
bug现象
在npm下包时出现的错误
bug原因
尚不明确,推测是node的版本问题,未证实
解决方法
清空npm下的包,换yarn或其他下包方式下包
解决步骤
1.输入
npm cache clear --force
清空npm的缓存
2.如果已经安装了yarn,直接用yarn指令安装包即可
yarn add 包名
3.如果没有安装yarn
npm i -g yarn
根据日志服务统计不同区域下载好律师app的情况,来做灰度发布
电子发票解析
Mongo面试题目
https://www.nowcoder.com/discuss/946492
1、mongodb是什么?
2、mongodb有哪些特点?
3、你说的NoSQL数据库是什么意思?NoSQL与RDBMS直接有什么区别?为什么要使用和不使用NoSQL数据库?说一说NoSQL数据库的几个优点?
4、NoSQL数据库有哪些类型?
5、MySQL与MongoDB之间最基本的差别是什么?
6、你怎么比较MongoDB、CouchDB及CouchBase?
7、MongoDB成为最好NoSQL数据库的原因是什么?
8、journal回放在条目(entry)不完整时(比如恰巧有一个中途故障了)会遇到问题吗?
9、分析器在MongoDB中的作用是什么?
10、名字空间(namespace)是什么?
11、 如果用户移除对象的属性,该属性是否从存储层中删除?
12、能否使用日志特征进行安全备份?
13、允许空值null吗?
14、更新操作立刻fsync到磁盘?
15、如何执行事务/加锁?
16、为什么我的数据文件如此庞大?
17、启用备份故障恢复需要多久?
18、什么是master或primary?
19、什么是secondary或slave?
20、我必须调用getLastError来确保写操作生效了么?
21、我应该启动一个集群分片(sharded)还是一个非集群分片的 MongoDB 环境?
22、分片(sharding)和复制(replication)是怎样工作的?
23、数据在什么时候才会扩展到多个分片(shard)里?
24、当我试图更新一个正在被迁移的块(chunk)上的文档时会发生什么?
25、如果在一个分片(shard)停止或者很慢的时候,我发起一个查询会怎样?
26、我可以把moveChunk目录里的旧文件删除吗?
27、我怎么查看 Mongo 正在使用的链接?
28、如果块移动操作(moveChunk)失败了,我需要手动清除部分转移的文档吗?
29、如果我在使用复制技术(replication),可以一部分使用日志(journaling)而其他部分则不使用吗?
30、当更新一个正在被迁移的块(Chunk)上的文档时会发生什么?
31、MongoDB在A:{B,C}上建立索引,查询A:{B,C}和A:{C,B}都会使用索引吗?
32、如果一个分片(Shard)停止或很慢的时候,发起一个查询会怎样?
33、MongoDB支持存储过程吗?如果支持的话,怎么用?
34、如何理解MongoDB中的GridFS机制,MongoDB为何使用GridFS来存储文件?
35、什么是NoSQL数据库?NoSQL和RDBMS有什么区别?在哪些情况下使用和不使用NoSQL数据库?
36、MongoDB支持存储过程吗?如果支持的话,怎么用?
37、如何理解MongoDB中的GridFS机制,MongoDB为何使用GridFS来存储文件?
38、为什么MongoDB的数据文件很大?
39、当更新一个正在被迁移的块(Chunk)上的文档时会发生什么?
40、MongoDB在A:{B,C}上建立索引,查询A:{B,C}和A:{C,B}都会使用索引吗?
41、如果一个分片(Shard)停止或很慢的时候,发起一个查询会怎样?
42、分析器在MongoDB中的作用是什么?
43、如果用户移除对象的属性,该属性是否从存储层中删除?
44、能否使用日志特征进行安全备份?
45、更新操作立刻fsync到磁盘?
46、如何执行事务/加锁?
47、什么是master或primary?
48、getLastError的作用
49、分片(sharding)和复制(replication)是怎样工作的?
50、数据在什么时候才会扩展到多个分片(shard)里?
51、 当我试图更新一个正在被迁移的块(chunk)上的文档时会发生什么?
52、 我怎么查看 Mongo 正在使用的链接?
53、mongodb的结构介绍
54、数据库的整体结构
55、MongoDB是由哪种语言写的
56、MongoDB的优势有哪些
57、什么是集合
58、什么是文档
59、什么是”mongod“
60、"mongod"参数有什么
61、什么是"mongo"
62、MongoDB哪个命令可以切换数据库
63、什么是非关系型数据库
64、非关系型数据库有哪些类型
65、为什么用MOngoDB?
66、在哪些场景使用MongoDB
67、MongoDB中的命名空间是什么意思?
68、哪些语言支持MongoDB?
69、在MongoDB中如何创建一个新的数据库
70、在MongoDB中如何查看数据库列表
71、MongoDB中的分片是什么意思
72、如何查看使用MongoDB的连接Sharding - MongoDB Manual21.如何查看使用MongoDB的连接
73、什么是复制
74、在MongoDB中如何在集合中插入一个文档
75、在MongoDB中如何除去一个数据库Collection Methods24.在MongoDB中如何除去一个数据库
76、在MongoDB中如何创建一个集合。
77、在MongoDB中如何查看一个已经创建的集合
78、在MongoDB中如何删除一个集合
79、为什么要在MongoDB中使用分析器
80、MongoDB支持主键外键关系吗
81、MongoDB支持哪些数据类型
82、为什么要在MongoDB中用"Code"数据类型
83、为什么要在MongoDB中用"Regular Expression"数据类型
84、为什么在MongoDB中使用"Object ID"数据类型
85、如何在集合中插入一个文档
86、"ObjectID"由哪些部分组成
87、在MongoDb中什么是索引
88、如何添加索引
89、用什么方法可以格式化输出结果
90、如何使用"AND"或"OR"条件循环查询集合中的文档
91、在MongoDB中如何更新数据
92、如何删除文档
93、在MongoDB中如何排序
94、什么是聚合
95、在MongoDB中什么是副本集
MongoDB和Redis比较
MongoDB和Redis都是NoSQL,采用结构型数据存储。二者在使用场景中,存在一定的区别,这也主要由于二者在内存映射的处理过程,持久化的处理方法不同。MongoDB建议集群部署,更多的考虑到集群方案,Redis更偏重于进程顺序写入,虽然支持集群,也仅限于主-从模式。
比较指标 | MongoDB(v2.4.9) | Redis(v2.4.17) | 比较说明 |
---|---|---|---|
实现语言 | c++ | c/c++ | - |
协议 | BSON,自定义二进制 | 类telnet | - |
性能 | 依赖内存,TPS较高 | 依赖内存,TPS非常高 | Redis优于MongoDB |
可操作性 | 丰富的数据表达,索引;最类似于关系型数据库,支持丰富的查询语句 | 数据丰富,较少的IO | MongoDB优于Redis |
内存及存储 | 适合大数据量存储,依赖系统虚拟内存,采用镜像文件存储;内存占用率比较高,官方建议独立部署在64位系统 | Redis2.0后支持虚拟内存特性(VM) 突破物理内存限制;数据可以设置时效性,类似于memcache | 不同的应用场景,各有千秋 |
可用性 | 支持master-slave,replicatset(内部采用paxos选举算法,自动故障恢复),auto sharding机制,对客户端屏蔽了故障转移和切片机制 | 依赖客户端来实现分布式读写;主从复制时,每次从节点重新连接主节点都要依赖整个快照,无增量复制;不支持auto sharding,需要依赖程序设定一致性hash机制 | MongoDB优于Redis;单点问题上,MongoDB应用简单,相对用户透明,Redis比较复杂,需要客户端主动解决.(MongoDB一般使用replicasets和sharding相结合,replicasets侧重高可用性以及高可靠,sharding侧重性能,水平扩展) |
可靠性 | 从1.8版本后,采用binlog方式(类似Mysql) 支持持久化 | 依赖快照进行持久化;AOF增强可靠性;增强性的同时,影响访问性能 | |
一致性 | 不支持事务,靠客户端保证 | 支持事务,比较脆,仅能保证事务中的操作按顺序执行 | Redis优于MongoDB |
数据分析 | 内置数据分析功能(mapreduce) | 不支持 | MongoDB优于Redis |
应用场景 | 海量数据的访问效率提升 | 较小数据量的性能和运算 | MongoDB优于Redis |
参考文档https://www.cnblogs.com/chinesern/p/5581422.html