系统中有大量用户导致的数据库查询慢

问题分析: 数据过多导致树高增加 MySQL 的默认存储引擎 InnoDB 采用了 B+树的数据结构。3 层树大概能存储 2KW 行数据量,超过了这个数会导致 3 层树变为 4 层树,增加了一次硬盘 IO 读取导致 SQL 变慢。 并发连接数不够 MySQL 的默认最大连接数是 151,可以在 /etc/my.conf 更改。具体可以看文档 max_connections 参数。 超过连接数会出现 too many connections 报错。 解决方案: 分库分表: 将数据按照某个维度水平的切割。 range 范围 例如: 用户 ID [0, 500W) 放库 1 表 1 用户 ID [500W, 1000W) 放库 2 表 2 👆这样会导致例外一个问题。 你会发现用户 ID 小的老用户很多都不上线了,用户 ID 新的用户还是很多,依旧导致了库 2 的连接数还是超了。 因此不能这么简单的通过 ID 的大小去分库分表。 这时候就可以引入了哈希(hash)。 哈希(hash) 哈希的方式可以使得用户 ID 分散到多个库、表上。 ...

五月 28, 2025

mac版本 Joplin 笔记与配置信息存储更改

Joplin 的官方 GUI 并未提供修改存储位置的选项,需要通过启动参数来进行修改。 由于笔者购买的是存储仅为 256GB 的”丐版“” Mac mini,而笔记内容占用的空间较大,因此希望将存储路径更改为 NAS 网络卷。 启动命令 open -a /Applications/Joplin.app --args --profile /Volumes/mac_data/joplin/note 需要修改的部分: /Applications/Joplin.app:替换为你自己的 Joplin 应用程序路径。 /Volumes/mac_data/joplin/note:替换为你希望使用的存储路径。 在 Mac 的终端中输入修改后的命令并执行时,你会看到它会尝试启动 Joplin 应用并使用指定的存储路径。 创建启动程序 打开Mac自带的自动操作程序 选择应用程序 在操作中选择运行Shell脚本 将启动命令输入右侧输入框 点击左上角存储 修改程序名称并存储到应用程序 到这里你将看到启动台中多了刚刚保存的应用程序! 点击程序验证一下效果 修改启动程序图标 在访达 -> 应用程序 中找到刚刚创建的启动程序 右键 -> 显示简介 将下载的图标拖入替代老的机器人图标 下面是Joplin的LOGO

四月 11, 2025

Go 中 Channel 可能会引发 Goroutine 泄漏

