2024年11月

大家好,我是汤师爷~

今天聊聊
商品概念模型设计。

优秀的商品概念模型应具备充分的灵活性和抽象性,以适应不同行业的需求变化,并在系统升级或业务调整时,能最小化重构的工作量。

商品模型是商品管理系统的核心,整体来看,可以划分为三个关键部分:

  • 基础资料:用于定义和管理商品的基本要素,是商品的“元数据”。包括但不限于品牌、类目、属性库、单位等关键信息。通过标准化这些基础资料,可以确保整个系统中商品信息的一致性和标准化。
  • 商品主档信息:这部分是商品的核心描述,包含关联的基础资料信息,以及商品的一些描述信息,例如名称、副标题、图片、编码、条码、商品描述、销售配置、供应链配置、财务配置等。
  • 渠道差异化信息:在全渠道零售环境中,这部分非常重要,它可以针对不同渠道提供个性化配置,例如渠道特定的价格、销售配置等。

基础资料

接下来,我们将详细介绍商品系统的基础资料。它包括商品类目、属性库和多单位等重要组成部分,这些元素共同构建了商品的基本框架。

1、商品类目

商品类目是一种系统化的分类方法,用于组织和管理各种商品。它为商品提供了一个层次化的结构,使商品能够被有效地归类、检索和管理。

商品类目通常采用树状结构,从顶层的大类逐步细分到具体的小类。每个层级都代表了商品的不同特征或属性。合理的类目管理能显著提升商品管理效率:

  • 对于消费者
    :类目导航优化了商品发现的路径,让用户能快速找到目标商品。
  • 对于商家
    :类目为商品提供了标准化的组织方式,帮助运营人员快速筛选、定位商品,支持库存管理和销售分析等操作。

商品类目通常分为前台类目和后台类目,以满足消费者和商家的不同需求。

前台类目面向消费者,优化了商品浏览和搜索体验。其核心特征是灵活性,根据用户购物习惯、促销活动和市场趋势动态调整。如图所示。

通过这样的调整,前台类目能够快速响应市场需求,提升用户购物体验和转化率。

后台类目主要服务于商家,为商品管理和数据分析提供稳定的框架。与前台类目的灵活性不同,后台类目结构相对固定,变更频率低。例如,按类目统计某一季度的商品销售额;通过后台类目筛选滞销商品,制定清库存策略。

2、属性库

属性库通过集中化管理,为商品搜索、分类和分析提供了坚实的基础,同时简化了复杂商品属性的维护流程。

商品属性,又称为产品属性、商品参数,是产品本身固有的特征。

不同行业的商品,差异非常大,有很多行业差异化属性。根据使用目的、用途不同,演化出各式各样的属性,有的用于展示,有的用于分析,有的用于经营管控。下面根据商品属性不同的分类法,逐一展开描述:

  • 描述属性:包括商品名称、商品描述、规格、型号、产地、等级、生产厂商、商品图片等。这些属性主要用于向消费者展示商品的基本信息。
  • 统计属性:品牌、类目、系列、款式、适用人群、适用年龄等。这类属性为商品数据统计和分析提供依据,例如统计某品牌的月销售额。
  • 考核属性:一般用于组织业绩考核。例如,基于品牌、分类或系列统计的销售额,用于评估部门或员工的业绩。
  • 物流属性:长、宽、高、净重、毛重、重量单位等。这些属性影响配送成本和仓储规划。例如,大件商品需要特殊的仓储和配送方案。
  • 管控属性:是否季节商品、是否有保险、是否支持配送、是否支持打折、是否保质期管控、是否串码管理等。这类属性为运营管控提供支持,例如控制保质期商品的销售策略。
  • 销售渠道属性:针对不同销售渠道的特殊属性。例如,美团、饿了么平台上商品的最小购买数量或平台分类。
  • 规格属性:该属性是组成SKU的特殊属性,直接影响 SKU 的生成,例如衣服的颜色、尺寸等。这类属性不仅影响消费者购买决策,也直接关系到商家的库存管理。

为了避免属性重复创建,同时提高管理效率和数据一致性,通常会建立一个统一的属性库。

属性库的结构由三部分组成:属性组、属性项、属性值。

  • 属性组:
    顶层分类,用于按属性的共性特征管理属性。例如,手机的属性组可包括“外观属性”(颜色、材质)和“性能属性”(处理器、运行内存)。
  • 属性项:
    具体的属性名,用于定义商品的某个特征,例如颜色、尺码、口味等。
  • 属性值:
    属性的具体内容,例如“颜色”的值为红色、绿色、蓝色等。

3、多单位

在零售场景中,不同消费者对商品的计量需求千差万别。例如,消费者希望按瓶购买饮料,而企业客户则希望按整箱下单。多单位功能为这一需求提供了灵活解决方案。

这种灵活性体现在多个方面:

  • 销售多样性:例如,一种饮料可以按瓶购买,也可以按整箱(含多瓶)购买。
  • 库存精确管理:商家可以同时跟踪单品和批量单位的库存,提高库存管理的精确度。
  • 定价策略优化:可以为不同单位设置不同价格,如单瓶价格和整箱优惠价,刺激消费。
  • 物流效率提升:支持按不同单位发货,优化仓储和配送流程。

在实现层面,多单位功能需要以下概念模型支持:

  • 单位:是商品的计量标准,如件、盒、瓶、公斤等。多单位功能则允许一个SKU支持多种计量方式。
  • 单位转换关系:为每种商品定义单位间的转换关系(如1箱=12瓶),并在库存管理、物流发货中支持动态换算。

多单位功能不仅满足了不同消费者的购买需求,还为商家提供了更灵活的经营策略,同时简化了库存管理流程,提高了整体运营效率。

商品主档信息

在介绍完商品系统的基础资料后,我们将深入探讨商品主档信息。

1、商品

商品指商家在零售环境中提供的具体产品或服务,旨在满足消费者的多样化需求。例如,在服装行业,一件商品可能有多种颜色和尺码的规格供消费者选择;而在生鲜行业,商品可能按照重量或数量进行销售。

