普通视图

Received before yesterday

Redis高可用方案

作者东篱
2025年3月11日 11:49

在使用Redis存储数据时,如果只部署一个Redis服务,可能会出现单点故障,一旦Redis服务寄掉了,整套服务都完蛋,因此我们需要一些Redis的高可用方案

Redis 高可用性方案的核心是通过 数据冗余自动故障切换 来确保在某个 Redis 节点宕机时,服务不会中断。以下是几种常见的高可用方案的 原理 解析:

Redis Sentinel

Redis Sentinel 是 Redis 官方提供的一种高可用性解决方案,用于监控、自动故障转移(failover)和提供通知服务。Sentinel 主要通过监控 Redis 实例的健康状况,自动切换主从节点,保证系统高可用。

一般情况用这个就行,这个也是Redis官方推荐的

原理:

  • 监控功能:Sentinel 会周期性地检查所有 Redis 实例的状态,包括主节点和从节点的健康状态。通过心跳检查,Sentinel 能判断节点是否宕机。
  • 自动故障转移:当 Sentinel 发现主节点宕机时,会从现有的从节点中选举一个新的主节点,并将原来的从节点设置为新的从节点。
  • 通知功能:Sentinel 会通过发布通知的方式告知其他 Redis 客户端主节点的变化。

与持久化(AOF 和 RDB)的关系:

  • AOF 和 RDB:Sentinel 本身不涉及数据持久化,它依赖于 Redis 的持久化机制(如 AOF 和 RDB)。在主节点发生故障切换时,新的主节点会从之前的从节点恢复数据。如果启用了 AOF 或 RDB,数据能够从持久化文件中恢复。
  • AOF(追加文件日志):Redis 以日志的方式将每次写操作追加到磁盘,可以保证数据几乎不丢失,即使在主节点崩溃后,也可以通过 AOF 恢复数据。
  • RDB(Redis 数据库快照):Redis 定期将内存中的数据快照保存到磁盘,如果配置了 RDB 持久化,在主节点崩溃时,会丢失最近的几秒/分钟数据,具体取决于上次保存的快照时间。相对于 AOF,RDB 可能丢失一些未同步到磁盘的操作。

通俗来讲,RDB就是备份一个存档,恢复数据只能恢复到上次备份的存档,而AOF相当于把之前的每一步操作都往磁盘里记录了一遍,恢复数据就是重新执行一遍所有未执行的操作就可以了

优缺点

  • 优点:简单、适用于小型 Redis 集群。
  • 缺点:仅支持单主从架构,扩展性差。

Redis Cluster

Redis Cluster 是 Redis 提供的一个分布式解决方案,能够将数据分片(sharding)存储在多个 Redis 实例上。Cluster 支持自动故障转移,能保证集群内节点的高可用性和分布式存储。

原理:

  • 数据分片:Redis Cluster 会将数据分布在多个节点上,每个节点负责一个数据范围(slot)。数据通过一致性哈希分配到不同的节点,保证负载均衡和高效存储。
  • 自动故障转移:每个数据分片(slot)有一个主节点和多个从节点。当某个主节点发生故障时,Redis Cluster 会自动选择一个从节点升级为新的主节点,并通过 Cluster Manager 实现故障转移。
  • 客户端透明切换:客户端直接连接到 Redis Cluster 中的任一节点,Cluster 会根据数据的 slot 值将请求路由到正确的节点。

与持久化(AOF 和 RDB)的关系:

  • AOF 和 RDB:Redis Cluster 结合持久化(AOF 或 RDB)来保证数据的持久化存储。每个节点都有自己的 AOF 或 RDB 配置,节点发生故障时,从节点会同步 AOF 或 RDB 中的数据来恢复。
  • AOF:适用于需要保证数据不丢失的场景,AOF 记录所有写操作,提供高精度的数据恢复。
  • RDB:适用于性能要求较高的场景,定期进行快照操作,恢复时可能会丢失最新的数据。

优缺点

  • 优点:高可用、高扩展性,适用于大规模应用。
  • 缺点:配置复杂,管理和维护要求较高。

实际上Cluster相比于Sentinel的特点是不是就是数据分片,拿后端举例子,就是分布式单体架构和微服务架构的区别,Sentinel就相当于分布式单体架构,只是通过整体的复制和故障转移保证可靠性,而Cluster就相当于微服务架构,将完整的数据拆分成很多数据分片,分别放在不同节点上维护,可靠性更高且便于横向拓展

Redis with Proxy(Twemproxy 或 Codis)

通过 代理层(如 TwemproxyCodis)来管理 Redis 集群,代理充当客户端和 Redis 实例之间的中介,提供负载均衡和故障转移。

原理:

  • 代理层:代理服务位于客户端和 Redis 实例之间,将客户端请求路由到对应的 Redis 实例。通过代理层实现负载均衡、故障转移和连接池管理。
  • 故障转移和负载均衡:代理层会监控 Redis 实例的健康状态,出现故障时会将请求转发到健康的 Redis 实例。
  • 集群管理:类似于 Redis Cluster 的数据分片,代理层将数据分发到多个 Redis 节点,并负责处理分片操作。

