当前位置: 首页 > news >正文

限流算法 + dfa敏感词过滤算法

社交场景设计

本文我们来做一个小场景:

【注意,本文借鉴内容偏多,引用的内容较多,如果想看原文,可以点击参考里面的链接查看原文】

1.引入

场景一:社交平台实时评论审核

  • 用户在帖子下发表评论,内容需实时审核是否包含敏感词;
  • 为了防止刷屏或恶意评论,需对每个用户或 IP 做限流。

这个时候我们要做一下:

  1. 入口限流
    • 在网关层或前端 API 服务,应用令牌桶(Token Bucket)或漏桶(Leaky Bucket)算法,对单个用户(或单个 IP)做 QPS/TPS 限制。
  2. DFA 过滤
    • 请求通过限流后,进入文本审核模块。
    • 利用 DFA 构建一棵敏感词自动机,对评论文字做单次扫描,时间复杂度 O(n)。
    • 若检测到敏感词,则返回“含违规内容”,同时可记录复审日志。

场景二:评论/私信接口防刷与内容合规

  • 电商平台的商品评论、卖家私信接口,既要防止机器刷单,又要保证留言内容不违规。

  • 接口调用量巨大,对审核时延和吞吐都有严格要求。

还有平时我们发弹幕,发得太快了,系统会提示你 “发得太频繁了”【限流降级一下】,如果你还有违禁词,还会给你变成 星星处理【敏感词处理】。

… 等等等

我相信大家都或多或少遇到过该情况的。那么本文就来探讨一下限流敏感词过滤

2.限流算法

限流,也称流量控制。是指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。限流会导致部分用户请求处理不及时或者被拒,这就影响了用户体验。所以一般需要在系统稳定和用户体验之间平衡一下。

比如说秒杀抢购,保护自身系统和下游系统不被巨型流量冲垮等

①固定窗口限流

将时间切分为等长的固定窗口(如每秒、每分钟),对每个窗口内的请求计数,超过阈值即拒绝。

这种算法实现简单,计数操作开销低;适用于对“平均速率”要求不高的场景。

但是要想一下这种问题,那就是边界问题:窗口边界突刺:如限 100 次/分钟,59 秒内 100 次 + 新窗口开始(第61秒) 100 次,这三秒内瞬间可达 200 次。

在这里插入图片描述

如上图所示,加入每一秒的请求数最多四个的话,在窗口的边界,上图可以看到这六个请求都是被放行的。

public class FixedWindowLimit implements Limit {private final ConcurrentHashMap<String, Pair> resourceMap = new ConcurrentHashMap<>(2);// 默认每个资源每秒访问50次@Overridepublic synchronized boolean isAccess(Resource resource) {long now = System.currentTimeMillis();Pair pair = resourceMap.computeIfAbsent(resource.getName(), k -> new Pair(50, 1000, System.currentTimeMillis()));long start = pair.getWindowStart();int windowSize = pair.getWindowSize();if (now - start >= windowSize) { // 超过窗口时间,重置窗口pair.setInit(now);return true;} else {pair.add();// 超过窗口内最大请求数,拒绝return pair.getCount().get() <= pair.getMaxCount();}}@Dataprivate static class Pair {private int maxCount; // 窗口内最大请求数private int windowSize; // 窗口大小,单位毫秒private Long windowStart; // 窗口开始时间private AtomicInteger count; // 窗口内请求数public Pair( int maxCount, int windowSize, Long windowStart) {this.maxCount = maxCount; this.windowSize = windowSize;this.windowStart = windowStart; this.count = new AtomicInteger(0);}public void add() {this.count.incrementAndGet();}public void setInit( long newTime) {this.count.set(1);this.windowStart = newTime;}}
}

②滑动窗口限流

基本思想

  • 时间窗口动态滑动:将时间划分为多个小的时间片段(如1秒分为10个100ms的格子),窗口随着时间推移向前滑动。
  • 统计窗口内请求数:仅统计当前时间点向前滑动一个完整窗口时长内的请求数量,超出阈值则拒绝请求。

为了平滑限流,将时间窗口动态滑动,通过两个相邻固定窗口的加权计数来近似当前窗口内请求数。

在这里插入图片描述

