【Java高并发系统与安全监控】高并发与性能调优实战:JVM+线程池+Redis+分库分表

文章目录

1. 高并发系统性能调优基础认知

1.1 高并发核心瓶颈与调优目标

高并发场景下,系统性能瓶颈主要集中在四个层面,调优需针对性突破:

  • 内存瓶颈:JVM堆溢出、GC频繁、缓存命中率低;

  • 线程瓶颈:线程池参数不合理、线程阻塞、上下文切换频繁;

  • 存储瓶颈:Redis缓存失效、数据库连接池耗尽、单表数据量过大;

  • 网络瓶颈:接口响应慢、连接超时、数据传输量大。

核心调优目标:高可用(99.99%+)、低延迟(接口响应<500ms)、高吞吐(QPS支持万级+)、高容错(故障自动降级)

1.2 性能评估指标与监控工具

(1)核心性能指标
  • 吞吐量(QPS/TPS):每秒处理请求/事务数;

  • 响应时间(RT):从请求发起至响应完成的耗时;

  • 并发数:系统同时处理的请求数;

  • 错误率:请求失败占比(允许值<0.1%);

  • GC指标:GC停顿时间(允许值<100ms)、GC频率(允许值<1次/分钟)。

(2)常用监控工具
  • JVM监控:jstat、jmap、jstack、VisualVM、Arthas;

  • 并发监控:JConsole、ThreadDump分析工具;

  • 缓存监控:Redis Desktop Manager、Redis Info命令;

  • 数据库监控:MySQL Slow Query、Percona Monitoring;

  • 全链路监控:SkyWalking、Pinpoint、Prometheus+Grafana。

2. JVM参数调优实战

JVM是Java程序运行的基石,高并发场景下,不合理的JVM配置会导致OOM、GC频繁、响应延迟等问题,需结合业务场景精准调优。

2.1 JVM核心调优参数(堆/非堆/GC)

JVM内存模型分为堆、非堆(方法区、元空间)、程序计数器、虚拟机栈、本地方法栈,核心调优聚焦堆和非堆:

(1)堆内存参数(最核心)

-Xms:初始堆大小(如2g),建议与-Xmx一致,避免频繁扩容;

-Xmx:最大堆大小(如2g),根据物理内存配置(建议为物理内存的1/2~2/3);

-Xmn:新生代大小(如512m),新生代=Eden+From Survivor+To Survivor,建议为堆的1/3~1/2;

-XX:SurvivorRatio:Eden与Survivor区比例(如8),即Eden:From:To=8:1:1;

-XX:MaxTenuringThreshold:对象晋升老年代阈值(如15),超过阈值的新生代对象进入老年代。

(2)非堆内存参数

-XX:MetaspaceSize:元空间初始大小(如256m),替代JDK8前的PermGen;

-XX:MaxMetaspaceSize:元空间最大大小(如512m),避免元空间溢出;

-XX:DirectMemorySize:直接内存大小(如1g),NIO操作会使用直接内存,需避免溢出。

(3)GC日志参数(必须开启)

-XX:+PrintGCDetails:打印GC详细日志;

-XX:+PrintGCTimeStamps:打印GC时间戳;

-XX:+PrintHeapAtGC:打印GC前后堆内存变化;

-Xloggc:./gc.log:GC日志输出路径。

2.2 主流GC收集器调优配置

JDK8默认GC收集器为Parallel Scavenge(新生代)+Parallel Old(老年代),高并发场景推荐使用G1或ZGC(JDK11+),以下为生产级配置:

(1)G1收集器(JDK8+推荐,适合大堆)
bash 复制代码
# 启用G1收集器
-XX:+UseG1GC
# G1混合回收周期中并行回收线程数
-XX:ParallelGCThreads=8
# 并发标记线程数
-XX:ConcGCThreads=2
# 堆内存占用达到该比例触发混合回收(默认45%)
-XX:InitiatingHeapOccupancyPercent=70
# 每次混合回收的最大时间(默认200ms)
-XX:MaxGCPauseMillis=100
# 禁止新生代提前晋升
-XX:G1PreventiveGCPolicy=region
    
(2)ZGC收集器(JDK11+,超低延迟,适合超大堆)
bash 复制代码
# 启用ZGC收集器
-XX:+UseZGC
# 最大堆大小
-Xmx8g
# ZGC并发线程数
-XX:ZGCThreads=4
# 启用ZGC内存压缩
-XX:+ZGCCompressedOops
    

2.3 实战案例:生产环境JVM配置模板

