Deploy_LLMs_ON_Linux

如何在 Linux 服务器上搭建本地LLMs 🤔

如何在 Linux 服务器上部署大语言模型,以 qwen1_5-32b-chat-q8_k_0 为例。服务器使用显卡 A4000,预算:$5950$ 元。

搭建 qwen1_5-32b-chat-q8_k_0

  1. 下载 🤗Hugging Face 库,这个库主要是用于下载模型使用。当然为了保证速度,我们可以使用 wget 命令替代他。如果你决定使用 wget 命令,你可以选择跳过这一步,具体的使用方式在第五步呈现。

    1
    (base) ➜  ~ pip install huggingface_hub

    或者是直接下载 modelscope 库,使用 modelscope 下载模型(⭐推荐)。

    1
    (base) ➜  ~ pip install modelscope
  2. 创建一个 LocalGit 文件夹,并进入该文件夹

    1
    2
    3
    (base) ➜  ~ mkdir LocalGit
    (base) ➜ ~ cd LocalGit
    (base) ➜ LocalGit
  3. 克隆 llama.cpp 的仓库

    1
    2
    3
    (base) ➜  LocalGit git clone https://github.com/ggerganov/llama.cpp
    (base) ➜ LocalGit cd llama.cpp
    (base) ➜ llama.cpp git:(master)
  4. 在有 GPU 的环境下编译 llama.cpp

    前置条件:安装 nvcc + cmake

    执行代码进行编译:

    1
    (base) ➜  llama.cpp git:(master) make LLAMA_CUBLAS=1 LLAMA_CUDA_NVCC=/usr/local/cuda/bin/nvcc

    如果出现错误:(base) ➜ llama.cpp git:(master) make LLAMA_CUBLAS=1 LLAMA_CUDA_NVCC=/usr/local/cuda/bin/nvcc Makefile:76: *** LLAMA_CUBLAS is removed. Use GGML_CUDA instead.. Stop.

    修改代码如下:

    1
    (base) ➜  llama.cpp git:(master) make GGML_CUDA=1 LLAMA_CUDA_NVCC=/usr/local/cuda/bin/nvcc

    为了加快编译速度,我们可以尝试以下命令添加参数 jj 后面的数字表示同时编译的线程数(可根据 CPU 核数决定),实测能缩短约 $1/3$ 的时间:

    1
    (base) ➜  llama.cpp git:(master) make -j6 GGML_CUDA=1 LLAMA_CUDA_NVCC=/usr/local/cuda/bin/nvcc
  5. 下载相应的模型

    ① 使用 Hugging Face 下载相应模型,实测服务器网速在 3M~6M 左右,具体方式如下:

    1
    (base) ➜  ~ huggingface-cli download Qwen/Qwen1.5-32B-Chat-GGUF qwen1_5-32b-chat-q8_0.gguf --local-dir . --local-dir-use-symlinks False

    ② 使用 wget 下载 modelscope 的模型文件,实测网速在 10M~22M 左右,这需要你先获取到模型的下载链接,具体方式如下:

    1
    (base) ➜  ~ wget https://www.modelscope.cn/models/qwen/Qwen1.5-32B-Chat-GGUF/resolve/master/qwen1_5-32b-chat-q8_0.gguf

    ③ 直接使用 modelscope 库下载模型,实测网速在 18M~65M 左右,具体方式如下:

    1
    2
    3
    4
    (base) ➜  ~ cd LocalGit 
    (base) ➜ LocalGit mkdir models
    (base) ➜ LocalGit cd models
    (base) ➜ models modelscope download --model=qwen/Qwen2-7B-Instruct-GGUF --local_dir . qwen2-7b-instruct-q8_0.gguf
  6. 使用 llama.cpp 的相关命令进行操作

    1
    (base) ➜  llama.cpp git:(master) ./main -m ../models/qwen1_5-32b-chat-q8_0.gguf -n 512 --color -i -cml -f prompts/chat-with-qwen.txt
    1
    (base) ➜  llama.cpp git:(master) ./llama-server -m ../models/qwen1_5-32b-chat-q8_0.gguf -ngl 80 -fa

    如果是 Qwen2-7B-Instruct-GGUF,可以参考官方文档:Qwen2-7B-Instruct-GGUF · 模型库 — Qwen2-7B-Instruct-GGUF · 模型库 (modelscope.cn)

    1
    (base) ➜  llama.cpp git:(master) ./llama-server -m ../models/qwen2-7b-instruct-q8_0.gguf -ngl 29 -fa
  7. 兼容 OpenAI API,使用 Python 代码测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import openai

    client = openai.OpenAI(
    base_url="http://localhost:8080/v1", # "http://<Your api-server IP>:port"
    api_key = "sk-no-key-required"
    )

    completion = client.chat.completions.create(
    model="qwen",
    messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "tell me something about michael jordan"}
    ]
    )
    print(completion.choices[0].message.content)
  8. 命令启动

    1
    2
    3
    4
    5
    ./llama-cli -m qwen2-7b-instruct-q5_k_m.gguf \
    -n 512 -co -i -if -f prompts/chat-with-qwen.txt \
    --in-prefix "<|im_start|>user\n" \
    --in-suffix "<|im_end|>\n<|im_start|>assistant\n" \
    -ngl 24 -fa