如上图所示,滑动窗口将窗口划分为了多个小的时间片段,只统计当前窗口内的请求数就可以了,窗口移动的时候,将前面的时间槽丢弃掉。

  • 限流精准:避免固定窗口的边界问题,流量控制更平滑。
  • 灵活调整:时间片粒度可调(越细越精准,但开销越大)。
public class SlidingWindowLimit implements Limit {private static final ConcurrentHashMap<String, Pair> map = new ConcurrentHashMap<>(2);@Overridepublic synchronized boolean isAccess(Resource resource) {long now = System.currentTimeMillis();Pair pair = map.computeIfAbsent(resource.getName(), k -> new Pair(100, 1000, 10));pair.refreshSlot(now);// 窗口内计数器总和超过阈值,则丢弃后续请求if (pair.getSum() >= pair.getMaxCount()) {return false;} else {pair.add();return true;}}@Dataprivate static class Pair {private int maxCount; // 阈值private int windowSize; // 窗口大小[毫秒]private int slotCount; // 窗口内区间个数private int slotSize; // 区间大小[毫秒]   windowSize/slotCountprivate long lastRefreshTime;private Queue<Integer> slots; // 区间计数器队列public Pair( int maxCount, int windowSize, int slotCount) {this.maxCount = maxCount;this.windowSize = windowSize;this.slotCount = slotCount;this.slotSize = windowSize/slotCount;this.slots = new LinkedList<>();for (int i = 0; i < slotCount; i++) {slots.offer(0);}this.lastRefreshTime = System.currentTimeMillis();}public void add() {Integer poll = slots.poll();poll = poll == null ? 0 : poll;slots.offer(poll + 1);}public int getSum() {return this.slots.stream().mapToInt(Integer::intValue).sum();}public void refreshSlot( long now ) {long timePassed  = now - lastRefreshTime; // 已流逝的时间if ( timePassed  >= slotSize ) { // 超过区间时间 -- 说明要移动窗口int goAhead = (int) timePassed / slotSize; // 需要往前移动几个区间int end = Math.min(slotCount, goAhead);for (int i = 0; i < end; ++i ) {// 移除窗口slots.poll();slots.offer(0); // 添加区间}lastRefreshTime += (long) goAhead * slotSize;}}}
}

他有什么缺点呢?

  • 实现复杂:需维护多个时间片的计数器和滑动逻辑。
  • 资源消耗高:时间片越多,内存和计算开销越大。

实际上我们可以借助Redis的zset来辅助实现滑动窗口的限流:

Redis ZSET 特性

  • ZSET 存储请求的时间戳(score)和唯一标识(member)。
  • 通过 ZREMRANGEBYSCORE 删除过期的请求,通过 ZCOUNT 统计当前窗口内的请求数。

主要是下面两个命令:

ZREVRANGEBYSCORE key min max 移除有序集合中给定的分数区间的所有成员

ZCARD key 获取有序集合的成员数

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import java.util.concurrent.TimeUnit;public class SlidingWindowLimiter {private final RedisTemplate<String, String> redisTemplate;private final String keyPrefix;  // 限流Key前缀(如 "rate_limit:api1")private final long windowMillis; // 时间窗口长度(毫秒)private final int maxRequests;  // 窗口内允许的最大请求数public SlidingWindowLimiter(RedisTemplate<String, String> redisTemplate,  String keyPrefix, long windowMillis, int maxRequests) {this.redisTemplate = redisTemplate;this.keyPrefix = keyPrefix;this.windowMillis = windowMillis;this.maxRequests = maxRequests;}/*** 检查是否允许请求* @param userId 用户标识(用于区分不同用户)* @return true: 允许;false: 拒绝*/public boolean allowRequest(String userId) {String key = keyPrefix + ":" + userId;long now = System.currentTimeMillis();long windowStart = now - windowMillis;// Lua脚本保证原子性操作String luaScript = "local key = KEYS[1]\n" +"local now = tonumber(ARGV[1])\n" +"local windowStart = tonumber(ARGV[2])\n" +"local maxRequests = tonumber(ARGV[3])\n" +"local member = ARGV[4]\n" +"\n" +"-- 删除窗口外的旧数据\n" +"redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)\n" +"\n" +"-- 获取当前窗口内的请求总数\n" +"local count = redis.call('ZCARD', key)\n" +"\n" +"-- 判断是否超过阈值\n" +"if count >= maxRequests then\n" +"    return 0\n" +"else\n" +"    -- 添加当前请求\n" +"    redis.call('ZADD', key, now, member)\n" +"    -- 设置Key的过期时间(避免冷数据长期占用内存)\n" +"    redis.call('PEXPIRE', key, windowMillis + 1000)\n" +"    return 1\n" +"end";// 执行Lua脚本Long result = redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),List.of(key),String.valueOf(now),String.valueOf(windowStart),String.valueOf(maxRequests),String.valueOf(now) + ":" + UUID.randomUUID() // 唯一标识);return result != null && result == 1;}
}
/*
(1) 原子性保障
Lua脚本:将 ZREMRANGEBYSCORE(清理旧数据)、ZCARD(统计请求数)、ZADD(记录新请求)合并为原子操作,避免并发问题。
(2) 内存管理
自动过期:通过 PEXPIRE 设置 Key 的过期时间(窗口长度 + 缓冲时间),防止长期不用的 Key 占用内存。
*/

③令牌桶限流

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。【引用的参考链接1中的图片】

在这里插入图片描述

优点

