2024年8月

NextChat介绍

One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support.

一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。

主要功能

  • 在 1 分钟内使用 Vercel
    免费一键部署
  • 提供体积极小(~5MB)的跨平台客户端(Linux/Windows/MacOS),
    下载地址
  • 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等
  • 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
  • 极快的首屏加载速度(~100kb),支持流式响应
  • 隐私安全,所有数据保存在用户浏览器本地
  • 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
  • 海量的内置 prompt 列表,来自
    中文

    英文
  • 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
  • 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
  • 拥有自己的域名?好上加好,绑定后即可在任何地方
    无障碍
    快速访问

image-20240826092034449

GitHub地址:
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web

SiliconCloud介绍

SiliconCloud
基于优秀的开源基础模型,提供高性价比的 GenAI 服务。

不同于多数大模型云服务平台只提供自家大模型 API,
SiliconCloud
上架了包括 Qwen、DeepSeek、GLM、Yi、Mistral、LLaMA 3、SDXL、InstantID 在内的多种开源大语言模型及图片生成模型,用户可自由切换适合不同应用场景的模型。

更重要的是,SiliconCloud 提供
开箱即用
的大模型推理加速服务,为您的 GenAI 应用带来更高效的用户体验。

对开发者来说,通过 SiliconCloud 即可一键接入顶级开源大模型。拥有更好应用开发速度和体验的同时,显著降低应用开发的试错成本。

image-20240822095915114

官网地址:
https://siliconflow.cn/zh-cn/siliconcloud

在NextChat中接入SiliconCloud API

点击Releases:

image-20240826092224306

选择对应操作系统的安装包:

image-20240826092417980

安装完成,打开界面如下所示:

image-20240826092515357

点击设置,进行如下设置:

image-20240826092603207

由于SiliconCloud提供的API服务已经兼容了OpenAI的格式,因此模型服务商可以不改,就把接口地址改成
https://api.siliconflow.cn
,填入API Key,填入模型名称即可。

模型名称可在SiliconCloud的文档中查看怎么写:

image-20240816112338058

地址:
https://docs.siliconflow.cn/reference/chat-completions-3

进行对话测试是否配置成功:

image-20240826093418970

配置已经成功。

SiliconCloud的好处就是提供了不同的先进开源大语言模型,针对一个特定的任务,或许收费低的模型与收费高的模型都能很好的完成,这时候换成收费低的模型,这样就可以节省成本了。

接入SiliconCloud的API可以轻松地体验不同的先进开源大语言模型。

比如我想使用meta-llama/Meta-Llama-3.1-405B-Instruct这个先进的开源模型,我只需更改一下模型名:

image-20240826094912309

体验meta-llama/Meta-Llama-3.1-405B-Instruct的回答效果:

image-20240826095232351

通过以上简单的几个步骤,就实现在NextChat中接入SiliconCloud API了,接入之后,就可以开始体验不同的开源先进大语言模型的回答效果了。

有时候,我们在使用SQLAlchemy操作某些表的时候,需要使用外键关系来实现一对多或者多对多的关系引用,以及对多表的联合查询,有序列的uuid值或者自增id值,字符串的分拆等常见处理操作。

1、在 SQLAlchemy 中定义具有嵌套
children
关系的表

要在 SQLAlchemy 中定义具有嵌套
children
关系的表,如表中包含
id

pid
字段,可以使用
relationship

ForeignKey
来建立父子关系。

首先,你需要定义一个模型类,其中包含
id

pid
字段。
id
是主键,
pid
是指向父记录的外键。然后,你使用
relationship
来建立父子关系。

from sqlalchemy importColumn, Integer, String, ForeignKeyfrom sqlalchemy.orm importrelationship, declarative_basefrom sqlalchemy.ext.asyncio importcreate_async_engine, AsyncSessionfrom sqlalchemy.orm importsessionmaker

Base
=declarative_base()classDictTypeInfo(Base):__tablename__ = 'dict_type_info'id= Column(Integer, primary_key=True, index=True)
name
= Column(String, index=True)
code
=Column(String)
remark
=Column(String)
seq
=Column(Integer)
pid
= Column(Integer, ForeignKey('dict_type_info.id')) #外键指向父节点的 id #定义 parent 关系 parent = relationship("DictTypeInfo", remote_side=[id], back_populates="children")#定义 children 关系 children = relationship("DictTypeInfo", back_populates="parent")

例子使用代码如下所示。

#创建异步引擎和会话
DATABASE_URL = "mysql+asyncmy://username:password@localhost/mydatabase"engine= create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal
= sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)

async
definit_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
#示例:如何插入数据并进行查询 async defexample_usage():
async with AsyncSessionLocal() as session:
async with session.begin():
#插入数据 parent_node = DictTypeInfo(name="Parent", code="P001", remark="Parent Node", seq=1)
child_node1
= DictTypeInfo(name="Child1", code="C001", remark="First Child", seq=1, parent=parent_node)
child_node2
= DictTypeInfo(name="Child2", code="C002", remark="Second Child", seq=2, parent=parent_node)
session.add(parent_node)
session.add(child_node1)
session.add(child_node2)
#查询数据 async with session.begin():
result
=await session.execute("SELECT * FROM dict_type_info WHERE pid IS NULL")
parent_nodes
=result.scalars().all()for node inparent_nodes:print(f"Parent Node: {node.name}, Children: {[child.name for child in node.children]}")