以"4核8G服务器+高并发接口服务"为例,JVM配置如下(适配JDK8+G1):

bash 复制代码
java -jar -Xms2g -Xmx2g -Xmn512m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:DirectMemorySize=1g \
-XX:+UseG1GC -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 -XX:InitiatingHeapOccupancyPercent=70 \
-XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gc.log \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof your-app.jar

说明:

  • 开启OOM自动dump堆内存,便于问题排查;

  • 线程数配置与CPU核心数匹配,避免上下文切换频繁;

  • 堆大小设置为2g,适配8G服务器(预留系统和其他组件内存)。

2.4 JVM调优避坑指南

  1. 避免设置-Xms≠-Xmx:会导致堆频繁扩容/缩容,增加GC压力;

  2. 避免新生代过大/过小:过大导致老年代变小,频繁Full GC;过小导致Minor GC频繁;

  3. 避免MaxTenuringThreshold设置过大:导致新生代对象长期存活,占用 Survivor 区;

  4. 禁止开启-XX:+UseConcMarkSweepGC(CMS):JDK9已废弃,高并发下容易出现卡顿;

  5. 必须开启GC日志:无日志无法定位内存问题,生产环境禁用-XX:+DisableExplicitGC。

3. 线程池配置优化与并发控制

线程池是Java并发编程的核心组件,合理配置线程池能避免线程频繁创建/销毁、资源耗尽等问题,高并发场景下需结合业务类型精准设计。

3.1 线程池核心参数设计原理

ThreadPoolExecutor核心参数(7个),决定线程池的工作机制:

  • corePoolSize:核心线程数(常驻线程),即使空闲也不销毁;

  • maximumPoolSize:最大线程数,核心线程满后可创建的最大临时线程;

  • keepAliveTime:临时线程空闲存活时间,超过时间则销毁;

  • unit:keepAliveTime时间单位(如TimeUnit.SECONDS);

  • workQueue:任务队列,核心线程满后存放任务的队列(如LinkedBlockingQueue);

  • threadFactory:线程工厂,用于创建线程(自定义线程名,便于排查);

  • handler:拒绝策略,任务队列满且线程数达最大值时的处理策略。

3.2 不同场景线程池配置方案

线程池配置需区分"CPU密集型"和"IO密集型"任务,核心公式:

  • CPU密集型(如计算、排序):线程数=CPU核心数+1(减少上下文切换);

  • IO密集型(如接口调用、数据库操作):线程数=CPU核心数×2(IO等待时线程可释放CPU)。

(1)CPU密集型线程池配置
java 复制代码
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CpuIntensiveThreadPool {
    // 获取CPU核心数
    private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();
    
    public static ThreadPoolExecutor getInstance() {
        return new ThreadPoolExecutor(
            CPU_CORES + 1,          // 核心线程数
            CPU_CORES + 1,          // 最大线程数(无临时线程)
            0L,                     // 临时线程存活时间(无临时线程,设为0)
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(1024),  // 任务队列(容量1024)
            r -> new Thread(r, "cpu-pool-" + r.hashCode()),  // 自定义线程名
            new ThreadPoolExecutor.AbortPolicy()  // 拒绝策略(默认,抛异常)
        );
    }
}
    
(2)IO密集型线程池配置
java 复制代码
public class IoIntensiveThreadPool {
    private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();
    
    public static ThreadPoolExecutor getInstance() {
        return new ThreadPoolExecutor(
            CPU_CORES * 2,          // 核心线程数
            CPU_CORES * 4,          // 最大线程数
            60L,                    // 临时线程空闲60秒销毁
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(2048),  // 任务队列(容量2048)
            r -> new Thread(r, "io-pool-" + r.hashCode()),
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略(当前线程执行)
        );
    }
}
    

3.3 动态线程池实现(结合Nacos)

固定线程池参数无法适配高并发场景的流量波动,动态线程池可通过配置中心(Nacos)实时调整参数,无需重启服务。

(1)引入依赖(pom.xml)
xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
    
(2)动态线程池配置类
java 复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@RefreshScope  // 开启配置动态刷新
public class DynamicThreadPoolConfig {

    @Value("${thread-pool.core-size:8}")
    private int coreSize;

    @Value("${thread-pool.max-size:16}")
    private int maxSize;

    @Value("${thread-pool.keep-alive-seconds:60}")
    private int keepAliveSeconds;

    @Value("${thread-pool.queue-capacity:2048}")
    private int queueCapacity;