商品的多样性不仅体现在种类上,还体现在规格和属性特征上,这些共同构成了商品在零售系统中的完整定义。

2、SKU

SKU(Stock Keeping Unit)是库存量单位,也称为最小库存单元,是库存管理的基本单位。

SKU 明确定义了具体商品的规格属性值。例如,"iPhone 16"这款商品的关键规格包括颜色(黑色、红色、银色、金色)和容量(128G、256G、512G),可以组合出 4×3=12 个 SKU。

之所以称为"最小库存单元",是因为 SKU 是库存管理中的实际管理对象。每个 SKU 都有明确的规格、价格、库存和条形码,是不可再细分的管理单位。无论是商品的采购、入库、销售、出库还是库存盘点,系统跟踪的对象都是 SKU。

3、商品类型

在新零售业务中,商品种类繁多,为了更高效地管理商品数据,需要将商品进行类型划分。商品类型不仅影响库存管理和交易方式,也直接决定消费者的购买体验。

  • 实物商品:以有形实体存在,不能通过网络来传递,必须依赖传统的物流运输系统来传递。例如,鸡蛋、大米、手机等。
  • 服务商品:能够实现交易的无形商品,无需物流参与,就能完成交易,例如,话费充值、游戏点券、线上课程等。
  • 组合商品:组合商品是由多个单独售卖的商品组成的捆绑销售商品,例如:下午茶套餐(包含咖啡、蛋糕、小食)、七夕美妆组合(包含口红、香水、护肤品)等。
  • 多规格商品:多规格商品是由多个 SKU 组成的商品集合,消费者只能选择其中一个 SKU。例如,以iphone16为例,关键规格有颜色(黑色、红色、银色、金色)、容量(128G、256G、512G),消费者选中了”黑色128G的iphone16“进行下单。

4、商品状态

商品状态是商品生命周期管理的核心,贯穿商品从创建到退市的全过程。在新零售系统中,商品状态用于标识商品在业务流程中的具体阶段,不同的状态对应不同的管理和运营策略。

商品的生命周期状态包括建档、新品、正常、预淘汰、淘汰、清理、待归档等。

  • 建档:
    商品信息初次录入系统,完成基础数据的创建,包括名称、类目、品牌、规格和价格等。这一阶段的商品尚未对外展示,仅供内部审核和信息完善,确保商品上线前具备完整的基础信息。
  • 新品:
    商品审核通过并正式上线,进入市场的初期阶段。此时通常会通过新品标签、首页推荐等方式进行重点推广,并配合首发折扣或赠品活动,吸引消费者关注。
  • 正常:
    商品进入稳定销售阶段,成为商家常规运营的一部分。此状态下的商品以正常定价销售,同时可能参与常规促销活动(如满减、限时折扣),以维持销量。
  • 预淘汰:
    商品销量下降或市场需求减弱,逐步退出核心销售渠道。在此阶段,商家减少库存补货,并通过特定促销活动(如清仓折扣)加速库存清理。
  • 淘汰:
    商品已停止销售,但库存尚未完全清理完毕。此状态的商品通常从前台下架,仅通过特定渠道(如线下门店或促销专区)进行有限销售,直至库存耗尽。
  • 清理:
    商品进入最终清仓阶段,彻底准备退市。商家集中处理剩余库存,通过降价促销、库存转移或销毁计划等手段清空库存,确保资源优化配置。
  • 待归档:
    商品生命周期结束,进入历史归档阶段。在此状态下,商品不可编辑或销售,但其数据被保留,用于查询和分析历史销售记录,为业务决策提供数据支持。

渠道差异化信息

在全渠道零售环境中,不同销售渠道的用户特征和需求各异。商家需要通过渠道差异化策略,灵活调整商品展示、价格体系及运营方式,才能更高效地满足消费者需求。

1、渠道级商品与 SKU 管理

为适应多渠道需求,新零售体系采用多层次的商品管理结构:

  • 商品库
    :作为企业商品信息的主数据,集中存储和管理所有商品数据,确保信息的一致性。
  • 渠道级商品
    :针对不同渠道(如微信商城、美团外卖、饿了么外卖、抖音、小红书等渠道)设置商品差异化信息。

2、多维度商品价格策略

渠道差异化管理的核心在于灵活的定价策略,通过多维度的价格体系满足不同场景需求:

  • 指导价
    :厂商建议的零售价格,为商家提供定价参考。
  • 渠道价格
    :根据渠道特性制定差异化定价,如外卖平台因配送成本较高,售价通常高于线下门店。
  • 日历价格
    :针对不同时间段制定动态定价策略,例如早餐时段的特价优惠。
  • 成本价
    :精确到 SKU 的单品成本,作为统计利润的基础依据。

3、渠道差异化的其他关键信息

其他的一些渠道相关的差异化信息:

  • 渠道销售状态
    :控制商品的上下架状态。例如,某商品可能仅在特定平台渠道展示,而其他平台隐藏。
  • 配送方式
    :支持快递、同城配送、自提等多种配送方式。例如,社区团购用户更倾向于自提,而电商用户更依赖快递配送。
  • 其他销售设置
    :设置购买数量上限、销售时段等规则。例如,通过限定销售时段实现高峰期促销,或设定单次购买数量上限避免库存被抢购。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

工作电脑为一体机。所有的USB接口都在屏幕的后面。插拔U盘极不方便。于是搜索USB小物件,看能不能通过小物件将USB的接口延长到屏幕前

寻觅一番,找到一个中意的小物件——
ELEKS MAKER 极客桌面控制器

如上图所示,小物件有着朋克风,充满现代感。带有3个USB2.0接口,日常工作使用足够。另外还有三个小按键,以及一个大旋钮。大旋钮可以旋转,也可以当按钮用。

按照使用说明,大旋钮的旋转是控制系统的音量,大旋钮的按键是切换系统是否静音。