代码说明

  1. 定义模型类
    (
    DictTypeInfo
    ):


    • id
      : 主键。
    • pid
      : 外键,指向同一个表的
      id
      ,表示父节点。
    • parent
      : 父关系,通过
      remote_side
      设定本模型的外键指向自身的主键。
    • children
      : 子关系,
      back_populates
      用于双向关系的映射。
  2. 创建异步引擎和会话
    :


    • 使用
      create_async_engine

      AsyncSession
      创建数据库引擎和会话,以支持异步操作。
  3. 插入和查询数据
    :


    • 插入数据示例展示了如何创建父节点和子节点,并将子节点关联到父节点。
    • 查询数据示例展示了如何查询所有父节点以及它们的子节点。

注意事项

  • remote_side
    : 在
    relationship
    中,
    remote_side
    是指定哪些字段是远程的一方(即子节点关系的目标)。
  • 确保在模型中定义了正确的外键约束。在你提供的模型中,
    pid
    列需要指向同一表中的
    id
    列。确保
    ForeignKey
    设置正确。
  • 异步操作
    : 使用
    AsyncSession

    asyncio
    进行异步数据库操作。
  • 创建表
    : 在初始化数据库时,确保表结构是正确的。

要使用
selectinload
加载某个
pid
下的对象及其子列表,可以通过 SQLAlchemy 的
selectinload
来优化加载子关系。
selectinload
可以减少 SQL 查询的数量,特别是在加载具有层次结构的数据时。

async defget_tree(pid: int):
async with AsyncSessionLocal() as session:
#通过 selectinload 加载所有子节点 stmt = select(DictTypeInfo).filter(DictTypeInfo.pid ==pid).options(selectinload(DictTypeInfo.children))
result
=await session.execute(stmt)
nodes
=result.scalars().all()return nodes

这样,调用
get_tree
函数获取指定
pid
的节点及其子节点,代码如下。

async defexample_usage():
nodes
= await get_tree(pid=1)for node innodes:print(f"Node: {node.name}, Children: {[child.name for child in node.children]}")

selectinload
:
selectinload
可以减少 N+1 查询问题,它通过一条额外的查询来加载相关对象。这适合用于层次结构数据的加载。通过这种方式,你可以使用 SQLAlchemy 的
selectinload
来高效地加载具有父子关系的对象,并优化数据库查询性能。

同样,我们在 SQLAlchemy 中实现多对多关系也是类似的处理方式。

在 SQLAlchemy 中,实现多对多关系通常需要创建一个关联表(association table),该表将存储两个相关联表的外键,从而实现多对多关系。以下是一个实现多对多关系的详细步骤。

1) 定义多对多关系的关联表

首先,需要定义一个关联表,该表包含两个外键,分别指向两端的主表。这通常使用
Table
对象来实现。

from sqlalchemy importTable, Column, Integer, ForeignKeyfrom sqlalchemy.ext.declarative importdeclarative_base

Base
=declarative_base()

association_table
= Table('association', Base.metadata,
Column(
'left_id', Integer, ForeignKey('left_table.id')),
Column(
'right_id', Integer, ForeignKey('right_table.id'))
)

在这个例子中,
association_table
是一个包含两个外键的中间表:
left_id

right_id
分别指向
left_table

right_table
的主键。

2)定义两端的模型并添加关系

在两端的模型中,使用
relationship
来定义多对多关系,并指定
secondary
参数为关联表。

from sqlalchemy.orm importrelationshipclassLeftModel(Base):__tablename__ = 'left_table'id= Column(Integer, primary_key=True)
name
= Column(String(50))
rights
= relationship("RightModel", secondary=association_table, back_populates="lefts")classRightModel(Base):__tablename__ = 'right_table'id= Column(Integer, primary_key=True)
name
= Column(String(50))
lefts
= relationship("LeftModel", secondary=association_table, back_populates="rights")
  • rights

    LeftModel
    中定义的关系属性,它将连接到
    RightModel
  • lefts

    RightModel
    中定义的关系属性,它将连接到
    LeftModel
  • secondary=association_table
    告诉 SQLAlchemy 使用
    association_table
    作为连接表。
  • back_populates
    用于双向关系的对称引用。

3)创建数据库并插入数据

下面的代码展示了如何创建数据库、插入数据并查询多对多关系。

from sqlalchemy importcreate_enginefrom sqlalchemy.orm importsessionmaker#创建数据库引擎
engine = create_engine('sqlite:///example.db')
Base.metadata.create_all(engine)

Session
= sessionmaker(bind=engine)
session
=Session()#创建模型实例 left1 = LeftModel(name="Left 1")
right1
= RightModel(name="Right 1")
right2
= RightModel(name="Right 2")#设置多对多关系 left1.rights =[right1, right2]#添加到会话并提交 session.add(left1)
session.commit()
#查询并打印关系 for right inleft1.rights:print(right.name) #输出: Right 1, Right 2 for left inright1.lefts:print(left.name) #输出: Left 1

你可以像操作普通列表一样来处理这些关系,例如添加、删除关联等:

#添加关系
left1.rights.append(RightModel(name="Right 3"))
session.commit()
#删除关系 left1.rights.remove(right2)
session.commit()

通过这些步骤,你可以在 SQLAlchemy 中实现和操作多对多关系。

2、在 SQLAlchemy 中联合多个表进行记录关联查询

例如,在我的框架中,字典大类和字典项目是不同的表进管理的,因此如果需要根据大类名称进行字典项目的查询,那么就需要联合两个表进行处理。

具体操作如下:创建一个查询,将
DictDataInfo
表与
DictTypeInfo
表联接(通过
DictType_ID

Id
列)

from sqlalchemy.future importselectfrom sqlalchemy.orm importaliasedfrom sqlalchemy.ext.asyncio importAsyncSessionfrom sqlalchemy.ext.asyncio importcreate_async_enginefrom sqlalchemy.orm importsessionmaker#假设你的数据库模型是 DictDataInfo 和 DictTypeInfo#需要提前定义好这两个模型类
DATABASE_URL= "mysql+asyncmy://username:password@localhost/mydatabase"engine= create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal
= sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)

