JUC 在实际业务场景的落地实践

JUC 在实际业务场景的落地实践

一、电商秒杀场景(核心:高并发库存扣减 + 限流)

痛点:

  • 秒杀瞬间 QPS 达 10 万 +,库存扣减需线程安全且无阻塞;
  • 需限制并发请求数,避免系统被打垮;
  • 防止超卖、库存负数。

选型:

需求点 选型组件 原因
高并发库存计数 LongAdder 分段 CAS 累加,高并发下吞吐量比 AtomicLong 高 10 倍以上,避免计数瓶颈
接口限流(防冲垮) Semaphore 控制同时进入秒杀的请求数,超出则直接拒绝
异步处理订单 ThreadPoolExecutor 库存扣减成功后,异步提交订单处理任务,提升响应速度

流程:

获取失败/超时 获取中断(InterruptedException) 获取成功 商品不存在 商品存在 库存 ≤ 0 库存 > 0 扣减失败 扣减成功 用户发起秒杀请求
seckill(goodsId, userId) 尝试获取限流许可
Semaphore.tryAcquire(50ms) 返回:活动太火爆,请稍后重试 线程中断,返回:请求被中断 校验商品是否存在
stockMap.get(goodsId) 返回:商品不存在 获取当前库存
stock.sum() 返回:没有库存了 CAS 原子扣减库存
stock.compareAndSet(当前库存, 库存-1) 返回:库存被抢光了,手慢无 异步提交订单处理任务
orderExecutor.submit() 生成秒杀订单
createSeckillOrder() 发送秒杀成功短信
sendSeckillSuccessMsg() 返回:秒杀成功,订单正在生成 释放限流许可
Semaphore.release() 请求结束 订单业务完成
入库/扣积分/锁库存 短信发送完成

代码:

java 复制代码
@Component
public class SeckillService {
    // 秒杀商品库存(LongAdder 适配超高并发)
    private final Map<Long, LongAdder> stockMap = new ConcurrentHashMap<>();
    // 秒杀限流信号量(最多 2000 个并发请求)
    private final Semaphore seckillSemaphore = new Semaphore(2000);
    // 订单处理线程池(手动创建,避免 OOM)
    private final ExecutorService orderExecutor = new ThreadPoolExecutor(
            10, 20, 60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10000),
            new ThreadFactory() {
                private final AtomicInteger count = new AtomicInteger(1);
                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "seckill-order-" + count.getAndIncrement());
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者执行,避免任务丢失
    );

    // 初始化秒杀商品库存
    @PostConstruct
    public void initSeckillStock() {
        LongAdder phoneStock = new LongAdder();
        phoneStock.add(1000); // 秒杀手机库存 1000 台
        stockMap.put(1001L, phoneStock); // 商品 ID:1001
    }

    // 秒杀核心接口
    public Result<String> seckill(Long goodsId, Long userId) {
        // 1. 尝试获取限流许可(超时 50ms 则拒绝)
        try {
            if (!seckillSemaphore.tryAcquire(50, TimeUnit.MILLISECONDS)) {
                return Result.fail("活动太火爆,请稍后重试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Result.fail("请求被中断");
        }

        try {
            // 2. 校验商品是否存在
            LongAdder stock = stockMap.get(goodsId);
            if (stock == null) {
                return Result.fail("商品不存在");
            }

            // 3. 原子扣减库存(sumThenGet:先求和再减 1,避免并发问题)
            long currentStock = stock.sum();
            if (currentStock <= 0) {
                return Result.fail("没有库存了");
            }
            // CAS 思想:确保扣减时库存未被修改
            boolean success = stock.compareAndSet(currentStock, currentStock - 1);
            if (!success) {
                return Result.fail("库存被抢光了,手慢无");
            }

            // 4. 异步处理订单(扣减成功后,异步生成订单、扣减积分等)
            orderExecutor.submit(() -> {
                createSeckillOrder(goodsId, userId); // 生成秒杀订单
                sendSeckillSuccessMsg(userId); // 发送秒杀成功短信
            });

            return Result.success("秒杀成功,订单正在生成");
        } finally {
            // 5. 释放限流许可(必须在 finally 中释放)
            seckillSemaphore.release();
        }
    }

    // 生成秒杀订单(业务逻辑)
    private void createSeckillOrder(Long goodsId, Long userId) {
        // 订单入库、扣减用户积分、锁定库存等逻辑
        log.info("生成秒杀订单:商品{},用户{}", goodsId, userId);
    }

    // 发送秒杀成功短信(业务逻辑)
    private void sendSeckillSuccessMsg(Long userId) {
        log.info("给用户{}发送秒杀成功短信", userId);
    }
}

注意:

  1. Semaphore 许可必须释放:无论秒杀成功 / 失败,都要在 finally 中 release,否则许可会被耗尽,后续请求全部被拒;
  2. 库存扣减需原子性:不能先 get 再 set(会导致超卖),必须用 LongAdder 的 CAS 方法;
  3. 线程池拒绝策略选型:秒杀场景用 CallerRunsPolicy,让主线程执行任务,避免订单丢失(比丢弃策略更友好)。

二、支付回调场景(核心:异步处理 + 顺序执行)

痛点:

  • 支付平台(微信 / 支付宝)会多次推送回调通知,需保证订单状态更新的顺序性;
  • 回调处理需异步执行,避免阻塞支付平台响应;
  • 防止重复处理同一回调(如多次推送同一支付成功通知)。

选型:

需求点 选型组件 原因
异步处理回调 ThreadPoolExecutor 异步处理回调逻辑,快速响应支付平台(要求 1 秒内返回)
订单状态顺序更新 newSingleThreadExecutor 单个订单的回调用单线程串行执行,避免并发修改订单状态(如 "支付中" 和 "支付成功" 并发)
防止重复处理 ConcurrentHashMap 缓存已处理的回调单号,原子判断是否已处理

流程:

已处理(返回非null) 未处理(返回null) 是 否 是 否 支付平台推送回调
POST /pay/callback Controller 接收回调参数
handlePayCallback(callbackDTO) 异步提交核心处理任务
coreExecutor.submit(processCallback) 立即返回 SUCCESS
快速响应支付平台 请求响应完成 处理回调核心逻辑
processCallback(callbackDTO) 获取回调单号/订单ID/支付状态 防重校验
processedCallbackMap.putIfAbsent(callbackNo) 日志记录:回调单号已处理,忽略 获取订单专属单线程池
orderCallbackExecutorMap.computeIfAbsent 提交订单状态更新任务
orderExecutor.submit() 更新订单状态
updateOrderStatus(orderId, tradeStatus) 支付状态是否为 SUCCESS? 触发支付成功业务
triggerOrderSuccessBiz(orderId) 订单状态更新完成(无后续业务) 订单业务处理完成 执行过程抛出异常 日志记录:处理订单回调失败 移除已处理标记
processedCallbackMap.remove(callbackNo)(允许重试) 异常处理完成 订单是否完成? 销毁订单专属线程池
destroyOrderExecutor(orderId) 线程池保留,等待后续回调 移除并关闭线程池,释放资源 回调处理全流程结束

代码:

java 复制代码
@Component
public class PayCallbackService {
    // 已处理的回调单号(防重)
    private final ConcurrentHashMap<String, Boolean> processedCallbackMap = new ConcurrentHashMap<>();
    // 订单回调处理线程池(每个订单一个单线程池,保证顺序)
    private final ConcurrentHashMap<Long, ExecutorService> orderCallbackExecutorMap = new ConcurrentHashMap<>();
    // 核心线程池(用于创建订单专属线程池)
    private final ExecutorService coreExecutor = Executors.newFixedThreadPool(10);

    // 支付回调入口
    @PostMapping("/pay/callback")
    public String handlePayCallback(@RequestBody PayCallbackDTO callbackDTO) {
        // 1. 快速响应支付平台(异步处理核心逻辑)
        coreExecutor.submit(() -> processCallback(callbackDTO));
        return "SUCCESS"; // 必须快速返回,否则支付平台会重试

    }

    // 处理回调核心逻辑
    private void processCallback(PayCallbackDTO callbackDTO) {
        String callbackNo = callbackDTO.getCallbackNo(); // 回调唯一单号
        Long orderId = callbackDTO.getOrderId(); // 订单 ID
        String tradeStatus = callbackDTO.getTradeStatus(); // 支付状态:SUCCESS/FAIL

        // 2. 防重处理(原子操作,避免重复处理)
        if (processedCallbackMap.putIfAbsent(callbackNo, Boolean.TRUE) != null) {
            log.info("回调单号{}已处理,忽略", callbackNo);
            return;
        }

        // 3. 获取订单专属单线程池(保证同一订单的回调顺序执行)
        ExecutorService orderExecutor = orderCallbackExecutorMap.computeIfAbsent(orderId, k -> 
                Executors.newSingleThreadExecutor(r -> new Thread(r, "pay-callback-" + k)));

        // 4. 提交订单状态更新任务(串行执行)
        orderExecutor.submit(() -> {
            try {
                // 更新订单状态(串行执行,避免并发问题)
                updateOrderStatus(orderId, tradeStatus);
                // 触发后续业务(如发货、积分发放)
                if ("SUCCESS".equals(tradeStatus)) {
                    triggerOrderSuccessBiz(orderId);
                }
            } catch (Exception e) {
                log.error("处理订单{}回调失败", orderId, e);
                // 异常时移除已处理标记,允许重试
                processedCallbackMap.remove(callbackNo);
            }
        });
    }

    // 更新订单状态(业务逻辑)
    private void updateOrderStatus(Long orderId, String tradeStatus) {
        log.info("更新订单{}状态为{}", orderId, tradeStatus);
        // 订单状态入库逻辑(如:待支付→支付成功)
    }

    // 订单支付成功后触发的业务(业务逻辑)
    private void triggerOrderSuccessBiz(Long orderId) {
        log.info("订单{}支付成功,触发发货、积分发放", orderId);
        // 发货通知、用户积分增加、商家结算等逻辑
    }

    // 销毁订单线程池(订单完成后清理资源)
    public void destroyOrderExecutor(Long orderId) {
        ExecutorService executor = orderCallbackExecutorMap.remove(orderId);
        if (executor != null) {
            executor.shutdown();
        }
    }
}

注意:

  1. 单线程池需清理:订单完成后要销毁专属线程池,否则线程数会随订单量增长导致资源耗尽;
  2. 防重标记需容错:处理失败时要移除已处理标记,允许支付平台重试;
  3. 快速响应支付平台:回调接口必须异步处理,1 秒内返回 SUCCESS,否则支付平台会多次重试。

三、物流轨迹同步场景(核心:定时任务 + 阻塞队列)

痛点:

  • 需定时从物流平台拉取轨迹数据(如每 5 分钟拉取一次);
  • 拉取的数据量可能很大,需异步处理入库;
  • 避免拉取任务和处理任务的耦合,防止处理慢导致拉取阻塞。

选型:

需求点 选型组件 原因
定时拉取轨迹 ScheduledThreadPoolExecutor 支持周期性任务,多线程执行,避免单线程阻塞
轨迹数据缓冲 LinkedBlockingQueue 阻塞队列缓冲拉取的数据,解耦拉取和处理流程
异步处理入库 ThreadPoolExecutor 多线程处理轨迹入库,提升处理效率

流程:

是 否 是 否 是 否 是 否 服务初始化
@PostConstruct 启动定时拉取任务
pullExecutor.scheduleAtFixedRate 启动5个轨迹处理消费者
processExecutor.submit(processTrack) 延迟1分钟启动,每5分钟执行一次
pullLogisticsTrack() 调用物流平台API拉取轨迹数据
logisticsApi.pullTrack() 拉取是否异常? 日志记录:拉取物流轨迹失败 日志记录:拉取到N条轨迹数据 遍历轨迹数据,放入阻塞队列
trackQueue.put(track) 队列是否已满? 阻塞等待,直到队列有空闲位置 数据成功入队 拉取任务本轮结束,等待下一轮调度 消费者线程循环执行
!Thread.currentThread().isInterrupted() 从阻塞队列取数据
trackQueue.take() 队列是否为空? 阻塞等待,直到队列有数据 获取轨迹数据LogisticsTrackDTO 轨迹数据入库
saveTrackToDb(track) 更新订单物流状态
updateOrderTrackStatus() 处理是否异常? 日志记录:处理物流轨迹失败 处理完成,回到循环 线程被中断(InterruptedException) 标记线程中断,退出循环 服务销毁
@PreDestroy 关闭拉取线程池
pullExecutor.shutdown() 关闭处理线程池
processExecutor.shutdown() 消费者线程终止 拉取任务停止调度 物流轨迹同步服务停止

代码:

java 复制代码
@Component
public class LogisticsTrackService {
    // 轨迹数据缓冲队列(容量 10000,避免内存溢出)
    private final BlockingQueue<LogisticsTrackDTO> trackQueue = new LinkedBlockingQueue<>(10000);
    // 定时拉取线程池(核心线程数 2)
    private final ScheduledExecutorService pullExecutor = Executors.newScheduledThreadPool(2);
    // 轨迹处理线程池(核心线程数 5)
    private final ExecutorService processExecutor = new ThreadPoolExecutor(
            5, 10, 60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(5000),
            new ThreadFactory() {
                private final AtomicInteger count = new AtomicInteger(1);
                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "logistics-process-" + count.getAndIncrement());
                }
            },
            new ThreadPoolExecutor.DiscardOldestPolicy() // 丢弃最老任务,保证最新数据
    );

    // 启动定时拉取任务
    @PostConstruct
    public void startPullTask() {
        // 每 5 分钟拉取一次物流轨迹(延迟 1 分钟启动)
        pullExecutor.scheduleAtFixedRate(this::pullLogisticsTrack,
                1, 5, TimeUnit.MINUTES);

        // 启动轨迹处理线程(5 个消费者)
        for (int i = 0; i < 5; i++) {
            processExecutor.submit(this::processTrack);
        }
    }

    // 拉取物流轨迹(生产者)
    private void pullLogisticsTrack() {
        try {
            // 调用物流平台 API 拉取轨迹数据
            List<LogisticsTrackDTO> trackList = logisticsApi.pullTrack();
            log.info("拉取到{}条物流轨迹数据", trackList.size());

            // 将数据放入阻塞队列(队列满则阻塞,避免数据丢失)
            for (LogisticsTrackDTO track : trackList) {
                trackQueue.put(track);
            }
        } catch (Exception e) {
            log.error("拉取物流轨迹失败", e);
        }
    }

    // 处理物流轨迹(消费者)
    private void processTrack() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 从队列取数据(队列空则阻塞)
                LogisticsTrackDTO track = trackQueue.take();
                // 轨迹数据入库
                saveTrackToDb(track);
                // 更新订单物流状态
                updateOrderTrackStatus(track.getOrderId(), track);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                log.error("处理物流轨迹失败", e);
            }
        }
    }

    // 轨迹入库(业务逻辑)
    private void saveTrackToDb(LogisticsTrackDTO track) {
        log.info("轨迹入库:订单{},轨迹{}", track.getOrderId(), track.getTrackDesc());
        // 轨迹数据入库逻辑
    }

    // 更新订单物流状态(业务逻辑)
    private void updateOrderTrackStatus(Long orderId, LogisticsTrackDTO track) {
        log.info("更新订单{}物流状态为{}", orderId, track.getTrackStatus());
        // 订单物流状态更新逻辑
    }

    // 关闭线程池
    @PreDestroy
    public void shutdown() {
        pullExecutor.shutdown();
        processExecutor.shutdown();
    }
}

