My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

解决过拟合问题

发表于 2024-09-17

什么是过拟合

训练得到的预测模型对于每个训练数据都非常吻合,导致对于新的测试数据无法正确评估的现象,就是过拟合
下面是线性回归和逻辑回归模型三种训练结果的展示

解决办法

  1. 收集更多的数据进行训练
  2. 选择和使用有价值的特性值参与运算
  3. 减小部分特征值(对于结果预测关系不大的特征值)的参数值(正则化)

正则化

在成本函数或者损失函数中增加正则化参数,避免过拟合

在梯度下降计算过程中,会使得每个参数在原来基础上乘以一个比1小的数据再去进行减法运算,从而使得梯度下降过程中实现参数进一步缩小

cost and gradient functions for both linear and logistic regression. Note:

Cost

The cost functions differ significantly between linear and logistic regression, but adding regularization to the equations is the same.

Gradient

The gradient functions for linear and logistic regression are very similar. They differ only in the implementation of $f_{wb}$.

线性回归正则化



逻辑回归正则化


实践案例

My Little World

逻辑回归模型

发表于 2024-09-17

背景&&解决问题

逻辑回归模型时一种解决二分类问题的算法

如果用线性回归去解决分类问题,会导致过拟合出现,每增加一个测试数据,都可能导致模型发生变化

通过使用sigmoid function函数,设置阈值,可以将线性回归产生的结果归类到两个结果上去

虽然目标是二分类,即结果只能是0 或者1
但是f(w,b) = g(w·x+b) 计算的结果值A只能无限接近这两个值
这里可以将A理解成结果是1 的可能性

决策边界

在逻辑回归模型f(w,b) = g(w·x+b) 中
z = w·x+b 又称为 决策边界,将不同结果的数据在坐标系中进行隔离
如果特征在z 中没有多项式运算,那么,得到的边界必定是直线的,但是如果有多项式,拿得到的边界线就是非线性的



成本函数

线性回归的平方误差算法的成本函数在应用到逻辑回归时,会产生多个局部最小值,再用梯度下降的算法去找参数时,无法找到最小值

逻辑回归引入逻辑损失函数,根据单个数据集随参数变化的趋势,判断整体的变化趋势
操作就是将原本平方误差除以2的操作移到求和之前,单独计算每个特征值的部分就是损失函数

Logistic Regression uses a loss function more suited to the task of categorization where the target is 0 or 1 rather than any number.

these definitions are used:

Loss is a measure of the difference of a single example to its target value while the

Cost is a measure of the losses over the training set

下面是推导过程和最终的式子





梯度下降找到w&b



My Little World

线性回归模型

发表于 2024-09-07

一些专业术语表达



x–> 训练数据入参,特征值,这里仅有一个,这个模型也被叫做 单变量线性回归模型 x(i) 第i个训练数据的x
y–> 训练数据入参,标记值, example 的lable ,y(i) 第i个训练数据的y
y-hat –> 出参,推测值, 模型(f(x)= wx + b) 的 结果值

成本函数

平方误差成本函数


在f(x) = wx+ b 的模型中
w: 斜率
b: 截距,intercepter 直接与Y轴交点距离远点距离

用J(w,b)表示成本函数,找到能够使J(w,b) 的值最小的w和b,就可以找到使f(x)最接近所有测试集的模型,最拟合训练数据的模型
先讨论在b = 0 的情况下,j(w) 随w 变化的趋势

找到使j(w) 最小的w,即U形线最凹的地方

现在把b的变化趋势也加入讨论,j(w,b) 随w,b 变化的趋势,将变成3d 的碗状

最凹的地方就是J(w,b)值最小的地方

利用等高线的表达方式,换一种视角找J(w,b)的最小值,就是将3D 图进行水平切割,得到关于w,b 的二维椭圆视图
不同(w,b) 组合可能会落在在同一条线上的,相同线上的J(w,b)值一样大
所以能够使J(w,b)值最小的(w,b)值,就是同心圆里面最里面那个圆上的多对(w,b), 当圆极限到一个点时,就只有一对(w,b)使J(w,b)最小

GradientDescentAlgorithm 梯度下降算法

是一种寻找使成本函数达到最小值参数的通用算法,现在不再局限与于f(x) = wx+b 单变量线性模型
对于多变量模型,也就意味着多个w, 这样J(w,b) 就会变成J(w0,…wi,b), J(w0, …wi) 的趋势不再是U形

梯度下降指的是,从一个点出发,环顾四周,找到能够下降的谷底的最快的方向,即梯度最陡的方向,下降一步,每走一步都按最陡方案下降,从而实现最快到达谷底的目的
达到谷底即意味着找到J(w0,…wi,b)最小值
但是梯度下降有一个特性,就是,虽然从同一点出发,如果第一步选择反向不同,或者走的方式不同,肯定会到达不同谷底
不同谷底意味着不同的J(w0,…wi,b)最小值,这些最小值,都叫做局部最小值

算法实现与理解