  • 支持突发:令牌在空闲时累积,请求可一次性消耗多枚令牌,应对短时高峰
  • 平均速率可控:长期看,令牌生成速率决定了整体吞吐上限,既可平滑亦可灵活

缺点

  • 需令牌维护:要定期“填充”令牌,并维护当前令牌数状态,带来额外逻辑开销
  • 可能延迟突发处理:如果令牌已被消耗,突发请求仍然会被拒绝或延后处理
public class TokenBucketLimit implements Limit {private static final int MAX_TOKEN = 50; // 每个桶最大50个private static final int FILL_RATE = 5; // 每秒往桶中放5个令牌private final ConcurrentHashMap<String, Pair> limitMap;public TokenBucketLimit() {limitMap = new ConcurrentHashMap<>(2);}@Overridepublic synchronized boolean isAccess(Resource resource) {long now = System.currentTimeMillis();String name = resource.getName();Pair pair = limitMap.get(name);if (pair == null) { // 说明该接口没有被访问过limitMap.put(name, new Pair(now, MAX_TOKEN));return true;} else {// 1.先放令牌addToken(now, pair);// 2.取令牌if (pair.getToken().get() > 0) {pair.getToken().decrementAndGet();return true;}}return false;}private void addToken(long now, Pair pair) {long during = now - pair.lastFillTime;int v = (int) (during * 1.0 / 1000 * FILL_RATE);if (v > 0) {pair.setToken(Math.min(MAX_TOKEN, pair.getToken().get() + v));pair.lastFillTime = now;}}@Data@AllArgsConstructor@NoArgsConstructorprivate static class Pair {private long lastFillTime; // 上一次填充令牌的时间戳private AtomicInteger token; // 令牌桶public Pair(long lastFillTime, int token) {this.lastFillTime = lastFillTime;this.token = new AtomicInteger(token);}public void setToken(int token) {this.token.set(this.token.get() + token);}}
}

④漏桶

漏桶算法是一种经典的**流量整形(Traffic Shaping)限流(Rate Limiting)**算法,通过固定速率处理请求,无论请求的到达速率如何波动,系统的处理速率始终保持恒定,从而平滑突发流量,保护下游系统不被压垮。其核心思想类似于“水桶漏水”——无论水流多快,漏水的速率是固定的。

  1. 漏桶容器
    • 所有请求先进入漏桶队列(桶的容量固定)。
    • 若桶已满,新请求被拒绝(溢出)。
  2. 恒定漏水速率
    • 漏桶以固定速率(如每秒10次)处理队列中的请求,无论请求的到达速率多快。如下图【引用的参考链接1中的图片】

在这里插入图片描述