Go 中 Channel 可能会引发 Goroutine 泄漏 疑问 什么是 Goroutine 泄漏? Goroutine 泄漏是指 Goroutine 在程序中被创建后,由于某种原因无法正常结束,并且永远不会被垃圾回收(GC)。这会导致 Goroutine 占用的资源(如内存、栈空间等)无法释放,随着时间的推移,可能会耗尽系统资源,导致程序崩溃。 Channel 如何导致 Goroutine 泄漏? Channel 是 Goroutine 之间同步和通信的重要机制。但是,如果 Channel 的使用不当,就可能导致 Goroutine 阻塞并最终泄漏。以下是导致泄漏的常见场景: 发送阻塞: Goroutine 尝试向一个已满的无缓冲 Channel 或已满的有缓冲 Channel 发送数据,如果没有其他 Goroutine 接收数据,发送操作会阻塞。 接收阻塞: Goroutine 尝试从一个空的无缓冲 Channel 或空的有缓冲 Channel 接收数据,如果没有其他 Goroutine 发送数据,接收操作会阻塞。 泄漏的原因是 goroutine 操作 channel 后,处于发送或接收阻塞状态,而 channel 处于满或空的状态,一直得不到改变。同时,垃圾回收器也不会回收此类资源,进而导致 gouroutine 会一直处于等待队列中,不见天日。 代码示例 package main import ( "fmt" "runtime" "time" ) func main() { ch := make(chan int) // 无缓冲 Channel go func() { ch <- 1 // 第一次发送成功(Channel 未满) fmt.Println("第一次发送成功") ch <- 2 // 第二次发送永久阻塞(Channel 已满且无接收者) fmt.Println("第二次发送成功(永远不会执行)") }() time.Sleep(500 * time.Millisecond) fmt.Println("接收到:", <-ch) // 只消费一次数据 // 监控 Goroutine 数量 for { fmt.Printf("当前 Goroutine 数量: %d\n", runtime.NumGoroutine()) // 2 time.Sleep(1 * time.Second) } } 在线运行 ...

三月 7, 2025

Go函数类型是否可以比较,为什么?

Go函数类型是否可以比较? 比较运算符 在Go官方文档比较运算符中有这一段话 Slice, map, and function types are not comparable. However, as a special case, a slice, map, or function value may be compared to the predeclared identifier nil. Comparison of pointer, channel, and interface values to nil is also allowed and follows from the general rules above. 切片(Slice)、映射(map)和函数类型是不可比较的。然而,作为一种特殊情况,切片、映射或函数值可以与预先声明的标识符 nil 进行比较。指针、通道(channel)和接口值与 nil 的比较也是允许的,并且遵循上述一般规则。 因此我们可以看到,Go函数类型之间是不能使用比较运算符的,但是可以和nil进行比较。 例 1: package main import "fmt" func foo() {} func main() { f1 := foo f2 := f1 if f1 == f2 { // 编译错误:invalid operation: f1 == f2 (func can only be compared to nil) fmt.Println("函数相等") } } 在线运行 ...

三月 5, 2025

Go: GPM的数量限制

G 协程的抽象 Goroutine (go /ruːˈtiːn/ 谐音 Go 如 听) 经 Golang 优化后的特殊“协程” G限制 语言上无任何的限制,但是理论上会受到机器的内存限制,每个G创建时会占用4KB左右的内存空间 注:Goroutine 创建所需申请的 2-4KB 是需要连续的内存块。 M 系统线程的抽象 在 Go 的并发模型中,G(Goroutine)是一个轻量级的执行单元,它需要通过系统线程(M)来执行。 每个 G 都会被映射到一个可用的 M 上,M 是操作系统层面的线程,负责实际的 CPU 执行。 M限制 M 是可以通过runtime下的 debug包SetMaxThreads函数去设置的。默认值为10,000 个线程 官方文档 SetMaxThreads 设置 Go 程序可以使用的操作系统线程的最大数量。如果程序尝试使用超过这个数量的线程,程序将崩溃。SetMaxThreads 返回之前的设置值。初始设置为 10,000 个线程。 ? 这个限制控制的是操作系统线程的数量,而不是 goroutine 的数量。只有当 goroutine 准备运行,但所有现有的线程都因系统调用、cgo 调用被阻塞,或者由于使用了 runtime.LockOSThread 被锁定给其他 goroutine 时,Go 程序才会创建一个新的线程。 SetMaxThreads 主要用于限制那些创建无限数量线程的程序的影响。其目的是在程序把操作系统拖垮之前先把程序本身终止掉。 P p即 processor,是 golang 中的调度器; p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合; ...

十二月 18, 2024

Go: Map 是并发安全的吗?

结论 并发读安全,并发写不安全 原因 // map(集合)底层结构 type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) clearSeq uint64 extra *mapextra // optional fields } // buckets的结构 type bmap struct { topbits [8]uint8 keys [8]keytype values [8]valuetype pad uintptr overflow uintptr } 从上方结构可以知道底层结构中存buckets和oldbuckets。发生扩容的时候。会New一个新的buckets地址,并将老的buckets地址写入到oldbuckets中。 ...

十二月 16, 2024

Go: 为什么Map是无序的

底层结构 // map(集合) type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) clearSeq uint64 extra *mapextra // optional fields } // buckets的结构 type bmap struct { topbits [8]uint8 keys [8]keytype values [8]valuetype pad uintptr overflow uintptr } 解释 Map中的数据存储在buckets和oldbuckets中,在发生扩容时,会创建新的buckets,并将老的buckets地址写入到oldbuckets中。 ...

十二月 16, 2024

Go: slice(切片) 和 map(集合) 未初始化操作会怎样

底层结构 // slice(切片) type SliceHeader struct { Data uintptr // 底层数组的地址 Len int // 长度 Cap int // 容量 } // map(集合) type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) clearSeq uint64 extra *mapextra // optional fields } 这2个类型都属于引用类型,特点是存储的是一个地址,且零值为nil ...

十二月 16, 2024

Go: slice(切片) 和array(数组) 的区别

底层结构 array(数组) Go数组与C数组十分类似,数组是具有相同唯一类型的一组已编号且长度固定的数据项序列 关键字: 相同类型,长度固定,序列 slice(切片) type SliceHeader struct { Data uintptr // 底层数组的地址 Len int // 长度 Cap int // 容量 } Go 语言切片是对数组的抽象。结构中包含底层数组、长度、容量 初始化 array(数组) var numbers [5]int // 声明长度为5的数组,数组内容全为默认零值,int的零值为0 var numbers = [5]int{1, 2, 3, 4, 5} // 声明长度为5的数组,数组内容全为{}内的值。 [1 2 3 4 5] numbers := [5]int{1, 2, 3}// 声明长度为5的数组,数组内容全为{}内的值,少的部分为默认零值。 [1 2 3 0 0] numbers := [5]int{1, 2, 3, 4, 5, 6}// 由于{}内的值超过了数组超度编译不通过 numbers := [...]int{1, 2, 3, 4, 5, 6}// 如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度 numbers := [...]int{1:1}// 如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度 numbers := [...]int{5: 1, 2, 3, 1: 11} // 5: 1, 2, 3 表示 在下标5开始 值为1,2,3 // 1: 11 表示 在下标1开始 值为11 // [0 11 0 0 0 1 2 3] slice(切片) s :=[] int {1,2,3} // 声明长度为3,容量为3的切片,内容是[1 2 3] numbers := []int{5: 1, 2, 3, 1: 11} // 5: 1, 2, 3 表示 在下标5开始 值为1,2,3 // 1: 11 表示 在下标1开始 值为11 // [0 11 0 0 0 1 2 3] 使用 make() 函数来创建切片 ...

十二月 16, 2024

服务设计_如何设计一个URL短链服务

什么是URL短链服务 URL短链服务的本质是通过HTTP 302重定向机制,将一个简短的URL重定向到原始的长URL。 短链服务解决了什么问题 解决消息发送的字数限制问题 例如,腾讯云SMS限制每条短信的字数为500个字符,而在营销短信中,通常会携带包含大量参数的URL(如邀请平台、邀请人、活动ID等)。这些参数使得URL变得非常冗长。通过URL短链服务,营销短信中的长URL可以被替换为一个简短的短链,节省了字数空间。 隐藏请求参数 以营销活动为例,URL中的常见参数可能包括活动ID等信息。如果我们不希望这些ID被随便修改,可以在参数中添加对应的活动ID KEY,这样只有当ID和KEY匹配时,用户才能进入相应的活动页面。然而,添加了KEY后,原本的URL会变得更加冗长。短链服务可以帮助隐藏这些请求参数,保持URL简洁且安全。 最基础的需求 长链登记 短链重定向 短链KEY为什么选择 Base62 编码 根据 RFC3986 标准,URL 由 ASCII 字符组成,以下字符可以安全地在 URL 中使用: 字母(a-z 和 A-Z) 数字(0-9) 部分特殊字符:$-_.+!*’(), 虽然RFC3986标准允许一些特殊字符,但有些特殊字符可能会对URL解析、传输或存储造成问题。例如,字符如 &, ?, =, # 等在查询参数或路径中有特定意义,因此它们可能引起冲突或产生解析错误。 为了避免这些潜在的麻烦,特别是在需要将复杂的查询参数或密钥编码为 URL 友好的格式时,我们通常会选择 Base62 编码,即只使用字母(大小写)和数字的组合。 短链KEY的长度选择 我们字符集已经确定为Base62,因此长度为1 可以存储 62种。每增加一位,存储的极限数量会按 62 的指数增长。 长度 存储极限 解释 1 62 1 位可以表示 62 种不同的组合 2 62 × 62 = 3,844 2 位可以表示 62 的平方,即 3,844 种不同的组合 3 62 × 62 × 62 = 238,328 3 位可以表示 62 的三次方,即 238,328 种不同的组合 4 62 × 62 × 62 × 62 = 14,776,336 4 位可以表示 62 的四次方,即 14,776,336 种不同的组合 5 62 × 62 × 62 × 62 × 62 = 916,132,832 5 位可以表示 62 的五次方,即 916,132,832种不同的组合 6 62 × 62 × 62 × 62 × 62 × 62 = 56,800,235,584 6 位可以表示 62 的六次方,即 56,800,235,584 种不同的组合 5位的存储极限已经达到916,132,832,这个数量已经非常大,足以支持大多数应用场景。然而,如果你希望进一步减少生成字符时的冲突、长远规划,选择 6 位 作为短链 KEY 长度会是一个不错的选择。 ...

十二月 9, 2024