注意:

  1. 阻塞队列容量要限制:避免无界队列导致内存溢出,根据业务量设置合理容量;
  2. 消费者线程需异常处理:捕获异常避免消费者线程挂掉,保证消费能力;
  3. 定时任务异常隔离:拉取任务失败时,不影响下一次执行(ScheduledThreadPoolExecutor 会自动重试)。

四、后台批量导出场景(核心:线程池 + 进度统计)

痛点:

  • 后台导出大量数据(如 100 万条订单),需分批处理,避免内存溢出;
  • 需实时返回导出进度给前端;
  • 导出任务需异步执行,不阻塞前端请求。

选型:

需求点 选型组件 原因
异步导出任务 ThreadPoolExecutor 分批处理导出任务,控制并发数,避免数据库压力过大
导出进度统计 AtomicLong 原子统计已处理条数,实时返回进度
任务结果存储 ConcurrentHashMap 缓存导出任务的状态和进度,供前端查询

流程:

是 否 前端发起导出请求
调用 startExport(param) 生成唯一任务ID(UUID) 统计导出总条数
countOrderTotal(param) 初始化导出进度对象
ExportProgress(status=PROCESSING) 缓存进度到ConcurrentHashMap
exportProgressMap.put(taskId, progress) 异步提交导出任务
exportExecutor.submit() 立即返回任务ID给前端
前端用于查询进度 前端轮询查询进度
调用 queryExportProgress(taskId) 从缓存获取进度
exportProgressMap.get(taskId) 返回进度信息给前端
包含status/processedCount/progressRate 执行导出核心逻辑
exportOrderData(taskId, param, progress) 计算总分批数
totalPage = ceil(总条数/1000) 循环分批处理
pageNum 从1到totalPage 分页查询订单数据
queryOrderByPage(pageNum, 1000) 写入订单数据到文件
writeOrderToFile(taskId, orderList) 原子更新已处理条数
processedCount.addAndGet(条数) 计算进度百分比
progressRate = 已处理/总条数*100 所有批次处理完成? 更新进度状态为SUCCESS
progress.setStatus(SUCCESS) 执行过程抛出异常 日志记录:导出失败(任务ID+异常信息) 更新进度状态为FAIL
设置errorMsg 导出任务完成 线程池队列满/线程数达上限 触发AbortPolicy拒绝策略 返回导出繁忙提示给前端 请求结束