  • 队列版漏桶

我发现它相较于上面三个限流算法,有一个很不一样的点,那就是中间有一个缓冲区,假设它的容量是k,然后漏水的速率也就是请求放行的速率是v,客户端发送过来的请求速率是v1。如果v1 > v的话,缓冲区就会缓存任务,该请求会存在等待的行为。这就是它区别于上述其他三种限流算法的一点。所以,我认为它严格平滑输出(恒定服务速率),适用于能够接受一定排队延迟的场景。

public class LeakyBucketLimiter {private final Queue<Request> bucket;  // 漏桶队列private final int capacity;           // 漏桶容量private final int leakRate;           // 漏水速率(请求/秒)private final ScheduledExecutorService scheduler;public LeakyBucketLimiter(int capacity, int leakRate) {this.capacity = capacity;this.leakRate = leakRate;this.bucket = new LinkedBlockingQueue<>(capacity);this.scheduler = Executors.newScheduledThreadPool(1);// 启动漏水任务(固定速率处理)scheduler.scheduleAtFixedRate(this::leak, 0, 1000 / leakRate, TimeUnit.MILLISECONDS);}/** 尝试将请求放入漏桶 */public boolean tryAccept(Request request) {if (bucket.size() >= capacity) {return false; // 桶已满,拒绝请求}return bucket.offer(request);}/** 漏水(处理请求) */private void leak() {if (!bucket.isEmpty()) {Request request = bucket.poll();processRequest(request); // 实际处理请求的方法}}private void processRequest(Request request) {// 实际业务逻辑(如调用下游API)System.out.println("处理请求: " + request.getId() + " at " + System.currentTimeMillis());}/** 关闭限流器 */public void shutdown() {scheduler.shutdown();}/** 请求对象示例 */static class Request {private final String id;public Request(String id) { this.id = id; }public String getId() { return id; }}public static void main(String[] args) {LeakyBucketLimiter limiter = new LeakyBucketLimiter(10, 5); // 容量10,速率5请求/秒// 模拟请求for (int i = 0; i < 20; i++) {System.out.println("请求 " + i + " 是否被接受: " + limiter.tryAccept(new Request("req-" + i)));}}
}
  • 计量版漏桶

该版本不维护显式队列,只用一个计数器(桶水位)和固定的泄漏速率来判定请求是否合规:每次到来请求前,先按漏桶速率“泄漏”令水位下降;若水位加上该请求的水量仍不超过桶容量,则通过并累加水量,否则立即拒绝。【但是这个我觉得不是很符合漏桶算法的模型,并且这个很像令牌桶,令牌桶是先放令牌,再拿走令牌;这里是先漏水,再加水】

public class LeakyBucketLimiter2 {// 最大容量(可接受的最大突发量)private final double capacity;// 泄漏速率:每毫秒可泄漏的“水量”private final double leakRatePerMillis;// 当前桶中“水位”private double water;// 上次执行泄漏操作的时间戳(毫秒)private long lastTime;public LeakyBucketMeter(double capacity, double leakRatePerSecond) {this.capacity = capacity;this.leakRatePerMillis = leakRatePerSecond / 1000.0;this.water = 0.0;this.lastTime = System.currentTimeMillis();}public synchronized boolean tryAcquire() {leak(); // 先按速率泄漏旧水if (water + 1.0 <= capacity) {water += 1.0;return true;  // 请求合规}return false;     // 请求超限,直接拒绝}private void leak() {long now = System.currentTimeMillis();double leaked = (now - lastTime) * leakRatePerMillis;// 水位不能低于0water = Math.max(0.0, water - leaked);lastTime = now;}
}

⑤ 其他

单机场景:优先选择Guava RateLimiter(简单)

分布式微服务:推荐Sentinel(功能全面、见后续文章)或Redisson(与Redis深度集成)

网关层防护:Nginx或Spring Cloud Gateway内置限流模块,结合黑白名单策略

3.敏感词过滤

敏感词过滤旨在从用户生成内容(如社交媒体、聊天室、评论区)中识别并移除涉及暴力、色情、政治敏感、隐私泄露等违规词汇或表达。

现在了解到的比较好的方案大致如下:

  • 机器学习与深度学习:结合自然语言处理(NLP)技术分析上下文语义,减少误判(如区分“苹果”作为水果或品牌);