J(w,b)关于w 导数 表征 U形趋势线上随w 变化的梯度值,也就是斜率
前面的系数α表征 下坡的步伐大小 是一个0-1 的正小数值,称为学习率
w,b 同时变化,同时更新

当梯度大于0时,temp_w 逐渐变小,J(w,b) 的值也逐渐变小
当梯度小于0 时,temp_w 逐渐变大(负斜率,绝对值在变小), J(w,b)的值逐渐变小
说明J(w,b) 的值随梯度的变化符合随w的变化趋势
当斜率绝对值逐渐变小时,就是都朝J(w,b) 最小值聚拢

实验

关于学习率

太小会增加计算步骤,从而使梯度算法变慢
太大可能导致过冲,永远无法到达最小值;甚至无法实现聚拢趋势,导致发散

局部最小值

如果当前参数已经使得成本函数到达一个局部最小值,那么J(w,b) 关于w 导数值将会是0,那么temp_w 会始终停留在一个固定的值,不再变化
梯度下降算法将不会再进行下一步的计算,保持当前参数在当前的这个一个局部最小值的状态

随这个梯度下降,我们可以知道我们正在朝成本函数最小值靠近,在学习率固定情况下,更新步骤也在下降(斜率本身朝0在逐渐变小),说明没有学习率的变化,也能到达局部最小值
但如果同时将学习率调小,降低下降的步伐,可以更小幅度的一点点接近最小值,最终找到成本函数最小值

小结

线性回归模型,成本函数,和梯度下降算法


上述梯度下降算法具体来说是批量梯度下降,因为每一步都用到了所有训练数据

多元线性回归

以上讨论的是只有一个特征值x 作为输入的情况,下面要讨论的是同时有n个特征值做输入的模型,被称为多元线性回归模型

一些符号的表示方法

模型表达式


向量化

好处

  1. 代码实现简洁
  2. 运算速度快



对于多元线性回归 的 梯度下降算法

w 相关的计算转成向量计算,不同权重值,取对应特征值进行计算

实验

实践技巧

如何更快的找到合适的参数去拟合训练集

特征缩放

参数大小与特征值大小关系

如果某一特征值(x1 属于[1000,5000]范围)相对其他特征值(x2,x3,..xn,属于[1,10]范围),数值范围较大,其对应参数w1 则相对其他较小
反之,如果特征值较小,则参数较大,这样的规律可以更快的找到适合的参数


如果特征值的取值范围非常不同时,会导致梯度下降速度变慢
但如果将所有特征值想办法归一到相同的范围内,就可以加快梯度下降过程,降低计算步骤

缩放计算方式

除以最大值

特征值除以各自范围的最大值

均值归一化

特征值 减去平均值,再除以极差值(范围最大值减去范围最小值)

Z-SCORE归一化

特征值 减去 标准差,除以 平均值

说明

并不一定非要落在-1 到1 之间,落在相同的数量级之间就可接受

如何找到成本函数最小值

学习曲线

绘制成本函数随迭代次数变化的趋势图,称为学习曲线,主要是根据曲线走势判断梯度是否在收敛
如果随迭代次数增加,成本函数一直呈下降趋势,说明梯度正在下降,是正常的
当下降到一定程度,随迭代次数成本函数不再有明显下降,说明梯度收敛到了极限,也就是找到了成本函数的最低点
但如果随次数增加,成本函数出现上升,这是不正常的,则说明学习率选取过大,需要重新选取,或者代码出现错误

自动收敛测试

自行定义一个极小的值,当某次迭代后成本函数值小于该值时,就认为梯度下降关闭,成本函数达到最小值,停止迭代,取当前迭代wb做模型参数
缺点就是极小值难以估计,且结果不太可靠

如何选择合适学习率

根据学习曲线调整学习率
学习率太大会导致学习曲线出现上升趋势
太小会导致迭代次数增加

可以先对不同量级的学习率进行测试,观察学习曲线变化快慢
然后进行N倍测试,再比较,通过多组测试观察学习曲线变化快慢进行选取

特征工程

通过转换或者合并直接(原始)的特征值,生成新的特征值进行模型训练

多项式回归

如果特征值只有一个的情况下,不希望得到线性回归的直线模型,希望用曲线去拟合训练集
可以采用下面两种方法去拟合

  1. 通乘方构建新特征值,但是在进行训练时要进行归一化处理,因为乘方处理后,各个特质值范围的数量级会变得不同
  2. 通过开方进行新特征值构建

如何在colab中运行github 上的jupiter文件

下面这个链接时梯度下降实现的.ipynb 文件

https://github.com/kaieye/2022-Machine-Learning-Specialization/blob/main/Supervised%20Machine%20Learning%20Regression%20and%20Classification/week1/work/C1_W1_Lab05_Gradient_Descent_Soln.ipynb

复制域名之后的path
kaieye/2022-Machine-Learning-Specialization/blob/main/
Supervised%20Machine%20Learning%20Regression%20and%20Classification/week1/work/C1_W1_Lab05_Gradient_Descent_Soln.ipynb
拼接到 https://colab.research.google.com/github/ 后面

得到访问链接