    @Bean
    public ThreadPoolExecutor dynamicThreadPool() {
        return new ThreadPoolExecutor(
            coreSize,
            maxSize,
            keepAliveSeconds,
            java.util.concurrent.TimeUnit.SECONDS,
            new java.util.concurrent.LinkedBlockingQueue<Runnable>(queueCapacity),
            r -> new Thread(r, "dynamic-pool-" + r.hashCode()),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    // 提供参数更新方法
    public void updateThreadPoolParams(int coreSize, int maxSize, int keepAliveSeconds) {
        ThreadPoolExecutor pool = dynamicThreadPool();
        pool.setCorePoolSize(coreSize);
        pool.setMaximumPoolSize(maxSize);
        pool.setKeepAliveTime(keepAliveSeconds, java.util.concurrent.TimeUnit.SECONDS);
    }
}
    
(3)Nacos配置(thread-pool-dev.yaml)
yaml 复制代码
thread-pool:
  core-size: 8
  max-size: 16
  keep-alive-seconds: 60
  queue-capacity: 2048
    

3.4 线程池常见问题与解决方案

(1)任务队列溢出(OOM)

原因:队列容量设置过大,任务堆积过多导致内存溢出;

解决方案:使用有界队列(如LinkedBlockingQueue指定容量),搭配合理的拒绝策略。

(2)线程池耗尽(所有线程阻塞)

原因:IO密集型任务线程数不足,导致线程长期阻塞;

解决方案:增加最大线程数,设置合理的keepAliveTime,避免临时线程过早销毁。

(3)拒绝策略不合理

推荐拒绝策略选型:

  • 核心业务:AbortPolicy(抛异常,快速失败);

  • 非核心业务:CallerRunsPolicy(当前线程执行)或DiscardOldestPolicy(丢弃最旧任务);

  • 禁止使用DiscardPolicy(静默丢弃任务,排查困难)。

(4)线程泄漏

原因:线程内任务长期阻塞(如死锁、无限循环),导致线程无法释放;

解决方案:使用线程监控工具(如Arthas)排查阻塞线程,设置任务超时时间(如Future.get(timeout))。

4. Redis缓存防护:穿透/击穿/雪崩

Redis作为高并发系统的核心缓存组件,能有效减轻数据库压力,但缓存失效场景(穿透、击穿、雪崩)会导致缓存雪崩,甚至数据库宕机,需针对性防护。

4.1 三大缓存问题场景剖析

| 问题类型 | 核心场景 | 危害 |

|----------|----------|------|

| 缓存穿透 | 查询不存在的key(如恶意攻击),缓存未命中,直接穿透到数据库 | 数据库压力暴增,可能宕机 |

| 缓存击穿 | 热点key(高并发查询)过期,大量请求同时穿透到数据库 | 数据库瞬间压力过大,热点接口延迟飙升 |

| 缓存雪崩 | 大量key同时过期,或Redis集群宕机,所有请求穿透到数据库 | 数据库雪崩,系统整体不可用 |

4.2 缓存穿透防护方案(布隆过滤器)

核心思路:提前过滤不存在的key,避免请求穿透到数据库,常用布隆过滤器(Bloom Filter)实现。

(1)布隆过滤器原理

布隆过滤器是一种概率型数据结构,通过多个哈希函数将key映射到bit数组,判断key是否存在(存在误判率,无漏判),适合存储海量key的存在性判断。

(2)Redis布隆过滤器实战

Redis 4.0+支持布隆过滤器插件(RedisBloom),以下为Java代码示例:

java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class BloomFilterManager {
    private RedissonClient redissonClient;
    private RBloomFilter<String> bloomFilter;

    // 初始化Redisson客户端
    @PostConstruct
    public void init() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        redissonClient = Redisson.create(config);
        
        // 初始化布隆过滤器(存储用户ID,预计100万数据,误判率0.01%)
        bloomFilter = redissonClient.getBloomFilter("user:id:bloom");
        bloomFilter.tryInit(1000000, 0.0001);
        
        // 预加载已存在的用户ID到布隆过滤器(实际场景从数据库批量加载)
        // bloomFilter.add("user1001");
        // bloomFilter.add("user1002");
    }

    // 判断key是否存在(存在则返回true,不存在返回false)
    public boolean contains(String key) {
        return bloomFilter.contains(key);
    }

    // 新增key到布隆过滤器
    public void add(String key) {
        bloomFilter.add(key);
    }
}
    
(3)缓存穿透防护流程
java 复制代码
@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private BloomFilterManager bloomFilterManager;
    @Autowired
    private UserMapper userMapper;

