Jdk
1. Objects比较两个对象是否相等
// 使用Objects#equals,避免空指针,比如Integer,帮你处理参数为null的情况;
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
2. Map取值不存在时给默认值
String value = map.getOrDefault("key", "default");
// 相似用法:解析字符串为数字
int num = NumberUtils.toInt(str, 0);
3. String.join分隔符连接字符串
String redisKey = String.join(".",activityId, userId, taskId);
4. Stream流操作lists.stream()
集合为null时调用stream()方法抛空指针异常!! Refer:常用的Stream流操作
List<String> strs = null; // NullPointerException
strs.stream().filter(StringUtils::isNotBlank).collect(Collectors.toList());
5. Optional解决NPE问题
Refer:JAVA8之妙用Optional解决NPE问题
// 1.赋值,为null给默认值
Integer pageSize = Optional.ofNullable(req.getPageSize()).orElse(10);
resultList.addAll(Optional.ofNullable(list).orElse(Lists.newArrayList()));
// 2.代替if != null语句 (不推荐,可读性下降)
Optional.ofNullable(req.getPageSize()).ifPresent(pageSize -> {
orderReq.setPageSize(pageSize);
// doSomething ...
});
Optional.ofNullable(req.getDeleteFlag()).map(Integer::valueOf)
.ifPresent(orderReq::setDeleteFlag);
// 3.多级取值 user.getAddress().getCity();
// 之前写法:
public String getCity (User user){
if (user != null) {
Address address = user.getAddress();
if (address != null && address.getCity() != null) {
return address.getCity();
}
}
return Strings.EMPTY;
}
// 现在写法:
String city = Optional.ofNullable(user)
.map(u -> u.getAddress())
.map(a -> a.getCity())
.orElse(Strings.EMPTY / null);
// 结合stream流:避免list为null时NPE
List<String> list = null;
List<Object> resultList = Optional.ofNullable(list).orElse(Lists.newArrayList())
.stream()
.filter(Objects::nonNull)
.map(item -> {
// doSomething ...
})
.collect(Collectors.toList());
}
6.Bigdecimal精确的小数计算处理
Refer: BigDecimal 详解 、BigDecimal踩坑总结 、DecimalFormat快速入门
// 作用
float和double处理小数时,会造成精度缺失,而bigDecimal提供了精确的运算;
简单金额场景用Long,涉及汇率、费率等复杂场景时用bigdecimal;
// 构建对象
BigDecimal("0.1") // 字符串
BigDecimal.valueOf(0.1f) // 小数
// 加减乘除,比较大小,格式化等
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
a.add(b); // 1.9
a.subtract(b); // 0.1
a.multiply(b); // 0.90
a.divide(b); // 无法除尽时,抛出ArithmeticException异常
a.divide(b, 2, RoundingMode.HALF_UP); // 1.11 ✅ divide使用3参数,HALF_UP小数四舍五入
// 比较大小
a.compareTo(b); // 1 (0相等,-1 a<b,1 a>b)
// 格式化为字符串
new DecimalFormat("0.00").format(a); // 1.00,保留2位小数
// 设置小数位数
setScale(2, BigDecimal.ROUND_HALF_UP); // 用于加减乘除之后
// 错误写法
进行加、减、乘、除、比较大小时,a和b都存在为null; // ❌ NPE异常
new BigDecimal(0.1f) // ❌ 实际存储值:100000001490116119384765625
a.equals(b); // ❌ a=1,b=1.0时为false(底层同时比较值大小和精度大小)
Guava、Apache、Spring ...
1. Lists.partition拆分大集合
将集合按照一定大小拆分成小集合;当集合为null时,抛出空指针异常!!
// com.google.common.collect.Lists#partition(List<T> list, int size)
for (List<String> subSkuCodes : Lists.partition(skuCodes, SIZE_LIMIT)) {}
Lists.partition(skuCodes, SIZE_LIMIT).forEach(subSkuCodes -> {});
2. Splitter切割字符串
Java中的分词工具类会有一些古怪的行为。例如:String.split()方法会丢弃尾部分隔符:
String[] split = ",a,,b,".split(","); // ["", "a", "", "b"]
Guava的Splitter可以让你使用一种非常简单流畅的模式来控制这些令人困惑的行为。
List<String> split = Splitter.on(',')
.trimResults()
.omitEmptyStrings()
.splitToList(",a,,b,"); // ["a", "b"]
// omitEmptyStrings()过滤掉空字符串
// trimResults()去掉字符串两端的空格
String str = ", a,,b c ,";
Splitter.on(',').splitt(str); // ["", " a", "", "b c ", ""]
Splitter.on(',').trimResults().split(str); // ["", "a", "", "b c", ""]
Splitter.on(',').omitEmptyStrings().split(str); // [" a", "b c "]
3. BeanUtils.copyProperties浅拷贝对象
- 对两个不同对象进行浅拷贝复制时,使用Spring的BeanUtils;
- 禁止使用Apache BeanUtils进行熟悉的copy,性能较差!
当sourceObject 或 argetObject为 null 时抛出参数不合法异常!!
// org.springframework.beans.BeanUtils#copyProperties(Object, Object)
// sourceObject == null || targetObject == null // IllegalArgumentException
BeanUtils.copyProperties(sourceObject, targetObject);
注意:公司建议使用convert()或transfer()按照每个字段进行转换,因为在含有泛型字段时容易映射出错,且是浅拷贝,当修改源对象时,目标对象也被修改了,可能会导致使用出错。
4. NumberUtils解析字符串为数字
// org.apache.commons.lang3.math.NumberUtils
int num = NumberUtils.toInt(str, 0);
Enum枚举类
1. 枚举类Enum写法
@Getter
@AllArgsConstructor
public enum RewardSourceEnum {
SIGN(1, "签到"),
VENUE(2, "浏览会场"),
ASSIST(3, "邀请助力"),
;
private long code;
private String msg;
public static RewardSourceEnum getByCode(long code) {
return Arrays.stream(RewardSourceEnum.values()).filter(item -> item.getCode() == code).findFirst().orElse(null);
}
}
线程池
线程池的四种拒绝策略:
- AbortPolicy中止策略:默认的策略,丢弃任务并抛出RejectedExecutionException异常;(常用)
- DiscardPolicy丢弃策略:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
- DiscardOldestPolicy丢弃策略:丢弃队列最前面的任务,然后重新提交被拒绝的任务;
- CallerRunsPolicy调用者运行策略:由提交任务的当前线程处理当前任务。(
常用)
1. 线程池执行工具类
public interface ClientExecutorService {
/**
* 有返回值
* 子线程遇到异常直接被捕获,主线程调用get()才能抛出此异常,否则无异常信息
*
* @param task
* @param <T>
* @return
*/
<T> Future<T> submit(Callable<T> task);
/**
* 无返回值
* 子线程遇到异常能抛出打印日志,主线程无法捕获
*
* @param task
*/
void execute(Runnable task);
}
@Component
public class ClientExecutorServiceImpl implements ClientExecutorService {
@Value("${sbt.clientExecutor.corePoolSize:32}")
private int corePoolSize;
@Value("${sbt.clientExecutor.maxPoolSize:32}")
private int maximumPoolSize;
@Value("${sbt.clientExecutor.maxWaitSize:1024}")
private int maxWaitSize;
private ExecutorService executorService;
/**
* 设置核心线程数,最大线程数,保持一致,避免线程创建开销
* CPU密集型:corePoolSize = CPU核数 + 1
* IO密集型: corePoolSize = CPU核数 * 2
*/
@PostConstruct
public void init() {
executorService = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
3L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(maxWaitSize),
new ThreadFactoryBuilder().setNameFormat("client-executor-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy());
}
@PostConstruct
public void init() {
final AtomicInteger threadCount = new AtomicInteger(0);
executorService = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
3L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(maxWaitSize),
r -> new Thread(r, ClientExecutorService.class.getSimpleName() + "-" + threadCount.getAndIncrement()),
(r, e) -> {
log.info("线程池打满,触发拒绝策略,由当前线程处理"); // 加了线程池打满提醒日志
if (!e.isShutdown()) {
r.run();
}
});
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return executorService.submit(task);
}
@Override
public void execute(Runnable task) {
executorService.execute(task);
}
}
参见:# 线程池拒绝策略应用场景 # 如何正确使用线程池submit和execute方法
2. 线程池使用注意
如果在主线程中异步执行一段逻辑,且失败时希望不影响主流程的执行,那么在默认拒绝策略AbortPolicy下需要进行try catch,如下:
try {
clientExecutorService.execute(() -> riskClientFacade.checkRisk(riskParam));
} catch (Exception e) {
log.error("clientExecutorService异步调用异常", e);
}
3. CompletableFuture-增强的Future,支持多线程异步任务编排
一句话:Future能做我能做,CountDownLatch能做的我也能做! 用到的场景:
- 异步执行多个下游调用(就正常线程池用法,如下);
- 读多个下游场景,流量高且可接受读不到数据,进行设置超时时间(CountDownLatch用法)
Ref:CompletableFuture 详解、 替代CountDownLatch!、 CompletableFuture使用大全
private void saveLotteryRecordAndIssueReward(LotteryReq req, List<LotteryRecordDO> batchRecordList, List<BagAssetDTO> rewardModelList, Integer totalRewardPoint) {
long start = System.currentTimeMillis();
//保存抽奖记录
CompletableFuture<Void> saveLotteryRecordFuture = CompletableFuture.runAsync(() -> saveLotteryRecord(req, batchRecordList), taskExecutor);
//保存进试炼背包
CompletableFuture<Void> saveAssetRecordFuture = CompletableFuture.runAsync(() -> saveBag(req, rewardModelList), taskExecutor);
//保存积分
CompletableFuture<Void> issuePointFuture = CompletableFuture.runAsync(() -> issuePoint(req, totalRewardPoint), taskExecutor);
CompletableFuture<Void> allRequests = CompletableFuture.allOf(saveLotteryRecordFuture, saveAssetRecordFuture, issuePointFuture);
try {
allRequests.get(10000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("multipleProcessRec allRequests get error!", e);
}
log.info("耗时={}", System.currentTimeMillis() - start);
}
// 场景:某一个业务接口,需要处理几百个请求,请求之后再把这些结果给汇总起来
// CountDownLatch实现
ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch countDown = new CountDownLatch(requests.size());
for (Request request : requests) {
executor.execute(() -> {
try {
//some opts, eg: setXxx()
} finally {
countDown.countDown();
}
});
}
countDown.await(200, TimeUnit.MILLISECONDS);
// CompletableFuture实现
List<CompletableFuture<Result>> futureList = requests
.stream()
.map(request->
CompletableFuture.supplyAsync(e->{
//some opts
},executor))
.collect(Collectors.toList());
CompletableFuture<Void> allCF = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
// allCF.get(200,TimeUnit.MILLISECONDS);
allCF.join();
RPC
1. rpc接口调用
采用门面模式(Facade Pattern),构建一个统一的外观对象与下游服务通信,易于管理使用。
@Service
public class LoveScreenAdminFacade {
@Autowired
private LoveScreenAdminApi loveScreenAdminApi;
public List<UserLoveTextVO> getLoveText(LoveScreenPageQueryParam param) {
try {
Result<List<UserLoveTextVO>> result = loveScreenAdminApi.getLoveText(param);
log.info("批量获取告白文案,req={}, resp={}", JsonUtils.serialize(param), JsonUtils.serialize(result));
return result.getData();
} catch (Exception e) {
log.error("批量获取告白文案异常,req={}", JsonUtils.serialize(param), e);
throw e;
}
// return null; 吃?不吃异常(业务中常见是吃异常,返回null,由业务上层来决定是否抛异常)
}
}
Mybatis
1. MySQL的结构设计
Refer:MySQL的字段类型,该怎么选?
2. mybatis-generator插件
使用mybatis-generator插件自动生成:xxxMapper、xxxMapper.xml和Entity文件。
pom.xml配置:
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<configuration>
<configurationFile>src/main/resources/generator-config.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies>
</plugin>
generator-config.xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3" defaultModelType="flat">
<plugin type="org.mybatis.generator.plugins.RowBoundsPlugin"/>
<!--去除注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!--数据库连接 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/ssm"
userId="root"
password="789456">
</jdbcConnection>
<!--默认false Java type resolver will always use java.math.BigDecimal if the database column is of type DECIMAL or NUMERIC. -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!--生成实体类 指定包名 以及生成的地址 (可以自定义地址,但是路径不存在不会自动创建 使用Maven生成在target目录下,会自动创建) -->
<javaModelGenerator targetPackage="org.balloon.model.entity" targetProject="src/main/java">
<property name="enableSubPackages" value="false"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!--生成SQLMAP文件 -->
<sqlMapGenerator targetPackage="resources/mybatis/mappers" targetProject="src/main">
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!--生成Dao文件 可以配置 type="XMLMAPPER"生成xml的dao实现 context id="DB2Tables" 修改targetRuntime="MyBatis3" -->
<javaClientGenerator type="XMLMAPPER" targetPackage="org.balloon.model.dao"
targetProject="src/main/java">
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<!--自动生成的数据库表 -->
<table tableName="student" enableSelectByExample="true" enableDeleteByExample="true"
enableCountByExample="true" enableUpdateByExample="true">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<table tableName="course" enableSelectByExample="true" enableDeleteByExample="true"
enableCountByExample="true" enableUpdateByExample="true">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<table tableName="ambassador" enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="status" javaType="Integer" jdbcType="TINYINT"/>
</table>
<!--自动生成的数据库表 domainObjectName和mapperName自定义entity和mapper类名 -->
<table tableName="count_config" domainObjectName="CountConfigEntity" mapperName="CountConfigMapper"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
</context>
</generatorConfiguration>
当mybatis-generator插件自动生成的Mapper不能满足需要时,我们可以进行拓展。
xxxMapperExt:
public interface StudentMapperExt extends StudentMapper {
int batchInsert(@Param("students") List<Student> students);
}
xxxMapperExt.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.balloon.model.dao.StudentMapperExt">
<resultMap id="BaseResultMapExt" type="org.balloon.model.entity.Student"
extends="org.balloon.model.dao.StudentMapper.BaseResultMap"/>
<sql id="Base_Column_List_Ext">
<include refid="org.balloon.model.dao.StudentMapper.Base_Column_List"></include>
</sql>
<insert id="batchInsert" parameterType="org.balloon.model.entity.Student">
insert into student(stu_code, stu_name, stu_sex, stu_age, stu_dept) values
<foreach collection="students" item="student" separator=",">
(#{student.stuCode}, #{student.stuName}, #{student.stuSex}, #{student.stuAge}, #{student.stuDept})
</foreach>
</insert>
</mapper>
在代码中,我们用xxxMapperExt来取代xxxMapper,如下:
@Autowired
private StudentMapperExt studentMapperExt;
3. Druid连接池配置
# count-config
db.conn.str=useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useLocalSessionState=true&tinyInt1isBit=false
mysql.count-config.url=jdbc:mysql://127.0.0.1:3306/count_config?${db.conn.str}
mysql.count-config.username=root
mysql.count-config.password=xiaowei789456
mysql.count-config.driverClass=com.mysql.jdbc.Driver
# 连接池大小配置
mysql.count-config.initial-size=10
mysql.count-config.min-idle=5
mysql.count-config.max-active=20
# 从连接池获取连接的最大等待时间 0.8s
mysql.count-config.max-wait=800
# 对空闲连接保活
mysql.count-config.keep-alive=true
# 空闲连接检测间隔 6s(配合min/max-evictable-idle-time-millis使用)
mysql.count-config.time-between-eviction-runs-millis=6000
# 最小空闲时间,即超过min-idle数量的连接的空闲存活时间 5min
mysql.count-config.min-evictable-idle-time-millis=300000
# 最大空闲时间,min-idle连接的空闲存活时间 1h
mysql.count-config.max-evictable-idle-time-millis=3600000
# 检测连接是否有效的sql
mysql.count-config.validation-query=SELECT 1
# 申请连接时判断连接是否有效
mysql.count-config.test-while-idle=true
# 在获得连接前是否要进行测试
mysql.count-config.test-on-borrow=false
# 在归还连接前是否要进行测试
mysql.count-config.test-on-return=false
Refer:DruidDataSource配置属性列表、druid连接池常用配置、Druid空闲连接检测 KeepAlive
4. Mybatis-Plus插件
代码生成器:自动生成Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码;
自动生成:
TeamInfoDO
TeamInfoMapper.xml
TeamInfoMapper
TeamInfoRepo
public class MySqlGenerator {
public static TemplateConfig getTemplateConfig(){
/**
* 配置自定义输出模板
*/
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setEntity("templates/entity.java");
templateConfig.setXml("templates/mapper.xml");
templateConfig.setService("templates/service.java");
templateConfig.setServiceImpl("templates/serviceImpl.java");
templateConfig.setController("templates/controller.java");
return templateConfig;
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = "./generatedcode/";
gc.setOutputDir(projectPath + "/src/main/java");
gc.setFileOverride(true);
gc.setActiveRecord(false);
gc.setEnableCache(false); // XML 二级缓存
gc.setBaseResultMap(true); // XML ResultMap
gc.setBaseColumnList(true);// XML columList
//使用java.util.Date来表示Date字段
gc.setDateType(DateType.ONLY_DATE);
gc.setAuthor("balloon");
gc.setOpen(false);
//实体属性 Swagger2 注解
//gc.setSwagger2(true);
gc.setEntityName("%sDO");
gc.setMapperName("%sMapper");
gc.setXmlName("%sMapper");
gc.setServiceName("%sService");
gc.setServiceImplName("%sServiceImpl"); // %sRepo
gc.setControllerName("%sController");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/test_repo?characterEncoding=utf8");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("789456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("dao");
pc.setParent("com.balloon");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
mpg.setTemplate(getTemplateConfig());
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(
"team_info",
"table2",
"table3"
);
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
System.out.println("自动生成mybatis-plus代码结束");
}
}
持久层接口: IService是一个通用Service层接口,封装了常见操作:get查询单行,remove删除,list查询集合,page分页查询;
public interface TeamInfoMapper extends BaseMapper<TeamInfoDO> {
}
@Service
public class TeamInfoRepo extends ServiceImpl<TeamInfoMapper, TeamInfoDO> {
}
条件构造器:AbstractWrapper:抽象基类,提供了所有Wrapper类共有的方法和属性;
QueryWrapper
LambdaQueryWrapper
UpdateWrapper
LambdaUpdateWrapper
// lambda查询
LambdaQueryWrapper<TeamInfoDO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TeamInfoDO::getActivityId, queryWrapper.getActivityId())
.eq(TeamInfoDO::getActivityType, queryWrapper.getActivityType())
.in(TeamInfoDO::getId, queryWrapper.getTheamIdList())
.eq(TeamInfoDO::getValid, 1);
List<TeamInfoDO> teamInfoList = teamInfoRepo.list(queryWrapper);
if (CollectionUtils.isEmpty(teamInfoList)) {
return Lists.newArrayList();
}
// lambda更新
LambdaUpdateWrapper<TeamMemberInfoDO> teamMemberUpdateWrapper = new LambdaUpdateWrapper<TeamMemberInfoDO>()
.set(TeamMemberInfoDO::getValid, 0)
.eq(TeamMemberInfoDO::getActivityId, teamMemberDeleteBO.getActivityId())
.eq(TeamMemberInfoDO::getActivityType, teamMemberDeleteBO.getActivityType())
.eq(TeamMemberInfoDO::getTeamId, teamMemberDeleteBO.getTeamId())
.eq(TeamMemberInfoDO::getMemberId, teamMemberDeleteBO.getUserId())
.last("limit 1");
LambdaUpdateWrapper<TeamInfoDO> teamInfoUpdateWrapper = new LambdaUpdateWrapper<TeamInfoDO>()
.eq(TeamInfoDO::getId, teamMemberDeleteBO.getTeamId())
.setSql("member_count = member_count - 1")
.last("limit 1");
transactionalManager.newTransactional(() -> {
teamMemberInfoRepo.update(teamMemberUpdateWrapper);
teamInfoRepo.update(teamInfoUpdateWrapper);
});
Redis
1. Redis事务、Pipeline和Lua脚本
三种将客户端多条命令打包发送给服务执行的方式,从而降低网络传输(RTT)开销,区别:Redis 管道、事务、Lua 脚本对比
- pipeline(管道):增强版multiSet,不能保证原子性;
- redis事务:保证了原子性,阻塞接收其它命令
- Lua脚本:原子性,阻塞接收其它命令;可根据中间值来编排后续命令;
/**
* 查询一批数据时,使用pipeline进行redis批量操作:
* 1. redis.multiGet查缓存;
* 2. 聚合不在缓存中的数据,走db查询;
* 3. 批量设置缓存(查不到设置空缓存防穿透);
*/
List<Object> result = redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.opsForValue().set("key", "value", 10, TimeUnit.SECONDS);
return null;
}
});
ref:[Spring boot 下使用Redis管道(pipeline)进行批量操作](https://juejin.cn/post/7232225892214636581)
public TreasureBoxTypeConfigDO selectByTypeCodeFromCache(String typeCode) {
if (StringUtils.isBlank(typeCode)) {
return null;
}
String redisKey = RedisKeyConstant.getTreasureBoxTypeConfigKey(typeCode);
String redisValue = stringRedisTemplate.opsForValue().get(redisKey);
if(StringUtils.isNotBlank(redisValue)) {
// 空缓存
if(StringUtils.equals(RedisKeyConstant.REDIS_NON_EXIST_VALUE, redisValue)) {
return null;
}
return JSON.parseObject(redisValue, TreasureBoxTypeConfigDO.class);
}
TreasureBoxTypeConfigDO entity = this.selectByTypeCodeFromDb(typeCode);
redisValue = Objects.isNull(entity) ? RedisKeyConstant.REDIS_NON_EXIST_VALUE : JSON.toJSONString(entity);
stringRedisTemplate.opsForValue().set(redisKey, redisValue, 10, TimeUnit.MINUTES);
return entity;
}
public List<TreasureBoxTypeConfigDO> selectByTypeCodesFromCache(List<String> typeCodes) {
if (CollectionUtils.isEmpty(typeCodes)) {
return Lists.newArrayList();
}
List<TreasureBoxTypeConfigDO> result = Lists.newArrayList();
// 先查缓存
List<String> redisKeyList = typeCodes.stream().map(RedisKeyConstant::getTreasureBoxTypeConfigKey).collect(Collectors.toList());
List<String> redisValueList = Optional.ofNullable(stringRedisTemplate.opsForValue().multiGet(redisKeyList)).orElse(Lists.newArrayList());
// 未命中缓存的key
List<String> noCacheTypeCodeList = Lists.newArrayList();
for (int i = 0; i < redisValueList.size(); i++) {
String redisValue = redisValueList.get(i);
if (StringUtils.isBlank(redisValue)) {
noCacheTypeCodeList.add(typeCodes.get(i));
continue;
}
if (StringUtils.equals(RedisKeyConstant.REDIS_NON_EXIST_VALUE, redisValue)) {
continue;
}
TreasureBoxTypeConfigDO entity = JSON.parseObject(redisValue, TreasureBoxTypeConfigDO.class);
result.add(entity);
}
if (CollectionUtil.isEmpty(noCacheTypeCodeList)) {
return result;
}
// 不在缓存走db
List<TreasureBoxTypeConfigDO> dbBoxTypeList = Optional.ofNullable(this.lambdaQuery()
.in(TreasureBoxTypeConfigDO::getTypeCode, noCacheTypeCodeList).list())
.orElse(Lists.newArrayList());
// 批量设置缓存
stringRedisTemplate.executePipelined((RedisCallback<String>) connection -> {
dbBoxTypeList.forEach(entity -> {
connection.set(
RedisKeyConstant.getTreasureBoxTypeConfigKey(entity.getTypeCode()).getBytes(StandardCharsets.UTF_8),
JSONUtil.toJsonStr(entity).getBytes(StandardCharsets.UTF_8),
Expiration.from(10, TimeUnit.MINUTES),
RedisStringCommands.SetOption.UPSERT);
});
// db不存在,空缓存
Set<String> dbBoxTypeSet = dbBoxTypeList.stream().map(TreasureBoxTypeConfigDO::getTypeCode).collect(Collectors.toSet());
List<String> dbNotExistKeyList = noCacheTypeCodeList.stream().filter(typeCode -> !dbBoxTypeSet.contains(typeCode)).collect(Collectors.toList());
dbNotExistKeyList.forEach(typeCode ->
connection.set(RedisKeyConstant.getTreasureBoxTypeConfigKey(typeCode).getBytes(StandardCharsets.UTF_8),
RedisKeyConstant.REDIS_NON_EXIST_VALUE.getBytes(StandardCharsets.UTF_8),
Expiration.from(10, TimeUnit.MINUTES),
RedisStringCommands.SetOption.UPSERT));
return null;
});
result.addAll(dbBoxTypeList);
return result;
}
2. Redis做分布式锁
在使用redis做分布式锁时,此种写法在用户连点两次时,会释放锁!!
// 错误写法
try {
redis.setnx(key, time);
} finnay {
redis.del(key);
}
// 正确写法
bool result = redis.setnex(key, time);
if (result) {
try {
// 业务逻辑
} finally {
redis.del(key);
}
}
3. 事务外删缓存/通知
@Transactional(rollbackFor = Exception.class)
public void save(Request request) {
// doSomething
MapperExt.insert(model);
// doSomething
MapperExt.update(model);
//事务提交之后执行方法(通常用于在提交事务之后执行一些清理或通知操作,如果事务回滚,则不会调用此方法)
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
cacheHandle(model.getId());
}
});
}
Caffeine
Caffeine是一个高性能的本地缓存库,提供了四种类型的Cache,对应着四种加载策略:手动加载(Cache)、自动加载(LoadingCache)、手动异步加载(AsyncCache)和自动异步加载(AsyncLoadingCache);
相较于Redis,Caffeine可以从策略配置上来避免缓存击穿(key过期后大流量并发访问场景)。
1. Caffeine使用姿势
策略配置效果:
@Repository
public class ActivityClientRepositoryImpl implements ActivityClientRepository {
private long activityCacheMaximumSize;
private int activityCacheExpireAfterAccess;
private int activityCacheRefreshAfterWrite;
private LoadingCache<String, ActivityModel> activityCache;
@PostConstruct
private void init() {
activityCache = Caffeine.newBuilder()
.maximumSize(activityCacheMaximumSize)
.expireAfterAccess(activityCacheExpireAfterAccess, TimeUnit.MINUTES) // 最后一次读或写后,达到指定时间key过期
.refreshAfterWrite(activityCacheRefreshAfterWrite, TimeUnit.SECONDS) // 写操作后达到指定时间,对key调用load方法进行刷新(其实是达到指定时间后第一次请求时才异步刷新)
.build(this::getByActivityId0); // 自动加载策略
}
private ActivityModel getByActivityId0(String activityId) {
// 从db中加载数据
return activity;
}
@Override
public ActivityModel getActivityFromCache(String activityId) {
if (activityId == null) {
return null;
}
return activityCache.get(activityId);
}
@Value("${scd.activityCache.maximumSize:10000}")
public void setActivityCacheMaximumSize(long maximumSize) {
if (activityCache != null) { // Caffeine支持在程序运行过程中策略的配置!
activityCache.policy().eviction().ifPresent(eviction -> {
eviction.setMaximum(maximumSize);
});
}
this.activityCacheMaximumSize = maximumSize;
}
@Value("${scd.activityCache.expireAfterAccess:10}")
public void setActivityCacheExpireAfterAccess(int expireAfterAccess) {
if (activityCache != null) {
activityCache.policy().expireAfterAccess().ifPresent(expiration -> {
expiration.setExpiresAfter(expireAfterAccess, TimeUnit.MINUTES);
});
}
this.activityCacheExpireAfterAccess = expireAfterAccess;
}
@Value("${scd.activityCache.refreshAfterWrite:30}")
public void setActivityCacheRefreshAfterWrite(int refreshAfterWrite) {
if (activityCache != null) {
activityCache.policy().refreshAfterWrite().ifPresent(refresh -> {
refresh.setExpiresAfter(refreshAfterWrite, TimeUnit.SECONDS);
});
}
this.activityCacheRefreshAfterWrite = refreshAfterWrite;
}
}
参见:# Caffeine GitHub # 如何把 Caffeine Cache 用得如丝般顺滑? # Caffeine本地缓存-策略测试
RocketMQ
1. 消息幂等处理
消息重复的场景:
1. 发送时重复:生产者发送消息到broker,由于网络问题broker
返回ack失败,导致生产者重试发送消息;(rocketMQ发送失败默认重试2次)
2. 消费时重复:消费者在处理业务时发生异常,或者返回消费状态
给broker时发生网络异常,导致broker重试发送;(rocketMQ消费失败默认重试16次)
最终导致的问题:消费者收到了重复内容的消息。
解决方案:在生产者端消息体中定义业务唯一标识的幂等字段,在消费端统一进行幂等处理;
不可使用消息ID、消息key作为幂等字段,在消息重复场景中不具有唯一性!
Refer:[跟我学RocketMQ之消息幂等](https://zhuanlan.zhihu.com/p/61143205)
Linux
1. 常用命令
# 1.查看进程
ps -ef
ps -ef | grep java
# 2.查看日志
tail -f file.log // 滚动显示日志
tail -n 1000 file.log // 显示倒数1000行
tailf -n 1000 file.log // 倒数1000行+滚动显示新日志
grep -A100 "text" file.log
less -N file.log // 显示行号
阅读模型命令:
前一页 b 后一页 SPACE
前半页 u 后半页 d
第一行 < 最后行 >
搜索 /text (下一个 n 上一个 N)
离开 q
# 3.项目jar包查看
jar -tf promocore.jar // 列出sbt项目jar包内容
jar -tf promocore.jar | grep user-api // 查看项目jar包中用户版本
## 4.查看文件大小
ls -lh // 文件大小