本地+redis多级缓存

由一次线上问题引发的思考,本地缓存+redis缓存的多级缓存方案。 一次线上发生OOM, CPU占用高排查,之前所有的缓存都是放在redis中,并发高时,大量请求去redis,服务反序列化,导致CPU占用高,内存占用高,最终达到资源上线被k8s杀掉。 后来想到了使用本地缓存,这种公共对象保存一份数据,不用反复序列化,减少CPU占用,减少内存占用,但是本地缓存有一个问题,多个副本在同一时间可能缓存数据不一致,虽然在我们这个场景下,这份公共数据更新不频繁,但是也有可能发生这个情况,所以想到了使用多级缓存,本地缓存+redis缓存,本地缓存作为一级缓存,redis作为二级缓存,当redis更新时会设置数据版本号(时间戳),本地获取时会比对版本号,如果相同redis就不返回数据,如果不同就返回数据,这样就可以保证数据一致性。 用go描述一下取值和设置值的逻辑,其他逻辑都比较简单,这里使用lua脚本来实现,版本和数据的原子性和比较版本和返回相应的返回值,还可以减少网络开销 package main import ( "fmt" "time" "github.com/go-redis/redis" ) //这段脚本是用来设置值的,设置值的时候会设置版本号,设置过期时间 const setval = ` local key_val = KEYS[1] local key_version = key_val .. "_version" local val = ARGV[1] local expire = ARGV[2] local version = ARGV[3] redis.call('SET', key_version, version) redis.call('EXPIRE', key_version, expire) redis.call('SET', key_val, val) redis.call('EXPIRE', key_val, expire) return nil ` //这段脚本是用来取值的,取值的时候会比较版本号,如果版本号不一致就返回值 const getval = ` local key_val = KEYS[1] local key_version = key_val .. "_version" local givenVersion = ARGV[1] local version = redis.call('GET', key_version) if not version then return nil end if version ~= givenVersion then local value1 = redis.call('GET', key_val) return {version, value1} end return {version} ` func main() { redisClient := redis.NewClient(&redis.Options{ Addr: "xxx", Password: "xxx", DB: 0, }) cmd := redisClient.Eval(getval, []string{"ab"}, "1") if cmd.Err() != nil { fmt.Println(cmd.Err()) } val := cmd.Val() fmt.Println(val) cmd = redisClient.Eval(setval, []string{"ab"}, time.Now().UnixNano(), 100, time.Now().Unix()) if cmd.Err() != nil && cmd.Err() != redis.Nil { fmt.Println(cmd.Err()) } } 说一下总体的思路 使用一个localstorage, redis, redis lua, singleflight ...

2023-11-24 14:20:35    |    1 分钟    |    170 字    |    Fengbin

DCDC原理 电感