    public User getUserById(String userId) {
        // 1. 布隆过滤器判断key是否存在,不存在直接返回null(避免穿透)
        if (!bloomFilterManager.contains(userId)) {
            return null;
        }
        
        // 2. 查询缓存
        String key = "user:info:" + userId;
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }
        
        // 3. 缓存未命中,查询数据库(加互斥锁,避免击穿)
        synchronized (userId.intern()) {
            user = (User) redisTemplate.opsForValue().get(key);
            if (user != null) {
                return user;
            }
            // 数据库查询
            user = userMapper.selectById(userId);
            if (user != null) {
                // 写入缓存(设置过期时间,避免雪崩)
                redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
            } else {
                // 不存在的key写入空缓存(过期时间5分钟,避免重复穿透)
                redisTemplate.opsForValue().set(key, null, 5, TimeUnit.MINUTES);
            }
        }
        return user;
    }
}
    

4.3 缓存击穿/雪崩防护实战

(1)缓存击穿防护(热点key)

方案1:互斥锁(synchronized/Redlock),确保只有一个线程查询数据库,其他线程等待;

方案2:热点key永不过期,通过后台线程定期更新缓存(适合不变或低频更新的热点数据);

方案3:缓存预热,系统启动时将热点key提前加载到缓存。

(2)缓存雪崩防护

方案1:过期时间随机化,避免大量key同时过期(如设置30±5分钟);

java 复制代码
// 随机过期时间(30分钟±5分钟)
int expireTime = 30 * 60 + new Random().nextInt(10 * 60);
redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
    

方案2:Redis集群部署(主从+哨兵/Cluster),避免单点故障;

方案3:缓存降级,Redis宕机时,通过熔断机制(如Sentinel)禁止查询缓存,返回默认值或服务降级提示;

方案4:多级缓存,增加本地缓存(如Caffeine),减少Redis依赖。

4.4 Redis缓存优化配置

生产环境Redis配置(避免缓存性能瓶颈):

yaml 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 100  # 最大连接数
        max-idle: 20     # 最大空闲连接
        min-idle: 5      # 最小空闲连接
        max-wait: 3000ms # 连接等待时间
      shutdown-timeout: 1000ms
    timeout: 3000ms  # 连接超时时间
    database: 0
    # 开启Redis缓存统计(便于监控)
    lettuce:
      cluster:
        refresh:
          adaptive: true
          period: 30000
    cache:
      type: redis
      redis:
        time-to-live: 3600000ms  # 默认过期时间
        cache-null-values: true  # 缓存空值(防穿透)
        use-key-prefix: true     # 使用key前缀

5. 数据库分库分表实战

高并发场景下,单表数据量达到千万级后,查询性能会急剧下降,分库分表是解决数据量大、并发高的核心方案,通过拆分数据提升数据库吞吐量。

5.1 分库分表核心原理与场景

(1)核心原理

分库:按业务或哈希规则将数据库拆分到多个实例(如用户库拆分为user_db1、user_db2),减轻单库压力;

分表:将单张大数据表拆分为多个小表(如order表拆分为order_1、order_2),提升单表查询速度。

(2)适用场景
  • 单表数据量>1000万;

  • 数据库CPU/IO使用率持续>80%;

  • 业务查询以单表为主,跨表查询较少。

5.2 分片策略选型(垂直/水平)

(1)垂直分片(按业务拆分)

规则:按业务模块拆分(如电商系统拆分为用户库、订单库、商品库);

优点:拆分简单,符合业务逻辑,便于维护;

缺点:存在跨库查询(如查询订单时关联用户),需通过分布式事务保证一致性。

(2)水平分片(按数据拆分)

规则:按字段哈希/范围拆分(如按用户ID哈希、按订单时间范围);

优点:单表数据量大幅减少,查询性能提升明显;

缺点:拆分规则复杂,需处理跨表分页、分布式事务等问题。

(3)分片键选择原则
  1. 选择查询频繁的字段(如用户ID、订单ID);

  2. 选择分布均匀的字段(避免数据倾斜);

  3. 避免使用频繁更新的字段(如订单状态)。

5.3 Sharding-JDBC实战配置

Sharding-JDBC是轻量级分库分表中间件,无需修改业务代码,通过配置即可实现分库分表,以下为"订单表水平分表"实战案例。

(1)引入依赖(pom.xml)
xml 复制代码
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.16</version>
</dependency>
    