拓展补充

Llama.cpp大模型量化简明手册_llamacpp量化-CSDN博客

【Llama2 windows部署详细教程】第二节:llama.cpp成功在windows上编译的秘诀_llama cpp 编译-CSDN博客

Windows Build llama.cpp

Windows 平台下构建 llama.cpp

在使用 LM-Studio 时,对于一些参数量不是很大的模型来说,大多数不需要进行模型的合并,如 qwen2-7b 等。这些模型往往只需要下载后加载到 LM-Studio 中即可。

但是对于参数量很大的模型,如 qwen2-72b-instruct 等,因为模型文件较大不利于传输,因此模型开发者可能会使用 llama.cppGGUF 模型进行拆分,所以这个时候我们在下载模型时就需要进行模型的合并。

qwen2-72b-instructq8 量化给出了两个模型文件,分别是:

1
2
qwen2-72b-instruct-q8_k_m-00001-of-00002.gguf
qwen2-72b-instruct-q8_k_m-00002-of-00002.gguf

为了使用这些分割后的 GGUF 文件,我们可以使用 llama-gguf-split 合并他们

1
llama-gguf-spilt --merge input.gguf output.gguf

CPP_11 Enum Class

C++ 11 枚举类 enum class

我们在 C++ 中常使用 enum 来给同一类别中的多个值命名,如:给颜色中的 0, 1, 2, 3, ... 值命名,可以用下面的写法:

1
2
3
4
5
6
7
enum Color {
Red,
Yellow,
Blue,
Gray,
...
};

C++C98 标准称 enum不限范围的枚举型别。因为 C++ 中的枚举量会泄露到包含这个枚举类型的作用域内,在这个作用域内就不能有其他实体取相同的名字。我们可以通过一段代码来演示这一现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

enum Color {
RED,
YELLOW,
BLUE,
GRAY
};

auto GRAY = 10;

int main(void) {
std::cout << GRAY << std::endl;
return 0;
}

当我们编译时,会出现重定义错误:error: 'auto GRAY' redeclared as different kind of entity

为了解决这一问题,C++ 11 新标准提供了 enum Class 枚举类。对于上面的代码,我们再一次做出演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

enum class Color {
RED,
YELLOW,
BLUE,
GRAY
};

auto GRAY = 10;

int main(void) {
std::cout << static_cast<int>(Color::GRAY) << std::endl;
std::cout << GRAY << std::endl;
return 0;
}

此时,输出 310。可以看到在全局作用域的 GRAY 被赋值成了 10,而枚举类中的 GRAY 还是 3,且必须使用作用域限定符进行访问。 这里可以看到我使用了一个 static_cast<int> (Color::GRAY) 进行了一个强制类型转换,这是因为 enum 不支持隐式类型转换。如果想要进行转换,则必须使用 static_cast 进行强制类型转换。