https://colab.research.google.com/github/kaieye/2022-Machine-Learning-Specialization/blob/main/Supervised%20Machine%20Learning%20Regression%20and%20Classification/week1/work/C1_W1_Lab05_Gradient_Descent_Soln.ipynb

如果.ipynb有依赖其他py 文件,可以点击上传图标直接上传

但是要注意这里上传的文件都是运行时的状态,页面关闭即销毁,
如果想要保存自己修改后的.ipynb文件可以通过添加副本到google/drive 来实现

My Little World

机器学习定义

发表于 2024-09-02

什么是机器学习

filed of study that gives computers the ability to learn without being explicity programmed.

让计算机在没有明确编程的情况下学习的研究领域

– Arthur Samuel (1959)

Supervised Learning 监督学习

learns from being given ‘right answers’

learns from data labeled with ‘right answers’

regression algorithms 回归算法

从无限多可能数字中预测数字

predict a number 预测无限可能中的一种
infinitely many possible outputs

classify algorithms 分类算法

predict categories 预测有限分类中的一类
small number of possible outputs

Unsupervised Learning 无监督学习

find sth interesting in unlabeled data
data only comes with input x ,but not output labels y, algorithm has to find structure in the data

clustering algorithms 聚类算法

place the unlabeled data (automatically group) into different clusters

eg. google news, grouping customers

==> group similar data points together

Anomaly detection 异常检测

find unusal data points (events)

eg. 金融诈骗中的交易异常

Dimensionality reduction 降维算法

compressn data using fewer numbers

压缩大数据集到小数据集,同时丢失尽可能少的信息

Reinforcement Learning 强化学习

My Little World

关于报文压缩方法的探究

发表于 2024-03-03

问题发现

项目中需要对大数据量请求时间进行缩短优化的工作,优化过程中发现,浏览器响应报文压缩方法为br的情况会比gzip的时间要长11-13s,具体表现如下

服务端响应用时45s

但是浏览器等服务端返回却花了58s

这样浏览器就会比服务器响应多等了58-45= 13s,不是很正常

现在直接拿浏览器请求的cUrl 发起请求

可以看到非浏览器请求的响应使用了gzip压缩,总用时48s, 服务端用时46s, 耗时差2s

可见使用gzip压缩算法耗时是远优于br压缩的

解决办法

想办法禁用掉br压缩方法

  1. 指定service Mesh压缩方法

第一步,检查服务集群是否开启了service mesh,开启后指定才有效

第二步,直接在【通用流量平台->稳定性管理】指定压缩方法

Service mesh 在指定压缩方法后,会对所有请求按指定的压缩算法进行压缩,不管content-length 大小,也不管上游是否已经指定了其他压缩方法,简单粗暴,适合快速解决问题

  1. TLB + 项目配置

该方法是在探究原因过程中发现,过程比较曲折,需要排查修改两个地方,着急解决问题不适宜

  1. 确认下自己的服务是否为node服务且有使用koa-compress插件(注意排查框架是否有默认注入),需要将br 压缩算法关闭,具体关闭形式可能因框架不同配置姿势不同,但可以参考下插件官方配置
  2. 关闭TLB 路由Ngnix默认br 压缩算法配置,禁止使用br算法

虽然复杂,但是方案会比方法一更合理一些

为什么不在发起请求时直接更改accept-encoding?

解决这个问题的另一条途径就是从源头,请求发起端就去掉相关br的设置,也就是更改accept-encoding, 让它不包含br,如果客户端不支持br 压缩,那请求响应自然是不能使用br 压缩的,但是天有不测风云,accept-encoding 是一个不能通过代码去修改的请求报文(详见),所以这条路是行不通的。

这到底是怎么回事?

虽然使用方法一可以快速彻底的解决掉问题,但是不应用方法一时,可以发现的一个明显问题就是不同请求的压缩方法不同,而且存在不使用压缩方法的情况,这就激起了作者尘封已久的好奇心,到底是谁在指定content-encoding呢?

接下来就需要看一下从服务端到客户端,到底是哪个环节在决定content-encoding

Koa-compress

鉴于本人node服务项目基于ACE1.X构建,在搜索代码进行排查时,并没有在配置文件中搜到相关的配置,重新查阅框架文档的时候,才注意到框架有进行默认注入,这就从服务端源头找到了一个会更改content-encoding的地方,俗话说,灯下黑,不过如此。

既然有使用koa-compress, 而且源码不是很复杂,那就简单探索下它的压缩原理

查看源码可知,当content-length大于1024b时,会根据Accept-encoding进行压缩

在Accept-Encoding值是’gzip, deflate, br’情况下

压缩方法的选择逻辑就是accept-encoding有br 会优先使用br,如果br被禁用就使用gzip

由于默认注入时,没有指定压缩阈值,所以当我们的请求数据过大, 大于1024b时,自然就会触发koa-compress进行br压缩,也就是说上面问题的出现,罪魁祸首就是koa-compress

但是当数据量小于1024b时,又会出现br,甚至不进行压缩又是怎么回事呢?

whisle插曲