也可以通过控制程序,来自定义大旋钮按键的功能。

于是,有了个想法,
将大旋钮的的按键,自定义为锁屏
。当我离开工位,按一下大旋钮,就将工作电脑锁屏了。

于是,打开控制程序,如下图

可以看出,控制程序分别对
三个按钮

一个大旋钮
的功能进行了设定的功能

但是
!对“按下按钮”的这个动作,只能指定多媒体键,也就是在若干个指定的多媒体键中间选择一个。并不能选择其他的快捷键。比如,锁屏的快捷键是Win+L,却是没法设定。

于是,在网上搜索一番。找到了两篇有用的文章

【转载】修改Windows下键盘按键对应功能的一些方案

Win10 64位电脑如何以桌面快捷方式创建一个一键锁屏程序?

第一篇文章,讲解了,如何修改
多媒体键
对应的功能

如下面所示,通过注册表,将
计算器多媒体键
的功能改为指向记事本(notepad.exe)

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AppKey\18]
"ShellExecute"="notepad.exe"

其中,ShellExecute字段指向其他程序。

也可以用Association字段指向扩展名,调用该扩展名对应的程序。

例如:

"Association"="mailto",表示调用默认的邮件程序

"Association"=".doc",表示调用默认的doc的程序,一般是Word

第二篇文章,讲解了,如何通过快捷方式,设定锁屏

在快捷方式下,通过

rundll32.exe user32.dll,LockWorkStation

调用系统的锁频程序

于是,灵光一闪,将上面两篇文章的内容合二为一

1、将按下按钮的多媒体快捷键改为“计算器”

2、编写注册表,将计算器多媒体键的对应的内容改为指向锁屏。

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AppKey\18]
"ShellExecute"="rundll32.exe user32.dll,LockWorkStation"

将注册表导入到系统内之后

此时,按小物件的大按钮,电脑立刻锁屏!

相信不少人和我一样,是从“
汉语新解
”这段爆火提示词中知道李继刚这位“神人”的。直到看到11月4日公众号
“数字生命卡兹克”
对继刚做了专访文章
《专访"Prompt之神"李继刚 - 我想用20年时间,给世界留一句话》
,让我初步了解到继刚其实是一位有理想、爱读书、善思考的人。这激发了我的强烈好奇心和沟通欲,于是11月13日我专程前往北京,约继刚进行了一次深度面谈。别具一格的是,作为赠送给“粉丝”的签名纪念,也是他刚使用Claude生成的一句话:
“写越简,用越广;删越多,存越精”

还是熟悉的地方(望京·聚宝源),还是熟悉的话题(提示工程),期间继刚思绪翻飞、滔滔不绝,完全不像一个“i人”,从这也能看出他对这个领域的专注与热爱,以至于饭后大家都意犹未尽,对我个人来说也是受益良多,故记述此文,以享诸位。

1. 提示词的本质

饭桌上火锅的温度刚上来,隐约中冒着热气,二人略作寒暄,我们便直入主题了。

继刚首先问:“你觉得提示词的本质是什么?”,并追加道:“我花了半年多的时间,终于把这个问题想明白了。”

“这个问题我尝试想过,但确实未做过深度思考,可能短时间也很难有确定结论。”,我略显尴尬并如实回复说。

“无妨,你放开想、敞开说,即便不对也没有关系,这个思考过程也很重要。”,继刚安慰我道,希望我不要有额外的压力。

我略作沉吟,细想了一会说道:“可能我现在还没法用合适的语言组织起来,但是可以用一些词描述我的理解,如自然形式的编程语言、大模型的解压密码、大模型推理方向的向导诸如此类。”

看我已绞尽脑汁,继刚决定不再“折磨”我了,回应我说:“你说的这些主要在描述提示词是什么,还是停留在表象,并非提示词的本质。就像射向标靶的无数根长矛,每根矛都指向靶心,但矛本身并不是靶心。”

“在我看来,提示词的本质,是表达。”
,继刚直接说出了他的结论。

提示词的本质

相信大多未对提示词本质做过相关思考的人,可能和我是相似的反应,面对这个结论仍是一脸懵逼,得其形,而不知其义。有一定的哲学基础的人,很清楚这就是继刚所理解的提示词的“道”,或者说是他认为的提示词的“第一性原理”。但是知“道”,并不意味着可以践“道”。

正如《道德经》所述:

有物混成,先天地生。……独立而不改,周行而不殆,……吾不知其名,强字之曰道,强为之名曰大。……

道就摆在那里,一直存在,但想做到知行合一并不容易。你可能理解“表达”是什么含义,但不一定能理解“提示词所表达的表达”是什么含义。道过于抽象,需要利用“实现”去对其进一步剖析,继刚给出了表达的第一步解析。

表达的含义解析

简而言之,本意是人的脑海中的想法,表示想做什么。文意是本意的符号化(提示词),表示想法如何描述。解意是让大模型理解人的想法,表示想法如何解读。通过这三个阶段的拆解,可以细致地还原提示词从人到大模型的过程,也就是提示词的本质。

提示词的目的是把人脑海中的想法(本意),精准无误地提供给大模型去理解(解意)。优秀的提示工程师善于通过控制提示词(文意),缓解本意与解意之间的差距。而首当其冲的,就是如何精准地描绘本意,告诉大模型,你到底想做什么?然后才是优化文意,尽量把本意无损的传递给大模型,也就是优化提示词。最后才是大模型,虽然不能通过提示词提升大模型的理解能力,但可以选择理解能力更好的大模型。

2. 如何清晰表达

继刚花了一年的时间去琢磨如何清晰地描述脑海中的想法,也就是如何描绘本意。

精准表达的方法

首先是经验,经验是想法的具象化,这是人理解想法的基础,没有体感经验的想法是空中楼阁。其次是词汇,词汇是经验在语言上的映射,是想法的符号化形式。最后是知识,知识是对词汇含义的详细解读和描述,是想法符号化为精确词汇的基础。有了以上的基础,才可以准确地表达脑海中的想法,实现清晰表达。