Deploy_LLMs_ON_PC

如何搭建运行在本地的 LLMs 🤔

[TOC]

🤗 1. 基于 LM-Studio

  1. 访问 LM-Studio,网址:LM Studio - Discover, download, and run local LLMs

    下载对应系统的安装包,然后双击运行即可。

  1. 访问 ModelScope魔搭社区 或者 🤗Hugging FaceHugging Face,这里以 ModelScope 为例,进入模型库,下载相应模型。
魔搭社区官网
找到需要的模型并下载
  1. 下载好响应的模型后,将模型组织好,放到相应的文件夹中,这里按照 models/Publisher/Repository/*.gguf 的路径组织模型路径,然后选择 Change 更改模型的位置。如果不按照该路径组织,则会出现 You have 1 uncategorized model files. 错误,如下图所示:
  1. 但是那种方式是不太推荐的,我们组织 USER/MODEL_NAME/*.gguf 的结构,这种结构会比较明了:
  1. 完成模型文件的下载和组织后,我们可以进入聊天页面,选择模型进行加载。这里为了节约空间,我删除了 nilera/Qwen1.5-7B-Chat-Q4-GGUF 目录下的文件。
  1. 选择模型加载,等待加载完成即可像平时使用其他大模型的时候一样使用这些模型。
  1. 但是如果我们想在代码中使用我们的大模型应该怎么做呢?我们可以选择 LM-StudioLocal Server 菜单项,选择 Start Server 即可部署到一个本地指定的端口(默认是 1234)。
  1. 右侧有许多样例,我们可以选择一段样例,如:chat(python),这里对这段代码进行简单的解释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Example: reuse your existing OpenAI setup
from openai import OpenAI

# Point to the local server
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")

completion = client.chat.completions.create(
model="Publisher/Repository", # 可以理解为模型路径, 这里以启动在这个端口的模型为准
messages=[
{"role": "system", "content": "Always answer in Chinese."}, # 系统设置: 每次都用中文回答
{"role": "user", "content": "Introduce yourself."} # 对话设置: 这里希望 AI 介绍一下他自己
],
temperature=0.7,
)

print(completion.choices[0].message) # 获取模型的回复
  1. 然后我们就可以愉快的使用 Python 调用我们的本地大模型了。

⛵ 2. 使用 PowerInfer 框架

PowerInfer 框架 GitHub 链接:SJTU-IPADS/PowerInfer: High-speed Large Language Model Serving on PCs with Consumer-grade GPUs (github.com)

$2024$ 年发布论文 PowerInfer-2[2406.06282] PowerInfer-2: Fast Large Language Model Inference on a Smartphone (arxiv.org)

Anaconda 命令使用:【anaconda】conda创建、查看、删除虚拟环境(anaconda命令集)_conda 创建环境-CSDN博客

参考博客:大模型笔记之-3090显卡推理70B参数模型|基于PowerInfer 一个 CPU/GPU LLM 推理引擎-CSDN博客

  1. 使用 Conda 创建环境,这里 Python 版本需要大于 3.8
1
conda create -n powerinfer1 python=3.8
  1. 激活 Conda 环境:
1
conda activate powerinfer1
  1. 克隆 PowerInfer 框架代码:
1
git clone git@github.com:SJTU-IPADS/PowerInfer.git
  1. 安装所需依赖:
1
pip install -r requirements.txt
  1. 使用 CMake 进行编译(CMake 版本需要大于:3.17+

    这里很大概率可能会出现编译器版本与 CUDA 版本不一致的情况,解决方案:fatal error C1189: #error: – unsupported Microsoft Visual Studio version! - CSDN博客

    这里我有三个 CUDA 版本,貌似修改其中任意一个就可以,这里我修改的是 CUDA v11.6 版本。

    ① 如果是 NVIDIA GPUs,需要使用如下方式进行编译:

    1
    2
    cmake -S . -B build -DLLAMA_CUBLAS=ON
    cmake --build build --config Release

    ② 如果是 AMD GPUs,需要使用下面的方式进行编译:

    1
    2
    3
    4
    # Replace '1100' to your card architecture name, you can get it by rocminfo
    CC=/opt/rocm/llvm/bin/clang CXX=/opt/rocm/llvm/bin/clang++ cmake -S . -B build -
    DLLAMA_HIPBLAS=ON -DAMDGPU_TARGETS=gfx1100
    cmake --build build --config Release

    ③ 如果是 CPU ONLY,需要使用下面的方式进行编译:

    1
    2
    cmake -S . -B build
    cmake --build build --config Release

    这里我有一块 Nvidia 1050ti 所以我使用方式 ①进行编译。

  2. 对于我们下载的模型,可以使用提供的方式进行转化,转化为 PowerInfer 可以使用的类型:

1
2
3
# make sure that you have done `pip install -r requirements.txt`
python convert.py --outfile /PATH/TO/POWERINFER/GGUF/REPO/MODELNAME.powerinfer.gguf /PATH/TO/ORIGINAL/MODEL /PATH/TO/PREDICTOR
# python convert.py --outfile ./ReluLLaMA-70B-PowerInfer-GGUF/llama-70b-relu.powerinfer.gguf ./SparseLLM/ReluLLaMA-70B ./PowerInfer/ReluLLaMA-70B-Predictor
1
python convert.py --outfile D:/LMStudio/models/Publisher/Repository/qwen1_5-7b-chat-q4_0.gguf ./SparseLLM/ReluLLaMA-70B ./PowerInfer/ReluLLaMA-70B-Predictor
  1. 或者将要 原始模型转化为 GGUF 模型
1
python convert-dense.py --outfile /PATH/TO/DENSE/GGUF/REPO/MODELNAME.gguf /PATH/TO/ORIGINAL/MODEL
  1. 运行模型
1
2
3
./build/bin/Release/main.exe -m C:/Users/NilEra/Downloads/llama-7b-relu.powerinfer.gguf -n 128 -t 2 -p "Once upon a time"

# 其中/home/user/data/ReluLLaMA-70B-PowerInfer-GGUF/llama-70b-relu.q4.powerinfer.gguf为GPTQ量化过的模型文件
  1. 一些问题:

Issus22

🔧 3. 在 Windows 上搭建 llama.cpp

  1. 在 Windows 上搭建 llama.cpp 是需要安装很多工具,且安装完成后也存在无法正常成功编译的情况(存在依赖、库等各种问题),因此这里我们可以使用 w64devkit 工具,使用他可以方便我们进行 llama.cpp 的编译。首先我们先下载 w64devkit

    参考网址01

    参考网址02

  2. 然后再 make 可以了

LLM_General_Education

🤗 大语言模型通识

大语言模型的配置需求

首先要搞清楚,本地可以部署什么大模型,取决于个人电脑的硬件配置,尤其需要关注 GPU 的显存。一般来说,只要本地机器 GPU 的显存能够满足大模型的要求,那基本上都可以本地部署。

那么大模型类别这么多,有 $7B$、$13B$、$70B$ 等等,GPU 显存如何准备呢?

在没有考虑任何模型量化技术的前提下,有公式如下:
$$
GB = B × 2
$$

其中为 $GB$ 模型显存占用,$B$ 为大模型参数量。

参考资料

AI大模型本地化部署Q/A硬件篇

如何找到最新的大模型、如何判断本地硬件是否满足大模型需求、如何快速部署大模型

大模型综合评测对比 | 当前主流大模型在各评测数据集上的表现总榜单 | 数据学习 (DataLearner)

MyBatisPlus

🐦MyBatis-Plus

[TOC]

🚪 1. 快速入门

1.1 入门案例

入门案例:基于课前资料提供的项目,实现下列功能:

  • 新增用户功能
  • 根据 id 查询用户
  • 根据 id 批量查询用户
  • 根据 id 更新用户
  • 根据 id 删除用户
1.1.1 引入 MyBatis-Plus 的依赖
1
2
3
4
5
6
<!-- MyBatisPlus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
1.1.2 定义 Mapper

自定义的 Mapper 继承 MybatisPlus提供的 BaseMapper 接口。

BaseMapper<T> 给我们提供了很多基础方法,如常用的增、删、改、查等操作。

BaseMapperInterface

1
public interface UserMapper extends BaseMapper<User> { }
1.1.3 直接使用

做完上面两步操作,此时我们不需要再编写复杂的 SQL 语句即可对数据库进行增、删、改、查操作。

当然对于原有 MyBatis 操作,我们也可以直接使用,因为 MyBatis-PlusMyBatis 是非侵入的,体现了其润物无声的特点。

1.2 常见注解

在初次使用 MyBatis-Plus 时,我们并没有指定要执行操作的表信息和字段信息,那么 Mybatis-Plus 是如何知道我们要查询的是哪张表?表中有哪些字段呢?

MyBatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。

在入门案例中 1.1.2 定义 Mapper 时,我们需要特别指定泛型类即 <User>。泛型中的 User 就是与数据库对应的 PO。

MyBatis-Plus 就是根据 PO 实体的信息来推断出表的信息,从而生成 SQL 的。默认情况下:

  • MyBatis-Plus 会把 PO 实体的类名驼峰转下划线作为表名,如类名为 UserInfo,反射的表名为 user_info
  • MyBatis-Plus 会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型;
  • MyBatis-Plus 会把名为 id 的字段作为主键。

但很多情况下,默认的实现与实际场景不符,因此 MyBatis-Plus 提供了一些注解便于我们声明表信息。

1.2.1 @TableName 注解
  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类
1
2
3
4
5
6
/* 标注实体类对应的表名 */
@TableName("t_user")
public class User {
private Long id;
private String name;
}