  • DFA/Trie树:利用确定有限状态自动机或字典树提升匹配效率,适用于海量敏感词库;

本文就不考虑深度学习了,直接看第二种,我们来手动实现一个简易的dfa算法。

DFA(Deterministic Finite Automaton,确定有限状态自动机)是一种高效的模式匹配算法,尤其在敏感词过滤、文本分析和词法解析领域应用广泛。它的构建基于状态转移树,每个状态对应敏感词中的一个字符,通过嵌套的哈希表或字典树(Trie树)实现层级关系。

在这里插入图片描述

具体构建步骤:首先初始化根节点,创建一个初始状态(根节点),通常用Map表示,作为所有敏感词匹配的起点;然后逐字符构建状态转移,例如敏感词“王八蛋”,按顺序处理字符“王”→“八”→“蛋”,对每个字符检查当前层级是否存在对应的子节点。若不存在,新建节点并标记字符,若存在则直接跳转到该节点;最后在敏感词的最后一个字符节点上设置结束标志(如isEnd: true),表示匹配完成。如下json表示【词库: ”王八“、”王八蛋“、”王八儿子“、”傻“】

{"王": {"isEnd": false,"八": { "isEnd": true,"蛋": {isEnd: true},"儿":{isEnd: false,"子": {isEnd: true}}}},"傻": { "isEnd": true }
}/*
根节点
├── 王 → 八 →(结束标记)
│          ├── 蛋 →(结束标记)
│          └── 儿 → 子 →(结束标记)
└── 傻 →(结束标记)
*/

从上面结构可以看出,王八蛋和王八儿子,他们是共享的前缀,以此来节省空间。

那么,匹配是怎么做到的呢?如果来了一句话 “张三是个王八蛋”

逐字符扫描与状态跳转

  • 初始状态:从根节点开始扫描文本。
  • 字符“张”“三”“是”“个”:均不在Trie树根节点的子节点中,跳过。
  • 字符“王”:匹配到根节点的子节点“王”,跳转到该节点。
  • 字符“八”:继续匹配到子节点“八”,此时路径“王→八”已构成敏感词“王八”(若需要最短匹配可触发拦截)
  • 字符“蛋”:继续匹配到子节点“蛋”,路径“王→八→蛋”触发敏感词“王八蛋”(最长匹配规则优先)

匹配终止与处理

  • 命中规则:当路径完整匹配敏感词时(如到达“蛋”节点且标记为结束),触发拦截或替换逻辑。
  • 替换示例:将“王八蛋”替换为“*”,输出结果为“张三是个*“

可以看到上述算法,只需要将目标字符串遍历一遍就可以了,时间复杂度是O(n),在我们最开始的场景来说效率还行,反正一条评论、聊天消息或者弹幕的话,字数通常也不会太多。

public class TrieNode {// 子节点(key是下级字符,value是对应的节点)private final Map<Character, TrieNode> subNodes = new HashMap<>();// 是否是关键词的结尾private boolean isKeywordEnd = false;// 添加子节点public void addSubNode(Character c, TrieNode node) {subNodes.put(c, node);}// 获取子节点public TrieNode getSubNode(Character c) {return subNodes.get(c);}// 判断是否是关键词结尾public boolean isKeywordEnd() {return isKeywordEnd;}// 设置关键词结尾public void setKeywordEnd(boolean keywordEnd) {isKeywordEnd = keywordEnd;}
}// 过滤器
public class SensitiveWordFilter {private final TrieNode rootNode = new TrieNode(); // 根节点private static final char FILTER_FLAG = '*';// 0.加载敏感词public void loadSensitiveWords(Set<String> sensitiveWords) {for (String word : sensitiveWords) addWord(word);}// 2.过滤敏感词public String filter(String text) {if (text == null || text.isEmpty()) return text;text = normalizeText(text); // 转换为小写并处理全角字符StringBuilder result = new StringBuilder(text);// 文本长度int length = text.length();// 遍历文本每个字符作为起始位置for (int i = 0; i < length; ++i ) {// 从当前位置开始检查敏感词int sensitiveWordLength = checkSensitiveWord(text, i);if (sensitiveWordLength > 0) {// 替换为相同长度的*for (int j = 0; j < sensitiveWordLength; j++) {result.setCharAt(i + j, FILTER_FLAG);}// 跳过已处理的敏感词长度i = i + sensitiveWordLength - 1;}}return result.toString();}// 1.添加敏感词到Trie树private void addWord(String keyword) {if (keyword == null || keyword.isEmpty()) return;keyword = normalizeText(keyword); // 转换为小写并处理全角字符TrieNode currentNode = rootNode;for (int i = 0; i < keyword.length(); ++i) {char c = keyword.charAt(i);TrieNode node = currentNode.getSubNode(c);if (node == null) {node = new TrieNode();currentNode.addSubNode(c, node);}currentNode = node;// 设置结束标识if (i == keyword.length() - 1) {currentNode.setKeywordEnd(true);}}}// 3.从文本中检查敏感词private int checkSensitiveWord(String text, int beginIndex) {TrieNode currentNode = rootNode;int length = 0;boolean isSensitiveWord = false;// 从beginIndex开始逐字符检查for (int i = beginIndex; i < text.length(); i++) {char c = text.charAt(i);// 过滤空格,支持如"傻 狗"这样的敏感词检测if (c == ' ') {length++;continue;}// 获取下一级节点currentNode = currentNode.getSubNode(c);if (currentNode == null) {// 没有匹配的继续字符,终止检查break;} else {// 匹配到一个字符,长度加1length++;// 如果是关键词结尾,则标记为敏感词if (currentNode.isKeywordEnd()) {isSensitiveWord = true;}}}// 如果不是敏感词,返回0if (!isSensitiveWord) length = 0;return length;}// 将文本转换为小写并处理全角字符private String normalizeText(String text) {StringBuilder sb = new StringBuilder();for (char c : text.toCharArray()) {// 全角转半角if (c >= 65281 && c <= 65374) {c = (char) (c - 65248);}// 统一大小写if (Character.isUpperCase(c)) c = Character.toLowerCase(c);sb.append(c);}return sb.toString();}
}

DFA通过Trie树实现敏感词的高效匹配,其核心在于公共前缀合并和逐字符状态跳转。实际应用中需结合词库动态更新、语义分析等策略提升准确性。现在我们就实现了这一种过滤方法。但是,这肯定是不足够应对我们这些聪明的人类的,首先如果我们的词库里面有**“黄色”,假如说有一条评论是“这个香蕉还没有变成黄色的,不能吃”**,那么上面这个dfa算法就不适用了,所以这就引入了一种上下文语境的问题。

// 测试一下
public class SensitiveTest {public static void main(String[] args) {SensitiveWordFilter filter = new SensitiveWordFilter();filter.loadSensitiveWords(Set.of("傻", "王八", "王八蛋", "王八儿子", "黄色"));String text = "张三是个大王八,真的是服了,这个黄色的香蕉是留给他的";String str = filter.filter(text);System.out.println(str);}
}
// 张三是个大**,真的是服了,这个**的香蕉是留给他的

敏感词过滤识别当然不可能会这么简单,实际场景可能比我们想到的复杂千百倍,除了上下文语境问题,还可能会有方言、同音词替换、用首字母骂人等等。最佳的肯定是通过人工智能与自然语言处理技术实现精准识别,结合动态规则与上下文分析降低误判率。本文仅简单探索一下DFA算法的实现。

end.参考