与持久化(AOF 和 RDB)的关系:

  • AOF 和 RDB:每个 Redis 节点依然需要配置 AOF 或 RDB 持久化,代理层只负责请求的路由和负载均衡,数据持久化和故障恢复依赖于 Redis 本身的机制。
  • AOF:适用于对数据一致性要求较高的应用,确保 Redis 服务在宕机后能恢复数据。
  • RDB:适用于性能要求较高的应用,虽然会有数据丢失,但恢复速度较快。

优缺点

  • 优点:通过代理层简化了 Redis 集群的访问,支持负载均衡,适用于中小型应用。
  • 缺点:代理层可能成为性能瓶颈,需要额外维护。

其实Twemproxy类似于nginx,只不过对Redis做了适配,实际上是为Redis提供了一种负载均衡策略,使其支持更高的并发。但是在实际生产中,使用Cluster可能更好,因为Cluster也提供了负载均衡,同时还具备自动故障转移和集群管理能力

总结:

方案 适用场景 可靠性 复杂度
Redis Sentinel 小到中规模应用,单主从结构 ⭐⭐⭐⭐
Redis Cluster 大规模、需要扩展的应用 ⭐⭐⭐⭐⭐
代理(Twemproxy, Codis) 需要负载均衡的应用 ⭐⭐⭐

如果只考虑 Redis 本身的高可用性,最推荐的方案是使用 Redis SentinelRedis Cluster

The post Redis高可用方案 first appeared on 东篱blog.

Celery任务队列的Redis高可用方案

作者东篱
2025年3月11日 11:49

在使用Clelty时,如果Redis 作为 Broker ,容易引发单点故障(SPOF,Single Point of Failure),如果 Redis 挂了,Celery 就无法提交和获取任务了,本文主要介绍一下解决方案。(直接使用RabbitMQ就行)

本文可以参考文章Redis高可用方案

🌟 解决 Redis 单点故障的方法

如果担心 Redis 挂掉影响 Celery,可以使用以下方案:

方案 1:Redis Sentinel(官方推荐)

Redis Sentinel 是 Redis 官方提供的高可用方案,它可以自动切换主节点,保证 Celery 任务队列的稳定性。

✅ 优点:

  • 自动故障转移,如果 Redis 主节点崩溃,Sentinel 会自动选出新的主节点。
  • 应用程序无感知切换,Celery 连接 Sentinel,Sentinel 负责返回当前的主节点地址。

✅ 配置 Celery 使用 Sentinel

1、启动 Redis Sentinel

  • 配置sentinel.conf,假设 Redis 运行在127.0.0.1:6379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
  • 启动 Sentinel:
redis-sentinel /etc/redis/sentinel.conf

2、修改 Celery 配置,使用 Sentinel

from celery import Celery
from kombu import Connection

broker_url = "sentinel://127.0.0.1:26379;sentinel://127.0.0.1:26380"
app = Celery('tasks', broker=broker_url, backend="redis://127.0.0.1:6379/1")

app.conf.broker_transport_options = {
    'sentinel': [('127.0.0.1', 26379), ('127.0.0.1', 26380)],
    'master_name': 'mymaster',
}
  • 这样,Celery 会自动连接 Sentinel 获取当前的 Redis 主节点,主从切换时不会影响任务执行

方案 2:Redis Cluster

Redis Cluster 是 Redis 自带的分布式方案,提供数据分片+高可用

✅ 优点:

  • 数据分片,支持大规模数据存储,适合高并发任务队列。
  • 内置故障转移,节点宕机时,集群会自动选出新的主节点。

✅ 配置 Celery 使用 Redis Cluster

1、启动 Redis Cluster(假设 6 个节点,3 主 3 备):

redis-cli --cluster create 192.168.1.1:6379 192.168.1.2:6379 192.168.1.3:6379 \
192.168.1.4:6379 192.168.1.5:6379 192.168.1.6:6379 --cluster-replicas 1

2、修改 Celery 配置

from celery import Celery

app = Celery('tasks', broker='redis://192.168.1.1:6379/0', backend='redis://192.168.1.1:6379/1')

app.conf.broker_transport_options = {
    'cluster': [
        "redis://192.168.1.1:6379/0",
        "redis://192.168.1.2:6379/0",
        "redis://192.168.1.3:6379/0",
    ]
}
  • 这样 Celery 会自动负载均衡多个 Redis 节点,即使某个节点挂了,任务队列也能继续运行

方案 3:RabbitMQ 代替 Redis(更稳定)

如果不想用 Redis,可以考虑 RabbitMQ,它是 Celery 官方默认的 Broker,提供持久化存储+集群高可用

✅ 优点:

  • RabbitMQ 的消息存储在磁盘中,即使崩溃重启,消息不会丢失
  • 支持 高可用模式(HA),多个节点一起工作,不会有单点故障
  • 任务队列 更稳定,支持事务、消息确认、队列优先级等功能

✅ 配置 Celery 使用 RabbitMQ

app = Celery('tasks', broker='pyamqp://guest@localhost//')
  • 这样 Celery 会连接 RabbitMQ,所有任务都会进入 RabbitMQ 队列,即使 Celery 挂了,任务也不会丢失

🚀 最佳实践(推荐)

方案 适用场景 可靠性 复杂度
Redis Sentinel 中小规模任务队列 ⭐⭐⭐⭐ 中等
Redis Cluster 高并发、大规模任务 ⭐⭐⭐⭐
RabbitMQ 需要强一致性任务(如支付、交易) ⭐⭐⭐⭐⭐