async
defget_dict_data(dict_type_name: str):
async with AsyncSessionLocal() as session:
#创建别名 DictData =aliased(DictDataInfo)
DictType
=aliased(DictTypeInfo)#联合查询并根据条件过滤 stmt =(
select(DictData)
.join(DictType, DictData.DictType_ID
==DictType.id)
.filter(DictType.name
==dict_type_name)
)

result
=await session.execute(stmt)
dict_data
=result.scalars().all()returndict_data#示例用法 importasyncio

async
defexample_usage():
dict_type_name
= "some_type_name"dict_data=await get_dict_data(dict_type_name)for data indict_data:print(data)

代码说明

  1. aliased
    : 使用
    aliased
    创建表的别名,这样可以方便地在查询中引用这些表。

  2. join
    : 使用
    join
    进行表连接。这里
    DictDataInfo
    表的
    DictType_ID
    列与
    DictTypeInfo
    表的
    id
    列连接。

  3. filter
    : 使用
    filter
    来添加条件筛选,筛选出
    DictTypeInfo
    表中
    name
    列等于
    dict_type_name
    的记录。

  4. select
    : 使用
    select
    语句来选择
    DictDataInfo
    表中的记录,这对应于
    Select(d => d)

  5. 异步操作: 由于使用的是 SQLAlchemy 的异步模式,所有数据库操作都在
    async with

    await
    语句中进行,以确保异步执行。

如果我们需要将获得的数据进行对象转换,我们可以使用下面的处理代码实现。

#定义 CListItem 类
classCListItem:def __init__(self, name, value):
self.name
=name
self.value
=value#定义示例列表和转换操作 defconvert_list_items(list_items):
dict_list
=[]if list_items: #确保 list_items 不是 None for info inlist_items.Items:
dict_list.append(CListItem(info.Name, info.Value))
return dict_list

3、使用sqlalchemy插入数据的时候,如何判断为非自增类型的时候,id赋值一个有序列的uuid值

有时候,我们的数据表主键是用字符串的,这种适用于很广的用途,比较容易在插入的时候就确定好id键的值,从而可以处理相关的内容。

但是,有时候我们可以让后端进行确定一个有序的ID值,那么使用SQLAlchemy 我们应该如何实现?

首先,确保你已经导入了
uuid
库,这是用于生成 UUID 的 Python 标准库。

有序 UUID 通常是基于时间的 UUID。你可以使用
uuid.uuid1()
来生成基于时间的 UUID。

defgenerate_sequential_uuid():return uuid.uuid1()  #基于时间生成有序UUID

在定义 SQLAlchemy 模型时,可以将
id
字段设置为使用该函数生成的 UUID。通常在模型中通过
default
参数设置默认值。

from sqlalchemy importColumn, Stringfrom sqlalchemy.ext.declarative importdeclarative_base

Base
=declarative_base()classMyModel(Base):__tablename__ = 'my_table'id= Column(String(36), primary_key=True, default=generate_sequential_uuid, nullable=False)#其他字段...

在插入新数据时,如果
id
字段为空,它将自动使用
generate_sequential_uuid
函数生成一个基于时间的 UUID。

这样就可以确保在插入数据时,非自增类型的
id
字段会被赋值为一个有序列的 UUID 值。

对于自增的整型
id
,SQLAlchemy 提供了自动处理机制。你只需要在模型中将
id
字段定义为
Integer
类型,并设置
primary_key=True
,SQLAlchemy 就会自动为该字段设置自增属性。

from sqlalchemy importColumn, Integer, Stringfrom sqlalchemy.ext.declarative importdeclarative_base

Base
=declarative_base()classMyModel(Base):__tablename__ = 'my_table'id= Column(Integer, primary_key=True, autoincrement=True)
name
= Column(String(50))#其他字段..

默认情况下,SQLAlchemy 会使用数据库的原生自增机制(如 MySQL 的
AUTO_INCREMENT
或 PostgreSQL 的
SERIAL
)。如果你需要使用自定义的自增策略,可以通过设置
Sequence
来实现(适用于支持
Sequence
的数据库,如 PostgreSQL)。

from sqlalchemy importSequenceclassMyModel(Base):__tablename__ = 'my_table'id= Column(Integer, Sequence('my_sequence'), primary_key=True)
name
= Column(String(50))

在上述代码中,
Sequence('my_sequence')
定义了一个序列,SQLAlchemy 将使用该序列生成自增的
id
值。

通过这些步骤,你可以轻松处理整型自增
id
字段,SQLAlchemy 会自动为每个新记录分配唯一的自增
id

4、在插入记录的时候,对字符串的数据处理

在批量插入数据字典的时候,我希望根据用户输入内容(多行数据)进行转化,把每行的数据分拆进行判断,如果符合条件的进行处理插入。

在 Python 中,可以使用字符串的
splitlines()
方法来实现相同的功能。

#假设 Data 和 input.Seq 是从输入中获取的
Data = "example\nline1\nline2\n"  #示例数据
input_seq = "123"  #示例序列字符串

#将 Data 按行拆分,并移除空行
array_items = [line for line in Data.splitlines() ifline]#初始化变量
int_seq = -1seq_length= 3str_seq=input_seq#尝试将 str_seq 转换为整数
ifstr_seq.isdigit():
int_seq
=int(str_seq)
seq_length
=len(str_seq)#打印结果 print(f"Array Items: {array_items}")print(f"int_seq: {int_seq}")print(f"seq_length: {seq_length}")
  • Python 的
    splitlines()
    方法将字符串按行分割,同时自动处理各种换行符(包括
    \n

    \r\n
    )。
  • 列表推导式
    [line for line in Data.splitlines() if line]
    移除了空行,类似于 C# 中的
    StringSplitOptions.RemoveEmptyEntries
  • 使用
    str_seq.isdigit()
    检查
    str_seq
    是否全部由数字组成,这类似于 C# 的
    int.TryParse