DCDC 升压原理 直流-直流(DC-DC)升压转换器是一种电子电路,用于将输入直流电压转换为较高的输出直流电压。其原理基于能量储存和传递的概念。 DC-DC升压转换器的基本原理: 开关器件: 这种转换器通常使用开关器件(如MOSFET)来控制输入电压的存储和传递。这些器件通过开关操作,控制能量流动的路径,实现电压转换。 储能元件: 其中一个核心组件是储能元件,比如电感或电容。在升压转换器中,电感是最常见的储能元件之一。当开关器件关闭时,电感储存电能;当开关器件打开时,储能元件释放储存的能量,以提供输出所需的较高电压。 控制电路: 升压转换器还包括一个控制电路,用于监测输出电压并控制开关器件的工作周期,以保持所需输出电压稳定。 脉宽调制(PWM): 控制电路通常使用脉宽调制技术,通过改变开关器件的工作周期和频率来调节输出电压。通过调整开关器件的工作时间和间隔,可以控制能量传递的速率,从而实现输出电压的精确调节。 在升压转换器中,输入电压经过周期性开关,并通过储能元件转换为较高的输出电压。这种转换方式允许在电子设备中提供所需的不同电压,例如从电池供电的设备需要较高的工作电压。 总的来说,DC-DC升压转换器通过开关器件、储能元件和控制电路来转换输入电压为所需的更高输出电压,使得在各种应用中能够实现稳定的电源供应。 如何控制电感储能后的电压 控制电感储能后的电压通常通过调整开关器件的工作周期和频率来实现。这涉及到脉宽调制(PWM)技术,通过改变开关器件的导通和关断时间,控制能量的传递速率,从而调节输出电压。 以下是实现这种控制的基本步骤: 脉宽调制(PWM): 使用PWM技术来调节开关器件的导通和关断时间。导通时间越长,电感中储存的能量就越多,从而提高输出电压。关断时间的调节则影响能量传递的速率。 控制电路: 设计一个控制电路,该电路监测输出电压并与设定值进行比较。根据比较的结果,调整PWM信号,以使输出电压保持在所需的水平。 反馈回路: 在控制电路中加入反馈回路,用于实时监测输出电压。一种常见的反馈方式是采用电压反馈,其中输出电压与参考电压进行比较,然后调整PWM信号以纠正任何电压偏差。 PID控制: 对于更复杂的系统,可能采用比例-积分-微分(PID)控制器。PID控制器可以更精确地调节输出电压,通过对比例项、积分项和微分项的权衡来实现快速而稳定的响应。 通过这些控制手段,可以实现对电感储能后的电压进行精确调节,使其保持在所需的水平,从而满足不同应用对电源电压的要求。 如何理解通过PWM来调节电感电压,举例说明 脉宽调制(PWM)是一种通过调整脉冲的宽度来控制平均电压的技术。在电感储能的情况下,PWM被用来调节开关器件的工作周期,从而影响电感中存储的能量量,最终调节输出电压。以下是一个简单的例子,说明如何通过PWM来调节电感电压: 基本电路架构: 考虑一个基本的升压DC-DC转换器,包含以下关键组件: 开关器件(例如MOSFET):用于控制电能的开关。 电感:储存能量的元件。 控制电路:监测输出电压并生成PWM信号。 工作原理: 当开关器件导通(关闭)时,电流通过电感,将能量储存在电感中。 当开关器件关断时,储存在电感中的能量通过二极管传递到输出端,提供给负载。 PWM控制: 控制电路监测输出电压,并将其与设定值进行比较。 如果输出电压低于设定值,控制电路将增加PWM信号的宽度,延长开关器件的导通时间,使电感中储存的能量增加。 如果输出电压高于设定值,控制电路将减小PWM信号的宽度,缩短导通时间,减少电感中的储能。 示例: 初始状态下,输出电压低于设定值。 控制电路增加PWM信号的宽度。 开关器件导通时间延长,电感中储存的能量增加。 输出电压逐渐上升。 控制电路监测到输出电压接近设定值,减小PWM信号的宽度。 开关器件导通时间缩短,电感中的储能减少。 输出电压稳定在设定值附近。 通过这种方式,PWM信号的调节直接影响了开关器件的工作周期,从而影响了电感中储存的能量,最终调整了输出电压。这种反馈控制系统能够在不同工作条件下稳定地提供所需的电压输出。 如何计算通断时间产生的电感电压 电感在开关电路中的通断时间会影响其产生的电压。在直流-直流(DC-DC)升压转换器中,可以使用以下基本公式来估算电感电压: $V_L = L \frac{\Delta I}{\Delta t}$ 其中: $(V_L)$ 是电感上的电压。 $(L)$ 是电感的电感值。 $(\Delta I)$ 是电感中电流的变化。 $(\Delta t)$ 是电流变化所经过的时间。 这个公式基于电感的电压-电流关系: $V_L = L \frac{di}{dt}$ ...

2023-11-23 14:42:12    |    2 分钟    |    324 字    |    Fengbin

Viper读取自定义远程配置