🔹 如果只是简单任务队列,Redis Sentinel 就够用了。
🔹 如果任务量很大,Redis Cluster 更合适。
🔹 如果任务不能丢,RabbitMQ 是最佳选择。


✅ 总结

🚀 Redis 默认是单点故障,但可以用 Sentinel 或 Cluster 解决。
🚀 如果任务特别重要,建议用 RabbitMQ 代替 Redis。
🚀 根据业务需求选择合适的 Broker 方案,避免单点故障! 💡

The post Celery任务队列的Redis高可用方案 first appeared on 东篱blog.

苍穹外卖项目实战

作者东篱
2025年1月25日 12:23

软件开发整体介绍

软件开发流程

image-20250317211931296

需求分析

  • 需求规格说明书:形成文档介绍
  • 产品原型:通过静态网页展示业务功能

设计

  • UI设计:将页面各个方面的细节设计完善
  • 数据库设计:先设计E-R图,然后再具体设计表的字段和类型等详细细节
  • 接口设计:就是设计接口文档(使用Apifox)

编码

  • 项目代码:业务逻辑的代码
  • 单元测试:用于测试项目代码的单元测试(开发人员自测)

测试

  • 测试用例:以接口为单位编写测试用例
  • 测试报告:对测试情况进行报告

上线运维

  • 软件环境安装:安装运行环境
  • 配置:进行一些线上的配置,如Nginx

角色分工

  • 项目经理:对整个项目负责,任务分配、把控进度
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等
  • UI设计师:根据产品原型输出界面效果图
  • 架构师:项目整体架构设计、技术选型等
  • 开发工程师:代码实现
  • 测试工程师:编写测试用例,输出测试报告
  • 运维工程师:软件环境搭建、项目上线

软件环境

  • 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
  • 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
  • 生产环境(production):即线上环境,正式提供对外服务的环境

Apifox等相关应用也应该采用与此统一的三种软件环境

苍穹外卖项目介绍

项目介绍

image-20250125095402962

image-20250125095622802

产品原型

image-20250125100337243

技术选型

image-20250125102924973

开发环境搭建

前端环境搭建

image-20250125105841391

后端环境搭建

注意,此处的搭建方式和目录结构并不是最佳实践,实际目录结构要根据具体项目来进行具体的架构设计

image-20250125105918916

image-20250125110227200

image-20250125110343659

image-20250125110700061

搭建好之后提交git仓库

数据库设计文档

image-20250125112438572

注意,这里的数据库设计并非最佳实践,实际上数据库的表名的最佳实践应该为模块名_功能点,如此更容易后期拓展其他模块

另外,用户名(账号)和密码这里做的是不错的,都应该是varchar,有的项目中账号使用数字,是不正确的做法

序号 数据表名 中文名称
1 employee 员工表
2 category 分类表
3 dish 菜品表
4 dish_flavor 菜品口味表
5 setmeal 套餐表
6 setmeal_dish 套餐菜品关系表
7 user 用户表
8 address_book 地址表
9 shopping_cart 购物车表
10 orders 订单表
11 order_detail 订单明细表

1. employee

employee表为员工表,用于存储商家内部的员工信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 姓名
username varchar(32) 用户名 唯一
password varchar(64) 密码
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
status int 账号状态 1正常 0锁定
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

2. category

category表为分类表,用于存储商品的分类信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 分类名称 唯一
type int 分类类型 1菜品分类 2套餐分类
sort int 排序字段 用于分类数据的排序
status int 状态 1启用 0禁用
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

3. dish

dish表为菜品表,用于存储菜品的信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 菜品名称 唯一
category_id bigint 分类id 逻辑外键
price decimal(10,2) 菜品价格
image varchar(255) 图片路径
description varchar(255) 菜品描述
status int 售卖状态 1起售 0停售
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

4. dish_flavor

dish_flavor表为菜品口味表,用于存储菜品的口味信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
dish_id bigint 菜品id 逻辑外键
name varchar(32) 口味名称
value varchar(255) 口味值

5. setmeal

setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 套餐名称 唯一
category_id bigint 分类id 逻辑外键
price decimal(10,2) 套餐价格
image varchar(255) 图片路径
description varchar(255) 套餐描述
status int 售卖状态 1起售 0停售
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

6. setmeal_dish

setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
setmeal_id bigint 套餐id 逻辑外键
dish_id bigint 菜品id 逻辑外键
name varchar(32) 菜品名称 冗余字段
price decimal(10,2) 菜品单价 冗余字段
copies int 菜品份数

7. user

user表为用户表,用于存储C端用户的信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
openid varchar(45) 微信用户的唯一标识
name varchar(32) 用户姓名
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
avatar varchar(500) 微信用户头像路径
create_time datetime 注册时间

8. address_book

address_book表为地址表,用于存储C端用户的收货地址信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
user_id bigint 用户id 逻辑外键
consignee varchar(50) 收货人
sex varchar(2) 性别
phone varchar(11) 手机号
province_code varchar(12) 省份编码
province_name varchar(32) 省份名称
city_code varchar(12) 城市编码
city_name varchar(32) 城市名称
district_code varchar(12) 区县编码
district_name varchar(32) 区县名称
detail varchar(200) 详细地址信息 具体到门牌号
label varchar(100) 标签 公司、家、学校
is_default tinyint(1) 是否默认地址 1是 0否