在 Python 中,可以使用
re.split()
函数来按照正则表达式分割字符串。以下是对应的 Python 代码:

importre#假设 info 是一个包含 Name 和 Value 属性的对象
classInfo:def __init__(self):
self.Name
= ""self.Value= ""info=Info()#dictData 是输入的字符串 dict_data = "example_name example_value" #使用正则表达式按照空白字符分割字符串 array = re.split(r'\s+', dict_data)#赋值给 info 对象的属性 info.Name =array[0]
info.Value
= array[1] if len(array) > 1 elsearray[0]#打印结果 print(f"Name: {info.Name}")print(f"Value: {info.Value}")

使用
re.split()
函数根据空白字符(包括空格、制表符等)分割字符串
dict_data

r'\s+'
是一个正则表达式,表示一个或多个空白字符。

如果你需要根据多个分隔符来分割字符串,同样可以使用正则表达式(
re
模块)的
re.split()
方法。

str_item = "1,2,3;4;5/6/7、8、9;10"

importre

result
= re.split(r"[;,|/,;、]+", str_item.strip())print(result)

结果输出:['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']

解释:

  • re.split(r'[;,|/,;、]', text) 中的 r'[;,|/,;、]' 是一个正则表达式模式:
    [] 表示字符类,表示匹配字符类中的任意一个字符。
    ;,|/,;、 分别表示分号、逗号,竖线,中文逗号,中文分号,和空格,这些字符都将作为分隔符。

使用正则表达式可以灵活处理多个分隔符,适用于更复杂的分割需求。

《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,全书共分10章,第1章主要让读者认识数据资产,了解数据资产相关的基础概念,以及数据资产的发展情况。第2~8章主要介绍大数据时代数据资产管理所涉及的核心技术,内容包括元数据的采集与存储、数据血缘、数据质量、数据监控与告警、数据服务、数据权限与安全、数据资产管理架构等。第9~10章主要从实战的角度介绍数据资产管理技术的应用实践,包括如何对元数据进行管理以发挥出数据资产的更大潜力,以及如何对数据进行建模以挖掘出数据中更大的价值。

图书介绍:
数据资产管理核心技术与应用

今天主要是给大家分享一下第五章的内容:

第五章的标题为数据服务

内容思维导图如下:

本文是接着

《数据资产管理核心技术与应用》读书笔记-第五章:数据服务(一)

继续往下讲。

1.5、  数据服务的监控与告警

在完成了数据服务的配置后,数据服务在调用时,还需要进行监控,在监控到发生故障时还需要支持自动发送告警通知信息,这样才能更好的保障数据服务的稳定性。在书中的数据监控与告警那一章节中,有提到数据服务的监控与告警的技术设计实现主要是通过异步采集数据服务的调用日志,然后再配合Prometheus与Grafana来完成,如下图所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

从图中可以看到数据服务的监控与告警的关键在于数据服务的日志数据采集,这就意味着数据服务在被调用时,需要输出日志,为了让数据服务的监控更加准确和细致,日志在设计时,通常建议包含如下表6中的常见字段。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

字段名称

字段描述

appId

被调用的数据服务的ID,这个ID代表了具体的某个数据服务的身份

requestArgs

调用数据服务时,传入的请求参数

cliendIp

数据服务平台端获取到请求方的IP地址

requestTime

请求方调用数据服务时的时间戳,通常建议精确到毫秒

receiveTime

数据服务平台端接收到请求的时间戳,通常建议精确到毫秒

responseTime

数据服务平台处理完请求后的响应给请求方结果时的时间戳,通常建议精确到毫秒

queryDataDuration

数据服务平台在查询数据过程中的耗时时长

responseMessage

数据服务平台处理完请求后响应给请求方的响应结果

exception

数据服务平台在处理请求的过程中发生的异常信息,如果没有异常时,该字段会保持为空

在输出日志时,可以通过JSON的格式,将表格中的字段都包含进去,然后再通过日志采集的方式采集到这些JSON日志后发送到消息队列中供数据处理程序做日志数据的解析,之后再发送到Prometheus的Pushgateway组件中。

常见的日志采集工具如下表所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

日志采集工具

描述以及下载与部署地址

Flume

Apache 基金会下的开源项目,使用Java语言实现的日志采集工具,Github地址为https://github.com/apache/logging-flume

Logstash

基于Pipeline 实现的开源日志采集工具,Github地址为https://github.com/elastic/logstash

Fluentd

基于C/Ruby实现的可插拔开源日志数据采集工具,Github地址为https://github.com/fluent/fluentd

Splunk

非开源的商业性质的日志采集和处理以及存储工具,官方网址为http://www.splunk.com/

在通过采集获取到JSON的日志数据后,经过对日志数据的加工处理后,通常可以生成如下的核心指标数据用于监控,如下图所示。

  • 请求处理的耗时很长时,代表数据服务的处理很慢,此时需要检查是否是数据服务的处理能力或者服务器资源不够。
  • 请求中网络的耗时很长时,很可能是网络的带宽不够或者网络经常性的出现了抖动等,需要对网络链路进行排查。
  • 数据查询的耗时很长时,代表了查询数据库查询很慢,此时需要检查数据库中是否有慢查询或者是数据库的资源不够。
  • 发生异常的次数代表了请求处理中发生了异常,如果异常次数达到了一定的阈值,那就需要排查是数据服务出现了故障还是请求方的请求参数错误等。
  • 调用次数代表了请求方的调用量,也是衡量请求方请求并发是否很大的一个重要指标,如果调用量超过了数据服务的处理能力,需要及时增加资源进行扩容或者需要及时询问请求方为啥调用量会非常大的原因,同时也需要检查是否是数据服务受到了外部的恶意攻击导致的。