3. 怎样提升效果

分析如何提升大模型的问答效果,继刚给出了这样的思考。

大模型效果提升的方法

  • 首先要认清的是,大模型是放大器,不是许愿器。种什么因,结什么果,提示词的的输入直接影响大模型的输出,这里对应解意的部分。

  • 其次,要定义清楚要解决的问题或任务是什么,描述清楚本意。这里继刚给了一个非常形象的描述:“人要比AI凶”。说白了就是不要惧怕AI,要有信心操纵好AI,有种“战略上要藐视敌人”的意思。

  • 最后才是提示词,通过文本把想法表达出来,对应的就是文意。提示词要足够精准,有助于大模型在Embedding语义空间的准确定位。提示词要足够简练,有助于大模型Attention机制实现重点意义的关联。

4. 提示工程方法论

以上,是继刚总结的提示工程的“道”,接下来描述提示工程的“术”,即如何写好提示词。

4.1 乔哈里视窗

提示词到底怎么写?乔哈里视窗本是关于沟通技巧的理论,继刚巧妙地将其迁移到人与大模型沟通的场景下,描绘了提示工程的基本沟通框架,有点“见人说人话,见鬼说鬼话”的莫明体感。

乔哈里视窗

静态来看,视窗中的一、四象限所描述的是大多数人比较熟悉的方式,第二象限对提问的能力有比较高的要求,需要人不断地学习(可参考:
问题之锤
),第三象限需要人和大模型协同探索未知的领域和边界。

动态来看,未来大模型知道的会越来越多(X轴不断下移),那么对人的每个个体来说,如何丰富自身的认知边界就显得非常重要了(控制Y轴)。

最后,针对不同的象限,可以灵活地调节提示词的描述方式,从小到大,从简到繁地优化提示词的整体状态(Debug提示词),这就是提示工程的基本逻辑。

4.2 极致压缩

继刚对自己的提示词风格做过总结,去年他致力于提示词的清晰表达,而今年则专注于提示词的压缩表达。

提示词怎么简化?对于大模型来说,最容易理解的符号是向量,既而是token、单词、句子等,而人则反之。显然,单词是人和大模型沟通中最高效的形式。而作为将函数作为一等公民的LISP语言(首个函数式语言),代码形式与数据形式完全相同,这种高度的简洁性设计极度适合充当单词之间的“粘合剂”,构建最极致压缩的提示词表示,简直是天作之合!

提示词的极致压缩

虽然这是继刚最初的个人猜想,但经过无数的实验验证,大模型(尤其是Claude)具备理解这种提示词形式的能力,真正做到了《庄子》中所说的“得其意,忘其言”,妙到毫颠!

荃者所以在鱼,
得鱼而忘荃
;蹄者所以在兔,
得兔而忘蹄
;言者所以在意,
得意而忘言

4.3 点亮星星

那么,如何找到最合适的单词呢?既然大模型具备理解单词的能力,那么选用哪个单词就是很关键的问题。如前面拆解本意时提到的“词汇”与“知识”概念,尽量选用词汇的定义而非词汇的描述(可参考:
定义之矛
),让提示词中的单词“直击本质”。

这件事情说起来容易,做起来一点也不简单……

《这就是ChatGPT》插图:Word向量空间

继刚常用“点亮星星”的比喻(可参考:
类比之弓
)来描述自己寻找本质词汇的过程。

想象我在一间没有灯光的屋子里(向量空间),周围都是黯淡无光的星星(单词向量),我可以喊星星的名字去点亮星星,当我按照顺序点亮星星时,它们之间的连线构成了一个星象图,大模型可以理解这个星象图的模式含义并做泛化输出,当我写提示词时,我的脑海其实在放烟花。

类比之弓:Embedding

4.5 Read in. Prompt out.

最后,怎么写好提示词呢?大家应该可以看到,丰富的知识积累、深度的词汇理解、成熟的工程素养,对于写好提示词都至关重要,这里没有捷径,套用《卖油翁》中的话,可以表述为:“无他,惟读书尔”。多读、多思、多写,每个人才能悟出自己的提示词之“道”。

5. 提示词工程师

再回头看提示词工程师这个角色,他是一个交叉领域的岗位。借用继刚的原话:“提示词工程师,既要有提示词的写作能力,又要有工程师的素养,谜底就在谜面上。”

提示词工程师的画像

热爱协作的技术人,或者喜欢技术的创作者,将是提示词工程师的最佳人才画像。王小波必然是创作者的典型,而技术人中,有一类角色也将十分契合,他们叫“开源布道师”……

6. 尾记

13日和继刚聊完后,脑子一直处于发热的状态,14-15这两天又赶上全球机器学习大会,开启了“疯狂社交”模式,根本无暇整理思路。比较巧合的是,16日PEC 2024(提示工程峰会)继刚又给了《提示词的道与术》的演讲,主题与我们面谈的内容基本一致,所以文章我也直接引用了他演讲PPT中的内容作为辅助素材。

建了一个小群,大家一块聊聊提示词技术,感兴趣的同学可以进群保持关注。致未来优秀的提示词工程师们,一起加油!!!

窗外灯光点点,总算对这部分的心得做完了细致整理,还被家里人偷拍深夜码字的状态……

另外,大家也可以直接关注云中江树的“结构词AI”公众号,找到
“Prompt设计的艺术与构建AI原生产品”
分论坛的直播回顾视频直接观看(第47min开始)。