9. shopping_cart

shopping_cart表为购物车表,用于存储C端用户的购物车信息。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 商品名称
image varchar(255) 商品图片路径
user_id bigint 用户id 逻辑外键
dish_id bigint 菜品id 逻辑外键
setmeal_id bigint 套餐id 逻辑外键
dish_flavor varchar(50) 菜品口味
number int 商品数量
amount decimal(10,2) 商品单价
create_time datetime 创建时间

10. orders

orders表为订单表,用于存储C端用户的订单数据。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
number varchar(50) 订单号
status int 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
user_id bigint 用户id 逻辑外键
address_book_id bigint 地址id 逻辑外键
order_time datetime 下单时间
checkout_time datetime 付款时间
pay_method int 支付方式 1微信支付 2支付宝支付
pay_status tinyint 支付状态 0未支付 1已支付 2退款
amount decimal(10,2) 订单金额
remark varchar(100) 备注信息
phone varchar(11) 手机号
address varchar(255) 详细地址信息
user_name varchar(32) 用户姓名
consignee varchar(32) 收货人
cancel_reason varchar(255) 订单取消原因
rejection_reason varchar(255) 拒单原因
cancel_time datetime 订单取消时间
estimated_delivery_time datetime 预计送达时间
delivery_status tinyint 配送状态 1立即送出 0选择具体时间
delivery_time datetime 送达时间
pack_amount int 打包费
tableware_number int 餐具数量
tableware_status tinyint 餐具数量状态 1按餐量提供 0选择具体数量

11. order_detail

order_detail表为订单明细表,用于存储C端用户的订单明细数据。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 商品名称
image varchar(255) 商品图片路径
order_id bigint 订单id 逻辑外键
dish_id bigint 菜品id 逻辑外键
setmeal_id bigint 套餐id 逻辑外键
dish_flavor varchar(50) 菜品口味
number int 商品数量
amount decimal(10,2) 商品单价

前后端联调

前后端都搭建好之后,通过实现登录等基础功能,进行前后端的联调,尽早联调防止后续出现问题

image-20250125113042698

Nginx反向代理

正向代理:代理客户端(隐藏用户)

反向代理:代理服务端(隐藏服务器)

image-20250125120554075

image-20250125120845201

image-20250125121040531

image-20250125121244660

image-20250125121444149

image-20250125121533164

补充知识

image-20250126103429705

最好是前端和后端都进行加密,如此一来更加安全

image-20250126120756918

实际开发过程中,最好是结合使用HTTP状态码、业务状态码以及业务信息,HTTP状态码负责划分错误大体信息,业务状态码负责明确错误详细信息,业务信息用来提供用户友好型的提示信息

另外,业务状态码可以采用阿里文档中的状态码,其中没有的状态码可以自己新增,需要遵守文档状态码的分类模式

因此Result枚举中应该如下所示:

image-20250126124937528

全局自定义异常捕获应该如下所示:

image-20250126125800124

前后端分离开发流程

image-20250126153641670

使用Swagger进行接口测试

注意,实际开发过程中使用Apifox进行调试和测试即可,比这个方便很多很多

但是实际开发过程中,Swagger还是需要使用的,因为其放在源码中,可以便捷的生成接口文档,算是一种冗余策略(Swagger注解和注释都要有)

image-20250126155038528

image-20250126155048167

image-20250126155308059

image-20250126155400037

image-20250126161550579

业务开发

JWT令牌完整流程

image-20250128101638833

JWT令牌原理

JWT令牌分为三部分:头部,载荷,签名

头部和载荷是不加密的,而签名是通过不可逆的签名算法获取到的密文

签名=不可逆的签名算法(头部+载荷,密钥)

JWT令牌=头部.载荷.不可逆的签名算法(头部+载荷,密钥)

前端与后端交互时,将JWT令牌给到后端,后端通过令牌中公开的头部和载荷,以及只有后端自己知道的密钥,再次进行签名,将此签名和前端传过来的前面对比一下,一样的话就通过校验,不一样就不通过

注:不可逆的签名算法指的是计算过程只能从左到右,不能从右到左的算法,例如36×78=2808,这个算式从左到右很好算,但是你只知道2808,想要知道算式左侧是啥就很难办)

如何获取到JWT令牌中存储的信息

image-20250128112148972

每个请求的处理都是独立的线程,而ThreadLocal(线程局部存储)提供了一种独立存储每个线程局部变量的方式,其提供的方法可以以线程独立的方式存取当前线程的数据,因此我们可以在解析JWT的时候,可以将JWT信息放到ThreadLocal中,从而使当前线程随时能访问JWT中的信息

public class JwtContext {
    // 使用 ThreadLocal 来存储每个线程的 JWT 信息
    private static ThreadLocal<String> jwtToken = new ThreadLocal<>();

    // 设置当前线程的 JWT 信息
    public static void setJwtToken(String token) {
        jwtToken.set(token);
    }

    // 获取当前线程的 JWT 信息
    public static String getJwtToken() {
        return jwtToken.get();
    }

    // 清理线程的 JWT 信息
    public static void clear() {
        jwtToken.remove();
    }
}

1. ThreadLocal 的工作机制