代码:

java 复制代码
@Component
public class OrderExportService {
    // 导出任务状态缓存(key:任务 ID,value:导出进度)
    private final ConcurrentHashMap<String, ExportProgress> exportProgressMap = new ConcurrentHashMap<>();
    // 导出线程池(核心线程数 3,避免数据库连接耗尽)
    private final ExecutorService exportExecutor = new ThreadPoolExecutor(
            3, 5, 60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadFactory() {
                private final AtomicInteger count = new AtomicInteger(1);
                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "order-export-" + count.getAndIncrement());
                }
            },
            new ThreadPoolExecutor.AbortPolicy() // 拒绝新任务,返回导出繁忙
    );

    // 启动导出任务
    public String startExport(ExportParam param) {
        // 生成唯一任务 ID
        String taskId = UUID.randomUUID().toString();
        // 初始化导出进度
        ExportProgress progress = new ExportProgress();
        progress.setTaskId(taskId);
        progress.setStatus("PROCESSING");
        progress.setTotalCount(countOrderTotal(param)); // 统计总条数
        exportProgressMap.put(taskId, progress);

        // 异步执行导出任务
        exportExecutor.submit(() -> {
            try {
                exportOrderData(taskId, param, progress);
                progress.setStatus("SUCCESS");
            } catch (Exception e) {
                log.error("导出订单失败,任务ID:{}", taskId, e);
                progress.setStatus("FAIL");
                progress.setErrorMsg(e.getMessage());
            }
        });

        return taskId;
    }

    // 导出订单数据(分批处理)
    private void exportOrderData(String taskId, ExportParam param, ExportProgress progress) {
        int pageSize = 1000; // 每批处理 1000 条
        int totalPage = (int) Math.ceil((double) progress.getTotalCount() / pageSize);

        // 分批查询并写入文件
        for (int pageNum = 1; pageNum <= totalPage; pageNum++) {
            // 查询当前批次数据
            List<OrderDTO> orderList = queryOrderByPage(param, pageNum, pageSize);
            // 写入文件(如 CSV/Excel)
            writeOrderToFile(taskId, orderList);
            // 更新进度(原子操作)
            progress.getProcessedCount().addAndGet(orderList.size());
            // 计算进度百分比
            progress.setProgressRate((double) progress.getProcessedCount().get() / progress.getTotalCount() * 100);
        }
    }

    // 查询导出进度(供前端调用)
    public ExportProgress queryExportProgress(String taskId) {
        return exportProgressMap.get(taskId);
    }

    // 统计订单总数(业务逻辑)
    private long countOrderTotal(ExportParam param) {
        // 数据库 count 统计逻辑
        return 1000000L; // 模拟 100 万条
    }

    // 分页查询订单(业务逻辑)
    private List<OrderDTO> queryOrderByPage(ExportParam param, int pageNum, int pageSize) {
        // 数据库分页查询逻辑
        return new ArrayList<>(); // 模拟数据
    }

    // 写入订单到文件(业务逻辑)
    private void writeOrderToFile(String taskId, List<OrderDTO> orderList) {
        // 文件写入逻辑(如写入服务器临时目录)
        log.info("任务{}写入{}条订单数据", taskId, orderList.size());
    }

    // 导出进度实体
    @Data
    public static class ExportProgress {
        private String taskId;
        private String status; // PROCESSING/SUCCESS/FAIL
        private long totalCount; // 总条数
        private AtomicLong processedCount = new AtomicLong(0); // 已处理条数
        private double progressRate; // 进度百分比
        private String errorMsg; // 错误信息
    }
}