在排查过程中,相同条件请求,在本地开启whistle代理,通过域名进行本地访问,出现了响应始终是gzip 的情况,这对于大于1024b的响应就不对了,按上面koa-compress逻辑,应该是br才对

经过在http\://localhost:8899/#network 抓包,可以发现whistle给本地服务的请求报文accept-encoding是不带br的

经过与whistle开发者请教(issue),whistle确实会篡改我们的报文,把accept-encoding中的br 去掉,这样就实现了响应始终是gzip压缩的效果,因此,在本地的测试推荐大家直接使用localhost访问,避免代理的干扰

以下在本地进行的测试也均是在关闭代理情况下进行

TLB

根据请求响应链路,响应从node服务返回后,会依次经过Mesh, TLB然后到浏览器

由于mesh 在不指定压缩算法的情况下是不参与压缩的,所以对于小于1024的数据压缩,矛头指向了TLB

在开始验证前,先来了解下TLB的压缩原理TLB压缩问题oncall排查手册

文中对我们比较重要的信息是这部分

文中配置与tlb同学确认后就是默认配置,这样对于我们验证就有了参照物

在关掉koa-compress 的br 压缩后,我进行了如下实验

  1. 构造响应不同content-length的接口
  2. 分别通过本地localhost 访问,域名访问,以及关掉tlb 的br 压缩后再通过域名访问以上接口(保证经过tlb)

得到如下结果(no表示不压缩)

content-length localhost:3000 域名访问 tlb 设置 brotli = off
117 no no no
152 no br no
204 no br gzip
958 no br gzip
1208 gzip gzip gzip

从koa-compress 压缩原理我们可以知道从服务端响应的数据,大于1024采用gzip,小于则不压缩

所以本地访问是符合预期的

经过域名访问,我们可以看到小于1024大于150的响应被用br进行压缩了, 符合br 大于150就压缩

当把tlb 上nginx的br开启指令关掉,我们可以看到小于1024大于200的响应被用gzip压缩了,符合gzip 大于200就压缩的逻辑

再看大于1024的最后一行,当服务端已经指定content-encoding的时候,tlb 是不会进行压缩的,会沿用上游指定压缩算法

综上看来,TLB 会在上游响应未指定content-encoding的时候进行小于1M响应数据的压缩, 默认大于150b时会使用br压缩,大于200b且禁用br情况下才会使用gzip,如果上游指定了content-encoding, 就沿用上游压缩算法

至此,响应报文的content-encoding 来源我们搞清楚了,接下来回到解决办法一,验证下service mesh指定压缩方法后报文变化

集群插曲

虽然文档中指令是默认指令,但不并是所有TLB集群的默认Ngnix 配置,如果出现了与上述结论异常的情况,需要邀请TLB 的同学帮忙查一下域名依赖的TLB 集群是否就是文档中的默认配置(因为只有TLB同学有权限可以查)

比如,相同600B请求,Boe 环境是br压缩,但是线上则变成了gzip

按上面的结论,服务器不会对小于1024的请求进行压缩,经过tlb 默认配置会使用br,boe 环境是正常的,线上是不正常的,经过排查发现,线上tlb 依赖的集群默认配置没有开启br ,所以再走默认配置会进行gzip压缩

Cloud IDE

这里需要注意一点的是,上面我们在发现小于1024的压缩算法异常时,访问的是cloud IDE 上启动项目后帮我们生成的域名,我们在本地请求接口是没有进行压缩的,也就是说cloud IDE生成的域名是有经过TLB的,而且其集群默认开启了br压缩

Service Mesh

实验条件(复现问题):

TLB nginx 不禁用br

不禁用koa=compress的br压缩算法

content-length localhost:3000 域名访问 域名访问
117 no no gzip
152 no br gzip
204 no br gzip
958 no br gzip
1208 br br gzip

我们从浏览器发起请求

在最后一个中间件打印响应头,说明服务器没有参与数据压缩 (可以通过设置priority让中间件在最后一个执行)

然后通过监听端口报文

tcpdump -i eth0 port 3000 -nn

基本上通过上述表现我们基本上是可以判断是mesh 进行了压缩

但是,我们现在监听的是3000配置端口(其他服务监听实例输出的端口)

如果3000端口吐出来的是经过了mesh的话,那通信的结构应该是这样

往深了想一下,上面的判断逻辑并不是非常精确

  1. node 最后吐出来的数据的header 可能跟我们上面在最后一个中间件打印的header并不同,也就是说我们在最后一个中间件打印的header 并不是最终实例吐出数据的header,有一些 header 是会在最后吐数据的时候装的
  2. Gzip 的请求头真的是mesh 加上去的吗?实例和mesh 之间不会还有其他服务?

要解决上面两个疑问,就要想办法去抓取一下mesh 接收的数据,也就是服务吐给mesh的数据

抓取mesh socket

当给服务开启mesh 服务时,mesh 会给环境注入一些环境变量

其中SERVICE_MESH_HTTP_EGRESS_ADDR 这个变量对应的地址就是服务交给mesh 转发的数据

