Redis+Lua限流的四种算法

在这里插入图片描述

1. 固定窗口(Fixed Window)

原理:
  • 固定窗口算法将时间划分为固定的时间段(窗口),比如 1 秒、1 分钟等。在每个时间段内,允许最多一定数量的请求。如果请求超出配额,则拒绝。
优点:
  • 实现简单,能够快速处理请求限流。
缺点:
  • 在窗口边界处可能出现流量突增的情况(称为“边界效应”),比如两个窗口交界处可能短时间内允许通过的请求数量翻倍。
Lua脚本:
-- KEYS[1]: 限流的键(通常为用户ID或者API)
-- ARGV[1]: 最大允许请求数
-- ARGV[2]: 窗口时间(以秒为单位)

local current = redis.call('GET', KEYS[1])

if current and tonumber(current) >= tonumber(ARGV[1]) then
    return 0  -- 返回0表示超出限流
else
    current = redis.call('INCR', KEYS[1])
    if tonumber(current) == 1 then
        redis.call('EXPIRE', KEYS[1], ARGV[2])  -- 设置窗口时间
    end
    return 1  -- 返回1表示未超限
end
Java模拟限流:
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;


/**
 * 固定窗口lua限流Demo
 *
 * @author strap
 */
public class FixedWindowExample {

    private static final String LIMIT_SCRIPT =
            "-- KEYS[1]: 限流的键(通常为用户ID或者API)\n" +
                    "-- ARGV[1]: 最大允许请求数\n" +
                    "-- ARGV[2]: 窗口时间(以秒为单位)\n" +
                    "\n" +
                    "local current = redis.call('GET', KEYS[1])\n" +
                    "\n" +
                    "if current and tonumber(current) >= tonumber(ARGV[1]) then\n" +
                    "    return 0  -- 返回0表示超出限流\n" +
                    "else\n" +
                    "    current = redis.call('INCR', KEYS[1])\n" +
                    "    if tonumber(current) == 1 then\n" +
                    "        redis.call('EXPIRE', KEYS[1], ARGV[2])  -- 设置窗口时间\n" +
                    "    end\n" +
                    "    return 1  -- 返回1表示未超限\n" +
                    "end";

    @SneakyThrows
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "FixedWindowExample", "10", "5");
                if (Long.valueOf(1).equals(o)) {
                    System.out.println(i + "=============================放行");
                } else {
                    System.out.println(i + "拦截=============================");
                }
            }
        }
    }

}


2. 滑动窗口(Sliding Window)

原理:
  • 滑动窗口改进了固定窗口的“边界效应”问题,它通过更细粒度的时间单位来平滑地控制请求。滑动窗口可以在较短的时间窗口内动态调整请求计数,防止瞬时流量激增。
优点:
  • 能平滑地限制请求,减少流量的突增问题。
缺点:
  • 相对固定窗口来说,滑动窗口实现复杂度更高。
Lua脚本:
-- KEYS[1]: 限流的键(通常为用户ID或者API)
-- ARGV[1]: 时间窗口(秒)
-- ARGV[2]: 最大允许请求数
-- ARGV[3]: 当前时间戳(毫秒)

-- 移除窗口外的请求
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[3] - ARGV[1] * 1000)

local count = redis.call('ZCARD', KEYS[1])

if tonumber(count) >= tonumber(ARGV[2]) then
    return 0  -- 请求被限制
else
    redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])  -- 添加当前请求的时间戳
    redis.call('EXPIRE', KEYS[1], ARGV[1])  -- 设置过期时间
    return 1  -- 请求允许
end
Java模拟限流:
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;


/**
 * 滑动窗口lua限流Demo
 *
 * @author strap
 */
public class SlidingWindowExample {

