分类 其它 下的文章

技术背景

在Jax的JIT即时编译中,会追踪每一个Tensor的Shape变化。如果在计算的过程中出现了一些动态Shape的Tensor(Shape大小跟输入的数据有关),那么就无法使用Jax的JIT进行编译优化。最常见的就是
numpy.where
这种操作,因为这个操作返回的是符合判定条件的Index序号,而不同输入对应的输出Index长度一般是不一致的,因此在Jax的JIT中无法对该操作进行编译。当然,需要特别说明的是,
numpy.where
这个操作有两种用法,一种是
numpy.where(condition, 1, 0)
直接给输入打上Mask。另一种用法是
numpy.where(condition)
,这种用法返回的就是一个Index序列,也就是我们需要讨论的应用场景。

普通模式

我们考虑一个比较简单的Toy Model用于测试:

\[E=\sum_{|r_i-r_j|\leq \epsilon}q_iq_j
\]

在不采用即时编译的场景下,Jax的代码可以这么写:

import os
os.environ['XLA_PYTHON_CLIENT_PREALLOCATE']='false'

import numpy as np
np.random.seed(0)
import jax
from jax import numpy as jnp

def func(r, q, cutoff=0.2):
    dis = jnp.abs(r[:, None] - r[None])
    maski, maskj = jnp.where(dis<=cutoff)
    qi = q[maski]
    qj = q[maskj]
    return jnp.sum(qi*qj)

N = 100
r = jnp.array(np.random.random(N), jnp.float32)
q = jnp.array(np.random.random(N), jnp.float32)

print (func(r, q))
# 1035.7422

那么我们先记住这个输出的结果,因为采用的随机种子是一致的,一会儿可以直接跟JIT的输出结果进行对比。

JIT模式

Jax的JIT模式的使用方法常见的就是三种,一种是在函数头顶加一个
装饰器
,一种是在函数引用的时候使用
jax.jit(function)
来调用,最后一种是配合
partial
偏函数
来使用,都不是很复杂。那么这里先用装饰器的形式演示一下Jax中即时编译的用法:

import os
os.environ['XLA_PYTHON_CLIENT_PREALLOCATE']='false'

import numpy as np
np.random.seed(0)
import jax
from jax import numpy as jnp

@jax.jit
def func(r, q, cutoff=0.2):
    dis = jnp.abs(r[:, None] - r[None])
    maski, maskj = jnp.where(dis<=cutoff)
    qi = q[maski]
    qj = q[maskj]
    return jnp.sum(qi*qj)

N = 100
r = jnp.array(np.random.random(N), jnp.float32)
q = jnp.array(np.random.random(N), jnp.float32)

print (func(r, q))

正如前面所说,因为
numpy.where
对应的输出是一个动态的Shape,那么在编译阶段就会报错。报错信息如下:

Traceback (most recent call last):
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 21, in <module>
    print (func(r, q))
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/traceback_util.py", line 162, in reraise_with_filtered_traceback
    return fun(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/api.py", line 622, in cache_miss
    execute = dispatch._xla_call_impl_lazy(fun_, *tracers, **params)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/dispatch.py", line 236, in _xla_call_impl_lazy
    return xla_callable(fun, device, backend, name, donated_invars, keep_unused,
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/linear_util.py", line 303, in memoized_fun
    ans = call(fun, *args)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/dispatch.py", line 359, in _xla_callable_uncached
    return lower_xla_callable(fun, device, backend, name, donated_invars, False,
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/profiler.py", line 314, in wrapper
    return func(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/dispatch.py", line 445, in lower_xla_callable
    jaxpr, out_type, consts = pe.trace_to_jaxpr_final2(
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/profiler.py", line 314, in wrapper
    return func(*args, **kwargs)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/interpreters/partial_eval.py", line 2077, in trace_to_jaxpr_final2
    jaxpr, out_type, consts = trace_to_subjaxpr_dynamic2(fun, main, debug_info)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/interpreters/partial_eval.py", line 2027, in trace_to_subjaxpr_dynamic2
    ans = fun.call_wrapped(*in_tracers_)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/linear_util.py", line 167, in call_wrapped
    ans = self.f(*args, **dict(self.params, **kwargs))
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 12, in func
    maski, maskj = jnp.where(dis<=cutoff)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1077, in where
    return nonzero(condition, size=size, fill_value=fill_value)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1332, in nonzero
    size = core.concrete_or_error(operator.index, size,
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/core.py", line 1278, in concrete_or_error
    raise ConcretizationTypeError(val, context)
jax._src.traceback_util.UnfilteredStackTrace: jax._src.errors.ConcretizationTypeError: Abstract tracer value encountered where concrete value is expected: Traced<ShapedArray(int32[])>with<DynamicJaxprTrace(level=0/1)>
The size argument of jnp.nonzero must be statically specified to use jnp.nonzero within JAX transformations.
The error occurred while tracing the function func at /home/dechin/projects/gitee/dechin/tests/jax_mask.py:9 for jit. This concrete value was not available in Python because it depends on the value of the argument 'r'.

See https://jax.readthedocs.io/en/latest/errors.html#jax.errors.ConcretizationTypeError

The stack trace below excludes JAX-internal frames.
The preceding is the original exception that occurred, unmodified.

--------------------

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 21, in <module>
    print (func(r, q))
  File "/home/dechin/projects/gitee/dechin/tests/jax_mask.py", line 12, in func
    maski, maskj = jnp.where(dis<=cutoff)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1077, in where
    return nonzero(condition, size=size, fill_value=fill_value)
  File "/home/dechin/anaconda3/envs/jax/lib/python3.10/site-packages/jax/_src/numpy/lax_numpy.py", line 1332, in nonzero
    size = core.concrete_or_error(operator.index, size,
jax._src.errors.ConcretizationTypeError: Abstract tracer value encountered where concrete value is expected: Traced<ShapedArray(int32[])>with<DynamicJaxprTrace(level=0/1)>
The size argument of jnp.nonzero must be statically specified to use jnp.nonzero within JAX transformations.
The error occurred while tracing the function func at /home/dechin/projects/gitee/dechin/tests/jax_mask.py:9 for jit. This concrete value was not available in Python because it depends on the value of the argument 'r'.

See https://jax.readthedocs.io/en/latest/errors.html#jax.errors.ConcretizationTypeError

想避免这个报错,要么就是对该函数不做编译(牺牲性能),要么就是自己写一个CUDA算子(增加工作量),再就是我们这里用到的NonZero定长输出的方法(预置条件)。

NonZero的使用

使用Jax的NonZero函数时,也有一点需要注意:虽然NonZero可以做到固定长度的输出,但是这个固定的长度本身也是一个名为
size
的传入参数。也就是说,NonZero的输出Shape也是要取决于输入参数的。Jax开发时也考虑到了这一点,所以在编译时提供了一个功能可以设置静态参量:
static_argnames
,例如我们的案例中,将
size
这个名称的传参设置为静态参量,这样就可以使用Jax的即时编译了:

import os
os.environ['XLA_PYTHON_CLIENT_PREALLOCATE']='false'

import numpy as np
np.random.seed(0)
import jax
from jax import numpy as jnp
from functools import partial

@partial(jax.jit, static_argnames='size')
def func(r, q, cutoff=0.2, size=5000):
    if q.shape[0] != r.shape[0]+1:
        raise ValueError("The q.shape[0] should be equal to r.shape[0]+1")

    dis = jnp.abs(r[:, None] - r[None])
    maski, maskj = jnp.nonzero(jnp.where(dis<=cutoff, 1, 0), size=size, fill_value=-1)
    qi = q[maski]
    qj = q[maskj]
    return jnp.sum(qi*qj)

N = 100
r = jnp.array(np.random.random(N), jnp.float32)
q = jnp.array(np.random.random(N), jnp.float32)
pader = jnp.array([0.], jnp.float32)
q = jnp.append(q, pader)

print (func(r, q))
# 1035.7422

可以看到,函数用Jax的JIT成功编译,并且输出结果跟前面未编译时候是一致的。当然,这里还用到了一个小技巧,就是NonZero函数输出结果时,不到长度的输出结果会被自动Pad到给定的长度,这里Pad的值使用的是我们给出的
fill_value
。因为NonZero输出的也是索引,这样我们可以把Pad的这些索引设置为
-1
,然后在构建参数
\(q\)
的时候事先在末尾
append
一个0,这样就可以确保计算的输出结果直接就是正确的。

总结概要

在使用Jax的过程中,有时候会遇到函数输出是一个动态的Shape,这种情况下我们很难利用到Jax的即时编译的功能,不能使得性能最大化。这也是使用Tensor数据结构来计算的一个特点,有好有坏。本文介绍了Jax的另外一个函数NonZero,可以使得我们能够编译那些动态Shape输出的函数。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/nonzero.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

我们中高端的windows笔记本上都可以看到Dolby音效,TV电视上也有支持Dolby显示选项。

杜比主要有几类:Dolby全景声(也叫Atmos)、Dolby视界(Vision)、杜比影院(Dolby Cinema)

作为OEM厂商,如何获取杜比授权呢?可以看下Dolby官网的申请流程:
如何获取杜比授权许可 - Dolby Professional

上面是官方合同的流程了,没啥特别的,只是起步阶段。下面我讲下了解到的杜比合作流程:

先是合同,与杜比签合同时,Dolby全景声、Dolby视界分别需要支付2.5万美金押金,押金后面可以退回的

需要说明的是,签合同只能是以一级母公司(如果有的话)来签,提供各种证明啥的

签完合同,后面就是杜比认证流程,也可以叫联调阶段。杜比会安排专人对接提供驱动和软件,适配后寄产品到台湾实验室调参数,整个认证测试大概半个月。这个阶段是不需要费用的

后面就是集成设备,杜比会给驱动以及管理App,OEM厂商集成到母盘生产。如果需要试用的话,Windows Store里也可以安装Dolby Access、Dolby Audio

杜比音效
杜比音效 - Dolby Professional - Dolby Professional
,在OEM端集成到PC上,需要安装驱动。比如这台联想天启,音频处理器这里有DolbyAPO SWC Device音频处理驱动:

windows系统有默认的windows sonic,如果没有可能是注册了“扬声器保护”。杜比Atoms音效可以通过windows函数注入算法,与windows sonic结合混音输出。

再说说真正的使用费用,杜比会统一设备生产数量(线上或者其它渠道),单方向告知每年需要缴纳多少专利费用也叫版税,大概1-2美金一台。

注意有坑:因为杜比basic原因(下面会讲),一旦与杜比签合同,公司之前所有windows设备也要补缴专利费,这可不是小数字

啥叫杜比basic,这里
杜比Windows许可计划常见问题 - Dolby Professional
有介绍,结合网上其它文章,总结如下:

Dolby Basic是杜比与Windows合作,从Win10开始默认内置的杜比音效(Dolby Audio),它能满足用户对个人视频及优质娱乐的优化期望。通过在 Windows 中提供对杜比音频 (Dolby Audio) 的完全支持,Microsoft 在 Windows 应用程序中为用户提供一致、兼容和高质量的音频/视频体验。

Win10内置杜比Audio音效,Windows版本包含 AC-3 编解码器,Edge浏览器也支持杜比Audio
Dolby Digital Plus - Dolby Professional
,也可以见杜比官网新闻:
微软采用杜比音效提升Windows 10的娱乐体验 | 杜⽐新闻中⼼

AC-3 是一种支持多声道(“环绕声”)音频的音频编解码器,它也被称为杜比数字“Dolby Digital”。

原本,杜比 Windows Basic 如果公司每年生产超过 10万 台设备,只需向杜比支付版税。每年制造不到 10万台设备的 OEM 可以免费获得杜比技术。

而上面申请Dolby全景声、视界,就有坑了,要补杜比Basic的费用。

针对这个问题最近有个好消息,
Win11 24H2不再内置杜比音效,不预装杜比音频解码器

Media Player 中的编解码器 - Microsoft 支持

24H2系统自带的播放器将无法直接播放采用 AC-3 格式的音频文件。所以最近有很多反馈24H2不支持AC-3杜比音效的问题:

win11 24h2 移除了ac-3编解码支持(已找到解决方法) - 电脑讨论(新) - Chiphell - 分享与交流用户体验

[笔记本]如何在 Windows 11 24H2 之后安装杜比音频解码器 |官方支持 |华硕全球

新版杜比(Dolby)的安装及设置-联想知识库

在 Windows 11 版本 24H2 中恢复 Microsoft Dolby Digital 解码器/编码器 MFT 支持-远景论坛-微软极客社区

大家可以从Store安装解码器扩展解决:
Dolby Digital Plus decoder for PC OEMs - Windows官方下载 | 微软应用商店 | Microsoft Store

24H2之前的版本不受影响。另外杜比最新的音频编解码格式是AC-4

默认不支持杜比音效,对OEM厂商是好事,只需要关注杜比认证,出了多少台安装杜比软件就缴纳多少版税。

参考资料:

“杜比音效”究竟是什么? - 知乎
杜比

Dolby Vision、 Dolby Atmos和 DolbyAudio是什么?有什么区别? - 知乎

常见问题 - Dolby Professional

Windows 11 24H2:不再支持 AC-3 音频编码器,及应对措施 - 系统极客

关于杜比全景声的一些概念,终于搞懂了!

Windows 上的空间音效和“杜比音效”_哔哩哔哩_bilibili

解析

shared pool

图解:
library cache里面,暂时可以认为存储着:
1、SQL以及对应的执行计划(所占空间比较小);
2、存储过程、函数、触发器、包,它们编译后的对象(所占空间往往比较大,特别是包所占的比较大)

对于shared pool管理和研究的时候,row cache一般不会出现问题,所以一般情况我们都不研究row cache,会出问题的经常是library cache,所以我们经常研究它

如果要执行包里面的一个存储过程的时候,oracle就会把整个包头(包规范)的部分调到library cache里面去,这时候就可能造成比较大的一个对象突然被调到library cache里去,这时候有可能会报4013错误;所以写包的时候,尽量不要把过多的存储过程或者函数放到一个包里面去

在shared pool里,有时候可能造成比较大的一个对象突然被调到library cache里去,这时候有可能会报4013错误,所以为shared pool专门设了一个,一个比较大的对象突然被调到library cache里去的空间:reserved空间,用来存储突然来的大对象的空间,占shared pool空间大小的5%,

假设library cache的大小为5G,缓存着大量的SQL以及SQL的执行计划,现在,要去执行一个SQL,就在library cache里面的这些大量的SQL以及SQL的执行计划里面找,这就有一个问题,该怎么找呢?假设library cache里面有100万行,然后一个一个去对比,就要找100万次,这显然就不合适;

管理SQL以及SQL的执行计划

链(chain)
那么library cache里面是如何组织和管理SQL以及SQL的执行计划的,以便于很方便的找到要执行的SQL?

图解:
用链把一个一个的SQL链起来,假设library cache有500M大小,用4个链来管理SQL;对于SQL1来说:SQL1经过hash以后,得到hash值,然后计算:hash值/4的余数(0、1、2、3......):如果余数 = 0,就把SQL1挂在0号链上;如果SQL1在library cache里面,server process就认为SQL1一定在0号链上,然后在0号链上找,不需要在另外的链上找;另外假设SQL2,经过hash,然后计算余数,假设余数 = 1,就在1号链上找,最终没有找到,它就不找了,因为也不可能在其他的链上,然后SQL2就要产生硬解析了

链的特点:
1、链的一种访问方式:只能遍历,不能随机访问(找到链的头部就可以一个一个的找,一直找到链的尾部)
2、一种链一种作用(比如SQL经过hash,然后挂在链上的,找的时候也是SQL经过hash然后在链上找)

library cache里面的链:就是SQL经过hash值的方式挂起来的(当初怎么组织的,找的时候就怎么找)

library cache的大小,会影响链的数量(library cache多大,链的数量是多少,这个是oracle自己去调整、适应的;我们也可以调整相关的参数来调整链的数量)

一个链可以认为是一个bucket(桶)

Hash(其实就是一个函数)

例如对于SQL:select name from t where id = :x;
oracle会把SQL语句里面的每一个字母,转换成一个ASCII码值,每一个字母对应着一个编码,

一个SQL --> 一堆的文本字母 --> 一堆的数字

hash值与SQL的几种对应关系:
1、如果SQL1和SQL2完全相同,那么它们的hash值一定相等
2、如果SQL1和SQL2不相同,那么它们的hash值一定不相等
3、如果SQL1和SQL2的hash值相等,但是SQL1和SQL2不一定相同

所以比较两个SQL时:
1、如果两个SQL的hash值不相等,那么两个SQL就不相;
2、如果两个SQL的hash值相等,那么还要比较两个SQL,一个字母一个字母的去比较

free空间

free里面的chunk是如何管理的?
也是使用链来管理的

从free空间里面找空闲的chunk(内存块),怎么找?
根据大小来找;比如现在我们需要一个4k大小的,就在free里面找一个4k的chunk,不行就找比4k大一点点的(比如:5k、8k、12k),然后找5k的,所以free里面是通过一种以free chunk的大小的链把一个一个的chunk挂在上面的


图解:
有三个链:2k、8k、16k,现在需要一个9k的,就在8k的链上找,找到一个12k的,可以,就用一下,用了9k还剩下3k,又挂到2k的链上。

游标(cursor)

一个SQL以及SQL对应的执行计划,叫做一个cursor,在library cache里面

父游标(parent cursor)

父游标说的是:SQL文本;同一个SQL可能对应多个执行计划(因为访问的用户不同,表的名字一样(都是t表),但是表的内容不一样;或者因为绑定参数的值的不同(同一个值,一个表里有1000万行,另外一个表里只有10行(这里表的名字相同)),执行计划也可能不同)

子游标(children cursor)

子游标就是:执行计划;子游标的个数不定,根据实际情况而定(比如10个、100个不确定)

version count(版本数量)
例如一个父游标有10个子游标,那么它的版本数量就是10

latch:锁(内存锁、闩锁),用来保护链的


图解:
现在有一个问题:oracle有好多上的server process,;假设server process1执行SQL1,server process2执行SQL2,然后server process1执行SQL1的时候要解析,在library cache里面没找到,SQL1就要发生硬解析;执行SQL2的时候,同样也如此,SQL2也需要硬解析;同时解析;
在解析的时候,就需要在free里面找一个free chunk,把它写进去;假设解析SQL1需要一个,9k的free chunk,解析SQL2,需要一个10k的free chunk,所以都需要在8k的链上找,这就有一个并发的问题:假设SQL1和SQL2找到相邻的两个free chunk;SQL1就要把它找到的free chunk2拿下来,把free chunk1指向free chunk3;SQL2把找到的free chunk3拿下来,然后把free chunk2指向free chunk4;这时候,free chunk2和free chunk3都没了,链就断了,所以对于这种情况链就需要并发保护,使用锁(latch)来进行保护;
latch(对于0号链申请的一个内存结构),用来保护0号链的,现在server process1读这个latch(里面有没有写相关信息),发现里面是空的,server process1就以S的方式写上,server process1以S的方式来访问0号链;
latch里面是空的,说明没有进程来对这个链进行保护修改;然后server process2也要来访问0号链,发现有一个进程在以S的方式访问,server process2想加一个X方式,但是server process1以S的方式持有着latch,server process2也想持有着latch,但是S和X不兼容,所以server process2就不能持有着latch,这时候server process2就发生一次latch misses(latch丢失);
假设server process1在cpu1上工作,server process2在cpu2上工作(有两个cpu),这时候,还有一个server process3在等着cpu空出来再进去,现在对于server process3来说两种选择:
1、server process2退出来(latch丢失),再进入cpu2,server process3工作一段时间以后退出来,server process2再进去,继续执行之前未执行完的操作,这个过程叫做:context switch(CS);
2、server process1持有latch的时间非常短,很短的时间内就执行完了,这时候,server process2不出来,它执行一个for循环,占用着cpu2,等server process1执行完S之后释放了latch就进去,这时候server process3就可以使用cpu1了;
再有假设,有四个cpu,server process2不出来,占用着cpu2,这时候server process3也持有着latch,server process2又再一次latch丢失了,然后server process4又持有着latch,server process2空转cpu,server process2又再一次latch丢失,server process2多次latch丢失以后,server process2就转为sleep状态,然后就退出cpu了,其他的server process就可以占用这个cpu了

因此,如果数据库出现严重的latch征用,就会表现出cpu很繁忙
sleep状态,说明出现了多次latch misses(latch丢失)

当server process1想访问0号链的时候,有两种访问方式:
1、S(共享锁)的方式(读链,在链上找大小)
2、X(排他锁)的方式(修改链,就是往链上挂东西和往链上摘东西)

S方式和X方式的关系

例如:1、SQL1:S 2、SQL1:S 3、SQL1:X
SQL2:S SQL2:X SQL2:S
1、SQL1和SQL2可以同时进行(S、S可以兼容)
2、SQL1和SQL2不能同时进行,要等SQL1找完了,SQL2再去修改(S、X不兼容)
3、SQL1和SQL2不能同时进行,要等SQL1修改完了,SQL2再去找(S、X不兼容)

latch的另外一种情况:
多个链用一个latch来保护,就很有可能出现latch丢失,所以我们可以通过调整参数来让多个链让多个latch来保护

latch的种类(绝大部分):
1、父latch:library cache latch
2、子latch:每一个链上latch(library cache latch里面一个一个的latch)

latch的工作方式:
1、sp1:S方式持有着latch
sp2:X方式,出现latch misses,空转着cpu,多次latch misses以后,变为sleep状态

2、sp1:S方式持有着latch 0
sp2:X方式,也想持有着latch 0,出现latch misses;但是sp2从latch 0上的链上能找到想要的东西,从latch 1上的链上也能找到想要的东西,一样;这时候,sp2就跑到latch 1去找东西了

但是绝大多数都是以第一种工作方式(willing to waiter)工作:愿意等,或者必须等的

前言

分类树
查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。

但就是这样一个简单的分类树查询功能,我们却优化了
5
次。

到底是怎么回事呢?

背景

我们的网站使用了
SpringBoot
推荐的模板引擎:
Thymeleaf
,进行动态渲染。

它是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。

它提供了一个用于整合SpringMVC的可选模块,在应用开发中,我们可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。

前端开发写好Thymeleaf的模板文件,调用后端接口获取数据,进行动态绑定,就能把想要的内容展示给用户。

由于当时这个是从0-1的新项目,为了开快速开发功能,我们第一版接口,直接从数据库中查询
分类
数据,组装成
分类树
,然后返回给前端。

通过这种方式,简化了数据流程,快速把整个页面功能调通了。

第1次优化

我们将该接口部署到dev环境,刚开始没啥问题。

随着开发人员添加的分类越来越多,很快就暴露出性能瓶颈。

我们不得不做优化了。

我们第一个想到的是:
加Redis缓存

流程图如下:

图片

于是暂时这样优化了一下:

  1. 用户访问接口获取分类树时,先从Redis中查询数据。
  2. 如果Redis中有数据,则直接数据。
  3. 如果Redis中没有数据,则再从数据库中查询数据,拼接成分类树返回。
  4. 将从数据库中查到的分类树的数据,保存到Redis中,设置过期时间5分钟。
  5. 将分类树返回给用户。

我们在Redis中定义一个了key,value是一个分类树的json格式转换成了字符串,使用简单的key/value形式保存数据。

经过这样优化之后,dev环境的联调和自测顺利完成了。

第2次优化

我们将这个功能部署到st环境了。

刚开始测试同学没有发现什么问题,但随着后面不断地深入测试,隔一段时间就出现一次首页访问很慢的情况。

于是,我们马上进行了第2次优化。

我们决定使用
Job
定期
异步
更新分类树到Redis中,在系统上线之前,会先生成一份数据。

当然为了保险起见,防止Redis在哪条突然挂了,之前分类树同步写入Redis的逻辑还是保留。

于是,流程图改成了这样:

图片

增加了一个job每隔5分钟执行一次,从数据库中查询分类数据,封装成分类树,更新到Redis缓存中。

其他的流程保持不变。

此外,Redis的过期时间之前设置的5分钟,现在要改成永久。

通过这次优化之后,st环境就没有再出现过分类树查询的性能问题了。

第3次优化

测试了一段时间之后,整个网站的功能快要上线了。

为了保险起见,我们需要对网站首页做一次压力测试。

果然测出问题了,网站首页最大的qps是100多,最后发现是每次都从Redis获取分类树导致的网站首页的性能瓶颈。

我们需要做第3次优化。

该怎么优化呢?

答:加内存缓存。

如果加了内存缓存,就需要考虑数据一致性问题。

内存缓存是保存在服务器节点上的,不同的服务器节点更新的频率可能有点差异,这样可能会导致数据的不一致性。

但分类本身是更新频率比较低的数据,对于用户来说不太敏感,即使在短时间内,用户看到的分类树有些差异,也不会对用户造成太大的影响。

因此,分类树这种业务场景,是可以使用内存缓存的。

于是,我们使用了Spring推荐的
caffine
作为内存缓存。

改造后的流程图如下:
图片

  1. 用户访问接口时改成先从本地缓存分类数查询数据。
  2. 如果本地缓存有,则直接返回。
  3. 如果本地缓存没有,则从Redis中查询数据。
  4. 如果Redis中有数据,则将数据更新到本地缓存中,然后返回数据。
  5. 如果Redis中也没有数据(说明Redis挂了),则从数据库中查询数据,更新到Redis中(万一Redis恢复了呢),然后更新到本地缓存中,返回返回数据。

需要注意的是,需要改本地缓存设置一个过期时间,这里设置的5分钟,不然的话,没办法获取新的数据。

这样优化之后,再次做网站首页的压力测试,qps提升到了500多,满足上线要求。

第4次优化

之后,这个功能顺利上线了。

使用了很长一段时间没有出现问题。

两年后的某一天,有用户反馈说,网站首页有点慢。

我们排查了一下原因发现,分类树的数据太多了,一次性返回了上万个分类。

原来在系统上线的这两年多的时间内,运营同学在系统后台增加了很多分类。

我们需要做第4次优化。

这时要如何优化呢?

限制分类树的数量?

答:也不太现实,目前这个业务场景就是有这么多分类,不能让用户选择不到他想要的分类吧?

这时我们想到最快的办法是开启
nginx

GZip
功能。

让数据在传输之前,先压缩一下,然后进行传输,在用户
浏览器
中,自动解压,将真实的分类树数据展示给用户。

之前调用接口返回的分类树有1MB的大小,优化之后,接口返回的分类树的大小是100Kb,一下子缩小了10倍。

这样简单的优化之后,性能提升了一些。

第5次优化

经过上面优化之后,用户很长一段时间都没有反馈性能问题。

但有一天公司同事在排查Redis中大key的时候,揪出了分类树。之前的分类树使用key/value的结构保存数据的。

我们不得不做第5次优化。

为了优化在Redis中存储数据的大小,我们首先需要对数据进行瘦身。

只保存需要用到的字段。

例如:

@AllArgsConstructor
@Data
public class Category {

    private Long id;
    private String name;
    private Long parentId;
    private Date inDate;
    private Long inUserId;
    private String inUserName;
    private List<Category> children;
}

像这个分类对象中inDate、inUserId和inUserName字段是可以不用保存的。

修改自动名称。

例如:

@AllArgsConstructor
@Data
public class Category {
    /**
     * 分类编号
     */
    @JsonProperty("i")
    private Long id;

    /**
     * 分类层级
     */
    @JsonProperty("l")
    private Integer level;

    /**
     * 分类名称
     */
    @JsonProperty("n")
    private String name;

    /**
     * 父分类编号
     */
    @JsonProperty("p")
    private Long parentId;

    /**
     * 子分类列表
     */
    @JsonProperty("c")
    private List<Category> children;
}

由于在一万多条数据中,每条数据的字段名称是固定的,他们的重复率太高了。

由此,可以在json序列化时,改成一个简短的名称,以便于返回更少的数据大小。

这还不够,需要对存储的数据做压缩。

之前在Redis中保存的key/value,其中的value是json格式的字符串。

其实
RedisTemplate
支持,value保存
byte数组

先将json字符串数据用
GZip
工具类压缩成byte数组,然后保存到Redis中。

再获取数据时,将byte数组转换成json字符串,然后再转换成分类树。

这样优化之后,保存到Redis中的分类树的数据大小,一下子减少了10倍,Redis的大key问题被解决了。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

【引言】

简体繁体转换器是一个实用的小工具,它可以帮助用户轻松地在简体中文和繁体中文之间进行转换。对于需要频繁处理两岸三地文档的用户来说,这样的工具无疑是提高工作效率的好帮手。本案例将展示如何利用鸿蒙NEXT提供的组件和服务,结合第三方库@nutpi/chinese_transverter,来实现这一功能。

【环境准备】

• 操作系统:Windows 10
• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
• 目标设备:华为Mate60 Pro
• 开发语言:ArkTS
• 框架:ArkUI
• API版本:API 12
• 三方库:chinese_transverter

【实现步骤】

1. 项目初始化

首先,确保你的开发环境已经安装了鸿蒙NEXT的相关工具链。然后,创建一个新的鸿蒙NEXT项目。

2. 引入第三方库

使用ohpm命令安装@nutpi/chinese_transverter库:

ohpm install @nutpi/chinese_transverter

3. 编写核心逻辑

接下来,在项目的主组件中引入所需的模块,并定义好状态变量和方法。这里的关键在于设置监听器以响应输入文本的变化,并调用转换函数来获取转换结果。

import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter";

@Entry
@Component
struct SimplifiedTraditionalConverter {
  @State @Watch('onInputTextChanged') inputText: string = '';
  @State simplifiedResult: string = '';
  @State traditionalResult: string = '';
  @State isInputFocused: boolean = false;

  onInputTextChanged() {
    this.simplifiedResult = transverter({
      type: TransverterType.SIMPLIFIED,
      str: this.inputText,
      language: TransverterLanguage.ZH_CN
    });
    this.traditionalResult = transverter({
      type: TransverterType.TRADITIONAL,
      str: this.inputText,
      language: TransverterLanguage.ZH_CN
    });
  }

  private copyToClipboard(text: string): void {
    const clipboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
    const systemClipboard = pasteboard.getSystemPasteboard();
    systemClipboard.setData(clipboardData);
    promptAction.showToast({ message: '已复制到剪贴板' });
  }

  build() {
    // UI构建代码...
  }
}

4. 构建用户界面

在build方法中,我们构建了应用的用户界面。这里主要包括一个可滚动的容器、输入区域、结果展示区以及操作按钮。

Scroll() {
  Column() {
    Text("简体繁体转换器")
      .width('100%')
      .height(54)
      .fontSize(18)
      .fontWeight(600)
      .backgroundColor(Color.White)
      .textAlign(TextAlign.Center)
      .fontColor(this.textColor);

    // 输入区域与清空按钮
    Column() {
      // ...省略部分代码...

      Text('清空')
        .borderWidth(1)
        .borderColor(this.themeColor)
        .fontColor(this.themeColor)
        .height(50)
        .textAlign(TextAlign.Center)
        .borderRadius(10)
        .fontSize(18)
        .width(`${650 - this.basePadding * 2}lpx`)
        .margin({ top: `${this.basePadding}lpx` })
        .onClick(() => this.inputText = "");
    }
    .width('650lpx')
    .padding(`${this.basePadding}lpx`)
    .margin({ top: 20 })
    .backgroundColor(Color.White)
    .borderRadius(10)
    .alignItems(HorizontalAlign.Start);

    // 结果展示区
    // ...省略部分代码...
  }
  .width('100%')
  .height('100%')
  .backgroundColor("#f2f3f5")
  .align(Alignment.Top)
  .padding({ bottom: `${this.basePadding}lpx` });
}

【完整代码】

// 导入必要的转换库,提供简体与繁体之间的转换功能
import { transverter, TransverterType, TransverterLanguage } from "@nutpi/chinese_transverter";
// 导入剪贴板服务,用于将文本复制到系统剪贴板
import { pasteboard } from '@kit.BasicServicesKit';
// 导入提示服务,用于向用户显示消息
import { promptAction } from '@kit.ArkUI';

// 使用@Entry装饰器标记此组件为应用的入口点
@Entry
  // 使用@Component装饰器定义一个名为SimplifiedTraditionalConverter的组件
@Component
struct SimplifiedTraditionalConverter {
  // 定义状态变量inputText,存储用户输入的原始文本,当其值变化时触发onInputTextChanged方法
  @State @Watch('onInputTextChanged') inputText: string = '';
  // 定义状态变量simplifiedResult,存储转换后的简体结果
  @State simplifiedResult: string = '';
  // 定义状态变量traditionalResult,存储转换后的繁体结果
  @State traditionalResult: string = '';
  // 定义状态变量isInputFocused,表示输入框是否获得了焦点
  @State isInputFocused: boolean = false;
  // 定义主题颜色
  @State private themeColor: string = '#439fff';
  // 定义文本颜色
  @State private textColor: string = "#2e2e2e";
  // 定义基础内边距大小
  @State private basePadding: number = 30;
  // 定义最小文本区域高度
  @State private minTextAreaHeight: number = 50;
  // 定义最大文本区域高度
  @State private maxTextAreaHeight: number = 300;

  // 当inputText状态改变时触发的方法,用于更新转换结果
  onInputTextChanged() {
    // 将inputText转换为简体,并更新simplifiedResult
    this.simplifiedResult = transverter({
      type: TransverterType.SIMPLIFIED,
      str: this.inputText,
      language: TransverterLanguage.ZH_CN
    });
    // 将inputText转换为繁体,并更新traditionalResult
    this.traditionalResult = transverter({
      type: TransverterType.TRADITIONAL,
      str: this.inputText,
      language: TransverterLanguage.ZH_CN
    });
  }

  // 将给定的文本复制到剪贴板,并显示提示信息
  private copyToClipboard(text: string): void {
    const clipboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 创建剪贴板数据
    const systemClipboard = pasteboard.getSystemPasteboard(); // 获取系统剪贴板
    systemClipboard.setData(clipboardData); // 设置剪贴板数据
    promptAction.showToast({ message: '已复制到剪贴板' }); // 显示复制成功的提示
  }

  // 构建组件的UI
  build() {
    Scroll() { // 创建可滚动的容器
      Column() { // 在滚动容器中创建垂直布局
        // 创建标题文本
        Text("简体繁体转换器")
          .width('100%')
          .height(54)
          .fontSize(18)
          .fontWeight(600)
          .backgroundColor(Color.White)
          .textAlign(TextAlign.Center)
          .fontColor(this.textColor);

        // 创建用户输入区域
        Column() {
          // 创建多行文本输入框
          TextArea({ text: $$this.inputText, placeholder: '请输入简体/繁体字(支持混合输入)' })
            .fontSize(18)
            .placeholderColor(this.isInputFocused ? this.themeColor : Color.Gray)
            .fontColor(this.isInputFocused ? this.themeColor : this.textColor)
            .borderColor(this.isInputFocused ? this.themeColor : Color.Gray)
            .caretColor(this.themeColor)
            .onBlur(() => this.isInputFocused = false) // 当输入框失去焦点时,更新isInputFocused状态
            .onFocus(() => this.isInputFocused = true) // 当输入框获得焦点时,更新isInputFocused状态
            .borderWidth(1)
            .borderRadius(10)
            .backgroundColor(Color.White)
            .constraintSize({ minHeight: this.minTextAreaHeight, maxHeight: this.maxTextAreaHeight });

          // 创建清空按钮
          Text('清空')
            .borderWidth(1)
            .borderColor(this.themeColor)
            .fontColor(this.themeColor)
            .height(50)
            .textAlign(TextAlign.Center)
            .borderRadius(10)
            .fontSize(18)
            .width(`${650 - this.basePadding * 2}lpx`)
            .margin({ top: `${this.basePadding}lpx` })
            .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) // 添加点击效果
            .onClick(() => this.inputText = ""); // 清空输入框
        }
        .width('650lpx')
        .padding(`${this.basePadding}lpx`)
        .margin({ top: 20 })
        .backgroundColor(Color.White)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Start);

        // 创建繁体结果展示与复制区域
        Column() {
          // 创建繁体结果标题
          Text(`繁体结果:`)
            .fontWeight(600)
            .fontSize(18)
            .fontColor(this.textColor);

          // 创建繁体结果展示文本
          Text(`${this.traditionalResult}`)
            .constraintSize({ minHeight: this.minTextAreaHeight, maxHeight: this.maxTextAreaHeight })
            .fontColor(this.themeColor)
            .fontSize(18)
            .textAlign(TextAlign.Start)
            .copyOption(CopyOptions.InApp)
            .margin({ top: `${this.basePadding / 3}lpx` });

          // 创建复制繁体结果按钮
          Text('复制')
            .enabled(this.traditionalResult ? true : false) // 只有当有繁体结果时,按钮才可用
            .fontColor(Color.White)
            .backgroundColor(this.themeColor)
            .height(50)
            .textAlign(TextAlign.Center)
            .borderRadius(10)
            .fontSize(18)
            .width(`${650 - this.basePadding * 2}lpx`)
            .margin({ top: `${this.basePadding}lpx` })
            .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
            .onClick(() => this.copyToClipboard(this.traditionalResult)); // 复制繁体结果到剪贴板
        }
        .width('650lpx')
        .padding(`${this.basePadding}lpx`)
        .backgroundColor(Color.White)
        .borderRadius(10)
        .margin({ top: `${this.basePadding}lpx` })
        .alignItems(HorizontalAlign.Start);

        // 创建简体结果展示与复制区域
        Column() {
          // 创建简体结果标题
          Text(`简体结果:`)
            .fontWeight(600)
            .fontSize(18)
            .fontColor(this.textColor);

          // 创建简体结果展示文本
          Text(`${this.simplifiedResult}`)
            .constraintSize({ minHeight: this.minTextAreaHeight, maxHeight: this.maxTextAreaHeight })
            .fontColor(this.themeColor)
            .fontSize(18)
            .textAlign(TextAlign.Start)
            .copyOption(CopyOptions.InApp)
            .margin({ top: `${this.basePadding / 3}lpx` });

          // 创建复制简体结果按钮
          Text('复制')
            .enabled(this.simplifiedResult ? true : false) // 只有当有简体结果时,按钮才可用
            .fontColor(Color.White)
            .backgroundColor(this.themeColor)
            .height(50)
            .textAlign(TextAlign.Center)
            .borderRadius(10)
            .fontSize(18)
            .width(`${650 - this.basePadding * 2}lpx`)
            .margin({ top: `${this.basePadding}lpx` })
            .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
            .onClick(() => this.copyToClipboard(this.simplifiedResult)); // 复制简体结果到剪贴板
        }
        .width('650lpx')
        .padding(`${this.basePadding}lpx`)
        .backgroundColor(Color.White)
        .borderRadius(10)
        .margin({ top: `${this.basePadding}lpx` })
        .alignItems(HorizontalAlign.Start);
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor("#f2f3f5")
    .align(Alignment.Top)
    .padding({ bottom: `${this.basePadding}lpx` });
  }
}