即服务会往这个地址吐数据,然后再由mesh从这里转发再吐出去

那我们接下来就要想办法去读这个socket

通过tcpdump对Unix Domain Socket 进行抓包解析

当我打算用curl 命令去执行相关方法时,却发现没有相应地址的socket

ok,拉mesh 同学onCall 说这种情况是因为服务器和mesh之间不是用的uds通信,用的ip PORT通信

ByteMesh HTTP 出流量接入

抓取PORT 9507

那我现在需要找到MESH_EGRESS_PORT具体是什么

无论是通过打印环境变量

还是通过 cat /proc/\${pid}/environ 查看配置文件,以及通过查看监听端口

基本都确定lookback通信的port 是9507

Ok 那我们再回到用tcpdump 抓包的方式,会发现什么也抓不到

陷入死胡同, 那就是说没有数据包经过mesh 接收数据的端口

重新认识Service Mesh

入流量

我们之前是通过入流量开启压缩算法的

入流量在整个通信链路中的作用是这样的

所以现在需要抓取的是入流量的端口ByteMesh WebSocket & HTTP/1.1 & HTTP/2协议接入约定

需要找到MESH_INGRESS_PORT 通过查看pid 下面 environ 文件可以看到port 为3000,也就是配置端口

然后尝试监听 https://www.cnblogs.com/zgq123456/articles/10251860.html

tcpdump -i any -A -s 0 ‘tcp port 3000 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)’

这一次看到了报文的整个变化过程如上两图

出流量

通过给服务开启出流量代理,可以看到两种通信地址

SERVICE_MESH_EGRESS_ADDR 即如果node 跟下游服务通信会走dsl 通信

MESH_EGRESS_PORT 即如果node 发起http 请求会通过这个端口与mesh 进行http 通信,过程同上面入流量过程

看一下UDS报文长啥样

还是参考这篇文章的方法

  1. 为方便后续指令执行,切到rpc.egress.sock所在文件夹,

  1. 将给到rpc.egress.sock 的数据转发到 8089

  1. 用curl 发起请求,并用tcpdump 对8089进行抓包

注意使用curl –-unix-socket /rpc.egress.sock 时 如果不支持–-unix-socket 参数,需要使用apt-get 升级curl 版本,如无法升级,可能是linux 版本不再维护,可尝试替换基础镜像(指定高版本linux 的)进行部署后再测试 虽然位于rpc.egress.sock 所在文件夹下执行,但是前面的/ 不能省

先用tcpdump -i any -netvv port 8089 看看能不能

加上-A -s , ===> tcpdump -i any -A -s 0 -netvv port 8089 看看具体报文

知识收获

cUrl

cUrl 命令相关参数

-v/–verbose 用于打印更多信息,包括发送的请求信息

-o /dev/null 把输出写到该文件中,保留远程文件的文件名

-w ‘%{size_download}\n’ 获取下载大小

--unix-socket 测试socket 地址,注意要求curl 版本7.50+,如果webshell 不支持,需要考虑更换tce基础镜像

常用linux命令

tcpdump

tcpdump -i eth0 port 3000 -nn

tcpdump -i eth0 -nn -vv

tcpdump -i lo -nn -vv

https://www.cnblogs.com/zgq123456/articles/10251860.html

查看

lsof -i | grep LISTEN

ps -le

ps -ef | grep node

安装

apt/apt-get update

apt/apt-get install 包名

My Little World

历史记录功能设计

发表于 2024-03-03

背景

根据用户反馈,查询条件多个时,想要重新看一下上次的查询结果,操作比较繁琐,希望可以有历史查询的功能,将最近查询的n次记录可以找到,方便回溯问题

方案

前端

在用户点击查询按钮的时候,将当前页面链接调接口保存起来,查询时链接会携带查询条件

后端

存储

历史记录需要跟用户身份做绑定,当前天级uv可达75人,不适宜用tcc或者wcc平台进行数据存储

所以需要申请资源进行数据存储

容量

一个连接大小按照500Byte算,如果只保存最近10条记录,那么一个用户需要5000b ==> 5kb

目前平台用户数以1000为底计算,一开始平台会需要 5kb * 1000 ==>5000kb ==> 5mb

(目前纯个人用户有530,加上以部门为单位申请的权限,各部门人数不确定)

假设半年后用户量翻倍那么存储空间需要增加一倍也就是10MB

负载

目前平台日pv 350,日uv 50, 大致计算一个用户一天会访问页面7次,四舍五入假设1天会进行10次查询

1个用户1天会进行10次数据库读写

那整个平台1天平均会进行500次读写,高峰假设1000次读写(75四舍五入)

平均 500 * 500 /(3600*24) ~~ 0.003kb/s 高峰1000*500/(3600*24) ~~~0.006kb/s

很低

数据结构

本来想如果数据库有数组的话,表结构就是用户id + 记录数组;

没有的话,我现在想了两种方案,

一个就是用字符串存这个数组,用户id + 记录数组字符串形式,相当于更新时要先获得这个字符串,转成数组后,看有没有10条,没有的话直接push,有的话,把时间最早的那条删除,push进数组,再转成字符串更新数据库,这样缺点就是展示的时候也得字符串转数组一下;