实现viper.remoteConfigFactory接口 type remoteConfigProvider struct{} //这个接口是在第一次读取远程配置时调用,viper.ReadRemoteConfig() func (rc remoteConfigProvider) Get(rp viper.RemoteProvider) (io.Reader, error) { log.Println("Getting config from remote", rp) r := bytes.NewReader([]byte("{\"a\":1}")) time.Sleep(time.Second) return r, nil } //这个接口是在客户端调用viper.WatchRemoteConfig(), 只监听一次变化 func (rc remoteConfigProvider) Watch(rp viper.RemoteProvider) (io.Reader, error) { //这里写你获取配置的方法 log.Println("Watching config from remote", rp) s := fmt.Sprintf(`{"app":{"name":"test","version":%d}}`, time.Now().Unix()) r := bytes.NewReader([]byte(s)) time.Sleep(time.Second) return r, nil } //这个接口是在客户端调用viper.GetViper().WatchRemoteConfigOnChannel()时,监听多次变化 func (rc remoteConfigProvider) WatchChannel(rp viper.RemoteProvider) (<-chan *viper.RemoteResponse, chan bool) { log.Println("Watching Channel config from remote", rp) ch := make(chan *viper.RemoteResponse) go func() { //defer close(ch) viper里面使用死循环来处理,如果关闭管道会返回nil, 而viper没有处理,会报painc,所以不用关闭,viper是想让你在整个生命周期都监听 //这里写你获取配置的方法 for i := 0; ; i++ { ch <- &viper.RemoteResponse{ Value: []byte(fmt.Sprintf(`{"app":{"name":"test","version":%d}}`, time.Now().Unix())), } time.Sleep(time.Second) } }() return ch, nil } 设置viper必要参数 func init() { viper.RemoteConfig = remoteConfigProvider{} //把我们写的provider设置好,否则监听不会生效 viper.SupportedRemoteProviders = []string{"app"} //这里名称需要设置好,和下面添加的名称需要一致 } 使用viper viper.SetConfigType("json") //需要和返回的数据格式一致,不然无法解析 viper.AddRemoteProvider("app", ":8080", "/aabb/cc") //app就是上面的SupportedRemoteProviders,viper内部会校验是否包含,后面两个参数根据自己的需求来填写 viper.ReadRemoteConfig() //加载远程配置 // viper.WatchRemoteConfig() //一次性监听配置,阻塞 viper.GetViper().WatchRemoteConfigOnChannel() //一直监听,非阻塞

2023-09-15 15:56:44    |    1 分钟    |    131 字    |    Fengbin

在docker中go编译应注意的问题

1.使用golang:tag 这种镜像编译后在alpine中会报错(not found) 原因是:go默认会使用glibc,而alpine中没有glibc,所以会报错 解决办法有三种: 连接RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86_64.so.2,建立一个glic链接 使用golang:tag-alpine镜像编译,编译系统和运行系统都用alpine 编译时禁用CGO CGO_ENABLED=0 go build编译出来的文件不依赖动态库

2023-09-07 11:17:42    |    1 分钟    |    18 字    |    Fengbin

Ubuntu当开发过程中主力机遇到的问题

使用deb包安装软件时想要拆卸 在使用dpkg -r拆卸软件需要知道包名,有的时候不好查,可以使用用Ubuntu Software打开deb包,点删除 安装virtualbox,启动虚拟机报 Kernel driver not installed (rc=-1908) 可以使用sudo apt install --reinstall linux-headers-$(uname -r) virtualbox-dkms dkms 安装完成后重启 使用clash for window时,无法正常代理 在ubuntu设置中,点击网络,设置网络代理配置好即可 使用拼音输入法 安装fcitx5 sudo apt install fcitx5 \ fcitx5-chinese-addons \ fcitx5-frontend-gtk4 fcitx5-frontend-gtk3 fcitx5-frontend-gtk2 \ fcitx5-frontend-qt5 安装词库 在https://github.com/felixonmars/fcitx5-pinyin-zhwiki/releases下载.dict结尾的词库文件,放入 ~/.local/share/fcitx5/pinyin/dictionaries/,没有创建即可。 设置默认输入法im-config命令 配置环境变量/etc/profile export XMODIFIERS=@im=fcitx export GTK_IM_MODULE=fcitx export QT_IM_MODULE=fcitx 在home目录.pam_environment添加配置 GTK_IM_MODULE DEFAULT=fcitx QT_IM_MODULE DEFAULT=fcitx XMODIFIERS DEFAULT=@im=fcitx SDL_IM_MODULE DEFAULT=fcitx 非root用户无法使用80端口 通过执行sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80命令,你在更改了系统配置文件,指定了非特权用户可以使用的TCP/UDP端口号的起始值为80。这意味着普通用户可以绑定和使用80端口,而无需特权身份(如root或管理员权限)。 不过这样重启后就无法使用80端口了,所以需要配置一个重启后也生效的配置文件,打开/etc/sysctl.conf文件,在文件后面追加 net.ipv4.ip_unprivileged_port_start = 80,这样重启后也可以生效。

2023-08-14 15:08:46    |    1 分钟    |    66 字    |    Fengbin

内存泄漏