    private static final String LIMIT_SCRIPT =
            "-- KEYS[1]: 限流的键(通常为用户ID或者API)\n" +
                    "-- ARGV[1]: 时间窗口(秒)\n" +
                    "-- ARGV[2]: 最大允许请求数\n" +
                    "-- ARGV[3]: 当前时间戳(毫秒)\n" +
                    "\n" +
                    "-- 移除窗口外的请求\n" +
                    "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[3] - ARGV[1] * 1000)\n" +
                    "\n" +
                    "local count = redis.call('ZCARD', KEYS[1])\n" +
                    "\n" +
                    "if tonumber(count) >= tonumber(ARGV[2]) then\n" +
                    "    return 0  -- 请求被限制\n" +
                    "else\n" +
                    "    redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])  -- 添加当前请求的时间戳\n" +
                    "    redis.call('EXPIRE', KEYS[1], ARGV[1])  -- 设置过期时间\n" +
                    "    return 1  -- 请求允许\n" +
                    "end";


    @SneakyThrows
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                long now = System.currentTimeMillis();
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "SlidingWindowExample", "5", "10", now + "");
                if (Long.valueOf(1).equals(o)){
                    System.out.println(i + "=============================放行");
                }else {
                    System.out.println(i + "拦截=============================");
                }
            }

        }
    }


}

3. 令牌桶(Token Bucket)

原理:
  • 令牌桶算法以恒定速率向桶中添加令牌。每次请求需要消耗一定数量的令牌,如果桶内有足够的令牌,允许请求通过;否则拒绝请求。令牌可以积累,从而允许短时间内的流量突发。
优点:
  • 允许短时间的流量突发,适用于需要应对高峰流量的场景。
缺点:
  • 如果高峰流量持续时间较长,可能导致后续请求被大量拒绝。
Lua脚本:
-- 当前的键
local key = KEYS[1]
-- 令牌桶的容量
local capacity = tonumber(ARGV[1])
-- 令牌的生成速率(个/秒)
local rate = tonumber(ARGV[2])
-- 当前时间戳(毫秒)
local now = tonumber(ARGV[3])
-- 请求的令牌数量
local requestedTokens = tonumber(ARGV[4])
-- 键的最大生命周期
local expire = math.ceil(capacity / rate)

-- 获取当前桶内的令牌数量,默认为capacity
local currentTokens = tonumber(redis.call('HGET', key, 'currentTokens') or capacity)
-- 获取上次令牌更新的时间
local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or 0)

-- 首次进来初始化令牌数量
if lastUpdate == 0 then
    redis.call('HSET', key, 'last_update', now)
    redis.call('HSET', key, 'currentTokens', currentTokens)
    redis.call('EXPIRE', key, expire)
else
    -- 计算在当前时间段内生成的令牌数量
    local tokensToAdd = math.floor((now - lastUpdate) / 1000 * rate)
    currentTokens = math.min(capacity, currentTokens + tokensToAdd)
end

-- 计算当前是否能提供请求的令牌数量
local isAllow = 0
if currentTokens >= requestedTokens then
    isAllow = 1
    redis.call('HSET', key, 'last_update', now)
    redis.call('HSET', key, 'currentTokens', currentTokens - requestedTokens)
    redis.call('EXPIRE', key, expire)
end

return {isAllow, currentTokens}
Java模拟限流:
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;

import java.util.List;

/**
 * 令牌桶lua限流Demo
 *
 * @author strap
 */
public class TokenBucketExample {