7. 参考资料

  1. 专访"Prompt之神"李继刚 - 我想用20年时间,给世界留一句话:
    https://mp.weixin.qq.com/s/JT2oOG2SYw2pDYEHlEmcyQ
  2. Claude Prompt: 汉语新解:
    https://mp.weixin.qq.com/s/7CYRPFQxi37ONTlX0hfzRQ
  3. Claude Prompt:问题之锤:
    https://mp.weixin.qq.com/s/KlkomVKEYKjVAb6NEXcjSg
  4. Claude Prompt:定义之矛:
    https://mp.weixin.qq.com/s/eNcqU-_-8SMpVBXAcgeQRQ
  5. Claude Prompt:类比之弓:
    https://mp.weixin.qq.com/s/p1viD22cPtD3iLzOIb_FMg
  6. 关于说话的一切:
    https://weread.qq.com/book-detail?type=1&senderVid=4000012&v=10132d20813ab77a6g012034
  7. 为什么伟大不能被计划:
    https://weread.qq.com/book-detail?type=1&senderVid=101531&v=0bf32020813ab7e6bg016510
  8. 深度学习的数学:
    https://weread.qq.com/web/bookDetail/01d327c071a122c701d71f3
  9. 拐点:
    https://weread.qq.com/web/bookDetail/08732220811e7ef55g012f82
  10. GPT图解大模型是怎样构建的:
    https://weread.qq.com/web/bookDetail/e0d32f10811e7ee55g010619
  11. 这就是ChatGPT:
    https://weread.qq.com/web/bookDetail/74332a90813ab86c4g019d98

如果多个实体类都有
isDelete
字段,并且你希望在插入时为它们统一设置默认值,可以采取以下几种方法来减少代码重复:

1. 使用基类(抽象类)

创建一个基类,其中包含
isDelete
字段和
@PrePersist
方法。然后让所有需要这个字段的实体类继承这个基类。

示例代码:

import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;

@MappedSuperclass
public abstract class BaseEntity {

    protected Integer isDelete;

    @PrePersist
    public void prePersist() {
        if (isDelete == null) {
            isDelete = 0; // 设置默认值为0
        }
    }

    // Getter 和 Setter
    public Integer getIsDelete() {
        return isDelete;
    }

    public void setIsDelete(Integer isDelete) {
        this.isDelete = isDelete;
    }
}

然后在其他实体类中继承
BaseEntity

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class MyEntity extends BaseEntity {

    @Id
    private Long id;

    // 其他字段、getter 和 setter
}

2. 使用 AOP(面向切面编程)

通过 Spring AOP 创建一个切面,在插入操作时检查并设置
isDelete
的默认值。这种方式不需要修改每个实体类,适合大规模应用。

示例代码:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.lang.reflect.Field;

@Aspect
@Component
public class DefaultValueAspect {

    @PersistenceContext
    private EntityManager entityManager;

    @Before("execution(* com.example.repository.*.save(..))") // 根据你的仓库路径调整
    public void setDefaultValues(Object entity) throws IllegalAccessException {
        Field[] fields = entity.getClass().getDeclaredFields();
        for (Field field : fields) {
            if ("isDelete".equals(field.getName())) { // 检查字段名
                field.setAccessible(true);
                if (field.get(entity) == null) {
                    field.set(entity, 0); // 设置默认值为0
                }
            }
        }
    }
}

3. 使用 JPA 审计功能

使用 Spring Data JPA 的审计功能,通过实现
AuditorAware
接口来统一处理审计字段,包括 createdBy,createdTime,updatedBy,updatedTime等,而
isDelete
这种状态字段在审计注释中并没有实现。

4. 使用事件监听@EntityListeners

JPA 提供了事件监听器的功能,你可以定义一个事件监听器来处理所有需要设置默认值的实体类。

示例代码:

import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.EntityListeners;

public interface DeletedField {

  	Integer getDeletedFlag();

	void setDeletedFlag(Integer deletedFlag);
}

public class DeleteDefaultValueListener {

	@PrePersist
	public void setDefaultValues(DeletedFlagField deletedFlagField) {
		if (deletedFlagField.getDeletedFlag() == null) {
			deletedFlagField.setDeletedFlag(0); // 设置默认值为0
		}
	}

}

@EntityListeners(DefaultValueListener.class)
@Entity
public class TableUserAccount extends EntityBase implements DeletedFlagField {

  	/**
	 * 删除标识(逻辑删除),1删除 0未删除
	 */
	@Column(name = "deleted_flag")
	private Integer deletedFlag;
}

5. 扩展JPA,对审计字段建立者和更新者的扩展

  • CreatedByField 建立者字段接口
  • UpdatedByField 更新者字段接口
  • CreatedByDefaultValueListener 建立者字段监听器
  • UpdatedByDefaultValueListener 更新者字段监听器
  • AuditorAwareImpl 审计接口,返回当前用户

CreatedByField

public interface CreatedByField {

	String getCreatedBy();

	void setCreatedBy(String createdBy);

}

扩展EntityBase实体,不使用默认的
CreatedBy

LastModifiedBy

@Getter
@Setter
@MappedSuperclass
@EntityListeners({ AuditingEntityListener.class, UpdatedByDefaultValueListener.class,
		CreatedByDefaultValueListener.class })
public abstract class EntityBase implements Serializable, CreatedByField, UpdatedByField {

	/**
	 * 创建人
	 */
	@Column(name = "created_by")
	private String createdBy;

	/**
	 * 修改人
	 */
	@Column(name = "updated_by")
	private String updatedBy;
	}

CreatedByDefaultValueListener

public class CreatedByDefaultValueListener implements ApplicationContextAware {

	private ApplicationContext applicationContext;

	@PrePersist
	public void setDefaultValues(CreatedByField createdByField) {
		if (createdByField.getCreatedBy() == null) {
			if (this.applicationContext.getBean(AuditorAwareImpl.class) != null) {
				createdByField.setCreatedBy(
						this.applicationContext.getBean(AuditorAwareImpl.class).getCurrentAuditor().orElse(""));

			}
		}
	}

	/**
	 * @param applicationContext
	 * @throws BeansException
	 */
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

}

本文分享自华为云社区
《【GaussTech技术专栏】GaussDB性能调优》
,作者:GaussDB 数据库。

