后端开发经验分享,纯干货 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
wayn111
V2EX    程序员

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

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

    前言

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

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

    在 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 publicclassFilterIpUtil{ privateFilterIpUtil(){ } privateList<String> strings =newArrayList<>(); // 代码块在 FilterIpUtil 实例初始化时才会执行 { // 在代码块中完成文件的第一次读写操作,后续不再读这个文件 System.out.println("FilterIpUtil init"); try(InputStreamresourceAsStream=FilterIpUtil.class.getClassLoader().getResourceAsStream("filterIp.txt")) { // 将文件内容放到 string 集合中 IoUtil.readUtf8Lines(resourceAsStream, strings); }catch(IOException e) { log.error(e.getMessage(), e); } } publicstaticFilterIpUtilgetInstance(){ returnInnerClassInstance.instance; } // 使用内部类完成单例模式,由 jvm 保证线程安全 privatestaticclassInnerClassInstance{ privatestaticfinalFilterIpUtilinstance=newFilterIpUtil(); } // 判断集合中是否包含目标参数 publicbooleanisFilter(String arg){ returnstrings.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("MasterDataSorce") @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  
       2024-01-15 14:00:15 +08:00
    感谢分享
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3870 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 33ms UTC 05:10 PVG 13:10 LAX 21:10 JFK 00:10
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86