ThreadLocal 为每个线程提供一个独立的存储空间。每个线程在访问 ThreadLocal 时,都会看到与自己相关的数据,而不会与其他线程共享数据。

  • 当你通过 ThreadLocal.set() 方法存储数据时,数据会被存储在当前线程的局部存储空间中。
  • 通过 ThreadLocal.get() 获取数据时,只能访问当前线程的存储数据。

2. 线程生命周期

  • 当线程结束时,ThreadLocal 中存储的数据会被销毁。这意味着如果某个请求处理结束,线程被回收,那么该线程在 ThreadLocal 中存储的任何数据都会随之销毁。
  • 如果 线程池复用线程(例如在 Web 应用中使用的线程池),则线程并不会完全销毁,而是会在下一次使用时继续工作。在这种情况下,ThreadLocal 数据可能在下一次线程复用时仍然存在,除非你显式地调用 ThreadLocal.remove() 来清除它。

3. 避免内存泄漏

在某些环境中,尤其是线程池复用的情况下,线程的生命周期比请求的生命周期长。如果在请求结束后没有显式地清除 ThreadLocal 中的内容,它可能会导致内存泄漏,因为 ThreadLocal 会保持对数据的引用,而这些线程可能长时间存在于线程池中。

为了避免这种情况,应该显式地清除 ThreadLocal 中的数据

4. 总结

  • 当线程结束时,ThreadLocal 存储的数据会被销毁,不会占用额外的内存
  • 但如果使用 线程池,在复用线程时,ThreadLocal 中的内容不会自动清除,这时需要手动调用 ThreadLocal.remove() 来确保清理,避免内存泄漏。

因此,为了确保每个请求处理完成后 ThreadLocal 中的数据被清理,应该在请求处理的结束阶段调用 ThreadLocal.remove(),尤其是在使用线程池的情况下。

分页查询接口设计

image-20250130114251709

实际开发过程中,分页查询不需要自己实现,直接使用mybits-flex中的即可

image-20250130115001095

统一的消息处理(如日期时间等)

为了符合工程化的统一处理,在项目实践中往往会在WebMvcConfiguration中扩展Spring MVC消息转换器,统一对日期类型进行格式化处理

image-20250130124406103

切面应用场景

一般是用于公共字段填充,以及很多的公共统一场景

image-20250130175354828

image-20250130180733566

文件上传应用场景

对于文件上传的应用场景,我们往往采用对象存储服务,如阿里云的OSS或腾讯云的COS,本地部署对象存储也是个方案,但考虑到云原生的对象存储既便宜又好用,因此使用云原生的对象存储是最佳实践

具体的使用方式和最佳实践可以参考该知乎问答中的内容:上传文件应该经过后端吗,还是直接上传至阿里oss?

1、从服务端获取上传凭证

2、使用上传凭证上传文件到OSS

工程化获取自定义yml属性的最佳实践

1、实现自定义属性类,并提交给Bean管理

2、在yml配置文件或配置类中进行配置

image-20250131222158407

3、在需要使用的位置通过依赖注入进行调用

image-20250131222328412

注意,虽然@value也可以实现读取yml属性值,但是其存在硬编码等问题,不符合工程化最佳实践

多表操作使用事务

如果一个接口(方法)要同时对多个表进行操作,一定要使用事务,以此来保证操作的原子性,也就是说要么保证对所有表的操作全部成功,要么全部失败,不允许只对其中部分表的操作成功,从而保证数据的一致性

1、对SpringBoot应用开启注解方式的事务管理

image-20250201114936532

2、对需要保证数据一致性的方法添加事务注解(保证该方法为原子性操作,要么全成功,要么全失败)

image-20250201115142462

事务注解作用:将当前方法交给Spring进行事务管理,方法执行前,开启事务,成功执行完毕则提交事务;出现异常则回滚事务

The post 苍穹外卖项目实战 first appeared on 东篱blog.

数据结构基础

作者东篱
2024年1月15日 17:04

程序=数据结构+算法(物体结构+物体行为),数据结构是数字世界模拟现实世界的基础,是一切程序的地基。

本篇文章主要是将数据结构的基础内容过一遍,查漏补缺的同时为考研408做准备。

绪论

信息化世界的组成

image-20240115171153068

  • 由此可见,【计算机组成原理、操作系统、数据结构、计算机网络】共同组成了我们的信息化世界。

数据结构的基本概念

数据

image-20240115171659686

数据元素和数据项

image-20240115172218576

数据对象

image-20240115172523889

数据结构的三要素

image-20240115174231014

物理存储结构

  • 线性存储

image-20240115173526627

  • 链式存储

image-20240115173611010

  • 索引存储

image-20240115173648386

  • 散列存储

image-20240115173817887

数据类型和抽象数据类型

image-20240115174651170

数据结构基本概念总结

image-20240115174840572

算法

image-20240115175838786

时间复杂度

image-20240115181207410

空间复杂度

递归调用算法空间复杂度的示例

image-20240116193219476

总结

image-20240116193314210

线性表

线性表的定义

image-20240116193808199

线性表的基本操作

image-20240116194350121

总结

image-20240116194727459

顺序表

定义

image-20240116200012696

总结

image-20240116201240871

基本操作

image-20240117215756926

image-20240119180511250

链表