  1. https://blog.csdn.net/crazymakercircle/article/details/130035504 【CSDN-限流】

相关文章:

  • MATLAB中的Switch语句讲解
  • 基于matlab/simulink锂电池算法学习集合(SOC、SOH、BMS)
  • React Flow 边的基础知识与示例:从基本属性到代码实例详解
  • LLM笔记(九)KV缓存调研
  • std::is_same
  • 5月18总结
  • leetcode报错原因总结需要背下来的程序片 [更新中]
  • 三:操作系统线程管理之线程概念
  • 2025年全国青少年信息素养大赛C++小学全年级初赛试题
  • 逻辑与非逻辑的弥聚
  • 【Linux】第二十章 管理基本存储
  • 双紫擒龙紫紫红指标源码学习,2025升级版紫紫红指标公式-重点技术
  • 基于单片机路灯自动控制仪仿真设计
  • 创建型:工厂方法模式
  • TASK03【Datawhale 组队学习】搭建向量知识库
  • 10.9 LangChain LCEL革命:43%性能提升+声明式语法,AI开发效率飙升实战指南
  • STM32H562----------启动时钟分析
  • Listener method could not be invoked with the incoming message
  • Linux之基础IO
  • 非线性1无修
  • 河南发布高温橙警:郑州、洛阳等地最高气温将达40℃以上
  • “上海-日喀则”援藏入境旅游包机在沪首航
  • 视频丨为救心梗同学缺席职教高考的小伙姜昭鹏完成补考
  • 特写|银耳种植“北移”到沧州盐山,村民入伙可年增收4万元
  • 国际乒联主席索林:洛杉矶奥运会增设混团是里程碑事件
  • 国家卫生健康委通报关于肖某引发舆情事件调查处置进展情况