2、数据服务的性能

一个好的数据服务除了需要有好的设计外,还需要有好的性能,性能最直观的表现就是数据的查询能力,数据的查询能力越快,那么数据服务的性能肯定也会越好,通常情况下性能的优化主要体现在SQL优化、数据库优化、架构设计优化、硬件优化等几个方面,如下图所示。

  • (一)、SQL优化
    :这个很容易理解,就是提高SQL语句的查询性能,定位一个SQL查询性能的常用步骤如下图所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

从图中可以看到:

第一步需要尽快的找到性能查询慢的SQL语句,可以通过查询数据库的慢查询日志或者对数据库的查询做监控等方式来获取慢SQL,只有知道了慢SQL才好去做下一步分析。

第二步通过查看SQL语句在数据库中的执行计划来分析SQL语句查询慢的具体原因,一般来说,不管是什么类型的数据库,都可以查看到其执行SQL语句时的执行计划。

第三步是根据分析到的原因来对SQL语句做调优,常用的调优方式就是如果没有索引,那就增加索引,如果是有索引,但是没有命中索引,那就调整SQL语句的写法让其正确的命中相关索引。

  • (二)、数据库优化
    :当数据量确实达到了超高的数据量级,通过SQL优化不能解决问题时,就需要通过数据库优化来解决性能问题。数据库优化的常用方式包括使用缓存、读写分离、分库分表等,如下所示。

A、使用缓存:指的是数据库查询的缓存,将一些常用的热数据提前加载到缓存中,通常情况下是尽可能给数据库分配比较大的内存,让数据查询时,将数据加载到缓存中,那么下次查询时,就不需要 从物理存储中拉取数据了,如下图所示。

B、读写分离:读写分离是一种从数据库角度来进行的架构优化,当数据服务是“读多写少”时,数据库因为数据量太大,不能扛住高并发的查询时,可以采用读写分离的方式,让更多的数据查询从从库的只读节点来查询,如下图所示。

C、分库分表:分库分表是针对单表数据量过大时的一种常用解决方案,当数据量达到单表的瓶颈时,采用分表的方式来让数据重新分布。当数据量达到单库的瓶颈时,采用分库的方式来让数据重新分布,如下图所示。

分库分表的常用方式如下:

1)、按照冷热数据分离的方式:通常将使用频率非常高的数据称之为热数据,查询频率较低或者几乎不被查询的数据称之为冷数据,冷热数据分离后,热数据单独存储,这样热数据的数据量量就下降下来了,查询的性能自然也就提升了,如下图所示。

除了按照图所示的方式来做冷热数据分离外,随着硬件技术的发展,比如像内存价格的下降以及SSD固态硬盘的出现, 还可以按照如下图所示的方式来自动做冷热数据加载和分离,可以根据一定的规则来判断什么时候需要将普通硬盘中的数据预加载到SSD或者内存中,来加快数据查询的性能,由于SSD和内存中不能存储大量的数据,所以还需要设置一定的规则,将SSD和内存中不查询的数据定期清除来释放缓存的空间。

2)、按照时间维度的方式:可以按照实时数据和历史数据分库分表,也可以按照年份、月份等时间区间来进行分库分表,如下图所示,目的是尽可能的减少单个库表中的数据量。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

3)、按照一定的算法计算的方式:当数据都是热数据的情况下,比如数据确实无法做冷热分离,所有的数据都经常会被查询,并且数据量又非常的大。此时就可以根据数据中的某个字段做算法计算(需要特别注意这个字段一般是数据查询时的检索条件字段),使得数据能均匀的落到不同的分表中去,查询时再根据查询条件中的该字段做算法计算就可以快速的定位到是需要到哪个表中去进行查询,如下图。

4)、按照时间维度的方式:可以按照实时数据和历史数据分库分表,也可以按照年份、月份等时间区间来进行分库分表,如下图所示,目的是尽可能的减少单个库表中的数据量。

5)、按照一定的算法计算的方式:当数据都是热数据的情况下,比如数据确实无法做冷热分离,所有的数据都经常会被查询,并且数据量又非常的大。此时就可以根据数据中的某个字段做算法计算(需要特别注意这个字段一般是数据查询时的检索条件字段),使得数据能均匀的落到不同的分表中去,查询时再根据查询条件中的该字段做算法计算就可以快速的定位到是需要到哪个表中去进行查询,如下图所示。

  • (三)、架构设计优化
    :当SQL优化和数据库优化不能解决性能问题时,就需要考虑从架构设计上来进行优化,常见的架构设计优化手段如下:

1)、通过消息队列削峰填谷:在调用量的峰值非常大时,通过消息队列缓冲调用的请求,然后让请求异步的处理完后,再同步给请求的调用方,如下图所示。《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

2)、通过使用分布式数据库来进行处理,分布式数据库是数据库中一种MPP(Massively Parallel Processing的简写)的架构实现,常见的分布式数据库包括Doris(可以通过官网
https://doris.apache.org/
了解更多关于Doris的介绍)、Greenplum(可以通过官网https://greenplum.org/了解更多关于Greenplum的介绍)等。

3)、部署架构的优化,比如可以通过Kubernetes的方式来部署,因为Kubernetes可以支持动态的扩缩容, 在保障了数据服务的性能的同时,还可以通过弹性的伸缩来控制成本。

  • (四)、硬件优化
    :硬件优化的常用手段就是对硬件资源进行扩容或者提高硬件资源的性能,常见的手段如下:
    • (1)、使用I/O读写更快的硬件,比如使用SSD硬盘来替代普通的机械硬盘。
    • (2)、通过增加服务器的数量或者增加服务器的配置来对服务器进行横向或者纵向的扩容。
    • (3)、增加网络的带宽或者使用带宽更大网络设备来提高网络通道的传输速度。