    private static final String LIMIT_SCRIPT = "-- 当前的键\n" +
            "local key = KEYS[1]\n" +
            "-- 令牌桶的容量\n" +
            "local capacity = tonumber(ARGV[1])\n" +
            "-- 令牌的生成速率(个/秒)\n" +
            "local rate = tonumber(ARGV[2])\n" +
            "-- 当前时间戳(毫秒)\n" +
            "local now = tonumber(ARGV[3])\n" +
            "-- 请求的令牌数量\n" +
            "local requestedTokens = tonumber(ARGV[4])\n" +
            "-- 键的最大生命周期\n" +
            "local expire = math.ceil(capacity / rate)\n" +
            "\n" +
            "-- 获取当前桶内的令牌数量,默认为capacity\n" +
            "local currentTokens = tonumber(redis.call('HGET', key, 'currentTokens') or capacity)\n" +
            "-- 获取上次令牌更新的时间\n" +
            "local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or 0)\n" +
            "\n" +
            "-- 首次进来初始化令牌数量\n" +
            "if lastUpdate == 0 then\n" +
            "    redis.call('HSET', key, 'last_update', now)\n" +
            "    redis.call('HSET', key, 'currentTokens', currentTokens)\n" +
            "    redis.call('EXPIRE', key, expire)\n" +
            "else\n" +
            "    -- 计算在当前时间段内生成的令牌数量\n" +
            "    local tokensToAdd = math.floor((now - lastUpdate) / 1000 * rate)\n" +
            "    currentTokens = math.min(capacity, currentTokens + tokensToAdd)\n" +
            "end\n" +
            "\n" +
            "-- 计算当前是否能提供请求的令牌数量\n" +
            "local isAllow = 0\n" +
            "if currentTokens >= requestedTokens then\n" +
            "    isAllow = 1\n" +
            "    redis.call('HSET', key, 'last_update', now)\n" +
            "    redis.call('HSET', key, 'currentTokens', currentTokens - requestedTokens)\n" +
            "    redis.call('EXPIRE', key, expire)\n" +
            "end\n" +
            "\n" +
            "return {isAllow, currentTokens}";

    @SneakyThrows
    public static void main(String[] args) {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            int rate = 1;       // 速率(令牌的生成速率(个/秒))
            int capacity = 10;  // 桶容量(令牌桶的容量)
            int everyTime = 1;  // 每次请求需要拿走的令牌数量
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                long now = System.currentTimeMillis();
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "TokenBucketExample", String.valueOf(capacity), String.valueOf(rate), String.valueOf(now), String.valueOf(everyTime));
                List<Object> resutl = (List)o;
                if (Long.valueOf(1).equals(resutl.get(0))){
                    System.out.println(i + "请求前桶内剩余令牌数:" + resutl.get(1) + "==============================放行");
                }else {
                    System.out.println(i + "请求前桶内剩余令牌数:" + resutl.get(1) + "=拦截=============================");
                }
            }
        }

    }


}

4.漏桶(Leaky Bucket)

原理:
  • 漏桶算法将请求流量放入一个“漏桶”中,桶以固定速率漏水(处理请求)。如果流量超过桶的容量,多余的请求将被拒绝。漏桶严格控制输出速率,因此不会出现流量突发。
优点:
  • 严格限制请求速率,适用于要求平滑流量的场景。
缺点:
  • 不允许流量突发,处理效率可能不及令牌桶。
Lua脚本:
-- 当前的键
local key = KEYS[1]
-- 漏桶的容量
local capacity = tonumber(ARGV[1])
-- 漏水速率(个/秒)
local rate = tonumber(ARGV[2])
-- 当前时间戳
local now = tonumber(ARGV[3])
-- 请求计数(进来的tokens数量)
local requestedTokens = tonumber(ARGV[4])
-- 键的最大生命周期
local expire = math.ceil(capacity / rate)

-- 获取当前漏桶内的令牌数量,默认为0
local currentTokens = tonumber(redis.call('HGET', key, 'tokens') or 0)
-- 获取上次漏桶令牌数量的更新时间
local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or now)
-- 漏桶在当前时间范围内已流出的令牌数
local leaks = math.floor((now - lastUpdate) / 1000 * rate)
-- 重新计算当前漏桶内的令牌数量 math.min(capacity, currentTokens + deltaTokens)
currentTokens = math.max(currentTokens - leaks + requestedTokens, 0)
-- 是否允许通过,默认不允许
local isAllow = 0
if currentTokens <= capacity then
    -- 当前令牌数量还能放进去
    isAllow = 1
    redis.call('HSET', key, 'tokens', currentTokens)
    redis.call('HSET', key, 'last_update', now)
    redis.call('EXPIRE', key, expire);
