2024年3月

本文首发于公众号:Hunter后端
原文链接:
在 Windows 上利用Qwen大模型搭建一个 ChatGPT 式的问答小助手

最近 ChatGPT 式的聊天机器人比较火,可以提供各种问答功能,阿里最近推出了 Qwen1.5 系列的大模型,提供了各个参数版本的大模型,其中有一些参数量较小的模型,比较适合我们这种穷* 用于尝试一下手动运行大模型。

今天我们就使用 Qwen1.5 大模型来尝试一下,自己搭建一个问答小助手。

1、配置

首先介绍一下搭建的环境,8g 内存,4g GPU 显存,win10系统,所以如果配置等于或高于我这个环境的也可以轻松实现这一次的搭建过程。

下面是搭建成功后一些问答的效果展示:

其中,因为显存限制,我这边分别使用 Qwem1.5-0.5B-Chat 和 Qwem1.5-1.8B-Chat 进行测试,0.5B 版本占用显存不到 2g,1.8B 版本显存占用不到 4g,这个 B 表示的是模型使用的参数量,在我电脑上 0.5B 的版本推理速度要比 1.8B 的速度要快很多,但是某些问题的准确性没有 1.8B 高。

接下来正式介绍搭建过程。

2、环境安装

使用 Qwen 这个大模型需要用到 CUDA 相关驱动以及几个 Python 库,torch,transformers,accelerate 等。

1. CUDA

首先,确认 Windows 机器上是否有相关驱动,这里我们可以在 cmd 里输入 nvidia-smi 查看相应输出,比如我的输入如下:

然后上张图里截出来的 CUDA Version 去下面这个地址下载 CUDA Toolkit:
https://developer.nvidia.com/cuda-toolkit-archive

到这一步完成,相应的 CUDA 准备工作就 OK 了。

建议可以先看下下面这个链接,里面有完整的安装示意流程:
Windows下CUDA安装

2. conda 环境准备

这里为了方便,我新建了一个 conda 环境,使用的 Python 3.10 版本

conda create -n qwen python=3.10

3. torch 库

为了使用 GPU,torch 库的版本需要是 cuda 版本的,在 Windows 版本下我直接安装其 whl 包,可以在下面的地址找到对应的版本:
https://download.pytorch.org/whl/torch_stable.html。

这里我下载的是文件名是
torch-2.2.1+cu121-cp310-cp310-win_amd64.whl

torch-2.2.1 表示的是 torch 的版本

cu121 表示的是 cuda 版本是 12.1,我们实际的 CUDA Version 是 12.4,没有最新的但是也能兼容

cp310 是 Python 的版本 3.10

win_amd64 则是 Windows 版本。

whl 包比较大,有 2 个多 g,下载后直接到对应的目录下执行下面的操作即可:

pip3 install torch-2.2.1+cu121-cp310-cp310-win_amd64.whl

4. transformers 库

transformers 库是使用大模型的基础库,这里注意下,Qwen1.5 版本的大模型是最近才出来的,所以 transformers 库需要比较新的才能支持,需要 >= 4.37.0

这里我们直接 pip3 install transformers 就会自动为我们安装最新的库,也可以直接指定这个版本。

5. accelerate 库

我在操作的过程中,还需要用到 accelerate 这个库,所以额外安装下:

pip3 install accelerate -i https://mirrors.aliyun.com/pypi/simple/

到这一步,我们的环境就安装好了,我们可以尝试一下是否可以正常使用 CUDA:

import torch
print(torch.cuda.is_available())
# True

输出为 True 则表示可以正常使用 CUDA。

3、下载模型

所有大模型的下载官方都会发布在 huggingface 网站上:
https://huggingface.co/。

我们可以在上面搜索到目前所有发布的大模型,包括 Qwen 系列,百川系列,ChatGLM 系列,Llama 系列等。

我们可以下载下一步执行代码的时候直接指定模型名称,会自动为我们下载,但是我习惯于先将其下载下来,然后在本地指定路径进行调用。

这里我们可以去这两个地址下载对应的文件:

https://huggingface.co/Qwen/Qwen1.5-0.5B-Chat/tree/main

https://huggingface.co/Qwen/Qwen1.5-1.8B-Chat/tree/main

分别是 Qwen1.5 的 0.5B Chat 版本和 1.8B Chat 版本。

其中,最主要的文件是 model.safetensors,这个就是大模型本身,也就是我们运行的时候需要加载的文件,可以看到这两个地址的这个文件分别是 1g 多和 3g 多。

