11-02
Java相关-分布式项目中事务,锁,缓存的知识点
title+以及深入实践
你好我是后脚跟的后脑勺,离进化为大脑袋还有很长的距离,不管怎样,既然要编写这篇文档,就说明这条职业之路还要继续进行下去,受"化内法师"的影响,除了上次了解到的关于基金类的知识之外(其实掌握的太浅,而且基金类的更专业的知识其实是由那些博士硕士类别的金融学专业,数学专业的大脑袋来操作的),所以基金类的项目就此打住.
而基金类,金融类的相关程序项目中,其实都使用到了金融类的计算及数据存储,比如计算时使用BigDecimal做类中的字段类型,以及数据库中decimal做存储.
本来记录这上面这些文字就够我躺平一会儿了,但是勇敢牛牛不怕困难,迎难而上才能越战越勇,对于分布式中相关的问题,也是近期被问到的高频的问题,当然在后脑勺的理解中,这也是一块要进阶去实践的地方,毕竟在mooc(icourse163)或者基础类的基础知识中,分布式事务,分布式锁,分布式缓存是基本很少讲到的,而很多讲到这些点的教程则分散在马士兵教育,尚硅谷.
开始之前
- mybatis中设定时间字段并只在insert或update时设定某值
实现baomidou下的MetaObjectHandler
注解fill=FieldFill.INSERT / INSERT_UPDATE
2.validation格式化的使用
validation包下的Pattern指定了某个字段要符合某种格式
设定一个Util,传入某个含有多个validation判定字段的实体类,返回不合规不符合要求字段异常集合
package com.chinacscs.fgf.gi.utils;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.groups.Default;
import java.util.*;
/**
* @author: cyz
* @create: 2021-03-10 17:46
**/
public class ValidatorUtil {
private static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
/**
* 默认校验Default.class
*/
public static <T> ValidatorResult validate(T t) {
Validator validator = factory.getValidator();
Set<ConstraintViolation<T>> validate = validator.validate(t, Default.class);
return getValidateResult(validate);
}
private static <T> ValidatorResult getValidateResult(Set<ConstraintViolation<T>> validate) {
Objects.requireNonNull(validate, "入参validate不能为空");
ValidatorResult validatorResult = new ValidatorResult();
boolean isValid = true;
Map<String, String> errorMsg = new HashMap<String, String>();
Iterator<ConstraintViolation<T>> iterator = validate.iterator();
while (iterator.hasNext()) {
isValid = false;
ConstraintViolation<T> next = iterator.next();
String propertyPath = next.getPropertyPath().toString();
String message = next.getMessage();
errorMsg.put(propertyPath, message);
}
validatorResult.setErrorMsg(errorMsg);
validatorResult.setValid(isValid);
return validatorResult;
}
public static class ValidatorResult {
boolean isValid;
Map<String, String> errorMsg;
public boolean isValid() {
return isValid;
}
public void setValid(boolean valid) {
isValid = valid;
}
public Map<String, String> getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(Map<String, String> errorMsg) {
this.errorMsg = errorMsg;
}
}
}
3.让我恶心的一个需求
package com.chinacscs.fgf.gi.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.chinacscs.fgf.gi.constant.excel.ExcelSuffix;
import com.chinacscs.fgf.gi.constant.excel.ExcelTempType;
import com.chinacscs.fgf.gi.converter.decimal.DecimalConverter;
import com.chinacscs.fgf.gi.converter.localdate.ExcelCommonDateConverter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* @Author: zhangQi
* @Date: 2021-01-27 10:59
* 用于模板填充excel
*
*/
@Component(value = "giExcelUtil")
public class GiExcelUtil {
private static Logger log = LoggerFactory.getLogger("Gi excel工具类");
private final static String fileExtension = ExcelSuffix.XLSX.getCode();
private static ResourceLoader resourceLoader;
private static final Integer SEQUENCE_INITIAL = 1;
//序号
private static final String SEQUENCE_NAME = "sequenceId";
//借据号
private static final String LOAN_NUMBER = "loanNo";
//失败列表失败原因
private static final String FAILED_REASONS = "failedReasons";
@Autowired
public void setResourceLoader(ResourceLoader resourceLoader) {
GiExcelUtil.resourceLoader = resourceLoader;
}
/**
* @param excelType excel类型
* @param excelTempType excel模板类型 failed失败列表 success成功列表
* @param titleName 定义的excel表头
* @param list 数据
* @param response 响应流
* @throws IOException
*/
public static <T> void excelFill(String excelType, String excelTempType, String titleName, List<T> list, HttpServletResponse response) {
// if (CollectionUtils.isEmpty(list)) {
// throw new RuntimeException("要导出的数据为空");
// }
//increment sequenceIds
list = listForIncrementSequence(list, SEQUENCE_INITIAL);
// 如果检测导出数据是有重复的,执行重复数据提示
if(excelTempType.equals(ExcelTempType.FAILED.getCode())){
if (checkOutListCondition(list, LOAN_NUMBER, l -> l.size() > 1).size() > 0) {
list = excelSortLoanNo(list);
}
}
log.info("开始excel的导出:{}", list);
//获取模板
org.springframework.core.io.Resource resource = null;
try {
resource = resourceLoader.getResource("classpath:template" + File.separator + "excel" + File.separator
+ excelTempType + File.separator + excelType + fileExtension);
} catch (Exception e) {
throw new RuntimeException("获取模板异常");
}
//经测试,需要加入registerConverter到此,而非model中的字段上
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(response.getOutputStream())
.withTemplate(resource.getInputStream())
//转时间
.registerConverter(new ExcelCommonDateConverter())
//decimal转去除多余小数点
.registerConverter(new DecimalConverter())
.build();
} catch (Exception e) {
throw new RuntimeException("excel模板创建异常");
}
log.info("excelWriter:{}", excelWriter);
WriteSheet writeSheet = EasyExcel.writerSheet(1).build();
// 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。
// forceNewRow 如果设置了true,有个缺点 就是他会把所有的数据都放到内存了,所以慎用
// 简单的说 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
// 如果数据量大 list不是最后一行 参照下一个
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
log.info("fillConfig:{}", fillConfig);
Map<String, String> map = new HashMap<String, String>();
map.put("title", titleName);
excelWriter.fill(map, fillConfig, writeSheet);
excelWriter.fill(list, fillConfig, writeSheet);
//关流
excelWriter.finish();
}
/**
* excel排序根据LoanNo
* 先进行整个list列表的序号填充
* 获取loanNo借据号不为空的进行排序,相同借据号的排序到一起
* 将相同借据号的数据提示,[与第2,3条借据号重复][与第1,3条借据号重复][与1,2条借据号重复]
* 可针对三种情况导出列表,<T>对导出的借据号重复的放在一起(并提示与哪一行重复),为null的排除
* TODO 因为现在导出列表时最后也有写入序号,所以在执行这个方法时只能取消导出时的写入序号操作
*
* @param list
* @param <T>
*/
public static <T> List<T> excelSortLoanNo(List<T> list) {
// list = listForIncrementSequence(list,1);
Predicate<List<T>> singleOrNot = equalsList -> equalsList.size() > 1;
//可针对三种情况导出列表,<T>对导出的借据号重复的放在一起(并提示与哪一行重复),为null的排除
List listVisiable = list.stream()
.filter(exl -> {
try {
Field field = exl.getClass().getDeclaredField(LOAN_NUMBER);
field.setAccessible(true);
return field.get(exl) != null;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
})
.sorted((in1, in2) -> {
Field field1 = null;
Field field2 = null;
try {
field1 = in1.getClass().getDeclaredField(LOAN_NUMBER);
field2 = in2.getClass().getDeclaredField(LOAN_NUMBER);
field1.setAccessible(true);
field2.setAccessible(true);
} catch (NoSuchFieldException e) {
log.error("excelSortLoanNo字段不存在");
}
return field1.hashCode() - field2.hashCode();
})
.collect(Collectors.groupingBy(exl -> {
try {
Field declaredField = exl.getClass().getDeclaredField(LOAN_NUMBER);
declaredField.setAccessible(true);
return declaredField.get(exl);
} catch (IllegalAccessException e) {
log.error("checkOutListCondition非法访问");
throw new RuntimeException("提取数据异常");
} catch (NoSuchFieldException e) {
log.error("checkOutListCondition字段不存在");
throw new RuntimeException("提取数据异常");
}
}))
.values().stream()
.filter(loanNoDuplicateList -> singleOrNot.test(loanNoDuplicateList))
.map(oneGroup -> {
//先设定sequenceId的话在进入这里就会被打乱了
List<Object> sequenceIds = oneGroup.stream().map(one -> {
try {
Field declaredField = one.getClass().getDeclaredField(SEQUENCE_NAME);
declaredField.setAccessible(true);
return declaredField.get(one);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}).collect(Collectors.toList());
return oneGroup.stream()
.peek(one -> {
try {
Field failedReasons = one.getClass().getDeclaredField(FAILED_REASONS);
failedReasons.setAccessible(true);
Object reasonsOld = failedReasons.get(one);
Field sequenceField = one.getClass().getDeclaredField(SEQUENCE_NAME);
sequenceField.setAccessible(true);
Object thisOneSequenceId = sequenceField.get(one);
List<Object> sequenceIdList = sequenceIds.stream()
.filter(se -> !se.equals(thisOneSequenceId)).collect(Collectors.toList());
Object[] sequenceIdArr = sequenceIdList.stream().toArray();
String sequenceIdArrStr = StringUtils.join(sequenceIdArr, ",");
String failedReasonsFresh = reasonsOld.toString()
//按测试需求,这里的字符串被更改处,都是在mapper中设定的failedReasons中的
//com/chinacscs/fgf/gi/mapper/GiExcelImportTempCommonMapper.java:23
.replace("excel内借据号重复", "excel内借据号重复,与序号" + sequenceIdArrStr + "条重复");
// String failedReasonsFresh = reasonsOld + "[该条借据号与序号" + sequenceIdArrStr + "重复]";
failedReasons.set(one, failedReasonsFresh);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
})
.collect(Collectors.toList());
})
.reduce((l1, l2) -> {
l1.addAll(l2);
return l1;
})
.filter(list1 -> !CollectionUtils.isEmpty(list1))
.orElse(Collections.EMPTY_LIST);
list.removeAll(listVisiable);
listVisiable.addAll(list);
return listVisiable;
}
/**
* 获取list设定sequenceId自增
*
* @param list 原始list
* @param initial 基数
* @param <T>
* @return 设定sequenceId后的list
*/
private static <T> List<T> listForIncrementSequence(List<T> list, Integer initial) {
AtomicInteger incrementId = new AtomicInteger(initial);
return list.stream()
.filter(l -> l != null)
.peek(i -> {
try {
Field field = i.getClass().getDeclaredField(SEQUENCE_NAME);
field.setAccessible(true);
field.set(i, String.valueOf(incrementId.get()));
incrementId.getAndIncrement();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}).collect(Collectors.toList());
}
/**
* 按条件获取数据
*
* @param dataList 数据列表
* @param equalsFeild 相同字段
* @param singleOrNot l->l.size() == 1 or l->l.size() > 1
* @param <T>
* @return
*/
public static <T> List<T> checkOutListCondition(List<T> dataList, String equalsFeild, Predicate<List<T>> singleOrNot) {
List conditionList = dataList.parallelStream()
.filter(el -> {
try {
Field declaredField = el.getClass().getDeclaredField(equalsFeild);
declaredField.setAccessible(true);
return declaredField.get(el) != null;
} catch (NoSuchFieldException e) {
log.error("checkOutListCondition字段不存在");
throw new RuntimeException("提取数据异常");
} catch (IllegalAccessException e) {
log.error("checkOutListCondition非法访问");
throw new RuntimeException("提取数据异常");
}
})
.collect(Collectors.groupingBy(i -> {
try {
Field declaredField = i.getClass().getDeclaredField(equalsFeild);
declaredField.setAccessible(true);
return declaredField.get(i);
} catch (IllegalAccessException e) {
log.error("checkOutListCondition非法访问");
throw new RuntimeException("提取数据异常");
} catch (NoSuchFieldException e) {
log.error("checkOutListCondition字段不存在");
throw new RuntimeException("提取数据异常");
}
}))
.values().stream()
.filter(list -> !CollectionUtils.isEmpty(list))
.filter(list -> singleOrNot.test(list))
.reduce((l1, l2) -> {
l1.addAll(l2);
return l1;
})
.filter(list -> !CollectionUtils.isEmpty(list))
.orElse(Collections.EMPTY_LIST);
return conditionList;
}
}
excelSortLoanNo
开始之前可以说记录的是后脚跟之前的做的项目中不管是他人的还是自己的让人有点疲乏的代码,总之这样拿出来就当自己清理了大脑吧.
让人恶心的同时还有springbatch异步同时跑批,在我看来现在如果一个数据要读一次并更改3种并写入其它三个表落库,最好的方法就是同时执行三个job.(关于spring官网中springbatch的分区的那种实现没有应用到,不过那个的话应该可以解决之前的问题)
开始之前2
奶头乐理论 可以百度或者google一下
mybatisplus配置分页查询
selectPage
mybatisplus的逻辑删除
//创建时写入
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
//创建&更新时写入
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* test 其已存在baseEntity
*/
@Version
private Long version;
/**
* 假性删除
*/
@TableLogic
private Integer deleteFlag;
package com.chinacscs.fgf.gi.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.joda.time.LocalDateTime;
import org.springframework.stereotype.Component;
/**
* @Author: zhangQi
* @Date: 2021-11-02 17:34
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime",new LocalDateTime(),metaObject);
this.setFieldValByName("updateTime",new LocalDateTime(),metaObject);
this.setFieldValByName("version",0L,metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime",new LocalDateTime(),metaObject);
}
}
之前都是deleteFlag而没有加注解,直接进行了判定,而实际可以加@TableLogic注解用来说明是执行update设定deleteFlag为1,而查询时也不用指定,mybatisplus会自动查询deleteFlag=0的数据返回
来自博客的基础理解
那天我骑着自行车在面试的问答中再次遇到分布式相关问题,在上海世纪公园转了好几圈后回到自己的小窝开始仔细整理学习了相关的知识点,而基于自己的理解,有些能懂有些不能懂,但大体总结出来先这样,有句话是,"知其然,知其所以然",那么这些整理类似的只代表"知其然".
分布式锁
分布式锁:多服务同库,使用乐观锁加版本号或悲观锁for update锁定行数据元组
redis实现分布式锁设定,设定业务key加超时时间,多个进程只有一个可以设定成功并执行业务逻辑操作,随后清除,等待下一个进程抢占并设定。redlock
zookeeper设定临时节点,类似往队列中放置锁标识,并且每次都先获取节点标识值最小的用来消费,即获取到锁并执行随后清除锁,则下一个标识值最小的再获取锁并执行操作。跟redis存放锁标识不同的是,zookeeper可以有种消息队列消费锁的感觉。
分布式事务
分布式事物:分布式事物同样也是面临多服务器多数据库的事物处理。
首先基于CAP理论,事物处理要满足一致性,可用性,分区容忍性。
而在分布式事物中,只能满足其二,CA.AP.CP
解决方案,
1 两阶段提交协议 2PC
阶段一,通过协调者通知参与者准备提交,各参与者反馈事物执行结果,但参与者不提交事物。
阶段二,协调者通知参与者开始提交事物,各参与者反馈事物处理结果,只要在这两个阶段中执行结果和提交结果有失败回复,则整个事物回滚。
2 事物补偿 TCC
TCC是基于2PC实现的业务层事物控制方案,try阶段检查及预留业务资源完成提交事物前的检查,并预留好资源。
confirm确定执行业务操作对try阶段预留的资源正式执行。
cancel取消执行业务操作对try阶段预留的资源释放。
以下单扣库存为例子,try去占库存,confirm阶段实际扣库存,如果库存扣减失败进行cancel事物回滚,释放库存。
3 消息队列实现最终一致性
本方案基于消息中间件也是建立在2PC之上。
将库存减少的消息进行消息队列处理,在之前先生成了订单表,为避免重复执行消息,执行减少库存时检查是否执行过此消息,(这里应该可以用生成出来的订单号做为一个标识?)
执行减少库存成功后将状态发回消息队列,订单服务接收到完成库存减少消息后删除原来的减少库存任务消息(可能带着订单号的标识的那条消息)
开发成本比TCC低,但基于本地事物实现,会频繁读写数据库记录,对高并发并不是最佳方案。
4 阿里的分布式框架seata
分布式缓存
分布式缓存:大型项目中缓存需要进行同步处理,比如没连同一个缓存服务器的不同位置的服务器里的服务。
如果不处理分布式缓存,则会遇到数据库的读写瓶颈。
1 Ehcache
Java实现的开源分布式缓存框架,可以减轻数据库负载,让数据保存在不同服务器内存中,扩展简单。
2 Cacheonix
同Java实现的分布式缓存系统
3 JBoss Cache
基于事物的Java缓存框架
4 Voldemort
基于键值的缓存框架,支持多台服务器之间的缓存同步。
来自尚硅谷的学习记录
什么是乐观锁
主要适用场景:
当要更新一条记录的时候,希望这条记录没有被别人更新,就是说实现线程安全的数据更新.
针对并发中,两个客户端执行操作同一条数据更新,这时牵扯到两个客户端要都开启事务,并可能产生数据不一致问题.
mybatisplus中提供了乐观锁插件
原理解析
通过对表中加version字段并注解mybatisplus的相关注解@Version,可以在更新数据的时候判定数据是否在其中一个事务中已经执行了提交,并且设定version版本号自动+1,而另一个事务即使也预先在事务开始时得到了原始的版本号,但是当执行的version+1的事务已经提交后,另一个则无法判定version=#{oldVersion},所以即无法执行提交事务.
深入的了解CAP理论
本文来自博客园,作者:ukyo--君君小时候,转载请注明原文链接:https://www.cnblogs.com/ukzq/p/15500160.html