注意:

  1. 分批处理数据:避免一次性查询 100 万条数据导致内存溢出,每批 1000-5000 条为宜;
  2. 进度统计原子化:用 AtomicLong 统计已处理条数,避免并发更新进度导致数据错误;
  3. 线程池核心数适配数据库连接:导出任务依赖数据库查询,核心线程数不能超过数据库连接池大小,避免连接耗尽。

五、避坑小指南

  1. 线程池不要用 Executors 一键创建:
    • newFixedThreadPool/newSingleThreadExecutor 用无界队列,任务堆积会导致 OOM;
    • newCachedThreadPool 核心线程数 0,高并发下会创建大量线程导致 CPU 100%;
    • 推荐手动创建 ThreadPoolExecutor,指定有界队列和合理的拒绝策略。
  2. 锁 / 许可必须手动释放:
    • ReentrantLock 必须在 finally 中 unlock;
    • Semaphore 必须在 finally 中 release;
    • 否则会导致死锁 / 许可耗尽。
  3. 并发容器慎用迭代器:
    • ConcurrentHashMap 迭代器是弱一致性的,不保证实时性;
    • CopyOnWriteArrayList 迭代时不会抛 ConcurrentModificationException,但迭代的是快照数据。
  4. 避免线程池任务无限堆积:
    • 有界队列 + 合理的拒绝策略(如 CallerRunsPolicy/DiscardOldestPolicy);
    • 监控线程池的队列长度,超过阈值时告警。
  5. 异步任务需考虑异常处理:
    • 线程池任务的异常不会主动抛出,需在任务内部捕获并记录日志;
    • 用 CompletableFuture 时,需用 exceptionally 处理异常。