end
return {isAllow, currentTokens}
Java模拟限流:
package com.strap.common.redis.demo;

import lombok.SneakyThrows;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;

import java.util.List;


/**
 * 漏桶lua限流Demo
 *
 * @author strap
 */
public class LeakyBucketExample {

    private static final String LIMIT_SCRIPT =
            "-- 当前的键\n" +
                    "local key = KEYS[1]\n" +
                    "-- 漏桶的容量\n" +
                    "local capacity = tonumber(ARGV[1])\n" +
                    "-- 漏水速率(个/秒)\n" +
                    "local rate = tonumber(ARGV[2])\n" +
                    "-- 当前时间戳\n" +
                    "local now = tonumber(ARGV[3])\n" +
                    "-- 请求计数(进来的tokens数量)\n" +
                    "local requestedTokens = tonumber(ARGV[4])\n" +
                    "-- 键的最大生命周期\n" +
                    "local expire = math.ceil(capacity / rate)\n" +
                    "\n" +
                    "-- 获取当前漏桶内的令牌数量,默认为0\n" +
                    "local currentTokens = tonumber(redis.call('HGET', key, 'tokens') or 0)\n" +
                    "-- 获取上次漏桶令牌数量的更新时间\n" +
                    "local lastUpdate = tonumber(redis.call('HGET', key, 'last_update') or now)\n" +
                    "-- 漏桶在当前时间范围内已流出的令牌数\n" +
                    "local leaks = math.floor((now - lastUpdate) / 1000 * rate)\n" +
                    "-- 重新计算当前漏桶内的令牌数量 math.min(capacity, currentTokens + deltaTokens)\n" +
                    "currentTokens = math.max(currentTokens - leaks + requestedTokens, 0)\n" +
                    "-- 是否允许通过,默认不允许\n" +
                    "local isAllow = 0\n" +
                    "if currentTokens <= capacity then\n" +
                    "    -- 当前令牌数量还能放进去\n" +
                    "    isAllow = 1\n" +
                    "    redis.call('HSET', key, 'tokens', currentTokens)\n" +
                    "    redis.call('HSET', key, 'last_update', now)\n" +
                    "    redis.call('EXPIRE', key, expire);\n" +
                    "end\n" +
                    "return {isAllow, currentTokens}";


    @SneakyThrows
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(true);
        try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
            int rate = 1;       // 速率(每秒恒定流出的token数量)
            int capacity = 10;  // 桶容量(可存放token数量)
            int everyTime = 1;  // 每次进来的token数量
            for (int i = 0; i < 100; i++) {
                Thread.sleep(100);
                long now = System.currentTimeMillis();
                Object o = jedis.eval(LIMIT_SCRIPT, 1, "LeakyBucketExample", String.valueOf(capacity), String.valueOf(rate), String.valueOf(now), String.valueOf(everyTime));
                List<Object> resutl = (List)o;
                if (Long.valueOf(1).equals(resutl.get(0))){
                    System.out.println(i + "当前桶内令牌数:" + resutl.get(1) + "==============================放行");
                }else {
                    System.out.println(i + "当前桶内令牌数:" + resutl.get(1) + "=拦截=============================");
                }
            }
        }
    }

}

算法对比

算法工作机制优点缺点使用场景
固定窗口固定时间窗口内计数实现简单,快速判断窗口边界可能导致流量突发(边界效应)简单的 API 限流,低要求的场景
滑动窗口滑动时间窗口内计数更精确地控制流量,减少流量突发实现较复杂,较高的性能开销动态限流场景,减少流量突增,如 API 网关
令牌桶令牌以固定速率生成,请求消耗令牌支持流量突发,且易于实现和理解如果高峰流量持续时间过长,会导致后续请求被拒绝适合支持突发流量的场景,如限速下载、API 限流
漏桶固定速率处理请求,严格控制输出流量严格控制流量,平滑输出不允许流量突发严格控制请求速率,如网络流量控制,负载均衡等