image-20240119182535106

注意上述红框中的内容,LinkListLNode*实际上是一样的东西,但是含义有区别。

单链表的定义

image-20240119182831295

单链表的基本操作

指定节点前插

巧妙方法:指定节点前插操作,除了通过遍历找到该节点的前一个节点之外,还有一种更快速的实现方法,就是在指定节点后面插入新节点,然后将新节点与指定节点的数据域互换。

image-20240119193848646

删除指定节点

巧妙方法:与上面说的指定节点前插的方法异曲同工,详细步骤见下图。不过这段代码有Bug,因为如果p结点是最后一个节点的话,p->next->data会发生异常。

image-20240119194421354

插入操作总结

image-20240119194921433

查找操作总结

image-20240121220717648

单链表的建立

尾插法:

image-20240122180913068

头插法:

image-20240122181051759

双链表

image-20240122182041692

循环链表

image-20240122192517784

静态链表

image-20240122192742890

image-20240122193640457

基本概念

image-20240123203537196

栈的顺序存储实现

image-20240124213256269

image-20240124214812219

image-20240124214902399

image-20240124215024321

image-20240124215128910

总结

image-20240124215159869

栈的链式存储实现

image-20240125180609417

队列

基本概念

image-20240125181239461

队列的顺序存储实现

image-20240125194305735

队列的链式存储实现

image-20240125195241669

双端队列

image-20240126180247127

image-20240126191540070

总结

image-20240126195052447

栈的应用

括号匹配算法

image-20240127221255124

实现

image-20240127221530180

总结

image-20240127221727019

表达式求值

表达式详解

中缀、后缀、前缀表达式

其实就是树的三种遍历顺序。

image-20240128203935184

中缀转后缀

image-20240128204540303

后缀表达式计算

image-20240128210228967

中缀转前缀

image-20240128210856898

总结

image-20240128211044670

使用栈进行表达式求值

中缀转后缀(机算)

image-20240129202031866

中缀表达式计算

image-20240129203140830

总结

image-20240129203637720

栈在递归中的应用

image-20240130204803805

image-20240130205038327

队列的应用

树的层序遍历

image-20240131213040551

图的广度优先遍历

image-20240131213115651

CPU先到先服务

image-20240131213153113

缓冲区队列

电脑是快速设备,打印机是慢速设配,通过缓冲区队列解决快速设备和慢速设备之间的速度不匹配问题。

image-20240131213314830

特殊矩阵的压缩存储

二维数组的存储结构

image-20240201202233149

行优先存储计算方法

image-20240201202438882

列优先存储计算方法

image-20240201202601061

对称矩阵的压缩存储

image-20240201202905898

三角矩阵压缩存储

image-20240201203216049

与对称矩阵的存储方式基本一致,只需要多加一个常量存储位置即可。

三对角矩阵的压缩存储

image-20240201204033983

稀疏矩阵的压缩存储

使用三元组

image-20240201204217293

使用三元组有个缺点,就是会使其失去随机存取的特性,每次找数据都要遍历所有三元组。

十字链表法

image-20240201204427887

总结

image-20240201204449274

定义

image-20240203202801242

基本操作

image-20240203203126288

总结

image-20240203203809505

串的存储结构

串的顺序存储

image-20240204202843658

串的链式存储

image-20240204203429880

总结

image-20240204203922364

字符串模式匹配

image-20240205213638721

朴素模式匹配算法

image-20240205213858356

image-20240205213924407

image-20240205214141787

image-20240205214448175

总结

image-20240205214503191

KMP算法

image-20240208203904027

其实就是先对模式串进行处理,找到模式串中重复的部分,比如我们已经匹配到了第五个字母,说明前四个字母goog在主串与模式串中是一样的我们会发现第四个字母与前面是存在重复部分的,即字母g,因此当我们匹配到第五个字母的时候,我们知道主串中在匹配第四个字母的时候已经有一个g了,就不需要再比一次了,所以模式串的指针直接从第二个字母开始比较。

换句话说,当我们匹配到第五个字符时,如果发现不匹配,根据部分匹配表,我们可以知道“goog”(前四个字符)中有多少字符是重复的前缀。在这个例子中,“g”是一个重复的前缀(在第一位和第四位)。如果第五个字符不匹配,我们可以将模式串移动,使模式串的第二个字符与主串中当前位置的字符对齐,而不是重新从“google”的第一个字符开始匹配。

next数组求法

image-20240212204840569

image-20240212205435760

image-20240212205733321

image-20240212205923301

KMP算法总结

image-20240216211915597

next数组的进一步优化

image-20240217171320999

image-20240217171640757

树与二叉树

基本概念

image-20240218195431050

结点、树的属性描述

image-20240218200825151

有序树和无序树

image-20240218200942066

森林

image-20240218201034901

总结

image-20240218201127761

常考性质

image-20240219164304075

image-20240219163114522

image-20240219164216761

image-20240219164152594

image-20240219165337480

image-20240219165900275

总结

image-20240219165922096

二叉树

image-20240219171529120

image-20240219171542108

几个特殊的二叉树

image-20240219183118151

image-20240219195658844

image-20240219195750938

总结

image-20240219195829251

The post 数据结构基础 first appeared on 东篱blog.

讯飞免费星火大模型部署教程

作者Echo
2023年11月25日 12:23