未完待续......《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,作者为张永清等著

大家好,我是程序员鱼皮。我们团队做的程序员面试刷题网站
面试鸭
上线不到半个月的时候,就被百度等各大搜索引擎收录和推荐了!

效果显而易见,用户搜索 “面试鸭” 的时候,第一个看到的就是我们自己的网站,从而给网站增加流量:

关于如何让搜索引擎更快地收录网站,其实是一门很深的学问,还有一个专业术语叫 SEO。对于个人站长来说,搜索引擎的流量是至关重要的,大家应该都希望自己的网站能够被更多人看到吧?暂且不说收益,有个访问量大的网站,写简历和面试的时候也能吹一波牛皮~ 所以我建议程序员朋友们最好对 SEO 有一定了解。

今天这篇文章,鱼皮就以自己的面试鸭网站为例,给大家分享一些 SEO 实用技巧,让大家的网站都能更快地被搜索引擎收录。

也可以观看视频讲解,热乎的:
https://www.bilibili.com/video/BV1tz421i7Q1

鱼皮 SEO 干货分享

一、什么是 SEO?

SEO 全称为 Search Engine Optimization,即搜索引擎优化,使网站更容易被搜索引擎收录和呈现,可以让更多人通过百度、谷歌之类的搜索引擎发现你的网站,从而提升网站的访问量和知名度。

在学习如何进行 SEO 优化前,我们先简单了解 SEO 的流程,也就是:搜索引擎是如何发现你的网站并且让用户搜索到的?

二、SEO 的流程

整个 SEO 过程可以分为四个主要阶段:抓取、收录、索引和排名。下面我就来详细讲解一下这四个步骤。

1、抓取

抓取是 SEO 流程的第一步,搜索引擎会派出一群爬虫程序(俗称蜘蛛),它们会在互联网上到处爬行,访问各个网站,并抓取网页的内容。这些蜘蛛会按照链接从一个页面跳转到另一个页面,尽可能地遍历整个网站。

2、收录

抓取完成后,搜索引擎会对网页内容进行分析,并决定是否将该页面收录到它的数据库中。被收录的页面才能在用户搜索时显示出来,因此,确保页面被收录是 SEO 中的关键一步。有的网站虽然有很多链接和内容,但是只要搜索引擎的蜘蛛不喜欢、不收录,那么别人即使专门对着你的网站搜,都搜不出来。

3、索引

索引是指搜索引擎将已收录的网页内容进行整理和分类,并建立一个巨大的索引库。这个过程类似于给每个网页打上标签,以便在用户搜索时,搜索引擎能够迅速找到与搜索词相关的网页。

比如我们的面试鸭网站内容中包括:Java面试题库、前端面试题库、C++面试题库,那么这几个词就有可能被设置为索引。用户搜索内容时如果包含了这些词,就有可能搜索我们的网站。

4、排名

现在网站那么多,同样的索引也非常多,如何保证用户优先搜到我们的网站呢?这就涉及到 SEO 的最后一步 —— 排名。

当用户在搜索引擎中输入关键词时,搜索引擎会根据其算法,从索引库中挑选出最相关的网页,并
按照相关性、权重、网站质量
等因素进行排序,决定哪些网页出现在搜索结果的前几页。

这就是 SEO 的流程,我下面要分享的 SEO 优化技巧,也都是围绕这些流程展开。

三、如何进行 SEO 优化?

1、关键词优化

关键词是指用户在搜索引擎中输入的词汇,可以通过在网站的 HTML 头信息中设置关键词(Keywords)和描述(Description)等信息,来增加网站的索引、并提高网页在相关搜索中的排名。

关键词的选择需要精确并且跟网站内容强相关,避免过度堆砌关键词,以免被搜索引擎判定为作弊行为。

比如要做一个面试刷题网站,可以设置下列关键词和描述:

<meta name="keywords" content="程序员面试,Java面试题,程序员求职,计算机">
<meta name="description" content="程序员面试刷题,就来面试鸭,程序员免费求职面试刷题网站。海量高频Java面试题,帮你备战技术面试。">

2、网站结构优化

网站结构优化分为 2 点:整站页面结构优化和每个页面的内容结构优化。

对于整个网站,页面嵌套的层次应当尽量扁平化、尽量缩短页面层级,以减少爬虫抓取的难度。

举个例子,下面两种网站结构,你觉得哪种结构的页面更容易被爬虫全部访问到呢?

答案不言而喻,对于你想更快被搜索引擎发现的重要页面,应该尽可能缩短跳转到该网站的路径,并且适当多增加一些跳转到该页面的入口。

对于每个页面,应当有明确的层次结构,可以使用合理的标题标签(如一级标题
<h1>
)使页面内容更加易于索引。

3、友情链接

我大学刚开始做个人网站的时候,就是通过友情链接的方式来增加网站的权重(虽然效果有限)。操作方法很简单,你的网站上添加别人网站的链接,别人的网站添加你的网站的链接,你们双方的网站互相推荐,就更容易提升在搜索引擎中的排名。

友链背后的原理也很简单。很多搜索引擎都会按照权重对网站进行排名,权重是怎么计算的呢?一种很简单的算法(Page Rank),就是每个网站都有自己的票数,每有一个别人的网站添加了跳转到你的网站的友情链接,就相当于给你的网站投一票,得票数高的网站就能获得更高的权重和排名。友链就相当于互相投票,比 1 票都没有的网站要好。

