V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wayn111
V2EX  ›  程序员

后端开发经验分享,纯干货

  •  2
     
  •   wayn111 ·
    wayn111 · 2022-11-30 19:52:18 +08:00 · 2605 次点击
    这是一个创建于 730 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    本文是博主从事后端开发以来,对公司、个人项目的经验总结,包含代码编写、功能推荐、第三方库使用及优雅配置等,希望大家看到都能有所收获

    一. 优雅的进行线程池异常处理

    在 Java 开发中,线程池的使用必不可少,使用无返回值 execute() 方法时,线程执行发生异常的话,需要记录日志,方便回溯,一般做法是在线程执行方法内 try/catch 处理,如下:

    @Test
    public void test() throws Exception {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000));
        Future<Integer> submit = threadPoolExecutor.execute(() -> {
            try {
                int i = 1 / 0;
                return i;
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                return null;
            }
        });
    }
    

    但是当线程池调用方法很多时,那么每个线程执行方法内都要 try/catch 处理,这就不优雅了,其实ThreadPoolExecutor类还支持传入 ThreadFactory 参数,自定义线程工厂,在创建 thread 时,指定 setUncaughtExceptionHandler 异常处理方法,这样就可以做到全局处理异常了,代码如下:

    ThreadFactory threadFactory = r -> {
        Thread thread = new Thread(r);
        thread.setUncaughtExceptionHandler((t, e) -> {
            // 记录线程异常
            log.error(e.getMessage(), e);
        });
        return thread;
    };
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000),
            threadFactory);
    threadPoolExecutor.execute(() -> {
        log.info("---------------------");
        int i = 1 / 0;
    });
    

    二. 线程池决绝策略设置错误导致业务接口执行超时

    先介绍下线程池得四种决绝策略

    • AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常,这是线程池默认的拒绝策略
    • DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略
    • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
    • CallerRunsPolicy:由调用线程处理该任务

    如下是一个线上业务接口使用得线程池配置,决绝策略采用 CallerRunsPolicy

    // 某个线上线程池配置如下
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                    50, // 最小核心线程数
                    50, // 最大线程数,当队列满时,能创建的最大线程数
                    60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间
                    new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
                new CustomizableThreadFactory("task"), // 自定义线程名
                    new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略
            );
    

    在某些情况下,子线程任务调用第三方接口超时,导致核心线程数、最大线程数占满、阻塞队列占满的情况下执行拒绝策略时,由于使用 CallerRunsPolicy 策略,导致业务线程执行子任务时继续超时,进而导致接口执行异常,这种情况下,考虑到子线程任务得重要性,不是很重要得话,可以使用 DiscardPolicy 策略,要是很重要,可以发送到消息队列中持久化子线程任务数据待后续处理

    三. 优雅的单例模式懒加载帮助类代码实现

    博主推荐通过静态内部类实现单例模式,并实现懒加载效果,代码如下

    // 使用静态内部类完成单例模式封装,避免线程安全问题,避免重复初始化成员属性  
    @Slf4j  
    public class FilterIpUtil {  
      
        private FilterIpUtil() {  
        }  
      
        private List<String> strings = new ArrayList<>();  
      
        // 代码块在 FilterIpUtil 实例初始化时才会执行  
        {  
        
            // 在代码块中完成文件的第一次读写操作,后续不再读这个文件
            System.out.println("FilterIpUtil init");  
            try (InputStream resourceAsStream = FilterIpUtil.class.getClassLoader().getResourceAsStream("filterIp.txt")) {  
                // 将文件内容放到 string 集合中  
                IoUtil.readUtf8Lines(resourceAsStream, strings);  
            } catch (IOException e) {  
                log.error(e.getMessage(), e);  
            }  
        }  
      
        public static FilterIpUtil getInstance() {  
            return InnerClassInstance.instance;  
        }  
        // 使用内部类完成单例模式,由 jvm 保证线程安全  
        private static class InnerClassInstance {  
            private static final FilterIpUtil instance = new FilterIpUtil();  
        }  
      
        // 判断集合中是否包含目标参数  
        public boolean isFilter(String arg) {  
            return strings.contains(arg);  
        }  
      
    }
    

    四. 使用 ip2region 实现请求地址解析

    在博主之前公司得项目中,ip 解析是调用淘宝 IP 还有聚合 IP 接口获取结果,通常耗时 200 毫秒左右,并且接口不稳定时而会挂。都会影响业务接口耗时,后来在 github 上了解到 ip2region 这个项目,使用本地 ip 库查询,查询速度微秒级别, 精准度能达到 90%,但是 ip 库还是有少部分 ip 信息不准,建议数据库中把请求 ip 地址保存下来。简介如下:

    ip2region v2.0 - 是一个离线 IP 地址定位库和 IP 定位数据管理框架,10 微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现基于 xdb 文件的查询,下面是一个 Spring 项目中 ip2region 帮助类来实现 ip 地址解析

    /**
     * ip2region 工具类
     */
    @Slf4j
    @Component
    public class Ip2region {
    
        private Searcher searcher = null;
    
        @Value("${ip2region.path:}")
        private String ip2regionPath = "";
    
        @PostConstruct
        private void init() {
            // 1 、从 dbPath 加载整个 xdb 到内存。
            String dbPath = ip2regionPath;
    
            // 1 、从 dbPath 加载整个 xdb 到内存。
            byte[] cBuff;
            try {
                cBuff = Searcher.loadContentFromFile(dbPath);
                searcher = Searcher.newWithBuffer(cBuff);
            } catch (Exception e) {
                log.error("failed to create content cached searcher: {}", e.getMessage(), e);
            }
        }
    
        public IpInfoBean getIpInfo(String ip) {
            if (StringUtils.isBlank(ip)) {
                return null;
            }
    
            // 3 、查询
            try {
                long sTime = System.nanoTime();
                // 国家|区域|省份|城市|ISP
                String region = searcher.search(ip);
                long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
                log.info("{region: {}, ioCount: {}, took: {} μs}", region, searcher.getIOCount(), cost);
                if (StringUtils.isNotBlank(region)) {
                    String[] split = region.split("\|");
                    IpInfoBean ipInfo = new IpInfoBean();
                    ipInfo.setIp(ip);
                    if (!"".equals(split[0])) {
                        ipInfo.setCountry(split[0]);
                    }
                    if (!"".equals(split[2])) {
                        ipInfo.setProvince(split[2]);
                    }
                    if (!"".equals(split[3])) {
                        ipInfo.setCity(split[3]);
                    }
                    if (!"".equals(split[4])) {
                        ipInfo.setIsp(split[4]);
                    }
                    return ipInfo;
                }
            } catch (Exception e) {
                log.error("failed to search({}): {}", ip, e);
                return null;
            }
    
            // 4 、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher
            // searcher.close();
    
            // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
            return null;
        }
    }
    

    要注意得就是 ip2region v2.0 版本使用的 xdb 文件不建议放在项目 resources 下一起打包,存在编码格式问题,建议通过指定路径加载得方式单独放在服务器目录下

    五. 优雅得 Springboot + mybatis 配置多数据源方式

    Springboot + mybatis 得项目中一般通过 @MapperScan 注解配置 dao 层包目录,来实现 dao 层增强,其实项目中配置一个@MapperScan 是指定一个数据源,配置两个@MapperScan就可以指定两个数据源,通过不同得 dao 层包目录区分,来实现不同数据源得访问隔离。

    比如下面代码中,com.xxx.dao.master 目录下为主数据源 dao 文件,com.xxx.dao.slave 为从数据源 dao 文件,这个方式比网上得基于 aop 加注解得方式更加简洁好用,也没有单个方法中使用不同数据源切换得问题,因此推荐这种写法

    /**
     * 主数据源
     */
    @Slf4j
    @Configuration
    @MapperScan(basePackages = {"com.xxx.dao.master"},
            sqlSessionFactoryRef = "MasterSqlSessionFactory")
    public class MasterDataSourceConfig {
    
        @Bean(name = "MasterDataSource")
        @Qualifier("MasterDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource clickHouseDataSource() {
            return DruidDataSourceBuilder.create().build();
        }
    
        @Bean(name = "MasterSqlSessionFactory")
        public SqlSessionFactory getSqlSessionFactory(@Qualifier("MasterDataSource") DataSource dataSource) throws Exception {
            MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
            sessionFactoryBean.setDataSource(dataSource);
            sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                    .getResources("classpath*:mapper/master/*.xml"));
            log.info("------------------------------------------MasterDataSource 配置成功");
            return sessionFactoryBean.getObject();
        }
    }
    
    /**
     * 从数据源
     */
    @Slf4j
    @Configuration
    @MapperScan(basePackages = {"com.xxx.dao.slave"},
            sqlSessionFactoryRef = "SlaveSqlSessionFactory")
    public class MasterDataSourceConfig {
    
        @Bean(name = "SlaveDataSource")
        @Qualifier("SlaveDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource clickHouseDataSource() {
            return DruidDataSourceBuilder.create().build();
        }
    
        @Bean(name = "SlaveSqlSessionFactory")
        public SqlSessionFactory getSqlSessionFactory(@Qualifier("SlaveDataSource") DataSource dataSource) throws Exception {
            MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
            sessionFactoryBean.setDataSource(dataSource);
            sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                    .getResources("classpath*:mapper/slave/*.xml"));
            log.info("------------------------------------------SlaveDataSource 配置成功");
            return sessionFactoryBean.getObject();
        }
    }
    

    数据源 yml 配置

    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        # 主库数据源
        master:
          url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password:
        slave:
          url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password:
    

    博主刚开始编码一、两年得时候一个项目中遇到了多数据源使用得问题,那时候题主便在网上搜索Spring 多数据源得帖子,大多数都是基于 Spring 提供得AbstractRoutingDataSource + AOP + 注解 来做动态切换,包括现在流行得 Mybatis plus 官方得多数据源解决方案也是这种做法,这种做法解决了博主当时得多数据源使用问题,后来加了一个需求,在一个定时任务中,查询两个数据源得数据,才发现动态切换在单个方法中不好用了,最后使用得原生 jdbc 数据源解决。多年后,博主在另一家公司得项目中又遇到了多数据源问题,但是这次博主在网上搜索得是Mybatis 多数据源,才发现了这个优雅得解决方案,进而推荐给大家

    六. Spring Security 项目中,使用 MDC 实现接口请求调用追踪,以及用户 ID 记录

    MDC 介绍

    MDC(Mapped Diagnostic Context ,映射调试上下文)是 log4j 、logback 及 log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。

    虽然 MDC 能够方便得实现接口请求调用追踪功能,但是它在子线程中会丢失父线程中添加得键值对信息,解决方法是通过父线程中调用线程池前调用 MDC.getCopyOfContextMap() ,然后在子线程中第一个调用 MDC.setConextMap() 获取键值对信息,完整实现代码如下:

    /**
     * 自定义 Spring 线程池,解决子线程丢失 reqest_id 问题
     */
    public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor {
    
        @Override
        public void execute(Runnable task) {
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    
        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    
        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }
    
    /**
     * MDC 帮助类,添加 reqest_id
     */
    public class ThreadMdcUtil {
    
    
        public static final String REQUEST_ID = "request_id";
    
        /**
         * 设置请求唯一 ID
         */
        public static void setTraceIdIfAbsent() {
            if (MDC.get(REQUEST_ID) == null) {
                MDC.put(REQUEST_ID, IdUtil.getUid());
            }
        }
    
        /**
         * 存在 userId 则添加到 REQUEST_ID 中
         * @param userId
         */
        public static void setUserId(String userId) {
            String s = MDC.get(REQUEST_ID);
            if (s != null) {
                MDC.put(REQUEST_ID, s + "_" + userId);
            }
        }
    
        public static void removeTraceId() {
            MDC.remove(REQUEST_ID);
        }
    
        public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
            return () -> {
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                setTraceIdIfAbsent();
                try {
                    return callable.call();
                } finally {
                    MDC.clear();
                }
            };
        }
    
        public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
            return () -> {
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                // 设置 traceId
                setTraceIdIfAbsent();
                try {
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            };
        }
    }
    

    Spring Security 中添加 token 过滤器

    /**
     * token 过滤器 验证 token 有效性
     *
     * @author ruoyi
     */
    @Slf4j
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        private TokenService tokenService;
    
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            try {
                // 入口传入请求 ID
                ThreadMdcUtil.setTraceIdIfAbsent();
                LoginUserDetail loginUser = tokenService.getLoginUser(request);
                if (Objects.nonNull(loginUser) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
                    // 记录 userId
                    ThreadMdcUtil.setUserId(String.valueOf(loginUser.getMember().getId()));
                    tokenService.verifyToken(loginUser);
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
                chain.doFilter(request, response);
            } finally {
                // 出口移除请求 ID
                ThreadMdcUtil.removeTraceId();
            }
        }
    
    }
    

    最后在 logback.xml 中添加 %X{request_id}

    <property name="pattern"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{request_id}] [%thread] [%-5level] %logger{36}:%L %M - %msg%n"/>
    

    日志打印效果如下:

    2022-11-27 21:29:48.008 [86c76336100c414dbe9217aeb099ccd5_12] [http-nio-82-exec-2] [INFO ] c.w.m.a.s.impl.IHomeServiceImpl:56 getHomeIndexDataCompletableFuture - getHomeIndexDataCompletableFuture:com.wayn.common.util.R@701f7b8e[code=200,msg=操作成功,map={bannerList=[{"createTime":"2020-06-26 19:56:03","delFlag":false,"id":14,"imgUrl":"https://m.360buyimg.com/mobilecms/s700x280_jfs/t1/117335/39/13837/263099/5f291a83E8ba761d0/5c0460445cb28248.jpg!cr_1125x449_0_166!q70.jpg.dpg","jumpUrl":"http://82.157.141.70/mall/#/detail/1155015","sort":0,"status":0,"title":"hh2","updateTime":"2022-06-19 09:16:46"}
    

    最后分析上诉日志:通过86c76336100c414dbe9217aeb099ccd5实现接口调用追踪,通过12用户 ID ,实现用户调用追踪

    七. alibaba excel 导出时自定义格式转换优雅实现

    官网介绍:EasyExcel 是一个基于 Java 的简单、省内存的读写 Excel 的开源项目。在尽可能节约内存的情况下支持读写百 M 的 Excel 。

    EasyExcelalibaba 出的一个基于 java poi 得 excel 通用处理类库,他的优势在于内存消耗。对比 easypoi 方案,EasyExcel 在内存消耗、知名度(大厂光环)上更出众些。

    博主在使用过程中发现导出 excel ,官网对自定义格式字段提供了 converter 接口,但只简单提供了CustomStringStringConverter 类代码,达不到博主想要得优雅要求,如下:

    public class CustomStringStringConverter implements Converter<String> {
        @Override
        public Class<?> supportJavaTypeKey() {
            return String.class;
        }
    
        @Override
        public CellDataTypeEnum supportExcelTypeKey() {
            return CellDataTypeEnum.STRING;
        }
    
        /**
         * 这里读的时候会调用
         *
         * @param context
         * @return
         */
        @Override
        public String convertToJavaData(ReadConverterContext<?> context) {
            return "自定义:" + context.getReadCellData().getStringValue();
        }
    
        /**
         * 这里是写的时候会调用 不用管
         *
         * @return
         */
        @Override
        public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
            return new WriteCellData<>(context.getValue());
        }
    
    }
    

    在以上代码中,打个比方想要实现性别字段得自定义格式转换,就需要在 convertToExcelData 方法中,添加如下代码

    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
        String value = context.getValue();
        if ("man".equals(value)) {
            return new WriteCellData<>("男");
        } else {
            return new WriteCellData<>("女");
        }
    }
    

    可以看到,非常得不优雅,对于这种类型字段,博主习惯使用枚举类来定义字段所有类型,然后将枚举类转换为 map(value,desc) 结构,就可以优雅得实现这个自定义格式得需求

    /**
     * 一、先定义 int 字段抽象转换类,实现通用转换逻辑
     */
    public abstract class AbstractIntConverter implements Converter<Integer> {
        abstract List<ConverterDTO> getArr();
    
        public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
            List<ConverterDTO> values = getArr();
            Map<Integer, String> map = values.stream().collect(toMap(ConverterDTO::getType, ConverterDTO::getDesc));
            String result = map.getOrDefault(value, "");
            return new WriteCellData<>(result);
        }
    
        static class ConverterDTO {
            private Integer type;
            private String desc;
    
            public Integer getType() {
                return type;
            }
    
            public void setType(Integer type) {
                this.type = type;
            }
    
            public String getDesc() {
                return desc;
            }
    
            public void setDesc(String desc) {
                this.desc = desc;
            }
    
            public ConverterDTO(Integer type, String desc) {
                this.type = type;
                this.desc = desc;
            }
        }
    }
    
    /**
     * 二、定义通用状态字段转换类
     */
    public class StatusConverter extends AbstractIntConverter {
    
        @Override
        List<ConverterDTO> getArr() {
            StatusEnum[] values = StatusEnum.values();
            return Arrays.stream(values).map(sexEnum -> new ConverterDTO(sexEnum.getType(), sexEnum.getDesc())).toList();
        }
    
        /**
         * 状态枚举
         */
        enum StatusEnum {
            MAN(0, "启用"),
            WOMAN(1, "禁用");
    
            private Integer type;
            private String desc;
    
            StatusEnum(Integer type, String desc) {
                this.type = type;
                this.desc = desc;
            }
            public Integer getType() {
                return type;
            }
            public String getDesc() {
                return desc;
            }
        }
    }
    

    最后再导出 ExcelProperty 中甜腻加 StatusConverter ,就优雅得实现了自定义格式得需求

    public class User extends BaseEntity {
        ...
        /**
         * 用户状态 0 启用 1 禁用
         */
        @ExcelProperty(value = "用户状态", converter = StatusConverter.class)
        private Integer userStatus;
        ...
    
    }
    

    八. Springboot 默认 redis 客户端 lettuce 经常连接超时解决方案

    不知道大家有没有遇到这种情况,线上项目使用 lettuce 客户端,当操作 redis 得接口一段时间没有调用后(比如 30 分钟),再次调用 redis 操作后,就会遇到连接超时得问题,导致接口异常。博主直接给出分析过程:

    1. 通过 wireshark 抓包工具,发现项目中 redis 连接创建后,一段时间未传输数据后,客户端发送 psh 包,未收到服务端 ack 包,触发 tcp 得超时重传机制,在重传次数重试完后,最终客户端主动关闭了连接。

    到这里我们就知道这个问题,主要原因在于服务端没有回复客户端(比如 tcp 参数设置、防火墙主动关闭等,都是针对一段时间内没有数据传输得 tcp 连接会做关闭处理),造成了客户端得连接超时

    面对这个问题有三种解决方案:

    • redis 操作异常后进行重试,这篇文章有介绍 生产环境 Redis 连接,长时间无响应被服务器断开问题
    • 启用一个心跳定时任务,定时访问 redis,保持 redis 连接不被关闭,简而言之,就是写一个定时任务,定时调用 redisget 命令,进而保活 redis 连接
    • 基于 Springboot 提供得 LettuceClientConfigurationBuilderCustomizer 自定义客户端配置,博主这里主要针对第三种自定义客户端配置来讲解一种优雅得方式

    Springboot 项目中关于 lettuce 客户端得自动配置是没有启用保活配置得,要启用得话代码如下:

    /**
     * 自定义 lettuce 客户端配置
     *
     * @return LettuceClientConfigurationBuilderCustomizer
     */
    @Bean
    public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
        return clientConfigurationBuilder -> {
            LettuceClientConfiguration clientConfiguration = clientConfigurationBuilder.build();
            ClientOptions clientOptions = clientConfiguration.getClientOptions().orElseGet(ClientOptions::create);
            ClientOptions build = clientOptions.mutate().build();
            SocketOptions.KeepAliveOptions.Builder builder = build.getSocketOptions().getKeepAlive().mutate();
            // 保活配置
            builder.enable(true);
            builder.idle(Duration.ofSeconds(30));
            SocketOptions.Builder socketOptionsBuilder = clientOptions.getSocketOptions().mutate();
            SocketOptions.KeepAliveOptions keepAliveOptions = builder.build();
            socketOptionsBuilder.keepAlive(keepAliveOptions);
            SocketOptions socketOptions = socketOptionsBuilder.build();
            ClientOptions clientOptions1 = ClientOptions.builder().socketOptions(socketOptions).build();
            clientConfigurationBuilder.clientOptions(clientOptions1);
        };
    }
    

    添加 lettuce 客户端的自定义配置,在 KeepAliveOptions 中启用 enable ,这样 lettuce 客户端就会在 tcp 协议规范上启用 keep alive 机制自动发送心跳包

    九. redis 客户端 lettuce 启用 epoll

    直接给 官网连接,配置很简单,添加一个 netty-all 得依赖,lettuce 会自动检测项目系统是否支持 epolllinux 系统支持),并且是否有netty-transport-native-epoll依赖( netty-all 包含 netty-transport-native-epoll ),都满足得话就会自动启用 epoll 事件循环,进一步提升系统性能

    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
    </dependency>
    

    十. Springboot web 项目优雅停机

    web 项目配置了优雅停机后,在重启 jar 包,或者容器时可以防止正在活动得线程被突然停止( kill -9 无解,请不要使用这个参数杀线上进程,docker compose 项目尽量不要用 docker-compose down 命令关闭项目,使用 docker-compose rm -svf 可以触发优雅停机),造成用户请求失败,在此期间允许完成现有请求但不允许新请求,配置如下:

    server: shutdown: "graceful"
    

    十一. nginx 配置通用请求后缀

    先说下这个配置产生得前提,博主公司 pc 客户项目是基于 electron 打包得网页项目,每次项目大版本更新时,为了做好兼容性,防止客户端网页缓存等,会使用一个新网页地址,打个比方:

    老网页地址,v1.1.0 版本网页访问地址: http://api.dev.com/pageV110

    新网页地址,v1.2.0 版本网页访问地址: http://api.dev.com/pageV120

    那么项目得 nginx 配置则则需要新加一个 v1.2.0 得配置如下:

    server {
       listen 80;
       server_name api.dev.com;
       client_max_body_size 10m;
    
       # 老网页 v1.1.0 配置
       location ~ ^/pageV110 {
                   alias  /home/wwwroot/api.dev.com/pageV110;
                   index  index.html index.htm;
           }
           
       # 新网页 v1.2.0 配置
       location ~ ^/pageV120 {
                   alias  /home/wwwroot/api.dev.com/pageV120;
                   index  index.html index.htm;
           }
    
    }
    

    那么博主在每次项目发布得时候就需要配合前端发版,配置一个新网页,故产生了这个通用配置得需求,如下:

    server {
       listen 80;
       server_name api.dev.com;
       client_max_body_size 10m;
    
       # 配置正则 localtion
       location ~ ^/pageV(.*) {
                   set $s $1; # 定义后缀变量
                   alias  /home/wwwroot/api.dev.com/pageV$s;
                   index  index.html index.htm;
           }
    
    }
    

    nginx 配置文件语法中,location 语句可以使用正则表达式,定义 set $s $1 变量,实现了通用配置

    十二. 关于开发人员的自我提升和突破

    博主这里主要总结了四点:

    1. 多和他人沟通,沟通能把复杂问题简单化,有时候开发阶段一个需求多问几句,可以减少因为个人理解差异导致的需求不一致问题,进而减少开发时间
    2. 建立长短期目标,观看技术视频、书籍给自己充电,比如 7 天利用业余时间看完一本电子书,三十天从零开始一个新项目等
    3. 善于总结,对于项目中的疑难 bug ,踩坑点要有记录,防止下次遇到再掉坑里
    4. 敢于尝试、担责,对项目、代码里明确不合理的地方要敢于跟他人沟通,修改问题代码,达到优化目的。对于自己造成的问题要承担,不要推卸责任。对于线上问题要重视,优先解决线上问题。
    6 条回复    2024-01-15 14:00:15 +08:00
    v2webdev
        1
    v2webdev  
       2022-11-30 20:14:13 +08:00
    湖北老乡?
    wayn111
        2
    wayn111  
    OP
       2022-11-30 21:06:12 +08:00
    @v2webdev 是的
    OnlyO
        3
    OnlyO  
       2022-12-01 09:07:26 +08:00
    都是干货,顶
    wayn111
        4
    wayn111  
    OP
       2022-12-01 11:50:10 +08:00
    @OnlyO 那确实
    wayn111
        5
    wayn111  
    OP
       2022-12-01 16:44:43 +08:00
    顶一顶,让更多人看到
    aobamaM
        6
    aobamaM  
       319 天前
    感谢分享
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2917 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 02:42 · PVG 10:42 · LAX 18:42 · JFK 21:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.