数据库性能调优是一项复杂且系统性的工作,需要综合考虑多方面的因素。因此,调优人员应对系统软件架构、软硬件配置、数据库配置参数、并发控制、查询处理和数据库应用拥有广泛而深刻的理解。

本文旨在剖析GaussDB性能调优的总体思路,探讨系统整体性能问题,以及对锁阻塞问题进行分析和优化。

1. 性能调优思路

GaussDB总体性能调优的思路是:先进行性能瓶颈点分析,找到相应的瓶颈点之后,再针对性地进行优化,直到系统性能到达业务可接受的范围内。

调优思路,如图1所示:

图1 GaussDB总体性能调优思路

首先,应该确认应用压力是否传递到数据库,可以通过分析数据库节点的资源使用情况,如CPU、I/O、内存以及数据库线程池、活跃会话等信息来辅助判断。GaussDB数据库的管控平台提供了丰富的监控指标体系,便于性能分析人员查看数据库的实时或者历史资源使用情况。

登录管控平台后,进入监控巡检菜单,选择监控大盘,即可查看对应实例的CPU/内存使用率,如图2所示:

图2 对应实例的CPU/内存使用率

点击磁盘/存储菜单,可以查看磁盘I/O使用率,重点关注磁盘读写速率以及时延是否符合预期,如图3所示:

图3 磁盘读写速率以及时延情况

点击网络菜单,可以查看网络传输速率及网卡是否有丢包、错包等情况,如图4所示:

图4 网络传输速率及网卡发送速率

选择连接菜单,可以查看数据库的连接及会话状态,如图5所示:

图5 连接及会话状态

图5中,如果活跃会话的占比远低于应用的并发数,说明数据库中大量会话处于空闲状态。同时,如果CPU使用率也很低,那么,就可以判断压力没到达数据库,此时需要排查应用端是否存在瓶颈。

导致应用侧瓶颈的问题比较常见的原因有:

  • 应用服务器资源瓶颈。比如,应用服务器的CPU满载,应用程序内存分配不足等;

  • 应用到数据库网络问题。比如,网络时延高,带宽满,存在丢包现象等;

  • 应用自身逻辑处理速度慢;

  • 应用配置不优,比如连接池参数、内存相关配置等设置不当。

例如,某个客户通过 jmeter 做大并发压测,性能不及业务预期。经过分析,发现是 jmeter 工具分配的最大可用内存不足,导致压力没有到达数据库。通过修改如下配置,问题得到了解决。

编辑jmeter.sh文件:set HEAP=-Xms1g -Xmx4g

确认压力到达数据库后,再针对相应的瓶颈点进行分析优化。主要从以下两个方面进行:

1)排查数据库中是否存在性能不优的业务SQL语句,并对性能不优的SQL进行优化。通过如下语句,查看数据库中耗时高的TOP SQL语句,并对那些执行性能不符合预期的SQL语句逐一进行分析与调优。

select unique_sql_id,substr(query,1,50) as query ,n_calls,round(total_elapse_time/n_calls/1000,2) avg_time,round(total_elapse_time/1000,2) as total_time from dbe_perf.summary_statement t where  n_calls>10 and avg_time>3  and user_name='root' order by total_time desc;

如图6所示,n_calls 表示该SQL语句在数据库中的执行次数,avg_time 为该SQL 语句的平均执行时间,total_time 为该SQL语句的总耗时。对于平均执行时间超过阈值的SQL语句,重点进行分析与优化。

图6 SQL语句指标及对应数据展示

针对执行性能不优的SQL语句,通过unique_sql_id可以查看该SQL语句的执行详情,帮助分析SQL语句的性能瓶颈点。

select * from dbe_perf.statement where unique_sql_id=3508314654;

如图7所示,该视图记录了SQL语句在数据库的详细执行情况,比如,总执行次数(n_calls)和总耗时(total_elapse_time),便于获取该SQL的总耗时以及平均耗时。

图7 SQL语句在数据库中的详细执行情况视图

行活动,
包括随机扫描、顺序扫描行数、返回的行数、插入/更新/删除的行数以及buffer命中的页面数等信息。此外,还记录了软解析(n_soft_parse)、硬解析(n_hard_parse)的次数,比如SQL大量硬解析导致的数据库CPU飚高,可以通过该指标进行分析定位。

时间模型
,包含db_time、cpu_time、execution_time、plan_time、data_io_time、net_send_info、net_recv_info、sort_time以及hash_time等指标,有助于判断SQL在数据库中的时间消耗在哪个阶段。例如,若某环境磁盘性能不佳,则data_io_time的耗时占比就会比较高。

如果需要进一步分析SQL本身的性能问题,比如执行计划是否最优、索引是否最优等性能问题,可以借助SQL的执行计划进行分析。

通过如下方式,可查看SQL的执行计划:

explain analyze SELECT c_id     FROM bmsql_customer     WHERE c_w_id = 1 AND c_d_id = 1 AND c_last = 'ABLEABLEABLE'     ORDER BY c_first;

结合SQL的执行计划,分析SQL性能的瓶颈点,再进行性能优化,如图8所示:

图8 SQL性能优化过程

2)从系统层面进行操作系统级和数据库系统级的调优,充分利用机器的CPU、内存、I/O和网络资源,避免资源冲突,从而提升整个系统查询的吞吐量。

2. 系统级性能问题分析

2.1 CPU使用率高

数据库的CPU使用率高,通常是由业务SQL语句引起的,我们可以通过如下方式,获取数据库中消耗CPU资源高的SQL语句,并对相应的业务SQL语句进行优化。

select unique_sql_id,substr(query,1,50) as query ,n_calls,round(total_elapse_time/n_calls/1000,2) avg_time,round(total_elapse_time/1000,2) as total_time,round(cpu_time/1000,2) as cup_time from dbe_perf.statement t where  n_calls>10 and avg_time>3  and user_name='root'  order by cpu_time desc limit 5;