除此之外,还有一些必要的配置文件比如 config.json,一些词表的文件用于加载的时候做映射操作。

注意
:上面的网址可能需要一些魔法操作,如果你没有魔法的途径,可以去魔搭社区找对应的版本,
https://www.modelscope.cn/search?search=Qwen1.5

这里,下载的大模型文件列表如下图所示:

至此,我们所有的准备工作就完成了,接下来我们可以开始写代码进行问答操作了。

4、对话代码

我们需要先加载大模型:

from transformers import AutoModelForCausalLM, AutoTokenizer

device = "cuda" # the device to load the model onto
path = r"F:\\models\\Qwen1.5-0.5B-Chat"

model = AutoModelForCausalLM.from_pretrained(
    path,
    torch_dtype="auto",
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(path)

这里的 path 就是我们下载的大模型的本地文件路径。

接下来下面的代码就是进行对话的操作了:

prompt = "你是谁"
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": prompt}
]
text = tokenizer.apply_chat_template(
    messages,
    tokenize=False,
    add_generation_prompt=True
)
model_inputs = tokenizer([text], return_tensors="pt").to(device)

generated_ids = model.generate(
    model_inputs.input_ids,
    max_new_tokens=512
)
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]

print(response)
# 我是来自阿里云的超大规模语言模型,我叫通义千问。我是一个能够回答问题、创作文字,还能表达观点、撰写代码的 人工智能模型。如果您有任何问题或需要帮助,请随时告诉我,我会尽力提供支持和解答。

1. 封装成函数

我们可以将上面下部分代码封装成函数,这样就可以每次直接调用函数来进行问答操作了:

def get_response(prompt):
    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": prompt}
    ]
    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True
    )
    model_inputs = tokenizer([text], return_tensors="pt").to(device)
    generated_ids = model.generate(
        model_inputs.input_ids,
        max_new_tokens=512,
        pad_token_id=tokenizer.eos_token_id
    )
    generated_ids = [
        output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
    ]
    response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
    print(response)

然后可以直接调用函数进行问答:

get_response("如何学习Python?")

2. 保存历史进行多轮对话

接下来我们可以保存对话历史来进行多轮对话,以下是代码:


def run_qwen_with_history():
    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        # {"role": "user", "content": prompt}
    ]

    while True:
        new_question = input("请输入你的问题:")
        if new_question == "clear":
            messages = [messages[0]]
            continue

        messages.append({"role": "user", "content": new_question})
        text = tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True
        )
        model_inputs = tokenizer([text], return_tensors="pt").to(device)

        generated_ids = model.generate(
            model_inputs.input_ids,
            max_new_tokens=512,
            pad_token_id=tokenizer.eos_token_id
        )
        generated_ids = [
            output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
        ]

        response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
        print(response)
        messages.append({"role": "system", "content": response})

在这里执行这个函数之后,会在命令行里输出
请输入你的问题:
,然后我们可以输入我们的问题,之后可以连续多轮输出,后台会记住我们之前的对话,从而实现多轮对话的功能。

5、总结

经过分别使用 0.5B 版本和 1.8B 的版本,在我电脑的配置里,0.5B 版本的输出会快一些,但是在某些问题回答的质量上不如 1.8B。

而 1.8B 版本答案质量相对较高,但是速度在 4g 显存的情况下,则非常慢。

以上就是本次使用 Qwen1.5 在 Windows 上搭建问答小助手的全过程,之后还可以将大模型提供接口操作,将其应用到 web 页面上,从而实现一个真正的 ChatGPT 式问答助手。

对于以上这些操作是直接使用的大模型,而真正要将其应用于生产,还需要对大模型进行微调,训练等一系列操作,使其更适用于实际场景,这些以后有机会再学习介绍吧。

如果想获取更多后端相关文章,可扫码关注阅读:
image

FastWiki加入
AIDotNet (github.com)
,FastWiki不再属于个人项目,目前FastWiki捐赠给了AIDotNet组织,AIDotNet拥有一下优秀项目:

项目名称 链接 描述
FastWiki https://github.com/AIDotNet/fast-wiki 基于.NET8+React+LobeUI实现的企业级智能客服知识库
AntSK https://github.com/AIDotNet/AntSK 基于.Net8+AntBlazor+SemanticKernel 和KernelMemory 打造的AI知识库/智能体
AIDotNet.API https://github.com/AIDotNet/AIDotNet.API AIDotNet API 提供了大部分的AI模型兼容OpenAI的接口格式,并且将所有模型的实现单独成类库打包成SDK使用,可快速使用入门,也可以使用AIDotNet API的服务部署成独立的AI中转服务, 在AIDotNet API中提供了基本的用户管理和权限管理,并且支持多模型转换,以便提供给服务OpenAI的API风格。