另一种就是用户id只和一条记录存在一起,不用一个字符串存整个10条记录,更新的时候我去拿数据的时候拿整个用户id所有的,超过10条的话就用数据库删除方法把时间早的删除了,再存进去最新的

看起来都挺麻烦

而且在实际接入数据库的过程中,还要手动执行命令行产生model相关文件

通过调研公司存储系统的各种方式,觉得redis可以更好的解决存储问题,redis支持List类型存储,

而且LPUSH, LPOP,EXPIRE方法可以很好的帮助实现数据存取更新缓存等问题,省了数据库建表等过程

缓存

redis可以很好的支持数据删除,在更新数据的时候重新设置过期时间即可保证删除不活跃用户的记录

实现

申请redis服务,用户工号做redis的key值,key值的value即用户的查询历史记录list,

写接口: 查记录,更新记录

前端在点击查询的时候调接口更新记录

参考文档

存储系统对比 (草稿)Storage System Comparision(Draft) #

数据结构与命令一览 List of data structure and commands #

https://redis.io/commands

My Little World

Mobx 运行机制深入研究

发表于 2024-03-03

追踪原理

官方文档

MobX 会对在执行 跟踪函数 期间 读取的任何现有的可观察属性做出反应

“读取” 是对象属性的间接引用,可以用过 . (例如 user.name) 或者 [] (例如 user[‘name’]) 的形式完成。

“追踪函数” 是 computed 表达式、observer 组件的 render() 方法和 when、reaction 和 autorun 的第一个入参函数。

“过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。

换句话说,MobX 不会对其作出反应:

从 observable 获取的值,但是在追踪函数之外

在异步调用的代码块中读取的 observable

Mobx 5 以下 MobX 不会追踪还不存在的索引或者对象属性(当使用 observable 映射(map)时除外)。

所以建议总是使用 .length 来检查保护基于数组索引的访问。

所有数组的索引分配都可以检测到,但前提条件必须是提供的索引小于数组长度。

核心概念

追踪属性访问,而不是值

1
2
3
4
5
6
7
8
9
let message = observable({
title: "Foo",
author: {
name: "Michel"
},
likes: [
"John", "Sara"
]
})

mobx会追踪箭头有没有变化

如果箭头发生变化,就会执行追踪函数

使用注意

处理数据时

1.更改没有被obserable的箭头,追踪函数不执行

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行

3.对新增的属性,可以使用set,get实现obserable

4.在异步代码中访问的obserable属性,不会引起追踪函数执行

1.更改没有被obserable的箭头,追踪函数不执行
autorun(() => {
    console.log(message.title)
})
message = observable({ title: "Bar" }) //指向message的箭头没有被obervable
autorun(() => {
    message.likes;//箭头没变,又没有访问数组里面的属性
})
message.likes.push("Jennifer");

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行
var title = message.title;
autorun(() => {
    console.log(title) //访问箭头没有变,还是指向老值的位置
})
message.title = "Bar" //箭头改了,但autorun里没有用到
const author = message.author;
autorun(() => {
    console.log(author.name) 
})
message.author.name = "Sara";//会执行跟踪函数,autorun里有访问name属性,这里指向name值得箭头改了
message.author = { name: "John" };//不会执行,没有访问author属性的箭头

正确使用

A:
autorun(() => {
    console.log(message.author.name)
})
message.author.name = "Sara";
message.author = { name: "John" };
B:
function upperCaseAuthorName(author) {
    const baseName = author.name;
    return baseName.toUpperCase();
}
autorun(() => {
    console.log(upperCaseAuthorName(message.author))
})
message.author.name = "Chesterton"

3.异步
const message = observable({ title: "hello" })
autorun(() => {
    console.log(message) //会执行两次,因为console.log是异步的,请确保始终传递不变数据 ( immutable data ) 或防御副本给 console.log。
})
message.title = "Hello world"
autorun(() => {
    setTimeout(
        () => console.log(message.likes.join(", ")), //异步执行,访问原始数据打印一次
        10
    )
})
message.likes.push("Jennifer");//不会引起autorun执行

4.MobX 5 可以追踪还不存在的属性
autorun(() => {
    console.log(message.postDate)
})
message.postDate = new Date()

组件使用时

子组件问题

MobX 只会为数据是直接通过 render 存取的 observer 组件进行数据追踪

所以当需要将数据传递给子组件时,要保证子组件也是一个obserable组件,可以做出反应

解决办法:

1.将子组件使用obserable函数处理

它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件

2.使用mobx-react的Obserable组件包裹子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
方法一:将子组件使用obserable函数处理
const MyComponent = observer(({ message }) =>
<SomeContainer
title = {() => <TitleRenderer message={message} />}
/>
)
const TitleRenderer = observer(({ message }) =>
<div>{message.title}</div>}
)
message.title = "Bar"
方法二:使用mobx-react的Obserable组件包裹子组件
const MyComponent = ({ message }) =>
<SomeContainer
title = {() =>
<Observer>
{() => <div>{message.title}</div>}
</Observer>
}
/>
message.title = "Bar"