(2)分表配置(application.yml)
yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1  # 数据源名称(分库时配置,本分表案例用单库)
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8
        username: root
        password: root
    rules:
      sharding:
        tables:
          t_order:  # 逻辑表名
            actual-data-nodes: ds0.t_order_${0..1}  # 实际表名(t_order_0、t_order_1)
            table-strategy:  # 分表策略
              standard:
                sharding-column: order_id  # 分片键(订单ID)
                sharding-algorithm-name: order_table_sharding  # 分表算法
        sharding-algorithms:
          order_table_sharding:  # 哈希分表算法
            type: HASH_MOD
            props:
              sharding-count: 2  # 分表数量(2张表)
    props:
      sql-show: true  # 显示分片SQL(便于调试)
  
(3)业务代码(无需修改,直接操作逻辑表)
java 复制代码
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    // 直接操作逻辑表t_order,Sharding-JDBC自动路由到实际表
}

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    public void createOrder(Order order) {
        // 插入数据,自动路由到t_order_0或t_order_1
        orderMapper.insert(order);
    }

    public Order getOrderById(Long orderId) {
        // 查询数据,自动路由到对应分表
        return orderMapper.selectById(orderId);
    }
}
    

说明:通过HASH_MOD算法,订单ID%2=0路由到t_order_0,订单ID%2=1路由到t_order_1,实现水平分表。

5.4 分库分表后问题解决方案

(1)跨表查询问题

解决方案:使用Sharding-JDBC的关联查询功能,或引入Elasticsearch存储分表数据,通过ES实现跨表分页查询。

(2)分布式事务问题

解决方案:集成Seata分布式事务框架,保证分库分表后的数据一致性(参考Seata AT模式)。

(3)数据倾斜问题

原因:分片键选择不当,导致部分分表数据量过大;

解决方案:优化分片键(如按用户ID+时间哈希),或对热点分表进行二次拆分。

(4)主键唯一问题

解决方案:使用分布式ID生成器(如雪花算法、Redis自增),避免分表主键重复。

6. 高并发系统性能监控方案

高并发系统调优后,需建立完善的监控体系,实时感知性能瓶颈和故障,核心监控维度:

  1. JVM监控:堆内存、非堆内存、GC频率、GC停顿时间(Prometheus+Grafana);

  2. 线程池监控:活跃线程数、任务队列长度、拒绝任务数(自定义监控指标+Actuator);

  3. 缓存监控:Redis命中率、连接数、内存使用率、缓存失效数;

  4. 数据库监控:慢查询、连接池状态、分库分表路由耗时;

  5. 全链路监控:SkyWalking追踪接口调用链路,定位慢接口和故障节点。

7. 总结与进阶方向

本文汇总Java高并发系统核心性能调优方案,从JVM参数调优、线程池配置、Redis缓存防护到数据库分库分表,覆盖高并发场景的核心瓶颈与解决方案,核心要点:

  • JVM调优:聚焦堆内存和GC收集器,避免OOM和频繁GC;

  • 线程池调优:区分CPU/IO密集型任务,动态调整参数,避免线程泄漏;

  • Redis防护:布隆过滤器防穿透,互斥锁防击穿,随机过期防雪崩;

  • 分库分表:优先垂直分片,合理选择分片键,用Sharding-JDBC快速落地。

进阶学习方向

  1. 分布式锁:Redis/ZooKeeper分布式锁实现,解决分布式环境下的并发安全问题;

  2. 异步编程:CompletableFuture、Netty异步模型,提升系统吞吐量;

  3. 服务治理:Sentinel流量控制、熔断降级,避免级联故障;

  4. 容器化部署:Docker+K8s部署高并发服务,实现弹性扩容。

相关推荐
星火开发设计1 小时前
关联式容器:map 与 multimap 的键值对存储
java·开发语言·数据结构·c++·算法
王德印2 小时前
工作踩坑之导入数据库报错:Got a packet bigger than ‘max_allowed_packet‘ bytes
java·数据库·后端·mysql·云原生·运维开发
那起舞的日子2 小时前
卡拉兹函数
java·算法
Stringzhua2 小时前
队列-双端队列【Queue2】
java·数据结构·算法·队列
好学且牛逼的马2 小时前
从伦敦地铁到云原生:Spring Cloud 发展史与核心知识点详解
java
好家伙VCC2 小时前
# IndexedDB实战进阶:从基础操作到高性能缓存架构设计在现代前端开发中,**IndexedDB** 作为浏览器端的持
java
夕除2 小时前
js--21
java·python·算法
追随者永远是胜利者2 小时前
(LeetCode-Hot100)21. 合并两个有序链表
java·算法·leetcode·链表·go
重生之后端学习2 小时前
994. 腐烂的橘子
java·开发语言·数据结构·后端·算法·深度优先