@TableName 注解除了指定表名以外,还可以指定很多其它属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
schema String “” schema
keepGlobalPrefix boolean false 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMap String “” xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)
autoResultMap boolean false 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludeProperty String[] {} 需要排除的属性名 @since 3.3.1
1.2.2 @TableId 注解
  • 描述:主键注解,标识实体类中的主键字段
  • 使用位置:实体类的主键字段
1
2
3
4
5
6
@TableName("t_user")
public class User {
@TableId(value="id", type=IdType.AUTO)
private Long id;
private String name;
}

TableId 注解支持两个属性:

属性 类型 必须指定 默认值 描述
value String “” 表名
type Enum IdType.NONE 指定主键类型

IdType支持的类型有:

描述
AUTO 数据库 ID 自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT
INPUT insert 前自行 set 主键值
ASSIGN_ID 分配 ID(主键类型为 NumberLongInteger)或(String(since 3.3.0),使用接口IdentifierGenerator 的方法 nextId (默认实现类为 DefaultIdentifierGenerator 雪花算法)
ASSIGN_UUID 分配 UUID,主键类型为 String*(since 3.3.0)*,使用接口 IdentifierGenerator 的方法 nextUUID(默认 default 方法)
ID_WORKER 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID)
UUID 32UUID 字符串(please use ASSIGN_UUID)
ID_WORKER_STR 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID)