FastWiki 更新内容
v0.1.5

  • 分享对话支持token使用限制
    1. 创建分享对话的时候可以设定这个对话的token使用上限和过期时间。
  • 分享对话支持删除
    1. 创建的分享对话可以直接删除
  • 优化分享对话功能
  • 增加用户权限管理
    • 提供管理员和普通用户权限。

快速入门

下载
docker-compose.yml
脚本

curl https://gitee.com/hejiale010426/fast-wiki/raw/master/docker-compose.yml -O docker-compose.yml

下载完成以后会在当前目录下生成一个
docker-compose.yml
文件,当然默认下载的并不能直接使用,我们使用nano编辑文件的内容:

nano docker-compose.yml

文件大概内容如下 ,如果你有代理的话则修改
OPENAI_CHAT_ENDPOINT

OPENAI_CHAT_EMBEDDING_ENDPOINT
的地址为你的代理地址,格式是
http://ip:端口
即可,然后
OPENAI_CHAT_TOKEN
需要提供您的OpenAI的Key或您的代理的Token,其他的都默认即可,
ASPNETCORE_ENVIRONMENT=Development
则会自动迁移数据库,并且下一个版本的更新也会自动更新,第一次执行务必使用。

version: '3.8'  # 可以根据需要使用不同的版本
services:
  fast-wiki-service:
    image: registry.cn-shenzhen.aliyuncs.com/fast-wiki/fast-wiki-service
    container_name: fast-wiki-service
    user: root
    restart: always
    ports:
      - "8080:8080"
    build: 
      context: .
      dockerfile: ./src/Service/FastWiki.Service/Dockerfile
    depends_on:
      - postgres
    volumes:
      - ./wwwroot/uploads:/app/wwwroot/uploads
    environment:
      - OPENAI_CHAT_ENDPOINT=https://api.openai.com
      - OPENAI_CHAT_EMBEDDING_ENDPOINT=https://api.openai.com
      - OPENAI_CHAT_TOKEN=您的TokenKey
      - OPENAI_CHAT_MODEL=gpt-3.5-turbo
      - OPENAI_EMBEDDING_MODEL=text-embedding-3-small
      - ASPNETCORE_ENVIRONMENT=Development

  postgres: # 当前compose服务名
    image: registry.cn-shenzhen.aliyuncs.com/fast-wiki/pgvector:v0.5.0 # 拉取的数据库镜像
    container_name: postgres  # 容器运行的容器名称
    restart: always  # 开机自启动
    environment:  # 环境变量
      POSTGRES_USER: token  # 默认账号
      POSTGRES_PASSWORD: dd666666 # 默认密码
      POSTGRES_DB: wiki # 默认数据库
      TZ: Asia/Shanghai  # 数据库时区
    volumes:
      - ./postgresql:/var/lib/postgresql/data # 将PostgreSql数据持久化

修改完上面的配置然后将容器执行启来:

docker-compose up -d

然后我们访问启用的访问的端口
http://ip:8080
, 进入首页以后点击立即开始。

image-20240314012829413

然后登录系统,默认的账号
admin
密码
Aa123456
,登录成功以后再点击立即开始,则进入首页。

image-20240314012901080

然后点击右上角的新增,输入测试应用名称,添加完成,点击左边菜单的第一个对话。

image-20240314013011591

进入对话就可以于AI进行对话了:

image-20240314013117535

上面只是简单的入门了FastWiki的对话功能,更多功能可以自行搭建测试。

AIDotNet 开源社区

AIDotNet 是一群热爱DotNet的开源爱好者组成,为了让DotNet赋能AI,让AI轻松带入您的产品,我们成立了AIDotNet组织。

whuanle
Token

  • Just for better technology, Microsoft MVP

whuanle
longdream

whuanle
whuanle

  • Interesting blogger, little programmer, amateur photographer, mainly studying microservice architecture, kubernetes, istio, Devops, etc., currently studying machine learning, mainly languages are C#, Go, Python. I like reading books, blogging, photography, sports and scenery.

whuanle
xuzeyu91

  • Good at. net microservice architecture system dapr k8s docker aigc, etc