常见的导致CPU资源消耗高的原因有:

  • SQL语句大量使用了全表扫描,这可能是由索引缺失、索引失效、执行计划不优等因素所导致。

  • SQL语句大量进行硬解析,通常是因为应用逻辑未使用PBE(Prepare Bind Execute)。

  • SQL语句扫描了大量的元组,比如,分区表分区剪枝失效,扫描了全分区,表中存在大量的死元组,导致扫描了大量无用页面等。

如果CPU使用率高是由非业务SQL语句引起的,可以借助火焰图来进行分析定位。通过火焰图,可以直观地了解程序中哪些函数占用了大量的 CPU 时间或资源,并且可以追踪函数调用路径。

GaussDB在内核505版本中内置了火焰图工具,默认每5分钟会自动采集一次,保存在$GAUSSLOG/gs_flamegraph/{datanode}路径下,详细信息可参考GaussDB产品文档《内置perf工具》章节。

例如,某客户在压测过程中发现数据库服务器的CPU SYS占用率超过70%,通过抓取压测期间的火焰图进行分析,如图9所示,发现数据库加载时,区文件的线程占比超过40%。

图9 某客户压测期间的火焰图

经分析,原因是在高并发频繁建立连接时,数据库每次建连都需要读取时区文件以获取时区信息,而应用未使用长连接,导致CPU SYS使用率飙升。

2.2 内存不足

内存资源,也是影响数据库性能的关键因素之一。在分析内存问题之前,我们先了解一下GaussDB的内存管理机制。

如图10所示,GaussDB的内存管理采用动态内存与静态内存相结合的方式,由参数 max_process_memory 控制数据库可用的最大内存。其中,静态内存区域主要用作数据库的共享缓冲区,用于缓存数据页面,由shared_buffers参数控制。动态内存区域,则由数据库根据需要进行动态分配,主要包括元数据的缓存、执行计划的缓存、用户建连以及内部线程的内存消耗等。

图10 GaussDB的内存管理机制

内存导致的性能问题,通常分为以下几个方面:

1)共享缓存区不足,导致SQL的buffer命中率低。为了查看相应的性能指标,可以借助GaussDB的管控平台或者WDR报告。通常情况下,TP数据库的buffer命中率应该在99%以上。如果数据库的buffer命中率较低,建议排查数据库的shared_buffers参数设置是否合理(如图11所示)。

图11 数据库的buffer命中率

2)在GaussDB中,SQL的hash join或者sort算子存在数据落盘操作,work_mem参数控制可下盘算子可用的物理内存空间。如果work_mem所限定的物理内存不够,算子运算的数据将被写入临时表空间,会带来5-10倍的性能下降。为了优化性能,可以查看SQL的执行计划,如果算子存在落盘的情况(如图12所示),可适当调整work_mem参数值。

图12 算子落盘情况

3)数据库动态内存不足,导致业务执行报错(ERROR:memory is temporarily unavailable )或者性能不足。当动态内存不足时,可以通过如下SQL语句找出内存消耗高的SQL语句,以便排查是否存在不优的SQL 语句。借助SQL的执行计划分析,可以检查是否有不合理的join顺序,或者是否存在非必要的排序操作,从而避免消耗大量内存。

select unique_sql_id,substr(query,1,50) as query ,n_calls,round(total_elapse_time/n_calls/1000,2) avg_time,round(total_elapse_time/1000,2) as total_time,hash_mem_used,sort_mem_used from dbe_perf.statement t where  n_calls>10 and avg_time>3  and user_name='root' order by (hash_mem_used+sort_mem_used) desc;

如果需要排查由非业务SQL语句导致的异常的内存消耗问题,比如内存堆积、内存泄露等,GaussDB提供了丰富的内存相关的监控视图,可以通过下面的视图(如图13所示),查看数据库节点的内存消耗情况。

图13 GaussDB内存相关的监控视图

基于上面的查询结果,如果dynamic_used_shrctx的占用率高,说明是全局共享动态内存的占用高。可以通过如下SQL语句,查看全局共享动态内存上下文的消耗情况。

select contextname, sum(totalsize)/1024/1024 totalsize, sum(freesize)/1024/1024 freesize, count(*) count from gs_shared_memory_detail group by contextname order by totalsize desc limit 10;

如果max_dynamic_memory的占用率高,但是dynamic_used_shrctx的占用率低,那么说明是线程或者会话占用的内存多。可以通过如下SQL语句,查询数据库线程的内存上下文消耗情况。

select contextname, sum(totalsize)/1024/1024 totalsize, sum(freesize)/1024/1024 freesize, count(*) sum from gs_thread_memory_context group by contextname order by sum desc limit 10;

查询结果如下图所示,可以看出,当前数据库中内存占用最高的为元数据的缓存(LocalSysCacheShareMemory)。结合图14中的查询结果,排查是否存在不合理的内存占用情况。

图14 数据库线程的内存上下文消耗情况

2.3 IO瓶颈

通过 iostat 命令,可以查看数据库节点 I/O 的繁忙度和吞吐量,分析是否存在由于 I/O 导致的性能瓶颈。如图15所示:

图15 数据库节点 I/O 的繁忙度和吞吐量

重点关注磁盘的读写吞吐量和读写时延。通常情况下,SSD盘的读写时延在2ms以下,单盘带宽在300MB以上。如果磁盘性能存在异常,优先排查硬件是否存在故障,如磁盘存在坏盘、慢盘、RAID卡故障或磁盘读写策略不正确等。如果磁盘硬件性能正常,而I/O 压力大,可以适当调整数据库I/O 相关的参数,以降低数据的I/O 消耗,从而优化数据库的整体性能。I/O 相关的关键参数链接如下:

后端写进程:
https://support.huaweicloud.com/distributed-devg-v2-gaussdb/gaussdb-12-1124.html

异步I/O:
https://support.huaweicloud.com/distributed-devg-v2-gaussdb/gaussdb-12-1125.html

2.4 网络异常