这里比较常见的有三种:

  • AUTO:利用数据库的 id 自增长
  • INPUT:手动生成 id
  • ASSIGN_ID:雪花算法生成 Long 类型的全局唯一 id,这是默认的ID策略
1.2.3 @TableField 注解
  • 描述:普通字段注解

  • 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@TableName("user")
public class User {
@TableId(value="u_id", type=IdType.AUTO)
private Long id;

@TableField("username") // 成员变量名与数据库字段名不一致
private String name;

private Integer age;

@TableField("isMarried") // 成员变量是以 `isXXX` 命名
private Boolean isMarried;

@TableField("`concat`") // 成员变量名与数据库一致, 但是与数据库的关键字冲突
private String concat;

@TableField(exist = false) // 成员变量不是数据库字段
private String address;
}

一般情况下我们并不需要给字段添加 @TableField 注解,一些特殊情况除外:

  • 成员变量名与数据库字段名不一致;
  • 成员变量是以 isXXX 命名,按照JavaBean的规范,MyBatis-Plus识别字段时会把 is 去除,这就导致与数据库不符;
  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用 @TableField 注解给字段名添加转义字符:``
  • 成员变量不是数据库字段,添加 exist = false

支持的其它属性如下:

属性 类型 必填 默认值 描述
value String “” 数据库字段名
exist boolean true 是否为数据库表字段
condition String “” 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s}参考(opens new window)
update String “” 字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性)
insertStrategy Enum FieldStrategy.DEFAULT 举例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
updateStrategy Enum FieldStrategy.DEFAULT 举例:IGNORED update table_a set column=#{columnProperty}
whereStrategy Enum FieldStrategy.DEFAULT 举例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
fill Enum FieldFill.DEFAULT 字段自动填充策略
select boolean true 是否进行 SELECT 查询
keepGlobalFormat boolean false 是否保持使用全局的 format 进行处理
jdbcType JdbcType JdbcType.UNDEFINED JDBC 类型 (该默认值不代表会按照该值生效)
typeHandler TypeHander 类型处理器 (该默认值不代表会按照该值生效)
numericScale String “” 指定小数点后保留的位数

