Welcome to my blog
RecordShit 关键记录
一切的一切源于那烦人的“便了么”,从去年年底到今年五月,我已经用了半年,却总是忘记点击“拉完了”按钮,导致一拉就拉了十几分钟,于是我决定自己写一个。 最开始我打算用 Flutter,反正用的是 ai,但随着项目开始我就遇到了很多困难,编程语言下载、Android Studio配置、app 打包等。后来我了解到了 uniapp 和 HBuilderX,“开发一次,多端覆盖”,这句口号吸引了我,了解后我快速上手,马上也得到了最初的版本,像下面这样。 不得不说确实简陋,至少清晰明了哈哈哈。最开始我是打算做多端的,先实现了 app 版本,然后用 uniapp 转小程序,结果效果不堪入目,各种卡片高度错乱,字体排版混乱,可能是我没有写好提示词,导致代码不兼容,后续维护也比较麻烦。经过我深思熟虑后,我选择同时进行两个版本的开发,app 不动,小程序用微信原生框架。我在这个方向进行了很长一段时间,遇到很多 bug,各种加载问题、组件、数据库部署等各种问题,如下图。当然在这两个多月开发中我学到了很多,中间也使用了各种 skill,不断在网上看 ui 设计的视频,学习一些好用的提示词等等。 今天是7月28日,距离第一次提交代码已经过去快两个月,我又做了一个重要的决定,放弃 app 版本,全面转向微信小程序的开发,主要还是双端维护过于复杂,看似功能完好,实际上是屎上绣花。小程序任何操作系统上都可以用,而 app 目前只能在安卓手机上使用,所以我果断转向小程序。还有个原因就是我的小程序备案通过了,app 在应用市场上架很麻烦,直接给安装包很多人不会用,而小程序就简单了,直接微信扫个码就好。 转小程序大概4天前了,小程序也是越来越好,等我做好第一个正式版,我就要发到朋友圈里嘻嘻。 好久没写博客,这一篇就先到这里。
云服务器 OOM
前一阵子,原来的云服务器快到期了,续费还不如买个新的,正好用来部署博客,想着负载不大,就选了实惠的规格,2核2G。 最近想要部署一个项目,后端是 docker 化的,在我换好镜像源,执行 docker-compose up -d ,一分钟后,我的 ide 和服务器断开了远程连接。重新连接依然不行。当我打开云厂商的监控页面时,看到恐怖的一幕,我的服务器好像要不行了。 然后我尝试多个工具进行远程登录,无一例外都失败了。 我尝试通过网页 VNC 登录,看到了 Out of memory: KIlled process,说明一点内存也没了。怪我选规格时没考虑那么多。后来 AI 分析:甚至连 SSH 守护进程都因为内存不够而被操作系统 OOM Killer 无情杀掉,远程连接彻底没戏。 我的第一想法是重启,但依旧没用。问题出在我的 docker 上,所有容器都配置了 restart: always,所以一开机容器自启动。经过简单算账后,我选择用钱来解决🧐。升级到了4G 内存。 其实光升级内存还是有点紧张的,同时我还修改了 docker 的配置文件,限制了每个容器的可用内存。 部署项目就简单了,先启动 docker 容器,然后启动后端 jar 包,前端用 pnpm 命令构建一下,再配置一下 nginx 反向代理,绑定域名就ok了。
十种排序算法整理
十种排序算法 冒泡排序 一共进行 n - 1 轮,在每一轮排序中对相邻两元素进行比较,大的排在后面。 /** * 冒泡排序 * 时间复杂度:最优 O(n),最坏 O(n²),平均 O(n²) * 空间复杂度:O(1),原地排序 * 稳定性:稳定 */ private static void bubbleSort(int[] arr) { int n = arr.length; boolean flag = true; for (int i = 0; i < n - 1; i++) { flag = true; for (int j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); flag = false; } } if (flag) break; } } 选择排序 同样进行 n - 1 轮,每一轮从待排序序列中挑出最小的元素,将其放至已排序序列的末尾。 ...
笔试收获
参加了一次笔试,收获颇多。 不同的输入方法 和力扣不一样,一些大厂的笔试包括面试时的算法题都是 ACM 模式,也就是需要自己处理输入,然后对输入处理并输出,而力扣是核心代码模式,不用你管输入,只需要实现指定方法,平台自会调用你的方法。在参加之前,我去卡码网上了解了一下,发现还是 Scanner 那一套,也就没在意,但当我真正开始做的时候感到有点不对,Scanner 恐怕不太够用。这篇博客主要讲一下 Java 里的输入流。 Java 的输入体系建立在 流 的概念之上,主要有两个抽象基类:InputStream 和 Reader ,前者为字节流,处理原始字节数据,如图片、音频等;后者为字符流,处理字符数据,如文本文件、控制台输入等。两者之间通过 InputStreamReader 作为桥梁,将字节流转换为字符流。 InputStream 字节流 InputStream 类定义了所有字节输入流必须实现的核心办法。包括 int read() , int read(byte[] b) , void close()等方法。 Java 的 IO 体系采用了装饰器模式,意味着可以通过组合不同的流来实现复杂的功能。这里将 InputStream 的子类大致分为两类。 第一种,这种类直接与特定的数据源进行交互,比如 FileInputStream ByteArrayInputStream 。 第二种,这种类不直接连接数据源,而是通过继承装饰器基类 FilterInputStream 来包装一个已存在的 InputStream ,给它提供一些额外的功能,比如缓冲、数据类型转换等。这种类的典型代表有 BufferedInputStream DataInputStream 等。 Reader 字符流 Reader 类是字符输入流的抽象基类,定义的方法有 int read() , int read(char[] cbuf) , void close() 等。 它的子类和 InputStream 一样,也分两种,这里不再多说。但它有一个特别的子类: InputStreamReader 从它的类名就能看出来,它是字节流到字符流的单向桥梁,它可以读取字节,然后根据指定的字符编码将其解码为字符。 System.in 它是 System 类里的一个静态变量,类型是 InputStream ,所以它是一个字节流。如果需要按字符或行读取,通常需要对其包装,比如刚才提到的 InputStreamReader 和常见的 Scanner。通常情况下,System.in 默认连接到控制台(键盘),同时它读取数据也是阻塞式的,程序会一直等待,直到有数据可读或者流被关闭。 ...
权限模型
常见的权限模型有两种:RBAC 和 ABAC。 两种模型介绍 RBAC 模型 Role-Based Access Control,翻译为基于角色的访问控制。角色拥有某些权限,通过赋予用户某种角色来达到给用户授予相关权限的目的,这就是基于角色。实现较简单。在一些大型组织中,角色可能爆炸式增长,导致管理复杂、权限冗余。 ABAC 模型 Attribute-Based Access Control,翻译为基于属性的访问控制,它比 RBAC 更加灵活,因为在 ABAC 模型中,一个操作是否被允许是基于对象、资源、操作和环境信息共同动态计算决定的。它的控制更细粒度,比如它能实现“仅允许大学生在水课上睡觉”的控制。 派聪明的权限控制 派聪明的权限模型是改进版的 RBAC,或者说简化版的 ABAC。设计了 admin 和 user 两种角色,管理员拥有管理知识库,查看用户列表等权限,普通用户拥有查看私有知识库等权限。但单纯的 RBAC 有两种局限性:权限控制力度不够细和没办法基于数据属性做动态授权,所以添加了组织标签机制。 在上传文件时,给每份文档标记上传者所属的标签,这样用户访问知识库,系统能够动态的基于当前用户的组织标签和文档的组织标签进行匹配过滤。从而实现更细粒度的数据隔离和动态授权。 此外,派聪明还考虑到了多级组织,设计了组织标签树。比如有这样三个层级标签:总部、研发部、后端部。一个拥有后端部组织标签的员工可以访问研发部、总部这两个父组织的资料。我个人在这一部分绕了很久,总感觉设计反了,现在是这样理解的,抛弃父标签级别高,所以权力大的想法,这样想: 父标签(如“总公司”):它定义的是一个最基础、最通用的权限范围。就像一张“员工卡”,让你能访问公司的基础公共资源。 子标签(如“技术部”、“财务部”):它在父标签的基础上,增加了更具体、更细分的权限。就像一张“技术部员工卡”,它包含了“员工卡”的所有权限,还额外增加了进入技术部专属区域的权限。 这就是派聪明中有关权限模块的设计。 转转统一权限系统的设计与实现 经过分析,转转技术部门选择了基于 RBAC 模型来实现,但它也有所改动,在原有基础上,新增了给用户直接增加权限的能力,也就是说既可以给用户添加角色,也可以给用户直接添加权限。所以用户最终的权限为 所拥有角色带来的权限和用户独立配置的权限的并集。 转转的技术文章里还从权限系统自身和具体业务系统两层设计了六种身份,这里不再多说。 参考文章: ✅派聪明 RAG用户管理模块设计方案 - 技术派 - Java技术社区 | RAG+Agent实战项目教程+AI助手 转转统一权限系统的设计与实现(设计篇)
手写简单IoC容器
学习 Spring,跟着二哥简单写个 IoC 容器。 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface MyAutowired { } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface MyComponent { } public class SimpleIoC { Map<Class<?>, Object> beans = new HashMap<>(); //扫描 public void scan(String packageName) { List<Class<?>> classes = getClassesInPackage(packageName); for (Class<?> clazz : classes) { if (clazz.isAnnotationPresent(MyComponent.class)) { registerBean(clazz); } } di(); } //获取包下的类,简化实现 private List<Class<?>> getClassesInPackage(String packageName) { // 实际实现需要扫描classpath,这里简化处理. return Arrays.asList(UserDao.class, UserService.class); } //注册bean private void registerBean (Class<?> clazz) { try { Object instance = clazz.getDeclaredConstructor().newInstance(); beans.put(clazz, instance); } catch (Exception e) { throw new RuntimeException("创建bean失败"); } } //获取bean @SuppressWarnings("unchecked") public <T> T getBean(Class<T> clazz) { return (T)beans.get(clazz); } //依赖注入 private void di() { for (Object bean : beans.values()) { Field[] fields = bean.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(MyAutowired.class)) { field.setAccessible(true); Object dependency = getBean(field.getType()); try { field.set(bean, dependency); } catch (Exception e) { throw new RuntimeException("依赖注入失败"); } } } } } } //DAO层 @MyComponent class UserDao { public void save(String user) { System.out.println("保存用户: " + user); } } // Service层 @MyComponent class UserService { @MyAutowired private UserDao userDao; public void createUser(String name) { userDao.save(name); System.out.println("用户创建完成"); } } class Test { public static void main(String[] args) { SimpleIoC ioc = new SimpleIoC(); ioc.scan("package"); UserService bean = ioc.getBean(UserService.class); bean.createUser("dong"); } } 参考链接: ...
LeetCode155.最小栈
第二次碰到这道题了,又学了两种解题方法,记录一下。 题目介绍 设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初始化堆栈对象。 void push(int val) 将元素val推入堆栈。 void pop() 删除堆栈顶部的元素。 int top() 获取堆栈顶部的元素。 int getMin() 获取堆栈中的最小元素。 解法一:使用辅助栈 这个也是最简单的,题目没有空间限制,考虑使用两个栈,主栈正常记录元素进出,辅助栈记录每个元素对应最小值的进出,辅助栈的栈顶保持为主栈所有元素的最小值。 当元素入栈时,比较该元素和辅助栈的栈顶,如果该元素更小,那么将其推入主栈的同时也将其推入辅助栈。 当元素出栈时,如果主栈栈顶元素值和辅助栈的栈顶元素值相等。那么将该元素从两个栈里同时弹出,否则正常弹出主栈栈顶元素。 class MinStack { Deque<Integer> stack1; Deque<Integer> stack2; public MinStack() { stack1 = new ArrayDeque<>(); stack2 = new ArrayDeque<>(); } public void push(int val) { stack1.push(val); if (stack2.isEmpty() || val <= stack2.peek()) { stack2.push(val); } } public void pop() { int num = stack1.pop(); if (num == stack2.peek()) stack2.pop(); } public int top() { return stack1.peek(); } public int getMin() { return stack2.peek(); } } 解法二:使用自定义链表 评论区有评论说面试官要求使用栈以外的数据结构来设计,所以找到这种方法。 ...
LeetCode8.字符串转换整数
一切源于一道题目:8. 字符串转换整数 (atoi) - 力扣(LeetCode) 考虑这样一个问题:给你一个数字字符串,如何在32位环境下安全的处理可能超过[-2^31, 2^31 - 1]范围的数字,不能使用64位变量临时存储。 因为我熟悉Java, 所以下面提到的数据类型都为 Java 中的数据类型。 Integer.MAX_VALUE = 2^31 - 1 = 2147483647; Integer.MIN_VALUE = -2^31 = -2147483648; 首先题目说不能使用64位变量,所以 Long 类型不能使用,那就一步一步累加,具体思路就是设置两个变量 digit, res,从头到尾遍历字符串每一位,同时计算 res = res * 10 + digit。但这样的话,累加过程中 res 可能会无征兆直接溢出,程序直接抛异常。那怎么办,介绍一下从 K神题解 里学来的方法。 整体思路也是一步一步累加,但提前预判溢出。 这里有个核心变量 bndry = 2^31 / 10 = 214748364,,它是32位有符号整数最大值去掉最后一位的结果。 在执行 res = res * 10 + digit 之前,先检查 res 与 bndry 的关系,有下面两种溢出的可能。 情况一:res > bndry ,意味着 res 乘10后,即使加上最小的数字0,结果也会超过 2147483647,溢出。 情况二:res == bndry,此时是否溢出取决于要累加的数字 digit。 如果 digit <= 7,此时正数负数都不会溢出,但等于七对正数来说已经达到最大值。 如果 digit = 8,拼接后值为 2147483648,比 Integer.MAX_VALUE 大 1,而负数即 -2147483648 还未溢出。 如果 digit > 8,正数负数都溢出。 思路还是很清晰的,在下一位拼接前进行判断可以很好的避开溢出问题。下面看一下题解代码 ...
Java集合源码阅读
先附上二哥网站上关于集合框架的结构图 版本为JDK21 ArrayList 扩容机制 先介绍一下 ArrayList 中的关键变量: transient Object[] elementData 底层用来存储元素的数组 private int size; 表示集合中元素的实际数量 private static final int DEFAULT_CAPACITY = 10; 默认初始容量 private static final Object[] EMPTY_ELEMENTDATA = {}; 当用户调用 new ArrayList(0) 时,elementData 会引用该数组。 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 当用户调用默认构造参数时会引用该数组。 private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } private Object[] grow() { return grow(size + 1); } 上面是有关扩容的源代码。 ...
MySQL锁机制八股整理
整理一下MySQL锁的八股,全文整理自二哥博客。 在MySQL数据库中,锁是用来协调多个进程或线程并发访问同一资源的机制。锁不仅保证了数据的一致性和有效性,而且是影响数据库并发访问性能的一个重要因素。 共享锁与排他锁 共享锁也叫 S(shared) 锁,允许多个事务进行读操作,阻塞写操作。 排他锁也叫 X(exclusive) 锁,只允许一个事务进行读写操作,阻塞其他事务的读写操作。 兼容性如下: 表锁与行锁 表锁:锁定整个表,资源开销小,加锁快,但并发度低,不会出现死锁,适合查询为主、少量更新的场景(如 MyISAM 引擎)。可以细分为表级S锁、表级X锁。 行锁:锁定单行或多行,开销大、加锁慢,可能出现死锁,但并发度高(InnoDB 默认支持)。可以细分为记录锁、间隙锁、临键锁,也可以分为共享锁和排他锁(与表级S锁、表级X锁一个意思,前提是行锁)。 表锁详细版: 表锁常见于 MyISAM 引擎, InnoDB 也可手动加锁,适合读多写少、全表扫描或者表结构变更的场景。 LOCK TABLES table_name READ; -- 显式加读锁 select... -- 其他会话可读,不可写 UNLOCK TABLES; -- 释放锁 LOCK TABLES table_name WRITE; -- 显式加写锁 INSERT/UPDATE/DELETE table_name; -- 其他会话读写均阻塞 UNLOCK TABLES; MyISAM 在执行 SELECT 时会自动加读锁,执行 INSERT/UPDATE/DELETE 时会加写锁。 对于 InnoDB 引擎,无索引的 UPDATE/DELETE 可能会导致锁升级为表锁。执行 ALTER TABLE 时会自动加表锁,阻塞所有读写操作。 行锁详细版: 行锁是 InnoDB 存储引擎中最细粒度的锁,它锁定表中的一行记录,允许其他事务访问表中的其他行。 底层是通过给索引加锁实现的,这就意味着只有通过索引条件检索数据时,InnoDB 才能使用行级锁,否则会退化为表锁。 ...