在传统集中式数据库环境下,应用服务器与数据库服务器通常部署在同一个机房内,从而确保应用与数据库间的网络开销较小。然而,在云+分布式数据库环境下,应用服务器到数据库服务器的网络链路较长,网络耗时对交易性能至关重要。在此情境下,我们不仅需要关注应用与数据库之间的网络状况(通常应该小于0.2ms),还需考虑数据库内部节点之间的网络情况,也会对性能产生较大的影响。

GaussDB要求AZ内网络时延小于0.2ms,AZ间的网络时延小于2ms,region间网络时延小于100ms。可以通过linux的ping命令,排查两个服务器之间的网络时延及丢包等情况,如图16所示:

图16 ping命令,排查两个服务器之间的网络时延及丢包等情况

通过 sar -n DEV 1 命令,查看网络的传输情况。

如图17所示,“rxkB/s”为每秒接收的千字节数,“txkB/s”为每秒发送的千字节数,主要关注每个网卡的传输量是否达到传输上限。

图17 sar -n DEV 1 命令,网络传输情况

3. 锁阻塞问题分析

数据库锁机制是一种用于管理并发访问的技术。它通过对数据库中的数据进行锁定,来确保在多个用户并发访问数据库时,数据的一致性和完整性。

在并发访问的场景下,经常会遇到因为锁冲突导致的性能问题。下面我们看一下在GaussDB中应该如何定位和分析锁冲突的问题。

如果应用正在运行,可以通过下面的SQL语句,查看当前数据库中正在执行的会话是否存在锁阻塞。

集中式场景:

SELECT a.pid as w_pid,a.query as w_query,a.state,d.query as locking_query,d.state as l_state,d.pid as l_pid,d.sessionid as l_sessionid
FROM pg_stat_activity AS a
JOIN pg_thread_wait_status b ON b.query_id = a.query_id
JOIN pg_thread_wait_status c 
ON c.sessionid = b.block_sessionid and c.node_name=b.node_name
JOIN pg_stat_activity d
on d.sessionid=c.sessionid
;

分布式场景:

SELECT a.pid as w_pid,a.query as w_query,a.state as w_state, a.datname, a.usename,d.query as lock_query,d.state as l_state,d.pid as l_pid,d.sessionid as l_sessionid
FROM pgxc_stat_activity AS a
JOIN pgxc_thread_wait_status b ON b.query_id = a.query_id
JOIN pgxc_thread_wait_status c ON c.sessionid = b.block_sessionid and c.node_name=b.node_name
JOIN pgxc_stat_activity d
on substring(d.global_sessionid,0,instr(d.global_sessionid,'#')) ilike substring(c.global_sessionid,0,instr(c.global_sessionid,'#'))
;

查询结果如图18所示,可以获取当前库中存在锁阻塞的SQL语句,同时获取到阻塞它的会话ID、线程ID以及对应的查询。

图18 锁阻塞查询结果展示

要找到并结束阻塞当前查询的会话,可以使用以下语句。

SELECT PG_TERMINATE_BACKEND(pid);

如果是历史的锁阻塞导致的性能问题,可以通过下面语句查询指定时间段内的数据库等待事件。如果发现有大量的acquire lock(包括transaction ID、relation、tuple)事件,表示该时间段内数据库存在锁阻塞问题。

select wait_status,event,count(*) from gs_asp where sample_time>='20241016 18:45:00' and sample_time <='20241016 19:00:00' group by 1,2 order by 3 desc;

ASP(Active Session Profile,活跃会话概要信息),通过采样实例中活跃会话的状态信息,以低成本的方式复现过去一段时间内的系统活动,主要包含会话基本信息、会话事务、执行的语句,等待事件,会话状态(如active、idle等)、当前正阻塞在哪个事件上、正在等待哪个锁或被哪个会话阻塞。

如图19所示,该时间段数据库占比最高的两个等待事件,一个是等待dn_6004_6005_6006分片返回执行结果,这需要进一步排查该分片上性能瓶颈的原因;另外一个等待事件是acquire lock(relation),表示存在大量的表级锁等待。

图19 特定事件内数据库占比最高的两个等待事件

结合数据库的归一化视图,可以获取数据库中存在锁等待的SQL语句,如图20所示:

图20 获取数据库中存在锁等待的SQL语句

通过该语句的Unique_query_id,获得查询阻塞该语句的query_id。

execute direct on datanodes $$select t1.unique_query_id,t1.thread_id,t1.sessionid,t1.wait_status,t1.event,t1.state,t2.query_id as lock_query_id from gs_asp t1,gs_asp t2 where t1.block_sessionid=t2.sessionid and  t1.unique_query_id=168353725$$;

如图21所示,lock_query_id 为阻塞该SQL语句的query_id。

图21 获取阻塞锁等待SQL语句的query_id

利用上一步查询出来的query_id,并结合gs_asp视图,可以通过如下语句获取该SQL语句的详情。查询结果如图22所示,可以看到,阻塞该语句的也是同一张表的update语句,这表明是由于并发更新同一行数据所导致的锁冲突。

图22 锁等待的SQL语句查询结果

通常情况下,解决并发更新锁冲突问题的解决思路需要从业务角度出发,审视存在并发更新同一行的情况是否符合业务场景。如果业务中不存在这样的场景,那应该从业务逻辑或者业务数据上进行优化,以避免并发更新同一行的情况发生。

4. 总结

数据库性能调优涉及硬件、操作系统、数据库、应用等多个层面,因此,在性能调优过程中,需要综合考虑各方面因素的影响。本文介绍了在GaussDB中分析性能问题时常见的手段和思路,帮助大家熟悉GaussDB数据库性能诊断常用的工具及使用方法。


华为开发者空间,汇聚鸿蒙、昇腾、鲲鹏、GaussDB、欧拉等各项根技术的开发资源及工具,致力于为每位开发者提供一台云主机、一套开发工具及云上存储空间,让开发者基于华为根生态创新。
点击链接
,免费领取您的专属云主机。

点击关注,第一时间了解华为云新鲜技术~