前言

发现两个多月没写文章了,不过考试月也没啥好写的。

最近大模型这么火,正好有个项目用到,于是便水一篇教程吧。

此篇教程为 科大讯飞的星火大模型 部署教程,部署完成后即可与智能助手进行聊天。

这里是关于部署到服务器端,如果有其他需求可以查看官方文档。

 

大模型简介

大语言模型 (英语:large language model,LLM) 是一种语言模型,由具有许多参数(通常数十亿个权重或更多)的人工神经网络组成,使用自监督学习半监督学习对大量未标记文本进行训练[1]。大型语言模型在2018年左右出现,并在各种任务中表现出色[2]

尽管这个术语没有正式的定义,但它通常指的是参数数量在数十亿或更多数量级的深度学习模型[3]。大型语言模型是通用的模型,在广泛的任务中表现出色,而不是针对一项特定任务(例如情感分析、命名实体识别或数学推理)进行训练[2]

尽管在预测句子中的下一个单词等简单任务上接受过训练,但发现具有足够训练和参数计数的神经语言模型可以捕获人类语言的大部分句法和语义。 此外大型语言模型展示了相当多的关于世界的常识,并且能够在训练期间“记住”大量事实[2]

参考资料:llm – 搜索 (wikipedia.org)

 

部署

API领取

首先前往科大讯飞的星火大模型官网 讯飞星火认知大模型-AI大语言模型-星火大模型-科大讯飞 (xfyun.cn)

进行注册,然后领取大模型的API

这里选择 API免费试用 ,然后进入如下页面

选择第一个 个人免费包免费试用 。等到领取成功后,后台会有如下界面

这里的 APPIDAPISecretAPIKey 就是接口信息,后面会用到。

 

Linux SDK 下载

进入上面页面后,点击 Linux SDK 右边的下载按钮。将会下载SDK包,感兴趣的也可以点击文档查看使用教程

下载完成后传到服务器,使用解压命令解压包

unzip Spark3.0_Linux_SDK_v1.1.zip

然后进入解压出来的包 Spark3.0_Linux_SDK_v1.1 , 里面应该包含如下文件

root@echofree:/opt# cd Spark3.0_Linux_SDK_v1.1/
root@echofree:/opt/Spark3.0_Linux_SDK_v1.1# ls
build  include  lib  src

 

动态库配置

进入 lib 目录,里面会有一个相关的调用库

root@echofree:/opt/Spark3.0_Linux_SDK_v1.1# cd lib/
root@echofree:/opt/Spark3.0_Linux_SDK_v1.1/lib# ls
libSparkChain.so

这里为了方便点,直接采用暴力方法,将库文件 libSparkChain.so 复制到 /usr/lib

cp libSparkChain.so /usr/lib

 

API接口配置

进入 src 目录,会有一个 demo.cpp 文件,进入此文件,修改如下信息

int initSDK()
{
    // 全局初始化
    SparkChainConfig *config = SparkChainConfig::builder();
    config->appID("appID")        // 你的appid
        ->apiKey("apiKey")        // 你的apikey
        ->apiSecret("apiSecret"); // 你的apisecret
        // ->logLevel(0)
        // ->logPath("./aikit.log");
    int ret = SparkChain::init(config);
    printf(RED "\ninit SparkChain result:%d" RESET,ret);
    return ret;
}

将这里的 三个API配置信息改为自己的即可。

 

demo测试

配置完成后就要测试连接了,使用 GNU 编译套件进行编译,命令如下

g++ -Iinclude src/demo.cpp -o demo -lSparkChain -lstdc++ -lpthread

如果你会 Makefile 的话,也可以复制下面的进行编译

CC = g++
CFLAGS = -Iinclude
LIBS = -lSparkChain -lstdc++ -lpthread
SRC = src/server.cpp
OUTPUT = demo

all: $(OUTPUT)

$(OUTPUT): $(SRC)
	$(CC) $(CFLAGS) -o $@ $^ $(LIBS)

clean:
	rm -f $(OUTPUT)

如果一切正常,文件夹下会生成一个可执行文件 demo

root@echofree:/opt/Spark3.0_Linux_SDK_v1.1# ls
build  demo  files  include  lib  src

运行看看

root@echofree:/opt/Spark3.0_Linux_SDK_v1.1# ./demo

######### llm Demo #########

init SparkChain result:0
######### 同步调用 #########

syncOutput: assistant:Hello

syncOutput: assistant:こんにちは

######### 异步调用 #########
0:assistant:Hello:myContext
2:assistant::myContext
tokens:1 + 5 = 6
0:assistant:こ:myContext
1:assistant:んに:myContext
1:assistant:ちは (:myContext
1:assistant:Konnichi:myContext
2:assistant:wa):myContext
tokens:12 + 10 = 22

很好,配置完成!

 

加点互动

下面就要给他加互动功能了,毕竟大模型不能进行交互聊天,那还要他做什么

修改 demo.cpp 文件,内容如下

记得修改下 API信息哦

#include "../include/sparkchain.h"
#include <iostream>
#include <string>
#include <atomic>
#include <unistd.h>
#include <regex>

#define GREEN "\033[32m"
#define YELLOW "\033[33m"
#define RED "\033[31m"
#define RESET "\033[0m"

using namespace SparkChain;
using namespace std;