whuanle
MrChuJiu

  • .NET and Angular Developer, Microsoft MVP

前言

你是否观察到过这种现象,在访问Github时,有的时候能正常访问,有的时候再次刷新就访问不了,那么能不能有什么办法能一直访问。答案是有,就是在DNS层面能保证一直稳定获取可用并且快速的IP,这就是FastGithub的原理,笔者结合Github上的开源Code来讲下DNS的一些细节。

FastGithub - DnsServer

当然,也许FastGithub的原理有很多种,这只是其中一种。

首先,构造一个DnsServer,

var dnsServer = new DnsServer(githubRequestResolver, "114.114.114.114"); 
dnsServer.Listen();

DnsServer的构造函数,第一个参数request resolver可以自定义自己的dns resolve方式,第二个参数定义了上游的DnsServer, 114.114.114.114是国内公共的Dns服务器地址,也可以选择其他的公共Dns服务器。

拓展阅读,有没有好奇过Dns是如何查询的,用的什么协议:

总结来说:

UDP协议,53号端口:用于大部分的普通DNS查询。
TCP协议,53号端口:用于数据量较大的DNS查询或需要可靠性传输的情况。

FastGithub - Dns

我们看下GithubRequestResolver:

    public class GithubRequestResolver : IRequestResolver
    {
        public Task<IResponse> Resolve(IRequest request, CancellationToken cancellationToken = default)
        {
            var response = Response.FromRequest(request);
            var question = request.Questions.FirstOrDefault();

            if (question != null && question.Type == RecordType.A)
            {
                var domain = question.Name.ToString();
                var fastAddress = domain.Contains("github", StringComparison.OrdinalIgnoreCase)
                ? this.results.FindFastAddress(domain)
                : default;

                if (fastAddress != null)
                {
                    var record = new IPAddressResourceRecord(question.Name, fastAddress);
                    response.AnswerRecords.Add(record);
                    this.logger.LogInformation(record.ToString());
                }
            }

            return Task.FromResult<IResponse>(response);
        }
    }

这段代码自定义了Dns的Resolve方式,当目前domain为github时,从库中找寻最开的IP地址,然后构造Dns Response返回,这就是这种FastGithub的核心。这个只是简单的示例代码,寻找最快的IP可以是先准备一批IP,然后查询哪个最快返回,也可以是先Lookup查找结果。

注意代码中需要判断Dns RecordType.A,只有A记录才需要重写Record,那么Dns都有什么类型,为什么只有A类型需要特殊处理?

Dns的类型

  • A: 记录的domain对应的IP,这也是为什么code里只需要A记录才需要重写IP的原因

  • AAAA: 记录的domain对应的IPv6, A记录对应的是IPv4

  • CNAME: domain的别名

  • TXT: domain的说明,举个例子

> nslookup -query=TXT baidu.com
> baidu.com       text =
"_globalsign-domain-verification=qjb28W2jJSrWj04NHpB0CvgK9tle5JkOq-EcyWBgnE"
baidu.com       text =
        "google-site-verification=GHb98-6msqyx_qqjGl5eRatD3QTHyVB6-xQ3gJB5UwM"
baidu.com       text =

        "9279nznttl321bxp1j464rd9vpps246v"
baidu.com       text =

        "v=spf1 include:spf1.baidu.com include:spf2.baidu.com include:spf3.baidu.com include:spf4.baidu.com mx ptr -all"
  • NS: NS记录用于告诉互联网查找域名信息要查询哪些服务器

  • MX: 返回domain的邮件服务器, 举个例子

> nslookup -query=MX microsoft.com
> microsoft.com   MX preference = 10, mail exchanger = microsoft-com.mail.protection.outlook.com

上述例子说明microsoft.com对应的邮件服务器为outlook.com, 而这种记录有一个很典型的应用,可以判断一个microsoft 365 tenant是否属于gcc high。大家知道微软的microsoft 365有多种类型,常用的有commercial, usgov以及21v, 而对应的mail server分别是outlook.com和outlook.us, 那么通过MX记录就可以很快的判断出来是属于哪种类型。对应的c#代码如下:

var lookup = new LookupClient();
var result = await lookup.QueryAsync(domain, QueryType.MX).ConfigureAwait(false);
foreach (var item in result.Answers)
{
    var mxRecord = item as MxRecord;
    if (mxRecord != null)
    {
        var exchange = mxRecord.Exchange;
        if (exchange.ToString().EndsWith("com"))
        {
            return AzureEnvironment.Worldwide;
        }
        else if (exchange.ToString().EndsWith("us"))
        {
            return AzureEnvironment.USGovGCCHigh;
        }
        else if (exchange.ToString().EndsWith("cn"))
        {
            return AzureEnvironment.China;
        }
    }
}