因一次线上内存泄漏问题的总结 go 内存泄漏大概有一下几种情况 长期存活的对象:如果某些对象的生命周期较长,但在使用完之后没有被正确释放,这些对象将一直保留在内存中,导致内存泄漏。 协程泄漏:在 Go 中,每个协程(goroutine)都是轻量级的并发执行单元。如果你启动了太多的协程并且没有适时关闭或终止它们,那么这些协程就会一直存在并占用内存,导致内存泄漏。 循环引用:如果存在对象之间的循环引用关系,即两个或多个对象相互引用,而没有其他的引用指向它们,这将阻止垃圾回收器回收这些对象,导致内存泄漏。 意外的全局变量引用:如果你在函数中意外保留了对全局变量的引用,即使函数执行完毕,这个函数所涉及的内存也无法释放,导致内存泄漏。 未关闭的文件或网络连接:如果在读写文件、网络通信或数据库连接等操作后,忘记关闭这些资源,将会导致系统资源泄漏,最终影响内存的使用。 不正确的缓存管理:当使用缓存来存储数据时,如果没有合适地管理缓存的大小和生命周期,可能会导致过多的对象一直存在于缓存中而无法被释放,从而引发内存泄漏。 未正确使用释放资源的函数:在使用第三方库或 API 时,如果没有按照正确的方式使用和释放资源,比如数据库连接或操作系统句柄等,将导致资源泄漏,可能会间接导致内存泄漏。 这次出问题的是未关闭网络连接(集群内其他服务)没有关闭http Response.Body,导致这两个程序内存一直增长,一个服务内存泄漏导致另一个服务goroutine无法释放,到达k8s内存上限被kill掉 排查方法 通过代码pprof抓取相应数据 导入net/http/pprof自动注册内置好的处理方法,开启默认http服务 我们主要看第二种,第一种可以自己看net/http/pprof里面的写法或者参考文档 用浏览器查看http[s]://xxx/debug/pprof页面,一共有如下几个指标 allocs: 内存过往统计,可以看运行过程中哪些过程使用内存最多 block: 看有哪些被阻塞了,比如各种io cmdline: 运行程序时的名称和参数 比如`./app a b c` goroutine: goroutine信息 heap: 在用堆内存信息 mutex: 锁信息 profile: CPU信息 threadcreate: 系统线程创建信息 trace: 追踪信息,比如GC, 各个goroutine调度等 其中allocs/block/goroutine/heap/mutex/profile 都可以使用go tool pprof -http=":8080" http[s]://host:port/debug/pprof/xxxxx(其中xxxxx用前面几种指标代替) 查看详细信息 trace需要先点击trace下载,然后使用go tool trace tracefile来查看

2023-08-03 09:57:26    |    1 分钟    |    55 字    |    Fengbin

51单片机注意事项