总结

  • 固定窗口简单易用,适合对流量要求不高的场景。
  • 滑动窗口平滑控制流量,适合对流量突发有一定需求但又希望平稳控制的场景。
  • 令牌桶允许突发流量,适合需要高效处理短时流量高峰的应用。
  • 漏桶严格控制请求速率,适合对平稳处理请求要求很高的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/902499.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

软工毕设开题建议

文章目录 &#x1f6a9; 1 前言1.1 选题注意事项1.1.1 难度怎么把控&#xff1f;1.1.2 题目名称怎么取&#xff1f; 1.2 开题选题推荐1.2.1 起因1.2.2 核心- 如何避坑(重中之重)1.2.3 怎么办呢&#xff1f; &#x1f6a9;2 选题概览&#x1f6a9; 3 项目概览题目1 : 深度学习社…

文档解析与向量化技术加速 RAG 应用落地

在不久前举办的 AICon 全球人工智能开发与应用大会上&#xff0c;合合信息智能创新事业部研发总监&#xff0c;复旦博士常扬从 RAG 应用落地时常见问题与需求&#xff08;文档解析、检索精度&#xff09;出发&#xff0c;分享了针对性的高精度、高泛化性、多版面多元素识别支持…

Linux系统下串口AT指令控制EC20连接华为云物联网平台

一、前言 在当今万物互联的时代背景下&#xff0c;物联网技术的快速发展极大地推动了智能化社会的构建。作为其中的关键一环&#xff0c;设备与云端平台之间的通信变得尤为重要。本文介绍如何在Linux操作系统环境下&#xff0c;利用串口通信来实现EC20模块与华为云物联网平台的…

Word中Normal.dotm样式模板文件

Normal.dotm文档 首先将自己电脑中C:\Users\自己电脑用户名\AppData\Roaming\Microsoft\Templates路径下的Normal.dotm文件做备份&#xff0c;在下载本文中的Normal.dotm文件&#xff0c;进行替换&#xff0c;重新打开word即可使用。 字体样式如下&#xff08;可自行修改&#…

基于opencv答题卡识别判卷

项目源码获取方式见文章末尾&#xff01; 回复暗号&#xff1a;13&#xff0c;免费获取600多个深度学习项目资料&#xff0c;快来加入社群一起学习吧。 **《------往期经典推荐------》**项目名称 1.【基于DDPG算法的股票量化交易】 2.【卫星图像道路检测DeepLabV3Plus模型】 3…

蓝桥杯 单片机 DS1302和DS18B20

DS1302 时钟 时钟试题 常作为实验室考核内容 控制三个引脚 P17 时钟 P23输入 P13复位 其他已经配置好 寄存器原理 定位地址 0x80地址 固定格式 0x57 5*107*1 57 小时写入格式 不同 首位区分 A上午 P下午 0为24小时制 1为12小时制 写入8小时 0x87 //1000 7 十二小时制 7…

H5的Canvas绘图——使用fabricjs绘制一个可多选的随机9宫格

&#x1f4e2;欢迎点赞 &#xff1a;&#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff0c;赐人玫瑰&#xff0c;手留余香&#xff01;&#x1f4e2;本文作者&#xff1a;由webmote 原创&#x1f4e2;作者格言&#xff1a;新的征程&#xff0c;最近一直被测试…

Django+Vue全栈开发旅游网项目首页

一、前端项目搭建 1、使用脚手架工具搭建项目 2、准备静态资源&#xff08;图片资源&#xff09; 将准备好的静态资源拖至public目录下 3、调整生产项目结构 公共的样式 公共js 首页拆解步骤 ①分析页面结构 标题、轮播图、本周推荐、精选景点、底部导航 ②新建页面组…

VAE中的“变分”什么