项目性能优化—使用JMeter压测SpringBoot项目

我们的压力测试架构图如下:

压力测试架构图

配置JMeter

在JMeter的bin目录,双击jmeter.bat

jmeter.bat

新建一个测试计划,并右键添加线程组:

添加线程组

进行配置

配置

一共会发生4万次请求。

ctrl + s
保存;

添加http请求:

添加Http请求

配置http请求:

配置http请求

配置断言,来判断当前请求是否成功:

配置断言

正常响应如下:

正常响应

配置断言

添加断言持续时间

断言持续时间

持续时间断言

添加
察看结果树
:

结果树

添加
汇总报告
:

汇总报告

相似的操作,也添加聚合报告:

添加聚合报告

相似的操作,添加图形结果:

图形结果

相似的操作,添加汇总图:

汇总图

发送请求测试

发送请求

发送请求

压力测试结果解析

聚合报告

聚合报告

  • 样本(Sample):发送请求的总样本数量
  • 响应时间(RT):平均值,P90,P95,P99,Min,Max
  • 异常(Error):出现错误的百分比
  • 吞吐量(Throughput):被测试接口的吞吐能力
  • 发送与接收数据量:KB/sec

察看结果树

察看结果树

记录了样本中每一次的请求

汇总报告

类似于聚合报告。

线程组配置解析

  1. 线程数:用来发送http请求的线程数量


    • 线程组常用来模拟一组用户访问系统资源(接口)。如果接口响应速度快,就没必要使用太多线程数,如果响应慢,需要使用很多的线程,来测试接口能扛住的压力。如果客户机没有足够能力模拟较重负载,可以使用
      JMeter分布式压力测试。
  2. Ramp-up(in-seconds):建立全部线程耗时:

    代表隔多长时间执行,默认值是0,0代表同时并发,用于告知JMeter要在多长时间内建立全部线程。

  3. 循环次数:循环执行多少次操作


    • 循环次数直接决定整个测试单个线程的执行时间,和整体测试执行时间。
    • 单线程执行时间
      = 单请求平均响应时间 * 循环次数
    • 整个测试耗时
      = 单线程执行时间 + (Ramp-Up - Ramp-UP / 线程数)

JMeter压测插件

Basic Graphs
主要显示:

  • Average Response Time平均响应时间
  • Active Threads活动线程数
  • Successful/Failed Transactions成功/失败事务数

Additional Graphs
主要显示

  • Response Codes
  • Bytes ThroughputConnect Times
  • Latency
  • Hits/s

开启插件下载

下载地址:
https://jmeter-plugins.org/install/Install/,官网上下载plugins-manager.jar直接在线下载,然后执行在线下载即可。

下载插件

下载好后放到
lib\ext
下;

再把JMeter重启;

可以看到插件管理器:

插件管理器

下载如下两个插件:

下载两个插件

安装成功后添加如下三个监听器:

响应时间:jp@gc - Response Times Over Time

活动线程数:jp@gc - Active Threads Over Time

每秒事务数:jp@gc - Transactions per Second

添加监听器

性能关键指标分析

  • TPS:每秒的事务数

    数字愈大,代表性能越好;

  • QPS:每秒的查询数量

    数字愈大,代表性能越好(1TPS >= 1QPS)

  • 吞吐量:每秒的请求数量

    数字愈大,代表性能越好

上周的时候有幸和京东大佬来了次线上“交流”(他问我答那种,懂的都懂),由于我下午临时有个会议要参加,原本计划1小时的“交流”缩短到30分钟,前二十分钟聊了聊项目,距离我开会还剩十分钟时,大佬突然问我道:假设有这么一个场景,用户下完单之后没有支付,然后30分钟之后订单自动取消了,你有什么设计思路去实现这个功能呢?

我在那一边盯着手表一边思考这个问题,咦,工作多载,做过订单相关的但没整过支付啊,网上看过一些支付的实现调一些支付宝、微信支付的接口一类的方案,但是大佬的这个问题的重点显然不在问我怎么调API,而是订单超时自动取消啊!大脑快快想。