六、总结

JUC 在实际业务中的落地核心是:

先拆解业务痛点(并发量、读写比例、是否异步、是否需要顺序执行),再匹配组件的核心特性,最后做好资源管控和异常处理

高并发计数 → LongAdder/Atomic 类;

限流 / 资源控制 → Semaphore;

异步任务 → 手动创建 ThreadPoolExecutor;

顺序执行 → newSingleThreadExecutor;

生产者消费者 → BlockingQueue;

进度统计 → AtomicLong/ConcurrentHashMap。

相关推荐
tryxr2 小时前
线程安全的类 ≠ 线程安全的程序
java·开发语言·vector·线程安全
superman超哥2 小时前
仓颉语言中错误恢复策略的深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
rchmin2 小时前
Java内存模型(JMM)详解
java·开发语言
studytosky2 小时前
Linux系统编程:深度解析 Linux 进程,从底层架构到内存模型
linux·运维·服务器·开发语言·架构·vim
陳10302 小时前
C++:string(3)
开发语言·c++
Wpa.wk2 小时前
Tomcat的安装与部署使用 - 说明版
java·开发语言·经验分享·笔记·tomcat
吧啦蹦吧2 小时前
java.lang.Class#isAssignableFrom(Class<?> cls)
java·开发语言
都是蠢货3 小时前
drop delete和truncate的区别?
java·开发语言
搬砖的kk3 小时前
Lycium++ - OpenHarmony PC C/C++ 增强编译框架
c语言·开发语言·c++