写在前面 VAE&#xff08;Variational Autoencoder&#xff09;&#xff0c;中文译为变分自编码器。其中AE&#xff08;Autoencoder&#xff09;很好理解。那“变分”指的是什么呢?—其实是“变分推断”。变分推断主要用在VAE的损失函数中&#xff0c;那变分推断是什么&#x…

第十二部分 Java Stream、File

第十二部分 Java Stream、File 12.1 Java Stream流 12.1.1体验Stream流 案例需求 按照下面的要求完成集合的创建和遍历 创建一个集合&#xff0c;存储多个字符串元素把集合中所有以"张"开头的元素存储到一个新的集合把"张"开头的集合中的长度为3的元素存…

OpenTelemetry 实际应用

介绍 OpenTelemetry“动手”指南适用于想要开始使用 OpenTelemetry 的人。 如果您是 OpenTelemetry 的新手&#xff0c;那么我建议您从OpenTelemetry 启动和运行帖子开始&#xff0c;我在其中详细介绍了 OpenTelemetry。 OpenTelemetry开始改变可观察性格局&#xff0c;它提供…

开挖 Domain - 前奏

WPF App 主机配置 Microsot.Extension.Hosting 一键启动&#xff08;配置文件、依赖注入&#xff0c;日志&#xff09; // App.xaml.cs 中定义 IHost private readonly IHost _host Host.CreateDefaultBuilder().ConfigureAppConfiguration(c > {_ c.SetBasePath(Envi…

JVM(HotSpot):GC之G1垃圾回收器

文章目录 一、简介二、工作原理三、Young Collection 跨代引用四、大对象问题 一、简介 1、适用场景 同时注重吞吐量&#xff08;Throughput&#xff09;和低延迟&#xff08;Low latency&#xff09;&#xff0c;默认的暂停目标是 200 ms超大堆内存&#xff0c;会将堆划分为…

CentOS 7 上安装 MySQL 8.0 教程

&#x1f31f; 你好 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

C#使用log4net结合sqlite数据库记录日志

0 前言 为什么要把日志存到数据库里&#xff1f; 因为结构化的数据库存储的日志信息&#xff0c;可以写专门的软件读取历史日志信息&#xff0c;通过各种条件筛选&#xff0c;可操作性极大增强&#xff0c;有这方面需求的开发人员可以考虑。 为什么选择SQLite&#xff1f; …

node和npm

背景&#xff08;js&#xff09; 1、为什么js能操作DOM和BOM? 原因&#xff1a;每个浏览器都内置了DOM、BOM这样的API函数 2、浏览器中的js运行环境&#xff1f; v8引擎&#xff1a;负责解析和执行js代码 内置API&#xff1a;由运行环境提供的特殊接口&#xff0c;只能在所…

Java面向对象编程高阶(一)

Java面向对象编程高阶&#xff08;一&#xff09; 一、关键字static1、static修饰属性2、静态变量与实例变量的对比3、static修饰方法4、什么时候将属性声明为静态的&#xff1f;5、什么时候将属性声明为静态的&#xff1f;6、代码演示 一、关键字static static用来修饰的结构…

从0到1学习node.js(npm)

文章目录 一、NPM的生产环境与开发环境二、全局安装三、npm安装指定版本的包四、删除包 五、用npm发布一个包六、修改和删除npm包1、修改2、删除 一、NPM的生产环境与开发环境 类型命令补充生产依赖npm i -S uniq-S 等效于 --save -S是默认选项npm i -save uniq包的信息保存在…

首席数据官和首席数据分析官

根据分析人士的预测&#xff0c;首席数据官&#xff08;CDO&#xff09;和首席数据分析官&#xff08;CDAO&#xff09;必须更有效地展示他们对企业和AI项目的价值&#xff0c;以保障其在高管层的地位。Gartner的最新报告指出&#xff0c;CDO和CDAO在AI时代需要重新塑造自身定位…