沉思十几秒左右,脑子里瞬时冒出了第一种方案,完全可以用定时JOB来实现啊,于是整理下思路便说道:“我们可以采用定时JOB方式去数据库中检查订单的状态,大概可以这样设计:1.
首先设计一个数据库表,用于存储订单信息,包括订单生成时间、支付状态等字段。2.
然后后端开启一个定时JOB,定期(如10秒钟)扫描数据库中的订单。3.
对于每个订单,检查其支付状态以及订单生成时间和当前时间相比是否超过了30分钟。4
如果订单未支付且时间差超过30分钟,则更新订单状态为已取消,并执行相应的取消逻辑,比如释放库存。”

京东大佬沉默了几秒,没有评价对与错,说道:“这可以作为一种实现方案,有没有更好的方案呢?”

更好的方案??我又开始催促我那每天被工作累死的脑细胞赶紧想了,还好,立马想了一个我自己都觉得不靠谱的方案。

我有点憋笑似的回答到:“也可以不使用定时JOB,用户去查询订单的时候,肯定要请求后端接口么,那时候做处理逻辑,针对性的再去判断自己订单有没有超时,超时的话直接修改状态,增加库存,将最新的结果反馈的页面上,而且服务器还没那么多的压力。”坦白讲,我当时居然还觉得自己的这个方案挺天才,显然,这个答案并不是大佬想听到的答案,还是问我有没有更好的思路。

还剩3分钟会议就要开始了,我实在没时间去想第三种方案了,便和大佬道了歉,得抓紧去开会了,大佬也比较理解,又和我另约了其他的时间再继续沟通。

于是回去开会、继续工作。。。

晚上下班的路上,我静下心来思索着白天的回答,发现自己说的这两种方案真的一个也不靠谱:首先第一种定时任务轮询数据库,为了减少和30分钟的误差,我们必然要把这个轮询订单的间隔时间配置的非常短,几秒那种,京东那么大的公司,每天产生那么都订单,用定时一直去访问DB中订单表,那必然对DB造成的压力太大,甚至会影响到别的业务,性能实在是太低了,怎么可能用这种方案呢?       再说第二种方案,看起来对服务器性能没损耗,而且对用户的感知也做到了打开页面就知道订单取消了,可是有个问题啊,假设用户不打开订单页面咋整,那库存岂不是永远无法释放了?一个用户没有及时打开购物APP看,成千上万个呢?若是京东真是使用的我这第二种“天才方案”,那得错过多少假库存不足引起的无法交易呢。 总而言之,两种方案都不是最优解!

反思过后,想了想其实可以使用一些延时处理的策略来实现,于是查了一些资料,下面是我整理的一些设计思路:

  1. 利用延时队列:DelayQueue:
    • DelayQueue是一个无界阻塞队列,其中的元素只能在其延迟期满时才能从队列中获取。
    • 将订单作为对象放入DelayQueue,对象的延迟时间设置为30分钟。
    • 使用生产者线程将新订单放入DelayQueue。
    • 消费者线程从DelayQueue中获取订单,如果订单在30分钟内未被支付,则执行取消操作。
  2. 定时任务 + 使用Redis的过期键:
    • Redis支持设置键的过期时间,当键过期时,Redis会自动删除该键。
    • 将订单信息存储为Redis中的键,键的过期时间设置为30分钟。
    • 定时扫描Redis,将对DB的压力转移到对Redis上。
    • 如果键不存在(即已过期),则返回订单已取消的提示,释放库存。
  3. 利用消息队列的延时消息:
    • 使用如RabbitMQ、Kafka等消息队列系统,它们支持发送延时消息。
    • 当订单生成时,发送一条延时为30分钟的消息到消息队列。
    • 消费者监听消息队列,当接收到延时消息时,检查对应订单是否已支付。
    • 如果未支付,则执行取消订单的逻辑。
  4. 时间轮算法:
    • 时间轮算法是一种高效处理定时任务或延时任务的算法。
    • 设计一个时间轮,每个槽位代表一定的时间间隔(如1分钟)。
    • 当订单生成时,将其放入对应的时间槽位中。
    • 时间轮不断转动,当转到某个槽位时,检查其中的订单是否已支付,未支付则取消。

在实现上述方案时,还需要考虑以下几点:

  • 并发控制
    :确保在检查订单状态和执行取消操作时,订单数据的一致性。
  • 事务性
    :更新订单状态和执行取消操作应在一个事务中完成,确保操作的原子性。
  • 通知机制
    :如果订单被取消,可能需要通知用户或相关系统。
  • 日志记录
    :记录订单取消的原因和时间,方便后续审计和排查问题。