1.3 常见配置

MyBatis-Plus 也支持基于 yaml 文件的自定义配置,详见官方文档:

MyBatis-Plus 官方文档

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:

  • 实体类的别名扫描包
  • 全局id类型
1
2
3
4
5
6
7
8
9
10
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 别名扫描包
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。
configuration:
mapper-underscore-to-camel-case: true # 是否开启下划线驼峰映射
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: auto # 全局 id 类型为自增长, 可以更换为其他方法, 如 assign_id
update-strategy: not_null # 更新策略: 只更新非空字段

需要注意的是,MyBatis-Plus也支持手写 SQL 的,而 mapper 文件的读取地址可以通过 mapper-locations 配置:默认值是classpath*:/mapper/**/*.xml,也就是说我们只要把 mapper.xml 文件放置这个目录下就一定会被加载。

🥑 2. 核心功能

入门案例中都是以 id 为条件的简单CRUD,一些复杂条件的 SQL 语句就要用到一些更高级的功能了。

2.1 条件构造器

除了新增以外,修改、删除、查询的 SQL 语句都需要指定 WHERE 条件。因此 BaseMapper 中提供的相关方法除了以 id 作为 WHERE 条件以外,还支持更加复杂的 WHERE条件。

img

参数中的 Wrapper 就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

img

Wrapper 的子类 AbstractWrapper 提供了 WHERE 中包含的所有条件构造方法:

img

QueryWrapperAbstractWrapper 的基础上拓展了一个 SELETE 方法,允许指定查询字段:

img

UpdateWrapperAbstractWrapper 的基础上拓展了一个 SET 方法,允许指定 SQL 中的 SET 部分:

img

接下来,我们就来看看如何利用 Wrapper 实现复杂查询。

2.1.1 QueryWrapper

无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。

当前表的结构如下:

# 名称 数据类型 注释 长度/集合 默认
1 id BIGINT 用户ID 19 AUTO_INCREMENT
2 username VARCHAR 用户名 50 无默认值
3 password VARCHAR 密码 128 无默认值
4 phone VARCHAR 注册手机号 20 NULL
5 info JSON 详细信息 无默认值
6 status INT 使用状态(1: 正常 2: 冻结) 10 “1”
7 balance INT 账户余额 10
8 create_time DATETIME 创建时间 CURRENT_TIMESTAMP
9 update_time DATETIME 更新时间 CURRENT_TIMESTAMP

接下来看一些例子:

查询:查询出名字中带o的,存款大于等于1000元的人。代码如下:

1
2
3
SELECT id,username,info,balance
FROM user
WHERE username LIKE "%o%" AND Balance >= 100
1
2
3
4
5
6
7
8
9
10
11
@Test
void testQueryWrapper() {
// 1. 构建查询条件 WHERE name LIKE "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2. 查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

更新:更新用户名为 Jack 的用户的余额为 2000,代码如下:

1
2
3
UPDATE user
SET balance=2000
WHERE (username="Jack")
1
2
3
4
5
6
7
8
9
10
@Test
void testUpdateByQueryWrapper() {
// 1.构建查询条件 WHERE name="Jack"
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");

// 2.更新数据, user 中非 NULL 字段都会作为 SET 语句
User user = new User();
user.setBalance(2000);
userMapper.update(user, wrapper);
}
2.1.2 UpdateWrapper

基于 BaseMapper 中的 UPDATE 方法更新时只能直接赋值,对于一些复杂的需求就难以实现。

例如:更新 id124 的用户的余额,扣 200,对应的SQL应该是:

1
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)

SET 的赋值结果是基于字段现有值的,这个时候就要利用 UpdateWrapper 中的 setSql 功能了:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testUpdateWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
// 1.生成SQL
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200") // SET balance = balance - 200
.in("id", ids); // WHERE id in (1, 2, 4)

// 2.更新, 注意第一个参数可以给null, 也就是不填更新字段和数据,
// 而是基于 UpdateWrapper 中的 setSQL 来更新
userMapper.update(null, wrapper);
}
2.1.3 LambdaQueryWrapper

无论是 QueryWrapper 还是 UpdateWrapper 在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?

其中一种办法是基于变量的 getter 方法结合反射技术。因此我们只要将条件对应的字段的 getter 方法传递给 MyBatis-Plus,它就能计算出对应的变量名了。而传递方法可以使用 JDK8 中的 方法引用Lambda 表达式。 因此 MyBatis-Plus 又提供了一套基于LambdaWrapper,包含两个:

  • LambdaQueryWrapper
  • LambdaUpdateWrapper

分别对应 QueryWrapperUpdateWrapper

其使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.lambda()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}

2.2 自定义 SQL

一般企业的开发规范中,是不允许开发人员将 SQL 脱离出 Mapper 层或者 mapper.xml 文件的,但是我们想要使用 MyBatis-Plus 又想要遵守企业的开发规范,应该如何做呢?

我们可以利用 MyBatis-PlusWrapper 来构建复杂的 WHERE 条件,然后自己定义 SQL 语句中剩下的部分。

① 基于 Wrapper 构造 WHERE 条件

1
2
3
4
5
6
7
8
List<Long> ids = List.of(1L, 2L, 4L);
int amount = 200;

// 1. 构造条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);

// 2. 自定义SQL方法调用
userMapper.updateBalanceByIds(wrapper, amount);

② 在 mapper 方法参数中用 @Param 注解声明 Wrapper 变量名称,这个名称必须是 ew

1
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);

③ 自定义 SQL,并使用 Wrapper 条件,这里 ew.customSqlSegmentWrapper 的一个方法,用于获取用户自定义SQL片段。如果使用出现问题,参考博客:MyBatis-Plus ${ew.customSqlSegment} 使用的史诗级大坑-CSDN博客

1
2
3
<update id="updateBalanceByIds">
UPDATE t_user SET balance = balance - #{amount} ${ew.customSqlSegment};
</update>

2.3 Service 接口

MyBatis-Plus 不仅提供了 BaseMapper,还提供了通用的 Service 接口及默认实现,封装了一些常用的 service 模板方法。 通用接口为 IService,默认实现为 ServiceImpl,其中封装的方法可以分为以下几类:

  • save:新增
  • remove:删除
  • update:更新
  • get:查询单个结果
  • list:查询集合结果
  • count:计数
  • page:分页查询

IServiceInterface

2.3.1 CRUD

首先了解基本的CRUD接口。

新增

  • save 是新增单个元素
  • saveBatch 是批量新增
  • saveOrUpdate 是根据id判断,如果数据存在就更新,不存在则新增
  • saveOrUpdateBatch 是批量的新增或修改

删除:

IServiceRemove
  • removeById:根据id删除
  • removeByIds:根据id批量删除
  • removeByMap:根据Map中的键值对为条件删除
  • remove(Wrapper<T>):根据Wrapper条件删除
  • ~~removeBatchByIds~~:暂不支持

修改:

IServiceUpdate

  • updateById:根据id修改
  • update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含setwhere部分
  • update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据
  • updateBatchById:根据id批量修改

Get:

IServiceGet

  • getById:根据 id 查询 1 条数据
  • getOne(Wrapper<T>):根据Wrapper 查询 1 条数据
  • getBaseMapper:获取Service 内的 BaseMapper 实现,某些时候需要直接调用 Mapper 内的自定义 SQL 时可以用这个方法获取到 Mapper

List:

IServiceList

  • listByIds:根据id批量查询
  • list(Wrapper<T>):根据Wrapper条件查询多条数据
  • list():查询所有

Count

IServiceCount

  • count():统计所有数量
  • count(Wrapper<T>):统计符合Wrapper条件的数据数量

getBaseMapper: 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法:

img

2.3.2 基本用法

由于 Service 中经常需要定义与业务有关的自定义方法,因此我们不能直接使用 IService,而是自定义 Service 接口,然后继承 IService 以拓展方法。同时,让自定义的 Service实现类 继承 ServiceImpl,这样就不用自己实现 IService 中的接口了。

具体方法如下:

① 首先,定义 IUserService 类,继承 IService

1
2
3
4
5
6
7
8
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {
// 拓展自定义方法
}

② 然后,编写 UserServiceImpl 类,继承 ServiceImpl,实现 UserService

1
2
3
4
5
6
7
8
9
10
11
12
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.service.IUserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

}

项目结构如下:

img

接下来,我们基于 RESTful 风格快速实现下面几个接口(什么是RESTful风格):

简单理解 REST 就是:URL 中只使用名词来定位资源,用 HTTP 协议里的动词(GETPOSTPUTDELETE)来实现资源的增删改查操作。

什么是 RESTful

  • 使用客户/服务器(B/S、 C/S)模型:客户和服务器之间通过一个统一的接口来互相通讯。
  • 层次化的系统:在一个 REST 系统中,客户端并不会固定地与一个服务器打交道。
  • 无状态:在一个 REST 系统中,服务端并不会保存有关客户的任何状态。也就是说,客户端自身负责用户状态的维持,并在每次发送请求时都需要提供足够的信息。
  • 可缓存:REST 系统需要能够恰当地缓存请求,以尽量减少服务端和客户端之间的信息传输,以提高性能。
  • 统一的接口。一个 REST 系统需要使用一个统一的接口来完成子系统之间以及服务与用户之间的交互。这使得 REST 系统中的各个子系统可以独自完成演化。

如果一个系统满足了上面所列出的五条约束,那么该系统就被称为是 RESTful 的。

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体
2 删除用户 DELETE /users/{id} 用户 id
3 根据 id 查询用户 GET /users/{id} 用户 id 用户VO
4 根据 id 批量查询 GET /users 用户 id 集合 用户VO集合
5 根据 id 扣除余额 PUT /users/{id}/deduction/{money} 1. 用户 id
2. 扣除金额

🕸 3. 拓展功能

🧩 4. 插件功能

SpringCloud

SpringCloud

[TOC]

1. 微服务

什么是微服务:微服务是一种软件架构风格,它是以专注于单一职责的很多小型项目为基础,组合出复杂的大型应用。

传统的单体项目开发将一个项目的所有模块都集中在这个项目中。而微服务可以理解为对一个单体项目的拆分,将单体项目的边界打破,并且将一个庞大的项目拆分成一个一个小项目。

当然微服务会涉及到一些问题,主要包括下面这些方面:

  • 服务拆分
  • 远程调用
  • 服务治理
  • 请求路由
  • 身份认证
  • 配置管理
  • 服务保护
  • 分布式事务
  • 异步通信
  • 消息可靠性
  • 延迟消息
  • 分布式搜索
  • 倒排索引
  • 数据聚合