当然,这种互相推荐的方式需要谨慎使用,避免过度交换链接,可能会导致权重分散。

4、Sitemap 文件

Sitemap 网站地图是一个列出了你网站所有页面的文件,通常放在网站的根目录下,或者通过 robots.txt 文件指定其位置。它可以帮助搜索引擎更快速地了解你网站的结构,并抓取你希望优先收录的页面。

就相当于你给爬虫发了一份地图,爬虫就不容易迷路,也不容易错过你网站的重要页面。

对于结构比较简单的网站,使用静态的、固定不变的 Sitemap 就足够了。如下图:

但对于内容持续更新的网站,还有更高级的操作,就是利用程序自动生成动态的 Sitemap,比如把每天新增的题目生成为一个 Sitemap 文件,便于让爬虫更快发现最新的内容。

此外,有些搜索引擎还支持主动上传提交 Sitemap 文件,可以进一步缩短网站被发现和收录的时间。

5、SSR 服务端渲染

注意,这里的 SSR 不是我们玩游戏抽卡说的那个!

SSR 服务端渲染是 SEO 最为有效的技术之一。是指在服务器端生成
完整的 HTML 页面
,并将其直接发送到浏览器。相比传统的前端 AJAX 动态请求数据的渲染方式,SSR 能够让搜索引擎更容易抓取到完整页面内容,从而提升 SEO 效果。

举个例子,如果是动态请求数据的前端网站,爬虫看到的网页内容可能是残缺不全的,如下图:

因为浏览器是从服务器拉取到网页后,再加载 JS 脚本,最后才发送请求来获取数据。

而如果使用了服务端渲染,服务器会完成对数据的请求,并且将数据拼装到页面里,再返回给前端,爬虫看到的网页内容更完整,如下图:

服务端渲染的效果虽然好,但也会加重服务器的压力,而且开发成本通常会更高。比如我们的面试鸭使用 Next.js 框架开发,开发过程中也踩了不少坑。

哦对了,用 PHP 来开发服务端渲染的网站是很方便的,这可能也是为什么以前 PHP 那么流行的原因之一。

6、SSG 静态站点生成

和 SSR 有点类似,SSG 是 SEO 优化的另一大杀器。是指在构建网站时,预先生成所有页面的
静态 HTML 文件
,并将其直接部署到服务器上。当用户访问网站时,直接获取到生成好的 HTML 文件即可,相比 SSR 服务端渲染,都不需要服务器再临时去请求数据了。

这种方法不仅大大提高了页面加载速度,而且使得搜索引擎能够更快、更完整地索引所有页面。所以很多博客站点生成器(比如 Hugo、VuePress、Hexo)都是将写好的文章打包生成为静态 HTML 后,再部署到服务器上。

当然,SSG 也不是银弹,适用于内容较为固定、更新频率较低的网站,如个人博客等。静态网站本质上就是一种缓存,如果网页内容频繁变化,就要频繁更新这个文件,也会有不小的成本。

所以我们可以想到一种更高级的策略:SSR + SSG 结合!内容相对固定的网页使用静态生成,内容变化的网页使用服务端渲染,不需要 SEO 的网页纯客户端渲染即可。

7、砸米

注意,以上方式并不保证绝对有效,只是增加搜索引擎收录和排名优化的概率,SEO 策略是需要持续调整和长时间验证的。

如果团队内没有懂 SEO 的技术人员,又想快速让自己的网站被搜索引擎推荐,那就只能 “砸米” 了,简单粗暴,就是花钱买广告,让你的网页在搜索结果的前几名出现。很多公司也都是这么做的,但对于没有收益的个人站长来说,还是老老实实用前面推荐的方式吧。


以上就是本期分享,其实还有更多非技术手段的策略,后面等我摸索明白了再给大家分享吧。如果有收获的话,记得关注鱼皮和点赞哦~

更多编程学习资源

组合逻辑电路

组合逻辑电路是数字电子学中一类基本的电路类型,它由一系列逻辑门组成,用于实现特定的逻辑功能。与时序逻辑电路不同,组合逻辑电路的输出完全取决于当前的输入信号,而不受之前输入的影响。换句话说,组合逻辑电路没有记忆功能,输出仅由当前时刻的输入决定。

组合逻辑电路的基本特点:

  1. 无记忆性
    :输出只取决于当前的输入状态,与过去的输入状态无关。
  2. 即时响应
    :一旦输入发生变化,输出也会立即做出响应。
  3. 稳定性
    :只要输入保持不变,输出也将保持不变。
  4. 可预测性
    :对于给定的输入,输出是可预测的。

组成部分:

组合逻辑电路通常由以下几种基本逻辑门构成:

  • 与门 (AND)
    :只有当所有输入都为高电平时,输出才为高电平。
  • 或门 (OR)
    :只要有任意一个输入为高电平,输出即为高电平。
  • 非门 (NOT)
    :对输入信号取反,即高电平变为低电平,低电平变为高电平。
  • 异或门 (XOR)
    :当输入信号不同时,输出为高电平;当输入信号相同时,输出为低电平。
  • 同或门 (XNOR)
    :与异或门相反,当输入信号相同时,输出为高电平;当输入信号不同时,输出为低电平。
  • 与非门 (NAND)
    :与门的输出取反。
  • 或非门 (NOR)
    :或门的输出取反。

应用示例:

  1. 加法器 (Adder)
    :用于执行二进制加法运算。
  2. 编码器 (Encoder)
    :将一组输入信号转换为相应的代码。
  3. 解码器 (Decoder)
    :将代码转换为一组输出信号。
  4. 多路复用器 (Multiplexer)
    :从多个输入信号中选择一个输出。
  5. 去多路复用器 (Demultiplexer)
    :将一个输入信号分配给多个输出通道。
  6. 比较器 (Comparator)
    :用于比较两个数字的大小。