下面的51均指的是STC89C52RC芯片,别的51是否也是这样有待验证 io相关注意事项 51在读取io时需要设置io口为高电平,然后再读取 8051(包括 STC89C52RC)的 I/O 口是开漏(open-drain)输出而非推挽(push-pull)输出。 开漏输出只能主动驱动低电平(0),不能主动驱动高电平(1)。要实现高电平,开漏输出需要通过外部的拉升电阻(通常连接到电源 VCC)实现。这也就是为什么在使用开漏输出的 GPIO 引脚时,你需要首先将它设置为高电平,然后才可以正确读取输入的低电平。 相比之下,推挽输出可以主动驱动低电平和高电平。这就使得在使用推挽输出时,不需要首先设定为高电平来正确读取低电平。 这就是为什么你在使用 STC89C52RC 或者其他 8051 系列的微控制器时,必须首先将引脚设为高电平,只有这样,你才能正确地读取被设为低电平的输入。因此,理解开漏和推挽输出的区别对于理解这个问题是非常重要的。每种类型的输出方式都有其优缺点,需要根据具体应用来选择。 内置EEPROM 在写入前需要先擦除 原因是内部的EEPROM是用Flash来做的,由于Flash的物理性质,只能从1变0,不能从0变1,所以先要擦除为0xFF, 然后在写入相应的数据,注意擦除是按扇区来擦除的 Flash存储器使用了浮动栅层的原理来存储数据,在晶体管中存储了电荷量,代表了0或1的状态。当需要将存储单元从1改为0时,可以通过施加适当的电压,将电荷从浮动栅层中移除,使得晶体管的导通特性被改变为阻断状态,表示0。 然而,将存储单元从0改为1是不可行的,因为在Flash存储器中,写入操作是通过电子隧穿来实现的。当试图将存储单元从0改为1时,需要将电子引入浮动栅层。但是,由于浮动栅层与控制栅层之间的绝缘层,电子无法通过正常的电子隧穿过程引入到浮动栅层中,因此无法实现将0写成1的操作。 为了将存储单元从0改为1,需要执行擦除操作。擦除会将整个存储单元中的电荷移除,将其重置为初始状态,通常是全1。然后再通过编程操作将所需的位写入为1。 由于擦除操作会擦除整个存储单元的数据,而不是单个位或字节,因此对Flash存储器进行写入操作时,通常需要将整个存储块或页擦除,然后再编程所需的位。这种擦除和编程的特性使得Flash存储器在数据写入方面有一定的限制和特殊性。 LCD1602异常 是否是每个阶段的延迟不够 调高延迟试一下是否正常 多写一些字符 如果出现屏幕位置不同,可能是算法问题 乱码(比如全屏写'1’,但是显示的是其他内容) 可能是虚焊 判断方法: 写一个程序,使io口高低电平切换,如果使用示波器可以间隔短点间隔时间, 如果使用万用表让间隔长一点比如5s切换一次电平 使用示波器或者万用表分别测量每个io对应的LCD接口,看是否有电平切换, 可以判断接口是否异常 再看芯片对应异常的io引脚如果输出是正常,则说明虚焊,如果引脚输出异常,则可能是芯片坏了

2023-07-27 16:14:07    |    1 分钟    |    38 字    |    Fengbin

三极管集电极电流产生原理

三极管作为一种重要的半导体器件,其集电结反向偏置时表现出的导电性违背了PN结的传统认知,这一现象的原因一直令人感到好奇。在本文中,笔者将通俗解释三极管反向偏置下集电结仍可导电的科学机理。 我们知道,PN结反向偏置时,少数载流子形成的漏电流非常微弱。但三极管的情况则完全不同,反向偏置的集电结也会产生明显的集电电流Ic。这主要归因于三极管独特的内部结构。 需要强调的是,这种反向偏置下的电流并非由反向击穿产生,而是有别于PN结的全新机理。三极管通过基极控制发射极附近的电子浓度。当基极电流注入电子时,发射极区域会聚集大量电子。这些电子可以跨越集电结的反向势垒,形成显著的反向漏电流。随着基极电流增大,发射极电子浓度提高,反向漏电流Ic也随之增加。 可以看出,三极管反向偏置下集电结的导电性,是由基极注入产生的大量电子引起的。这种机理完全不同于PN结中仅依赖少数载流子的反向漏电流。正是三极管的独特结构及工作原理导致了反向偏置下也存在明显Ic的现象。 值得注意的是,PN结在正向偏置时,电流主要由多数载流子形成;而反向偏置时,则由少数载流子形成微弱的电流。三极管反向偏置下Ic的产生,则利用了基极注入的大量电子,从电流形成机理上破除了PN结的限制。 通过解析三极管反向偏置导通的科学机理,我们加深了对其工作原理的理解。这也展示了半导体器件的奥秘比单纯PN结更为复杂。希望本文能够满足读者对这个有趣现象的好奇心,并成为理解半导体物理的一个窗口。 注意: 在严格意义上说PN并不能单向导电,我们所说的单向导电只是因为反向电流非常微弱 PN结在正向偏置时,电流主要由多数载流子形成;而反向偏置时,则由少数载流子形成微弱的电流

2023-07-19 16:43:28    |    1 分钟    |    9 字    |    Fengbin

复位电路电容选择

${T = R C}$ 变换为 ${C = \frac T R }$ 其中T为时间 R为电阻 C为电容 以1ms复位时间来算, 如果电阻选10kΩ那么 $C = \frac {0.001s} {10000Ω} = 100nF$

2023-06-08 11:06:21    |    1 分钟    |    23 字    |    Fengbin

外部中断和定时器

都说学习硬件是学习计算机组成原理的快速途径,我也入坑了,这里记录一些学习中初学者可能感觉难受的难点 一个MCU就好比一个很复杂的函数,他需要两种初始化,一种就是硬件初始化(也就是最小系统+外设电路), 一种是软件初始化(设置寄存器来使用某些功能) 中断 其实也没什么难的,难的是和GPIO那种简单方式的对比,都是固定套路 中断套路 编写好中断处理函数 设置触发方式(低电平触发或者下降沿触发) 开启某一中断 开启总中断 定时器 套路 设置定时器模式 设置定时器初值 开启定时器 (到目前定时器已经可以使用了) 定时器中断开启 (如果还想定时中断的话,需要剩下3,4两步) 总中断开启 中断优先级可以进行调整

2023-06-06 11:09:27    |    1 分钟    |    20 字    |    Fengbin