// async status tag
static atomic_bool finish(false);
// result cache
string final_result = "";

class SparkCallbacks : public LLMCallbacks
{
    void onLLMResult(LLMResult *result, void *usrContext)
    {
        int status = result->getStatus();
        printf(GREEN "%d:%s:%s:%s \n" RESET, status, result->getRole(), result->getContent(), usrContext);
        final_result += string(result->getContent());
        if (status == 2)
        {
            printf(GREEN "tokens:%d + %d = %d\n" RESET, result->getCompletionTokens(), result->getPromptTokens(), result->getTotalTokens());
            finish = true;
        }
    }

    void onLLMEvent(LLMEvent *event, void *usrContext)
    {
        printf(YELLOW "onLLMEventCB\n  eventID:%d eventMsg:%s\n" RESET, event->getEventID(), event->getEventMsg());
    }

    void onLLMError(LLMError *error, void *usrContext)
    {
        printf(RED "onLLMErrorCB\n errCode:%d errMsg:%s \n" RESET, error->getErrCode(), error->getErrMsg());
        finish = true;
    }
};

int initSDK()
{
    // 全局初始化
    SparkChainConfig *config = SparkChainConfig::builder();
    config->appID("appID")        // 你的appid
        ->apiKey("apiKey")        // 你的apikey
        ->apiSecret("apiSecret"); // 你的apisecret
        // ->logLevel(0)
        // ->logPath("./aikit.log");
    int ret = SparkChain::init(config);
    printf(RED "\ninit SparkChain result:%d" RESET,ret);
    return ret;
}

void syncLLMTest()
{
	cout << "\n######### 同步调用 #########" << endl;
	// 配置大模型参数
	LLMConfig *llmConfig = LLMConfig::builder();
	llmConfig->domain("generalv3");
	llmConfig->url("ws(s)://spark-api.xf-yun.com/v3.1/chat");

	Memory* window_memory = Memory::WindowMemory(5);
	LLM *syncllm = LLM::create(llmConfig, window_memory);

	// Memory* token_memory = Memory::TokenMemory(500);
	// LLM *syncllm = LLM::create(llmConfig,token_memory);

	int i = 0;
	//const char* input = "";
	while (1)
	{
		char input[256]; // 定义一个足够大的字符数组来接收用户输入

		printf("请输入问题 (输入 'q' 退出):");
		scanf("%s", input);

		if (strcmp(input, "q") == 0) {
			break; // 如果输入是 'q',则退出循环
		}

		// 同步请求
		LLMSyncOutput *result = syncllm->run(input);
		if (result->getErrCode() != 0)
		{
			printf(RED "\nsyncOutput: %d:%s\n\n" RESET, result->getErrCode(), result->getErrMsg());
			continue;
		}
		else
		{
			printf(GREEN "\nsyncOutput: %s:%s\n" RESET, result->getRole(), result->getContent());
		}

	}
	// 垃圾回收
	if (syncllm != nullptr)
	{
		LLM::destroy(syncllm);
	}
}


void uninitSDK()
{
    // 全局逆初始化
    SparkChain::unInit();
}

int main(int argc, char const *argv[])
{
    cout << "\n######### llm Demo #########" << endl;
    // 全局初始化
    int ret = initSDK();
    if (ret != 0)
    {
        cout << "initSDK failed:" << ret << endl;
        return -1;
    }

    syncLLMTest(); // 同步调用
   

    // 退出
    uninitSDK();

    return 0;
}

如果你仔细观察,会发现少了一部分代码。

星火大模型的接口调用给了两种方式,一种是同步,一种是异步

 

这里我用的是同步,所有文字都输出完,才会打印在终端。

正常的大模型,应该都是异步调用,即慢慢打印出来,这里留给读者自己修改了。

下面看下运行效果

root@echofree:/opt/Spark3.0_Linux_SDK_v1.1# ./demo

######### llm Demo #########

init SparkChain result:0
######### 同步调用 #########
请输入问题 (输入 'q' 退出):徐州天气怎么样

syncOutput: assistant:今天徐州市的天气是多云,气温在3℃到11℃之间,有点冷。东风4-5级,湿度为53%。空气质量良好,PM2.5指数为60。在这样的天气条件下,适宜旅游、钓鱼和户外运动,但要注意保暖。同时,感冒较易发生,请注意保持干净整洁的环境和清新流通的空气。
请输入问题 (输入 'q' 退出):你是什么

syncOutput: assistant:您好,我是科大讯飞研发的认知智能大模型,我的名字叫讯飞星火认知大模型。我可以和人类进行自然交流,解答问题,高效完成各领域认知智能需求。
请输入问题 (输入 'q' 退出):q
root@echofree:/opt/Spark3.0_Linux_SDK_v1.1#

效果还是不错的。

 

整点花活

既然一切都配置ok了,那肯定得进行应用开发了,这里来个小demo

这里是使用 Qt 开发的一个非常质朴的聊天界面,也就是开头所说的项目中正好用到大模型的地方。由于时间紧张,技术能力有限,就直接搬上去了。

具体原理就是使用 Linux的 socket多线程 与界面进行通信。服务器端负责接收客户端的信息并进行回复。

后续

由于技术有限,并且考试月繁忙,等到有空闲时间了,再写个单独的交互界面。

❌