芙影 - 学习
type: channel
link: https://cixingji.cloud/index.php/category/学习
items: 线程池及应用
items: 弹幕系统
items: Netty及应用
items: 计网
items: Docker配置
items: MYSQL结构与索引
items: MYSQL事务和锁
items: Spring Note 2(IOC AOP)
items: MYSQL日志
items: 引入Canal实现机制

线程池及应用
type: item
date: 2025-10-31T22:01:03+08:00
description: CompletableFuture 异步线程池顾名思义就是，管理一些线程的资源池，提供限制和管理资源的方式，维护一些基本信息。Executor框架Java5后面引进的一种框架，有队列，拒绝策略等东西由三部分组成：任务Task：定义做什么Runnable不返回结果、Callable返回结果可抛异常任务的执行Executor（ThreadPoolExecutor关键类继承于此）：谁来做怎么做异步计算的结果Future：如何获取结果运行流程：先决定用Runnable、Callable哪个接口通过调用 ExecutorService 的 submit() 方法来提交任务，具体的执行则由其实现类（如 ThreadPoolExecutor）来完成。给一个结果Future，可以主进程干别的获取结果Future.get，计算完成直接取走，计算未完成线程堵塞ThreadPoolExecutor核心参数image-20251010161133982ThreadPoolExecutor 3 个最重要的参数：corePoolSize：核心线程数：线程池中保持活动状态的线程数量，即使是空闲的（常驻店员）maximumPoolSize：最大线程数：线程池能够容纳同时执行的最大线程数（常驻店员+临时工最大值）只有workQueue和corePoolSize都满了时候才招临时workQueue：用于保存等待执行任务的阻塞队列（等候区）ThreadPoolExecutor其他常见参数 :unit : keepAliveTime 参数的时间单位。keepAliveTime:非核心线程在没有作业的时候的等待时间threadFactory :用于创建新线程，可以用它来为线程池中的线程命名。handler :拒绝策略：当任务队列已满且线程数达到maximumPoolSize时，用于处理新提交任务的策略。线程池创建一、通过ThreadPoolExecutor 构造函数创建没啥说的，上面参数一个一个写就行了构造函数。二、通过 Executors 工具类创建（不推荐）Executors提供了几个常用的静态方法创建线程池：Executors.newFixedThreadPool(int nThreads): 创建一个固定大小的线程池。Executors.newSingleThreadExecutor(): 创建一个只有一个线程的线程池。Executors.newCachedThreadPool(): 创建一个可缓存的线程池，线程数根据任务量动态调整。Executors.newScheduledThreadPool(int corePoolSize): 创建一个可以执行定时任务的线程池。[11]为什么不推荐用Executors 创建前两个：使用了无界的LinkedBlockingQueue作为任务队列。如果任务的生产速度远快于消费速度，队列中的任务会持续堆积，最终可能导致服务器内存溢出（OOM）。后两个：允许创建的线程数量几乎是无限的。当有大量并发请求时，它会不断创建新线程，这可能耗尽CPU和内存资源，导致系统崩溃。image-20251010164219604工作过程image-20251010165637571线程池拒绝策略CallerRunsPolicy, 使用线程池的调用者所在的线程去执行被拒绝的任务，除非线程池被停止或者线程池的任务队列已有空缺。AbortPolicy, 直接抛出干不了的异常DiscardPolicy, 不做任何处理，静默拒绝提交的任务。DiscardOldestPolicy, 抛弃最老的任务，然后执行该任务。还有自定义拒绝方式CompletableFuture为什么不用Future本身了呢？虽然他可以异步计算，但是获取结果的时候必须使用Future.get()的方式阻塞调用线程，或者轮询询问是否完成。我的项目创建CompletableFuture使用的是通过自定义线程池，根据runnable构建执行任务。也就是下面这种ompletableFuture.runAsync(() -> { redisUtil.zset("user_video_history:" + uid, vid); // 添加到/更新观看历史记录 videoStatsService.updateStats(vid, "play", true, 1); }, taskExecutor);而不是使用了默认内置线程池ForkJoinPool.commonPool()，因为这个线程池的处理线程数量是电脑的CPU核数-1，不如自定义1.有并行加载数据的，比如同时加载用户信息和视频信息，加快加载速度2.有异步通知消息的，就是如果有人点赞了异步去通知，作用就是用户不需要等结果，还有就是网络IO发消…

弹幕系统
type: item
date: 2025-10-30T22:42:00+08:00
description: 常规代码首先Controller层、Service层不必多说，常规流程的代码，这里说一下实现的功能有什么：1、根据弹幕id集合获取弹幕列表2、删除弹幕DanmuWebSocketServer技术考量1、首先因为DanmuWebSocketServer 类的实例不是由 Spring 容器管理的标准 Bean，想拿到数据库和缓存的内容只能定义成静态字段获取2、Session 是 中代表一个客户端与服务器之间完整、双向、持久化 WebSocket 连接的 Java 对象。基于SpringBoot容器。ServerEndpoint一、连接时执行 @OnOpen public void onOpen(Session session, @PathParam("vid") String vid) { if (videoConnectionMap.get(vid) == null) { Set<Session> set = new HashSet<>(); set.add(session); videoConnectionMap.put(vid, set); } else { videoConnectionMap.get(vid).add(session); } sendMessage(vid, "当前观看人数" + videoConnectionMap.get(vid).size()); // System.out.println("建立连接，当前观看人数: " + videoConnectionMap.get(vid).size()); }1、@OnOpen：连接就执行2、 videoConnectionMap.put(vid, set)：连接注册3、sendMessage(vid, "当前观看人数"...：广播观看人数的消息二、收到消息时触发1、@OnMessage2、Token验证：目的是做到只有登录用户才能发弹幕3、持久化弹幕到数据库4、（序列化后）广播弹幕String dmJson = JSON.toJSONString(danmu); sendMessage(vid, dmJson);三、连接关闭执行1、@OnClose2、在Map中移除当前的连接记录3、如果没人了就直接移除这个视频，有人就更新在线人数if (videoConnectionMap.get(vid).size() == 0) { videoConnectionMap.remove(vid); } else { sendMessage(vid, "当前观看人数" + ...)重点发送信息 public void sendMessage(String vid, String text) { Set<Session> set = videoConnectionMap.get(vid); // 使用并行流往各客户端发送数据 set.parallelStream().forEach(session -> { try { session.getBasicRemote().sendText(text); } catch (Exception e) { e.printStackTrace(); } }); } }session.getBasicRemote().sendText(text)实现发送并且动态渲染广播机制服务器里面存在一个通讯录// 对每个视频存储该视频下的session集合 private static final Map<String, Set<Session>> videoConnectionMap = new ConcurrentHashMap<>();有用户连接就会把session存到对应视频的集合里面然后像每个连接都发一遍消息就是广播// 步骤1：根据视频ID，获取所有观看该视频的客户端连接 Set<Session> set = videoConnectionMap.get(vid); // 步骤2：使用并行流遍历所有连接，并向每个连接发送消息 set.parallelStream().forEach(session -> { try { // 步骤3：通过session对象发送文本消息 session.getBasicRemote().sendText(text);

Netty及应用
type: item
date: 2025-10-29T18:11:00+08:00
description: Netty项目的实时通信就是基于Netty实现的。以下是Netty部分的笔记：引子首先说NIOimage-20251016212853121BIO：同步阻塞 IO （个人理解就是悲观锁）--数据的读写必须堵塞在一个线程里进行NIO： 同步非阻塞（个人理解就是多线程）--- 面向缓冲的，基于通道的IO方法AIO：异步非阻塞，就是NIO2代，基于事件和回调机制例如你有1000个IO操作，如果使用NIO，是1000个线程反复调用read。如果使用多路复用，可以1个线程反复调用select，在数据就绪后，再调用read。还是能节约资源的[!NOTE]异步 IO 是基于事件和回调机制实现的，也就是应用操作之后会直接返回，不会堵塞在那里，当后台处理完成，操作系统会通知相应的线程进行后续的操作。Netty是什么？Netty是一个基于NIO的CS框架，可以快速简单的开发网络应用程序；简化了TCP和UDP套接字服务器等网络编程。应用场景首先就是常用的框架比如RocketMQ、Elasticsearch实现自己的HTTP服务器，类Tomcat可以自己实现一个实时通信系统，我的项目就是基于Netty实现的通信各部件1、Channel：通道，所有数据的操作以及状态变更都在这个通道上，每个用户的WebSocket连接就是一个Channel。2、EventLoop:事件循环，单线程执行器负责处理一个或多个Channel上的所有I/O事件，永不停止。​ 单线程处理多个Channel：IO多路复用，同redis的单线程监听的大量客户端 连接，也就是NIO​ 永不停止：也就是说一直循环等待，永远不停止，只要有被分配到他头上的 Channel来了操作，就是他来执行。3、EventLoopGroup：事件循环组，一组事件循环的集合，用来管理和分配，项目中有BOSS和WORKER这两个4、Pipeline：通道管道，是依附在Channel上的处理器链，也就是责任链模式的工作机制启动流程（详细见实时通信的实现逻辑）(入站)客户端发消息，服务器收消息:WebSocketHandler+ChatHandler+IMServerIMServer：客户端发起WebSocket连接到端口Boss接受连接，创建ChannelBoss将这个新的Channel注册到work上的一个EventLoop上开始执行这个Channel上的Pipeline处理数据读取过来责任链传过来的数据JSON 反序列化成对象，并且再次拿到UserId封装到里面持久化数据：chatDetailedMapper.insert(chatDetailed);完整存 redisUtil.zset("chat_detailed_zset")消息id缓存准备推送数据：推送数据：获取发送者自己的Channel集合（多端同步）获取接收者所有的Channel集合（消息转发）出站：推送消息到对应的EventLoop上，启动出站Pipelinefor (Channel channel : from) { channel.writeAndFlush(IMResponse.message("whisper", map)); }(出站)服务端发消息，客户端收消息：WebSocketHandler业务触发：定位到目标用户的全部在线 Channel。写入数据：承接上面的推送writeAndFlush()启动出站Pipeline流：消息从 Pipeline 的尾部（靠近应用层）开始向前（靠近网络层）流动，执行出站处理器。编码：IMResponse 对象编码-->WebSocketFrame 转换为网络可传输的原始字节流。发送：EventLoop 线程将这些字节流写入操作系统的 Socket 发送缓冲区，数据通过网络发送给客户端。触发时机：聊天消息推送触发，评论通知推送，点赞通知推送总结：整个过程是围绕 EventLoop 和 Pipeline 循环往复：收消息（入站）： EventLoop 读数据 → Pipeline 从头到尾处理（解码、认证、业务）。发消息（出站）： 业务逻辑触发 → Pipeline 从尾到头处理（业务编码、协议编码） → EventLoop 写数据。心跳机制说心跳机制先说一下 TCP长连接和短连接长连接就是在S和C之间建立连接一直持续，短连接就是读写完成之后就关闭连接为什么需要心跳机制？在长连接的情况下，如果一方断开连接了，CS之间没有交互的话，是不知道对方已经断开了的，所以引入心跳机制：如果处于Idle状态（连接但无交互），C或S会发送一个特殊的数据包给对方，接收方接收到这个特殊数据报文，也返回一个特殊的数据报文，这就是一个PING-PONG 交互。Netty的心跳机制TCP本身有心跳机制，但是不太灵活，所以一般在…

计网
type: item
date: 2025-10-28T11:09:00+08:00
description: TCP三次握手一次握手:客户端发送带有 SYN（SEQ=x） 标志的数据包 -> 服务端，然后客户端进入 SYN_SEND 状态，等待服务端的确认；二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态；三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端，然后客户端和服务端都进入ESTABLISHED 状态，完成 TCP 三次握手。为什么三次握手第一次握手：Client 什么都不能确认； Server 确认了对方发送正常，自己接收正常第二次握手：Client 确认了：自己发送、接收正常，对方发送、接收正常； Server 确认了：对方发送正常，自己接收正常第三次握手：Client 确认了：自己发送、接收正常，对方发送、接收正常； Server 确认了：自己发送、接收正常，对方发送、接收正常三次握手就能确认双方收发功能都正常，缺一不可。image-20251028110654478含义SYN:同步。 用于发起连接请求ACK:确认，表示确认号字段有效SEQ=x： 报文段的序列号。它表示该报文段所携带的数据的第一个字节在整个字节四次挥手第一次挥手：客户端发送一个 FIN（SEQ=x） 标志的数据包->服务端，用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。第二次挥手：服务端收到这个 FIN（SEQ=X） 标志的数据包，它发送一个 ACK （ACK=x+1）标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态，客户端进入 FIN-WAIT-2 状态。第三次挥手：服务端发送一个 FIN (SEQ=y)标志的数据包->客户端，请求关闭连接，然后服务端进入 LAST-ACK 状态。第四次挥手：客户端发送 ACK (ACK=y+1)标志的数据包->服务端，然后客户端进入TIME-WAIT状态，服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复，就证明服务端已正常关闭，随后客户端也可以关闭连接了为什么要四次挥手?TCP 是全双工通信，可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知，待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候，则发出连接释放通知，对方确认后就完全关闭了 TCP 连接。举个例子：A 和 B 打电话，通话即将结束后。第一次挥手：A 说“我没啥要说的了”第二次挥手：B 回答“我知道了”，但是 B 可能还会有要说的话，A 不能要求 B 跟着自己的节奏结束通话第三次挥手：于是 B 可能又巴拉巴拉说了一通，最后 B 说“我说完了”第四次挥手：A 回答“知道了”，这样通话才算结束。image-20251028110709814

Docker配置
type: item
date: 2025-10-26T20:46:00+08:00
description: Docker运行笔记准备把项目优化，使用Docker打包环境配置，这样双机运行更方便一些省略双机的docker安装过程IDEA实现打包镜像1、安装docker插件2、在项目下创建Dockerfile文件（与pom.xml同目录）#基础镜像JRE8 FROM anapsix/alpine-java:8_server-jre_unlimited #时区设置 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime #目录 WORKDIR /app #端口 EXPOSE 7070 #jar实际名称 COPY ./target/backend-0.0.1-SNAPSHOT.jar app.jar # 运行Spring Boot应用 CMD java -jar app.jar3、配置docker连接4、构建镜像注意：如果要在本机上使用docker运行，数据库配置需要改动，如果依旧使用127.0.0.1，docker容器不能访问，遂改成${DB_HOST:127.0.0.1}，并且在运行的时候，指定DB_HOST的值为当前电脑ip即可。

MYSQL结构与索引
type: item
date: 2025-10-24T19:15:00+08:00
description: 存储引擎InnoDB和MyISAM区别InnodBmyisam事务支持不支持索引聚集索引非聚集索引（见B+树部分）锁最小是行锁最小是表锁count不保存行数保存，查行数很快恢复支持redo不支持索引索引是什么？就是数据库的目录为什么索引能提高速度？速度很快，其次是数据唯一性有保障（可以创建唯一索引），加速排序和分组（因为索引本身是排好序的）聚集索引和非聚集索引聚集索引：数据的物理顺序和列值的逻辑顺序相同（主键）查询的时候，因为聚集索引 B+ 树的叶子节点存储了完整数据，直接查就行非聚集索引：就是顺序没关系（除了聚集就全是非聚集）查询的时候，因为非聚集索引 B+ 树的叶子节点存储的是 索引值 和 对应主键值1、需要先在这个树找到该行的主键值2、使用主键值，去聚集索引 B+ 树二次查找这个过程就叫做「回表」索引的数据结构是树，因为树天生有二分查找的特性，查询速度高索引失效索引失效就是违反了数据库使用索引的原则，转变成了全表扫描查询条件类1、Like通配符前置LIKE '%关键字这个没什么说的，就是索引是左侧顺序匹配的，相当于词典，只给右边的字母显然不能找到索引。2、最左前缀原则在使用复合索引的时候，只有带着最左边的索引成分的时候，索引才能正常使用。索引失效的时候，查询的性能就变成了全表扫描，效率慢。CREATE INDEX idx_col ON table (a, b, c);a是最左边WHERE a = 1 AND b = 2可以，因为有a，索引成立WHERE b = 2 AND c = 3不可以，因为没有a，索引失效3、否定条件比如 ！= not in，范围太大，使用索引成本高于直接进行全表扫描。操作索引类1、对索引进行运算where DATE(time) = '2025-1-1'因为这相当于对索引列的所有数据都计算一遍，等同于全表扫描。2、索引隐形转换比如id是int，结果和string类型比较，内部转换，同1。MYSQL索引为什么使用 B+树？平衡二叉树AVL：AVL实现平衡主要是靠旋转，旋转是非常耗时的，所以不能用（增加和删除）红黑树：和AVL相比，红黑树不追求完美的平衡，只是大致的平衡。对比AVL，查询略降，删除大提高因为红黑树同时引入了颜色，当插入或删除数据时，只需要进行 O(1)次数的旋转以及变色就能保证基本的平衡，不需要像 AVL 树进行 O(lgn)次数的旋转。总的来说， 红黑树的统计性能高于 AVL。红黑树应用：HashMap链表+红黑树解决哈希冲突 ，TreeMap 红黑树存储排序键值对在“高且瘦”的树中，你每次比较都可能需要访问一个新的节点。如果每个节点都位于磁盘的不同页（Page）上，那么查找一个数据就可能需要很多次磁盘I/O。比如树高20，就需要最多20次I/O，这无法接受。B树：矮胖树，一个节点可以有多个子节点，每个节点都会存储数据本身，节点内的键值是排好序的，节点之间也满足排序关系。B+树直接看和B树的区别数据存储：B+树的Data只存在于叶子节点中，非叶子节点只有索引。（B树什么节点都存数据）叶子结点：B+树中只有叶子节点有指针，叶子节点之间通过指针链接起来形成一个有序链表（B树叶子节点是相互独立的）查找：B+树什么查找都得走到叶子节点，查找路径是稳定的（B树可能在非叶子节点就命中）Data，可能是行的全部数据（如 Innodb 的聚簇索引），也可能只是行的主键（如 Innodb 的辅助索引），或者是行所在的地址（如 MyIsam 的非聚簇索引）。(重要)由于B+树非叶子节点不存Data，所以每个节点能存的索引更多，树更矮。因此，B+树优势：1、因为树的结构更矮胖，磁盘IO次数少（读写）2、因为叶子节点是通过双向链表连接的，查询到一条数据就可以沿着链表的上下直接顺序查询，IO路径规律，范围查询效率更高SELECT * FROM users WHERE age BETWEEN 20 AND 30IO路径规律——>磁头不需要大动3、因为Data都存在于叶子节点里面，最差的情况就是一般情况，查询性能稳定。4、因为双向链表和Data存于叶子，全表遍历效率高，只需要遍历链表就行SQL优化慢SQL优化1、定位慢查询在配置文件启用，long_query_time = 1：设置阈值；low_query_log = 1：开启慢查询日志。然后执行 SHOW FULL PROCESSLIST；看status和time2、分析慢查询情况定位之后，对这个语句执行加上EXPLAIN；看type：类型，all是全表，其他次之看rows：行数3、解决

MYSQL事务和锁
type: item
date: 2025-10-23T22:56:00+08:00
description: 锁表级锁和行级锁的区别qubie表级锁行级锁范围锁整张表锁当前字段实现简单，加锁快困难，加锁慢与引擎无关有关，因引擎谁有MYISAM、InnoDBInnoDB（默认行）InnoDB的几种表级锁表锁表级读锁：阻止其他的对表的写表级写锁：阻止其他对表的读写元数据锁MDL作用：防止执行CRUD的时候表结构变更数据库本身会加MDL，不用我们来操作写锁获取优先级高于读锁对一张表进行 CRUD 操作时，加的是 MDL 读锁；对一张表做结构变更操作的时候，加的是 MDL 写锁；那它在什么时候释放？-->在事务提交之后意向锁作用：为了快速判断表里是否有行锁为什么要判断？因为读锁和行锁是读写互斥，写写互斥的在加上行共享锁之前，先在表级别加上一个“意向共享锁”在加上行独占锁之前，先在表级别加上一个“意向独占锁”这里面谈到普通的select语句是不算里面的，这类是通过MVCC实现的一致性，无锁InnoDB的几种行级锁锁的兼容性分类共享锁S：读锁，事务在读取记录的时候获取共享锁，允许多个事务同时获取锁排它锁X：写锁，事务在修改记录的时候获取排它锁，不允许多个事务.....。锁的范围分类记录锁：锁定单个记录。间隙锁：锁定一个id数字集合（范围），不包括记录本身。前开后开临键锁：记录锁+间隙锁，锁定一个范围，包含记录本身，主要目的是为了解决幻读问题。前开后闭什么时候加行级锁？普通的 select 语句是不会对记录加锁的，MVCC实现一致性首先记住，加锁的对象都是索引！！！，基本单位就是默认的next-key lock唯一索引等值查询情况查询的记录 存在，定位到记录之后，退化成 X记录锁查询的记录 不存在，定位到第一个大于查询记录的记录之后，退化成 间隙锁，锁住这个记录和上一条记录的范围思考：不沿用next-key lock的原因是因为这个场景下幻读只需要间隙锁就能解决了为什么 id = 5 记录上的主键索引的锁不可以是 next-key lock？如果是 next-keylock，就意味着其他事务无法删除 id = 5 这条记录，但是这次的案例是查询 id = 2 的记录，只要保证前后两次查询 id = 2 的结果集相同，就能避免幻读的问题了。唯一索引范围查询情况先说明范围查询的整体机制：会对每一个扫描到的索引加next-key lock，下面细分情况实际在 Innodb 存储引擎中，会用一个特殊的记录来标识最后一条记录，该特殊的记录的名字叫supremum pseudo-record大于情况：往后推，前加一个包含自己的，后面以此递归最后扫到supremum加 (xxxx, +∞]大于等于情况：等值的地方变成记录锁小于或小于等于情况：条件值不在表里，终止扫描记录的Next-Key锁退化成间隙锁小于情况：条件值在表里，终止扫描记录的Next-Key锁退化成间隙锁小于等于情况：条件值在表里，终止扫描记录的Next-Key锁不退化成间隙锁举例：有个表有4个数据项idname10A20B30C40D情况5：10满足加(Previous, 10]，20满足加(10, 20]，30满足加(20, 30]，到边界了，终止扫描SELECT * FROM products WHERE id <= 30;情况4：10满足加(Previous, 10]，20满足加(10, 20]，30不满足，退化，加(20, 30)SELECT * FROM products WHERE id < 30;情况1：定位到30，加(20, 30]，40满足加(30, 40]，接着扫描，加锁(40, +∞]SELECT * FROM products WHERE id > 20;情况2：20，临键锁退化，变成记录锁锁20，接着同情况1SELECT * FROM products WHERE id >= 20;情况3：10满足加(Previous, 10]，20满足加(10, 20]，然后到30，不满足，退化，加(20, 30)SELECT * FROM products WHERE id <(<=) 25;什么时候加表级锁？当执行UPDATE、DELETE的语句的时候，WHERE没有命中索引或者索引失效的时候，就会对全表扫描加锁。事务四大特性原子性：事务是最小的执行单位，不能分割 --- undo log保证的隔离性：并发访问数据库的时候，事务彼此之间是不可见的（取决于事务隔离级别）比如先后扣款，先扣款事务还没做完，后扣款事务来了看到的也是原数量持久性：一个事务被提交之后，他对数 Q`据库的改变是永久的，即使数据库发生故障。 --- redo log保证的一致性：执行事务前后，数据保持一致（指变化总和）并发事务带来的问题（问题）脏读一个事务读取数据并且修改了这个数据，这个时候第二个事务读取了这个未…

Spring Note 2(IOC AOP)
type: item
date: 2025-10-23T22:20:00+08:00
description: 谈谈对IOC的理解（是什么）IOC也就是控制反转，要谈谈IOC，就要先说一下控制反转是什么。传统的开发方式就是A依赖于B，那就是A 手动new一个B的对象出来；而IOC的方式是不需要new对象，而是通过IOC容器也就是Spring来帮助我们实例化对象，需要什么对象，就去IOC容器里取出来就行。所以说控制反转的意思就是“把对象创建管理什么的 权利交给外部环境IOC”。（为什么）那为什么要使用IOC？解决了什么问题？对象之间的耦合度变低，也可以说依赖程度；资源容易管理，比如使用spring容器很容易实现一个单例，而传统模式得自己写。没有IOC是这样的：Service 层想要使用 Dao 层的具体实现的话，需要通过 new 关键字在UserServiceImpl 中手动 new 出 IUserDao 的具体实现类 UserDaoImpl（不能直接 new 接口类）。放图...有IOC是这样的：放图...谈谈对AOP的理解（是什么）aop就是面向切面编程，主要的思想就是把横切关注的地方从核心业务逻辑分离出来，切面Aspect：对关注点封装的类。通知类型有哪些？Before前置通知：在方法调用之前触发After后置通知：在方法调用之后触发AfterReturning返回通知：在方法调用完成，返回返回值之后触发AfterThrowing异常通知：在方法调用中抛出异常后触发Around环绕通知：可以在任意前后触发，还可以直接拿到目标对象，要执行的方法（为什么）为什么要用AOP？解决了什么问题？比如日志记录、事务管理、权限控制、接口限流等，如果一直重复实现这些行为，代码会特别冗余，难以维护。// 日志注解 @Target({ElementType.PARAMETER,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /** * 描述 */ String description() default ""; /** * 方法类型 INSERT DELETE UPDATE OTHER */ MethodType methodType() default MethodType.OTHER; } // 日志切面 @Component @Aspect public class LogAspect { // 切入点，所有被 Log 注解标注的方法 @Pointcut("@annotation(cn.annotation.Log)") public void webLog() { } /** * 环绕通知 */ @Around("webLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { // 省略具体的处理逻辑 } // 省略其他代码 }@Log(description = "method1",methodType = MethodType.INSERT) public CommonResponse<Object> method1() { // 业务逻辑 xxService.method1(); // 省略具体的业务处理逻辑 return CommonResponse.success(); }（怎么实现的？）spring AOP就是基于动态代理实现的，我详细聊聊Spring AOP 的实现机制Spring AOP 的实现机制A.如何实现剥离？一共有三个对象，真实对象（Target）、代理对象（Proxy）、增强逻辑（Advice）工作流程：接收到方法调用。执行增强逻辑（如 @Before 的通知）。将调用转发给真实对象。执行增强逻辑（如 @After 的通知）。返回结果。B.机制一：JDK动态代理（基于接口）条件：至少实现了一个接口为什么至少实现了一个接口？因为JDK动态代理生成的类已经继承了Proxy类，由于单继承，所以它只能通过实现接口的方式实现代理原理：利用了Java原生的API Proxy InvocationHandler运行的时候动态生成一个代理类，这个类和真实对象是兄弟关系，实现同一个接口通过实现InvocationHandler插入增强的逻辑调用代理对象，走的增强逻辑C.机制二：CGLIB动态代理（基于继承）条件：无（没实现接口只能用它）原理：利用字节码运行的时候操作字节码，生成一个代理类，这个类和真实对象是父子关系子类会重写目标中所有非final的方法重写的方法中加入增强逻辑为什么不能重写final方法？因为final方法不能重写public Object invoke(Object proxy, …

MYSQL日志
type: item
date: 2025-10-23T18:02:00+08:00
description: 三大日志binlog二进制日志Server层的日志，逻辑日志，用来全量数据备份和主从复制。写入时机在每完成一次更新操作之后，生成一条binlog刷盘时机什么时候刷盘由sync_binlog参数决定0：不去强制要求，由系统自行判断何时写入磁盘；1：每次事务提交后，都要将binlog写入磁盘；N：每N个事务，才会将binlog写入磁盘。方案STATEMENT：记录数据变更的SQL语句本身好：日志文件体积小，一SQL记一次坏：可能存在数据不一致，对于动态函数---NOW()UUID()LIMIT等依赖上下文，会导致主从数据不一致ROW：记录数据最终被修改成什么样好：数据一致性最高，绝对一致主从坏：日志文件体积大，更新多少行就记多少MIXED：折中判断特点追加写，写满一个文件，就创建一个新的文件继续写，不覆盖，存全量日志--->恢复，主从复制注意的是只记录库、表结构改变和数据修改的日志redolog重做日志存储引擎层面的，物理日志，用来保障事务的持久性和崩溃恢复Mysql有Buffer Pool缓冲池，跟redis一样。如果在Buffer Pool中的页的修改还没持久化到磁盘中，这个时候宕机了，事务更改就消失了引入WAL技术，就是先写到日志，再写到磁盘上image-20251010110214968写入时机当执行一条更新语句（如 UPDATE）时，先在 Buffer Pool 中查找对应的数据页。如果不在，就从磁盘加载到 Buffer Pool。接着，修改 Buffer Pool 中的这个数据页。此时，这个被修改了但还没刷回磁盘的数据页，就是所谓的 “脏页”。标记脏页的同时，InnoDB把这个页面做了什么物理修改记录记录到Redo Log Buffer中每一次数据变更都会产生对应的redo log例如：在表空间xx文件、第xx页、偏移量xx的位置写入了xxx数据刷盘时机事务提交时，先把 Redo Log Buffer 中对应的所有日志记录，强制刷盘，写到redolog中， 此时，现在已经算是被持久化了。（由参数调控）trx_commit = 1：每次事务提交时，都必须立刻将 redo log 从内存刷入磁盘。（默认）trx_commit = 0：不保证事务提交时刷盘，而是大约每秒钟由后台线程将 redo log 从内存刷入磁盘一次。不安全trx_commit = 2：每次事务提交时，只把 redo log 从内存写入到操作系统的页缓存（Page Cache），不直接写入磁盘文件。然后大约每秒钟，操作系统会把它的缓存统一刷到磁盘。 折中因为即使此时Buffer Pool 中的脏页还没有写回数据库，崩溃的时候，也可以依靠Redo log来进行重做，恢复到事务最后一次提交的状态这就是WAL思想，先写日志，后写数据两阶段提交不一致问题保障binlog和redolog的数据一致性，不一致的问题：先写 Redo Log，后写 Binlog，但在写 Binlog 前宕机：Redo Log 已经持久化，数据可以恢复。Binlog 没有记录。后果： 主库数据是完整的，但从库（通过 Binlog 复制）会缺失这条数据，导致主从数据不一致。先写 Binlog，后写 Redo Log，但在写 Redo Log 前宕机：Binlog 已经记录，从库会复制这条数据。Redo Log 没有完全持久化，崩溃恢复时主库会回滚这条数据（WAL 思想失败）。后果： 从库多出一条数据，主从数据不一致。阶段一：Prepare 准备阶段数据变更： 事务执行所有 SQL 语句，修改 Buffer Pool 中的数据页，并生成相应的 Redo Log 记录。Redo Log 刷盘：存储引擎将 Redo Log Buffer 中的数据刷入磁盘。标记 XID： 在这条 Redo Log 记录上，会标记一个事务 ID (XID)，并将其状态设置为 "Prepare"（准备状态）。状态： Redo Log 已持久化到磁盘，但事务在 InnoDB 内部仍未最终提交。如果此时宕机，恢复时 Redo Log 会发现自己处于 "Prepare" 状态，等待 Binlog 的最终判断。阶段二：Commit 提交阶段Step 2.1：写入 Binlog（Server 层操作）写入 Binlog：Server 层开始处理，将这个事务对应的 Binlog 事件写入到 Binlog 文件中。binlog刷盘： 此时，Binlog 也被强制刷盘。记录 XID： 在 Binlog 事件中，同样会记录这个事务的 XID。状态： Redo Log（Prepare 状态）和 Binlog 都已持久化到磁盘。Binlog 成为崩溃恢复的最终裁决者。Step 2.2：写入 Redo Log Commit 标记（InnoD…

引入Canal实现机制
type: item
date: 2025-10-20T21:35:00+08:00
description: 当前项目在更新数据的时候，是单纯的先更数据库再删除缓存，没有加入删除失败重试的机制，通过选型，选择了Canal进行监听Mysql的操作，并且接管删除缓存和重试操作。首先就是得知道Canal的原理，为什么能实现以上流程。Canal的原理要了解Canal的原理，首先得知道Mysql主从复制的原理Mysql主从复制第一步： 数据库发生变化，Master 写到 binlog。第二步： Slave 进程连接 Master，把 binlog 里的内容 复制 到自己的 relay log 中。第三步：Slave 另一个进程 读取 relay log，并把它里面的操作 执行 在自己的数据库上。通俗易懂来说就是 「抄作业」Canal实现监听的机制第一步：canal模拟slave的交互协议，伪装自己是slave，向master发送dump协议（其实就是不断传输的一个动作）第二步：master收到dump请求，建立连接，开始推送binlog给canal第三步：canal解析binlog（字节流）Canal实现同步的机制还在学习......Canal实现监听安装Canal安装完成修改配置# position info canal.instance.master.address=127.0.0.1:3306 canal.instance.master.journal.name=binlog.000001 canal.instance.master.position=157配置以上信息及账号密码启动Canal这里发生报错了，一顿排查之后发现是因为我的数据库的binlog不叫binlog，叫DESKTOP-xxxx-bin.000001，修改配置文件之后启动成功SHOW MASTER STATUS;image-20251019165338280Spring配置 <dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.client</artifactId> <version>1.1.4</version> </dependency>canal: # 部署到208上，port默认为11111 server: 127.0.0.1:11111 # 实例名，与之前复制example之后那个文件夹名称一致 destination: redis # 设置canal消息日志打印级别 logging: level: top.javatool.canal.client: warn创建账号CREATE USER canal IDENTIFIED BY 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; -- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ; FLUSH PRIVILEGES;创建监听类import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.client.CanalConnectors; import com.alibaba.otter.canal.protocol.CanalEntry; import com.alibaba.otter.canal.protocol.Message; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.net.InetSocketAddress; import java.util.List; @Component public class CanalListener { /** * 解析数据 * * @param beforeColumns 修改、删除后的数据 * @param afterColumns 新增、修改、删除前的数据 * @param dbName 数据库名字 * @param tableName 表大的名字 * @param eventType 操作类型（INSERT,UPDATE,DELETE） * @param timestamp 消耗时间 */ private static void dataDetails(List<CanalEntry.Column> beforeColumns, List<CanalEntry.Column> afterColumns, String dbName, String tableName…