设计过程:

设计组合逻辑电路通常涉及以下几个步骤:

  1. 定义功能
    :明确电路的功能需求。
  2. 真值表
    :创建一个真值表来列出所有可能的输入组合以及对应的期望输出。
  3. 布尔表达式
    :根据真值表推导出布尔表达式。
  4. 简化表达式
    :使用布尔代数规则简化布尔表达式。
  5. 逻辑门实现
    :将简化的布尔表达式转换为具体的逻辑门电路。

实现工具:

现代设计过程中常常使用计算机辅助设计(CAD)工具来进行电路设计和模拟,以确保电路的功能正确性和性能优化。

总结:

组合逻辑电路是数字电路设计的基础,广泛应用于各种电子设备中。它们简单、可靠,能够高效地完成特定的逻辑运算任务。

时序逻辑电路

时序逻辑电路是数字电子学中另一类重要的电路类型,与组合逻辑电路不同,时序逻辑电路不仅考虑当前时刻的输入信号,还会根据电路内部的状态信息来确定输出信号。这意味着时序逻辑电路具有存储功能,可以记住过去的状态信息,并将其与当前输入结合来决定输出。

时序逻辑电路的基本特点:

  1. 存储性
    :时序逻辑电路包含存储元件(如触发器、寄存器等),能够存储数据或状态。
  2. 时钟控制
    :大多数时序逻辑电路的工作受到时钟信号的控制,即只有在时钟信号的上升沿或下降沿到来时,电路才会更新其内部状态。
  3. 动态行为
    :时序逻辑电路的行为随时间变化,其输出不仅取决于当前的输入信号,还取决于电路内部的当前状态。
  4. 复杂性
    :相比组合逻辑电路,时序逻辑电路通常更复杂,设计和分析也更为复杂。

时序逻辑电路的组成部分:

时序逻辑电路通常由以下几部分组成:

  • 触发器 (Flip-flops)
    :基本的存储单元,可以保存一个二进制位的信息。
  • 寄存器 (Registers)
    :由多个触发器组成,用于存储多位的数据。
  • 计数器 (Counters)
    :能够根据时钟信号进行递增或递减操作。
  • 状态机 (Finite State Machines, FSMs)
    :一种高级形式的时序逻辑电路,根据输入信号和当前状态来决定下一个状态及输出信号。

应用示例:

  1. 寄存器
    :用于暂时存储数据,例如 CPU 中的通用寄存器、指令寄存器等。
  2. 计数器
    :用于实现计数功能,如时钟计数器、地址计数器等。
  3. 有限状态机 (FSM)
    :用于实现复杂的控制逻辑,如微处理器中的控制单元、通信协议控制器等。

设计过程:

设计时序逻辑电路通常涉及以下几个步骤:

  1. 定义功能
    :明确电路的功能需求。
  2. 状态图
    :绘制状态图来表示电路的各种可能状态及其之间的转换。
  3. 状态编码
    :为每一个状态分配一个二进制代码。
  4. 状态方程
    :根据状态图和状态编码,推导出状态方程。
  5. 输出方程
    :根据状态图和状态编码,推导出输出方程。
  6. 逻辑门实现
    :将状态方程和输出方程转换为具体的逻辑门电路。

实现工具:

现代设计过程中通常使用硬件描述语言(HDL)如 VHDL 或 Verilog 进行电路的设计与描述,并借助计算机辅助设计(CAD)工具进行仿真、综合和布局布线等步骤。

总结:

时序逻辑电路是现代数字系统的核心组件之一,广泛应用于微处理器、存储器、通信系统等各种电子设备中。它们通过存储状态信息,使电路能够在不同的时间点上执行复杂的逻辑操作,从而实现更高级的功能。

Combinational Logic Loop

组合逻辑环(Combinational Logic Loop)是指在一个数字电路或逻辑设计中出现的一种错误配置,其中存在一个反馈回路,使得信号不断地循环反馈给自身而没有明确的稳定状态。这种环路通常会导致电路无法达到稳定的输出状态,从而导致功能上的错误。

组合逻辑环的特点:

有了上述基础知识,就不难理解组合逻辑环路了

  1. 无稳定状态
    :组合逻辑电路应该基于输入产生确定的输出,但当存在环路时,输出会依赖于之前的输出状态,形成一种反馈机制,使得输出无法稳定下来。
  2. 无限循环
    :信号会在环路内部不断循环,理论上永远不会停止,除非外部条件改变或电路断开环路。
  3. 竞态条件
    :由于信号不断地在环路内循环,可能会导致竞态条件,即信号到达的时间顺序不同,导致不同的结果。

组合逻辑环的原因:

  • 直接连接
    :两个或多个逻辑门直接相互连接形成环路。
  • 通过外部电路间接形成
    :虽然每个单独的部分都是正确的组合逻辑,但是它们之间的连接方式形成了一个环路。

避免组合逻辑环的方法:

  1. 仔细设计
    :在设计电路时避免任何可能形成环路的连接。
  2. 使用时序逻辑
    :如果需要存储状态或反馈,应使用时序逻辑(如触发器)而不是组合逻辑。
  3. 验证设计
    :使用逻辑仿真工具来验证设计中不存在环路。

示例:

假设有一个简单的组合逻辑电路,包含1个非门(NOT gates),它的输出连接到自己的输入。这样的设计就构成了一个组合逻辑环路,因为非门的输出会直接影响其输入,形成一个无限循环的状态,无法确定其最终状态。

总结:

组合逻辑环是一种电路设计错误,通常需要避免。在数字电路设计中,正确的做法是使用时序逻辑(如触发器、寄存器等)来处理需要存储状态的情况,而不是让组合逻辑元件形成环路。