避免在本地字段中缓存 observable

1
2
3
4
5
6
7
8
9
10
@observer class MyComponent extends React.component {
author;
constructor(props) {
super(props)
this.author = props.message.author;//message.author发生变化时不会引起render
}
render() {
return <div>{this.author.name}</div> //.name可以引起render
}
}

优化,使用计算属性,或者在render函数中进行间接引用

@observer class MyComponent extends React.component {
    @computed get author() {
        return this.props.message.author
    }

其他

1.从性能上考虑,越晚进行间接引用越好

2.数组里面的是对象而不是字符串,那么对于发生在某个具体的对象中发生的变化,渲染数组的父组件将不会重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Message = observer(({ message }) =>
<div>
{message.title}
<Author author={ message.author } />
<Likes likes={ message.likes } />
</div>
)
const Author = observer(({ author }) =>
<span>{author.name}</span>
)
const Likes = observer(({ likes }) =>
<ul>
{likes.map(like =>
<li>{like}</li>
)}
</ul>
)
变化 重新渲染组件
message.title = “Bar” Message
message.author.name = “Susan” Author (.author 在 Message 中进行间接引用, 但没有改变)*
message.author = { name: “Susan”} Message, Author
message.likes[0] = “Michel” Likes

一些 对比

autorun vs compute

当使用 autorun 时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发

computed(function) 创建的函数只有当它有自己的观察者时才会重新计算,否则它的值会被认为是不相关的

如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。

而 autorun 中的值必须要手动清理才行

autorun vs reaction

reaction(() => data, (data, reaction) => { sideEffect }, options?)

它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入。

传入 reaction 的第二个函数(副作用函数)当调用时会接收两个参数。

第一个参数是由 data 函数返回的值。

第二个参数是当前的 reaction,可以用来在执行期间清理 reaction

reaction 返回一个清理函数。

不同于 autorun 的是当创建时 **效果 **函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。

在执行 效果函数时访问的任何 observable 都不会被追踪。

效果函数仅对数据函数中访问的数据作出反应,这可能会比实际在效果函数使用的数据要少。

此外,效果 函数只会在表达式返回的数据发生更改时触发。 换句话说: reaction需要你生产 效果函数中

所需要的东西。

useObserver vs Observer vs observer

相关文档

1.虽然只是在返回DOM的地方使用 useObserver(), 但是,当dom中数据改变的时候,整个component都会重新render

1
2
3
4
5
6
7
8
9
10
function Person() {
console.log('in useObserver');//点击按钮会触发执行
const person = useLocalStore(() => ({ name: 'John' }));
return useObserver(() => (
<div>
{person.name}
<button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
</div>
));
}

2.Observer 标签组件可以更精准的控制想要重新渲染的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function ObservePerson() {
console.log('in Observer');//点击按钮不会执行
const person = useLocalStore(() => ({name: 'John'}))
return (
<div>
The old name is: {person.name} //点击按钮不会更新
<div>
<Observer>{() => <div>{person.name}</div>}</Observer> //点击按钮会更新
<button onClick={() => (person.name = 'Mike')}>
I want to be Mike
</button>
</div>
</div>
)
}

3.与useObserver相比,除了使用方法不同,目前不知道区别在哪,有时间需要探究一下

const ObserverLowercasePerson: React.FC<any> = observer(() => {
    console.log('in Observer') //点击按钮也会执行
    const person = useLocalStore(() => ({name: 'John'}));
    return (
        <div>
            <div>The name is: {person.name}</div>
            <button onClick={() => (person.name = 'Mike')}>
                Change name
            </button>
        </div>
    )
})
```
My Little World

趋势图卡片实现原理

发表于 2024-03-02

背景

春节看板项目中有对数据进行趋势分析的展示,其中一种卡片的展示形式经过思考后可以提炼成组件向外提供服务,于是进行封装上传Semi物料市场

设计

  1. 既要支持一个卡片的展现,也要支持多个卡片的展示,所以数据源应该是一个数组 list
  2. 每个卡片的大小应该是一样的,所以应该给一个统一设置卡片大小的属性 size
  3. 卡片可以静态展示,也可以有响应事件,这里先支持一个点击事件 onClick
  4. 单个卡片的功能需要展示趋势折线图,标题,提示,数量,还要支持定制颜色

所以单个卡片的数据结构应该是这样的

name 标题名 string 或者 ReactNode
tip 提示(可选) string 或者 ReactNode
tipNormalShow 提示图标展示方式,默认值false,鼠标滑过才展示 boolean FALSE
hoverLayer 鼠标hover是否展示蒙层效果,默认false,不展示 boolean FALSE
lineColor 折线的颜色,涉及渐变色计算,配置成十六进制格式 string #E91E63 或者 #00B3A1根据卡片位置奇偶情况切换默认颜色
value 标题下的数据 string 或者 ReactNode
xData 折线图x轴数据 Array[string或者number]
yData 折线图y轴数据 Array[string或者number]
loading 数据加载状态开启,默认false,不开启 boolean
noDataTip 没有数据时的提示 string 或者 ReactNode 抱歉,没有数据可展示
errorInfo 错误展示 {text: ‘xxx’, color: ‘xxxx’}

实现

将List 传进来的数据,循环成多个卡片,将单个卡片信息,onClick 和size都传递给卡片组件

卡片根据传递进来属性的不同状态,展示相关信息

另外折线图依靠echart来画,所以需要根据颜色和卡片位置生产曲线配置,这里依靠getChartOption

计算渐变颜色同转换成rgb格式,设置透明度来实现渐变

体验

在线体验地址:https://semi.bytedance.net/material/zh-CN/playground/219

My Little World

拖拽渲染问题的深入研究

发表于 2024-03-01

背景

使用拖拽组件进行拖拽排序
1.原展示模块内容需要进行缩略展示,具备收缩展开的能力
2.排序的内容复杂,需要异步获取数据,循环的时候传进去的关键值(如id)作为参数拉取数据,渲染图表

方案一

保留原组件渲染逻辑,同时将数据源传入排序组件(排序组件显示标题类信息代表原模块),
然后根据是否进入排序状态,保留二者其一,就是排序时展示排序组件,非排序时展示模块内容
问题

从排序状态回到正常展示状态时,因为正常展示的组件DOM在进入排序状态时被销毁
这时再回来,相当于从无到有要重新创建,会引起数据重新请求

方案二

将原展示模块组件作为排序组件项进行渲染,在进入排序状态时将展示模块高度减小,仅保留标题部分充当缩略信息展示
因为展示模块DOM始终存在,所以可以解决掉方案一展示模块DOM消失再创建的数据拉取问题
原理

DOM的新建跟更新流程不同,在这种情况下,新建过程会需要去请求接口拉数据,而如果仅仅是更新的话,可以依赖react的key的关键作用减少DOM 的重建过程,只是进行调换顺序即可
这里在将数据源列表渲染出来的时候,将数据的特征值赋值给key,即排序前后,展示模块key不变就不会被重新新建渲染, 只是进行排序处理
解决方案一产生的问题

在进入排序状态时,将展示模块组件高度设置为0,overflow:hidden,就看不到展示组件,但DOM 依然存在
这时再使用排序组件展示缩略信息即可

小结

无论哪种方案,在结束排序后,都要更新数据源,但数据源里面的对象不能变,因为展示模块会依赖其中的具体对象里的信息进行数据拉取
即

My Little World

学习关于产品的一些思维

发表于 2022-09-03

产品经理决策力工具

象限法:
把想做的事情拆成两个指标去做
让这个两个指标做xy轴
在xy轴包围的空间内,分成四个象限
把要做的事情按照xy轴的值分布在四个象限中
然后决定要在四个象限中寻找最优解

假设思维: 把未来要做的事情一步一步的假设出来
用户思维: 用使用者的思维设计功能

产品路线图roadmap4个核心要素

  1. 里程碑是要有意义的
  2. 跟各个方向的工作协同进行
  3. 可能不是串行的而是并行的,需要准备多种方案
  4. 基于产品框架

五张图说明产品

  1. 核心功能体验图,主要功能的流程图
  2. 模块图,将功能具体的实现划分不同模块,即可以概览具备的的功能,也方便进行任务分配
  3. 功能树,一个模块具体具备的功能内容,相当于再细分
  4. 页面关系图,页面的操作流程,可以跟功能树对比查看是否有功能遗漏,上面提到的模块功能树都会最终落到页面上
  5. 交互设计图,不是最重要的,但要有自解释性,每个人都能看懂

用户留存率—-> 指标之王
算法

  1. 新增留存率 所有新用户中有多少比例下个时间周期会出现
  2. 活跃留存率 所有用户中(活跃用户),包括新用户,有多少比例会在下个时间周期出现,即有多少人会成为下个周期的活跃用户
    统计分析
    用户活跃度
    cohort, 横纵都是第1-n周
    每一行代表当前周用户留存率再往后几周的留存率请款
    每一列代表当前周中,阁用户留存率情况,可以看到每周留存率在这一周的变化情况
    对角线从左上到右下,上面数据表示次周留存率,如果呈下降趋势,说明产品在新客中粘性在下降,留存率整体在下降
    将对角线数据处理形成折线图可直观看到用户留存趋势

RFM,用户贡献值(下了多少单,总消费金额…),根据用户贡献值可采取不同的营销策略

DAU,WAU,MAU,日、周、月活跃用户,一般让DAY/MAU的值作为一个用户粘性的指标
以DAY/MAU为y轴,DAU为x轴,形成折线趋势图,让趋势保持稳定上升是一个产品的发展方向

如何提升留存?
不要去想现有总用户如何去留存,去观察哪些用户值得留存,想办法让这些用户实现留存提升

设计一套CRM系统
CRM系统: 维护公司与客户关系 ===> 用户运营战略执行系统

1234…25
YooHannah

YooHannah

246 日志
1 分类
21 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces