快速开发 拒绝delay🙅🏻‍♀️

704 阅读9分钟

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快速入门

// 作用
    floatdouble处理小数时,会造成精度缺失,而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能做的我也能做! 用到的场景:

  1. 异步执行多个下游调用(就正常线程池用法,如下);
  2. 读多个下游场景,流量高且可接受读不到数据,进行设置超时时间(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使用姿势

策略配置效果: image.png

@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 // 文件大小