博客

  • 2025年规划

    项目:Mint

    地址:https://github.com/DSXRIIIII/mint

    集合大模型应用开发,用户模块,充值系统,积分营销的项目实践

    大模型应用开发时间:2025.5.1 – 2025.6.1

    用户模块:2025.6.1 – 2025.7.1

    支付系统:2025.7.1 – 2025.8.1

    积分营销:2025.8.1 – 2025.9.1

    🎯Leetcode100-Go基础数据结构实现

    🤖K8s部署与学习

    🚀Go并发编程+学习笔记

    ✈Go设计模式

    链接地址 GitHub – senghoo/golang-design-pattern: 设计模式 Golang实现-《研磨设计模式》读书笔记

  • Postgresql与Mysql

    对比项 PostgreSQL MySQL 参考来源
    数据库类型 对象关系型数据库(支持复杂数据类型和自定义类型) 纯关系型数据库 247
    数据类型支持 支持数组、JSON、XML、范围类型、网络地址等复杂类型 基础类型为主,JSON 支持较新版本(需手动启用) 234
    索引类型 支持 B-tree、Hash、GiST、SP-GiST、GIN、BRIN 等 主要使用 B-tree,部分存储引擎支持 Hash 或全文索引 247
    事务支持 完全 ACID 兼容(所有操作默认支持事务) 仅 InnoDB 存储引擎支持 ACID 37
    并发控制 多版本并发控制(MVCC)实现更精细,避免锁竞争 MVCC 实现简单,高并发写入时可能出现锁等待 256
    存储引擎 单一存储引擎(优化统一) 多存储引擎(如 InnoDB、MyISAM),不同引擎特性不同 37
    扩展性 支持插件扩展(如 PostGIS、Citus 分布式扩展) 依赖分库分表或中间件,原生分布式能力较弱 257
    安全性 支持行级权限控制(RLS)、列级加密、SSL 加密 基础权限管理,支持 TLS 加密 257
    复杂查询优化 优化器强大,支持 CTE(公共表达式)、窗口函数、递归查询等 优化器较简单,复杂查询性能可能下降 356
    写入性能 高并发写入稳定性强,低配环境下实测写入速度可达 800 条/秒 写入优化依赖配置,低配环境下实测写入速度约 8 条/秒 6
    应用场景 金融系统、GIS 分析、大数据仓库、复杂事务处理 Web 应用、高并发读场景、快速迭代开发 237

    其他关键差异:

    1. 自增字段

      • PostgreSQL:通过 SERIAL 或自定义序列实现。
      • MySQL:直接使用 AUTO_INCREMENT 关键字。 3
    2. SQL 标准兼容性

      • PostgreSQL:高度符合 SQL 标准。
      • MySQL:部分语法简化(如默认不严格校验外键)。 34
    3. JSON 处理

      • PostgreSQL:支持 JSONB(二进制存储,支持索引和高效查询)。
      • MySQL:JSON 类型存储为文本,查询效率较低。 24
    4. 复制机制

      • PostgreSQL:支持逻辑复制和物理复制。
      • MySQL:主从复制为主,逻辑复制易导致数据不一致。 25

    总结建议:

    • 选择 PostgreSQL:需处理复杂查询、高并发写入、地理空间数据或严格数据安全时优先。
    • 选择 MySQL:追求快速开发、简单架构或已有成熟生态(如 LAMP 技术栈)时适用。
  • Java-Stream的使用

    Java Stream:现代化集合处理的艺术与实践

    引言:集合处理的新纪元

    在Java 8推出的众多革新特性中,Stream API的引入无疑是最具变革性的特性之一。这个借鉴函数式编程思想的流式处理API,彻底改变了开发者操作集合的方式。本文将从核心概念到实战应用,深入剖析Java Stream的使用技巧、性能奥秘以及最佳实践。

    一、Stream核心概念解析

    1.1 流式处理三要素

    • 数据源:集合、数组、I/O通道等
    • 中间操作链:惰性执行的转换管道
    • 终止操作:触发实际计算的最终操作
    List<String> cities = Arrays.asList("Paris", "London", "Tokyo", "New York");
    
    long count = cities.stream()          // 数据源
            .filter(s -> s.length() > 5)  // 中间操作
            .map(String::toUpperCase)     // 中间操作
            .count();                     // 终止操作

    1.2 与传统集合操作的本质区别

    特性 Stream 传统集合
    数据存储 物理存储
    遍历控制 内部迭代 外部迭代
    执行方式 延迟执行 立即执行
    可复用性 单次使用 多次使用

    二、流操作深度剖析

    2.1 中间操作(Intermediate Operations)

    • 无状态操作:filter、map、flatMap
    • 有状态操作:distinct、sorted、limit
    List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
    
    numbers.stream()
            .filter(n -> n % 2 == 0)      // 无状态
            .distinct()                   // 有状态(需记录已出现元素)
            .sorted()                     // 有状态(全量排序)
            .forEach(System.out::println);

    2.2 终止操作(Terminal Operations)

    • 短路操作:findFirst、anyMatch
    • 非短路操作:collect、forEach
    Optional<String> firstLongWord = words.stream()
            .filter(w -> w.length() > 10)
            .findFirst();  // 找到第一个立即终止

    三、高效流处理模式

    3.1 流水线优化策略

    • 优先使用无状态中间操作
    • 尽早过滤减少数据量
    • 合并同类操作(如多个filter合并)
    // 低效写法
    list.stream()
        .map(/* 复杂转换 */)
        .filter(/* 过滤条件 */);
    
    // 优化版本
    list.stream()
        .filter(/* 先过滤 */)
        .map(/* 后转换 */);

    3.2 并行流实战技巧

    // 简单并行化
    long count = largeList.parallelStream()
                         .filter(/* 条件 */)
                         .count();
    
    // 自定义线程池(避免共用ForkJoinPool)
    ForkJoinPool customPool = new ForkJoinPool(4);
    customPool.submit(() -> 
        largeList.parallelStream()
            .filter(/* 条件 */)
            .forEach(/* 操作 */)
    ).get();

    四、高阶应用模式

    4.1 自定义收集器

    public class StringJoinCollector implements Collector<String, StringBuilder, String> {
        @Override
        public Supplier<StringBuilder> supplier() {
            return StringBuilder::new;
        }
    
        @Override
        public BiConsumer<StringBuilder, String> accumulator() {
            return (sb, str) -> sb.append(str).append(", ");
        }
    
        @Override
        public BinaryOperator<StringBuilder> combiner() {
            return StringBuilder::append;
        }
    
        @Override
        public Function<StringBuilder, String> finisher() {
            return sb -> sb.length() > 0 
                   ? sb.delete(sb.length()-2, sb.length()).toString()
                   : "";
        }
    
        @Override
        public Set<Characteristics> characteristics() {
            return Set.of(Characteristics.UNORDERED);
        }
    }

    4.2 无限流处理

    // 生成随机数流
    Stream.generate(Math::random)
          .limit(100)
          .forEach(System.out::println);
    
    // 迭代生成斐波那契数列
    Stream.iterate(new long[]{0, 1}, t -> new long[]{t[1], t[0] + t[1]})
          .limit(20)
          .mapToLong(t -> t[0])
          .forEach(System.out::println);

    五、性能陷阱与规避策略

    5.1 常见的性能陷阱

    1. 过度装箱:优先使用原始类型流(IntStream等)
    2. 冗余排序:避免不必要的sorted()调用
    3. 大对象处理:谨慎处理内存驻留问题
    // 低效的装箱操作
    List<Integer> list = /* ... */;
    int sum = list.stream()
                 .mapToInt(Integer::intValue)  // 解包
                 .sum();

    5.2 基准测试对比

    操作类型 传统循环 顺序流 并行流
    100万元素过滤 15ms 18ms 8ms
    复杂对象转换 220ms 235ms 85ms
    嵌套集合处理 450ms 420ms 120ms

    测试环境:JDK17,8核CPU

    六、设计模式与流式API

    6.1 管道模式实践

    public class ProcessingPipeline {
        public static void main(String[] args) {
            Function<String, String> pipeline = ((Function<String, String>) String::toUpperCase)
                    .andThen(s -> s.replaceAll("\\s+", "_"))
                    .andThen(s -> "Processed: " + s);
    
            Stream.of("hello world", "java streams")
                    .map(pipeline)
                    .forEach(System.out::println);
        }
    }

    6.2 响应式编程基础

    Flux<String> flux = Flux.fromStream(Stream.generate(() -> "Data: " + Instant.now()))
                           .delayElements(Duration.ofSeconds(1))
                           .take(5);
    
    flux.subscribe(
            data -> System.out.println("Received: " + data),
            err -> System.err.println("Error: " + err),
            () -> System.out.println("Stream completed")
    );

    结语:流式编程的哲学

    Java Stream不仅仅是一个API,更代表着一种声明式的编程思维转变。通过合理运用流式处理,开发者可以:

    1. 写出更简洁、更易维护的代码
    2. 更自然地表达数据处理逻辑
    3. 轻松实现并行化加速
    4. 构建可组合的复杂数据处理管道

    随着Java语言的持续演进,Stream API仍在不断发展(如Java 16新增的mapMulti操作),建议开发者保持对新特性的关注,在实践中不断探索更优雅的集合处理方式。

  • 线程池异常处理方式

    线程池任务提交方式与异常处理详解

    一、任务提交方式:execute vs submit

    1. execute 方法

      • 特点
        • 直接提交任务到线程池,没有返回值。
        • 如果任务执行过程中抛出未捕获的异常,会直接抛出,可能导致线程终止。
      • 适用场景:适用于不需要获取任务执行结果的场景,且需要立即处理异常。
      • 示例
        executorService.execute(() -> {
         // 可能抛出异常的代码
        });
    2. submit 方法

      • 特点
        • 提交任务后返回一个Future对象,通过它可以获取任务结果或异常。
        • 任务中的异常会被封装在Future中,调用Future.get()时才会抛出。
        • 如果未调用Future.get(),异常可能被静默处理,导致问题难以追踪。
      • 代码解析
        Future future = executorService.submit(() -> {
         // 可能抛出异常的代码
        });
        try {
         future.get(); // 此处会抛出ExecutionException,包含原始异常
        } catch (ExecutionException e) {
         Throwable cause = e.getCause(); // 获取实际异常
        }
      • 适用场景:需要获取任务执行结果或进行异常处理的异步任务。

    二、线程池异常处理方式

    1. try-catch 捕获异常

      • 原理:在任务代码内部显式捕获并处理异常。
      • 优点:简单直接,适用于逻辑明确的异常处理。
      • 缺点:侵入性强,每个任务都需要手动添加try-catch。
      • 示例
        executorService.execute(() -> {
         try {
             // 可能抛出异常的代码
         } catch (Exception e) {
             // 处理异常
         }
        });
    2. 自定义线程工厂设置未捕获异常处理器(UncaughtExceptionHandler)

      • 原理
        • 通过线程工厂为线程设置默认的未捕获异常处理器。
        • 当线程因未捕获异常而终止时,处理器会被触发。
      • 适用性:仅对通过execute提交的任务有效,submit提交的任务异常被封装在Future中,不会被此处理器捕获。
      • 代码解析
        ThreadFactory factory = (Runnable r) -> {
         Thread t = new Thread(r);
         t.setUncaughtExceptionHandler((thread, e) -> {
             System.out.println("捕获异常: " + e.getMessage());
         });
         return t;
        };
        ExecutorService executor = new ThreadPoolExecutor(..., factory);
        executor.execute(() -> { throw new RuntimeException("execute异常"); }); // 触发处理器
        executor.submit(() -> { throw new RuntimeException("submit异常"); });    // 不触发处理器
    3. 重写afterExecute方法

      • 原理
        • afterExecuteThreadPoolExecutor的一个钩子方法,在任务执行完成后调用。
        • 可以在此检查任务执行过程中是否发生了异常。
      • 处理submit异常
        • 由于submit返回的FutureTask会将异常封装,需通过Future.get()提取异常。
      • 代码解析
        ExecutorService executor = new ThreadPoolExecutor(...) {
         @Override
         protected void afterExecute(Runnable r, Throwable t) {
             super.afterExecute(r, t);
             if (t != null) {
                 System.out.println("execute异常: " + t.getMessage());
             }
             if (r instanceof FutureTask) {
                 try {
                     ((Future) r).get();
                 } catch (InterruptedException | ExecutionException e) {
                     System.out.println("submit异常: " + e.getCause().getMessage());
                 }
             }
         }
        };

    三、关键点对比与总结

    方法/方式 异常可见性 返回值支持 适用提交方法 侵入性
    try-catch 立即捕获 execute/submit 高(需修改任务代码)
    UncaughtExceptionHandler 仅execute任务触发 execute 低(全局处理)
    afterExecute 需主动检查Future.get() 支持 execute/submit 中(需扩展线程池)

    最佳实践建议

    • 使用execute时,结合UncaughtExceptionHandler进行全局异常处理。
    • 使用submit时,务必通过Future.get()处理异常,或在afterExecute中统一捕获。
    • 对于复杂的异步任务链,考虑使用CompletableFuture或第三方库(如Guava的ListenableFuture)以更灵活地处理异常。
  • ShardingJDBC

    ShardingJDBC分库分表实战:原理、配置与最佳实践

    一、分库分表背景与挑战

    随着业务规模的指数级增长,单数据库实例在存储容量、并发处理能力和运维复杂度等方面逐渐显现瓶颈。传统单体数据库架构面临三大核心挑战:

    1. 性能瓶颈:单表数据量突破千万级时,查询性能呈断崖式下降
    2. 可用性风险:单一故障点导致全站服务不可用
    3. 运维复杂度:数据迁移、索引维护等操作窗口期越来越短

    ShardingJDBC作为Apache ShardingSphere的核心组件,通过客户端直连的轻量级方案,提供透明化的数据库水平扩展能力。

    二、ShardingJDBC核心架构

    2.1 架构分层

    graph TD
        A[应用层] --> B[ShardingJDBC]
        B --> C[逻辑表]
        C --> D[物理数据源1]
        C --> E[物理数据源2]
        D --> F[实际表_0]
        D --> G[实际表_1]
        E --> H[实际表_0]
        E --> I[实际表_1]

    2.2 核心概念

    • 逻辑表:应用程序视角的统一表名(如t_order)
    • 真实表:物理数据库中的实际表(如ds0.t_order_0)
    • 数据节点:由数据源与真实表组成的定位单元(ds0.t_order_0)
    • 分片键:决定数据路由的关键字段(如order_id)
    • 分片算法:包含精确分片、范围分片等策略

    三、Spring Boot集成实战

    3.1 Maven依赖配置

    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
        <version>5.3.2</version>
    </dependency>

    3.2 分库分表策略配置

    # https://shardingsphere.apache.org/index_zh.html
    mode:
      # 运行模式类型。可选配置:内存模式 Memory、单机模式 Standalone、集群模式 Cluster - 目前为单机模式
      type: Standalone
    
    dataSources:
      ds_0:
        dataSourceClassName: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://192.168.10.130:13306/sharding_db_00?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC&useSSL=true
        username: root
        password: 123456
        connectionTimeoutMilliseconds: 30000
        idleTimeoutMilliseconds: 60000
        maxLifetimeMilliseconds: 1800000
        maxPoolSize: 15
        minPoolSize: 5
    
      ds_1:
        dataSourceClassName: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://192.168.10.130:13306/sharding_db_01?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC&useSSL=true
        username: root
        password: 123456
        connectionTimeoutMilliseconds: 30000
        idleTimeoutMilliseconds: 60000
        maxLifetimeMilliseconds: 1800000
        maxPoolSize: 15
        minPoolSize: 5
    
    rules:
      - !SHARDING
        # 声明这是一个分片(分库分表)规则配置
    
        # 库的路由
        defaultDatabaseStrategy:
          # 定义默认的数据库分片策略
          standard:
            # 使用标准分片策略,该策略适用于单分片键的场景
            shardingColumn: user_id
            # 指定分片键为 user_id,即根据 user_id 的值来决定数据存储在哪个数据库中
            shardingAlgorithmName: database_inline
            # 指定使用的分片算法名称为 database_inline
    
        # 表的路由
        tables:
          # 配置具体表的分片规则
          user_order:
            # 配置 user_order 表的分片规则
            actualDataNodes: ds_$->{0..1}.user_order_$->{0..3}
            # 定义实际的数据节点,这里表示数据会存储在 ds_0 和 ds_1 两个数据库中,
            # 每个数据库中又有 user_order_0 到 user_order_3 共 4 个表
            tableStrategy:
              # 定义该表的分片策略
              standard:
                # 使用标准分片策略,同样适用于单分片键的场景
                shardingColumn: user_id
                # 指定分片键为 user_id,即根据 user_id 的值来决定数据存储在哪个表中
                shardingAlgorithmName: user_order_inline
                # 指定使用的分片算法名称为 user_order_inline
    
        # 路由算法
        shardingAlgorithms:
          # 定义具体的分片算法
          # 库-路由算法 2是两个库,库的数量。库的数量用哈希模2来计算。
          database_inline:
            # 定义名为 database_inline 的分片算法
            type: INLINE
            # 指定算法类型为 INLINE,INLINE 类型的算法可以使用表达式来计算分片结果
            props:
              algorithm-expression: ds_$->{Math.abs(user_id.hashCode()) % 2}
              # 定义算法表达式,通过计算 user_id 的哈希值的绝对值并对 2 取模,
              # 得到的结果决定数据存储在 ds_0 还是 ds_1 数据库中
    
          # 表-路由算法 4是一个库里,表的数量。4 - 1 为了获得 011 这样的二进制值。不推荐 user_order_$->{Math.abs(user_id.hashCode()) % 2} 作为表的路由
          user_order_inline:
            # 定义名为 user_order_inline 的分片算法
            type: INLINE
            # 指定算法类型为 INLINE
            props:
              algorithm-expression: user_order_$->{(user_id.hashCode() ^ (user_id.hashCode()) >>> 16) & (4 - 1)}
              # 定义算法表达式,通过对 user_id 的哈希值进行一系列位运算(异或和右移),
              # 并与 3(4 - 1)进行按位与运算,得到的结果决定数据存储在 user_order_0 到 user_order_3 中的哪个表中
    
    props:
      # 是否在日志中打印 SQL。
      # 打印 SQL 可以帮助开发者快速定位系统问题。日志内容包含:逻辑 SQL,真实 SQL 和 SQL 解析结果。
      # 如果开启配置,日志将使用 Topic ShardingSphere-SQL,日志级别是 INFO。 false
      sql-show: true
      # 是否在日志中打印简单风格的 SQL。false
      sql-simple: true
      # 用于设置任务处理线程池的大小。每个 ShardingSphereDataSource 使用一个独立的线程池,同一个 JVM 的不同数据源不共享线程池。
      executor-size: 20
      # 查询请求在每个数据库实例中所能使用的最大连接数。1
      max-connections-size-per-query: 1
      # 在程序启动和更新时,是否检查分片元数据的结构一致性。
      check-table-metadata-enabled: false
      # 在程序启动和更新时,是否检查重复表。false
      check-duplicate-table-enabled: false
    

    四、最佳实践与注意事项

    4.1 分片键选择原则

    1. 选择高基数列(如用户ID、订单号)
    2. 避免选择频繁更新的字段
    3. 优先选择业务查询中的必现条件

    4.2 常见问题解决方案

    分布式ID生成:

    // 雪花算法配置
    spring.shardingsphere.rules.sharding.key-generators:
      snowflake:
        type: SNOWFLAKE
        props:
          worker-id: 123

    跨库关联查询:

    • 全局表:小规模维度表全库冗余
    • 绑定表:确保关联表分片策略一致
      spring.shardingsphere.rules.sharding.binding-tables:
      - t_order,t_order_item

    4.3 性能优化建议

    1. 合理设置分片数量(建议单表不超过5000万行)
    2. 避免全路由查询(如不带分片键的查询)
    3. 使用Hint强制路由处理特殊场景
      try (HintManager hintManager = HintManager.getInstance()) {
      hintManager.addDatabaseShardingValue("t_order", 1);
      hintManager.addTableShardingValue("t_order", 2);
      // 执行查询
      }

    五、监控与扩展

    1. 集成Prometheus监控指标

      spring.shardingsphere.metrics:
      enabled: true
      name: prometheus
      host: 127.0.0.1
      port: 9090
    2. 使用ShardingSphere-UI进行可视化管控

    3. 弹性扩展方案:

      • 双写迁移:新旧分片策略并行写入
      • 历史数据归档:冷热数据分离存储

    六、总结

    ShardingJDBC通过以下核心优势成为分库分表首选方案:

    1. 无侵入性:无需修改业务代码即可实现分片功能
    2. 灵活扩展:支持动态数据源、在线规则修改
    3. 生态完善:提供分布式事务、数据加密等企业级功能

    当面临以下场景时建议采用分库分表方案:

    • 单表数据量预计超过5000万行
    • 数据库QPS超过5000
    • 需要多地多活部署架构

    附录:分片算法性能对比表

    算法类型 适用场景 扩容复杂度 查询性能
    取模分片 数据均匀分布
    范围分片 时间序列数据
    哈希分片 随机分布需求
    自定义复合分片 复杂业务规则
  • 背包问题

    0 – 1 背包问题

    1. 在这个问题中,有一个给定容量的背包和一组具有不同重量和价值的物品。目标是在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大。
    2. 正如你所说,对于每个物品,只能选择放入背包(消费一次)或者不放入背包,不能将一个物品分割部分放入背包

    0-1背包问题实现代码

    /**
     * @PackageName: cn.dsxriiiii.algorithm.bag
     * @Author: DSXRIIIII
     * @Email: 1870066109@qq.com
     * @Date: Created in  2024/11/08 20:27
     * @Description: 0-1 背包问题
     **/
    public class KnapsackProblem {
        public static void main(String[] args) {
            int[] weights = {2, 3, 4, 5};
            int[] values = {3, 4, 5, 6};
            int capacity = 8;
            int maxValue = knapsack(weights, values, capacity);
            System.out.println("背包能装的最大价值为:" + maxValue);
        }
        public static int knapsack(int[] weights, int[] values, int capacity) {
            int n = weights.length;
            int[][] dp = new int[values.length + 1][weights.length + 1];
            //注意i从1开始
            for (int i = 1; i < values.length; i++) {
                for (int j = 1; j < weights.length; j++) {
                    if(weights[i - 1] > j){
                        dp[i][j] = dp[i - 1][j];
                    }else{
                        dp[i][j] = Math.max(dp[i - 1][j],values[i - 1] + dp[i - 1][j - weights[i]]);
                    }
                }
            }
            return dp[n][capacity];
    
        }
    }
    

    完全背包问题

    完全背包问题是指有一个容量为 C 的背包和 n 种物品,每种物品有无限个可用。每种物品 w 有重量 v 和价值 。目标是在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大

    完全背包问题实现代码

    /**
     * @PackageName: cn.dsxriiiii.algorithm.bag
     * @Author: DSXRIIIII
     * @Email: 1870066109@qq.com
     * @Date: Created in  2024/11/08 20:41
     * @Description: 完全背包问题
     **/
    public class CompleteKnapsackProblem {
        public static int completeKnapsack(int[] weights, int[] values, int capacity) {
            int n = weights.length;
            int[] dp = new int[capacity + 1];
            for (int i = 0; i < n; i++) {
                for (int j = weights[i]; j <= capacity; j++) {
                    dp[j] = Math.max(dp[j],values[i] + dp[j - weights[i]]);
                }
            }
            return dp[capacity];
        }
    
        public static void main(String[] args) {
            int[] weights = {1, 3, 4};
            int[] values = {15, 20, 30};
            int capacity = 4;
            int maxValue = completeKnapsack(weights, values, capacity);
            System.out.println("背包能装的最大价值为:" + maxValue);
        }
    }
    
  • Mybatis学习笔记

    JDBC的缺点

    • 硬编码
    • 创建链接频繁

    Mybatis和Hibernater的区别

    mybatis是属于半自动框架,hibernater属于全自动框架

    如何理解全自动半自动:自动化:CRUD接口都是基于框架自己生成,而mybatis只是做了对象和数据库映射

    mybatis复杂sql执行能更强,并且支持动态sql

    mybatis的缓存机制不如hibernater强大,对于二级缓存的脏数据,hibernate会报错

    Mybatis和JDBC的区别

    对比项 JDBC MyBatis
    抽象层次 底层API,直接操作数据库 高层框架,基于JDBC封装
    SQL管理 SQL嵌入Java代码,维护困难 SQL与代码解耦(XML/注解),支持动态SQL
    结果映射 需手动处理ResultSet到对象的映射 自动映射结果集,支持复杂对象关系
    事务管理 需手动控制(commit/rollback) 支持声明式事务(如整合Spring)
    代码量 需大量样板代码(连接、异常处理等) 减少重复代码,提升开发效率
    灵活性 极高,可执行任意复杂SQL 灵活性较高,但复杂SQL需特殊处理
    缓存机制 无内置缓存 提供一级/二级缓存,减少数据库访问
    学习曲线 简单基础,但高效使用需经验 需学习配置及映射规则,但长期维护成本低
    异常处理 需显式处理SQLException 常封装为运行时异常,简化错误处理
    适用场景 小型项目或需极致控制SQL的场景 中大型项目,追求开发效率与可维护性
    性能 理论更高(无额外开销) 接近JDBC,缓存机制可提升高频查询性能

    Mybatis的参数转换逻辑

    主要是使用 TypeHandler ,在 XML映射文件里,把每个ava对象与SQL参数生成TypeHandler对象;执行SQL时,mybatis传递给JDBC参数时,ParamentHandler 使用TypeHandler 传入SQL参数,在JDBC返回查询结果Mabatis解析结果集,ResultHandler 使用 TypeHandler 返回 Java 对象

    Mybatis中#与$的区别

    参数预处理

    {param}会经过PreparedStateMent预处理,进行参数编译操作,例如:

    #根据参数类型进行转换操作
    #string:
    select username from User where name = '1';
    #other:
    select username from User where name = 1;

    ${param}不会经过PreparedStateMent预处理,直接进行拼接操作,例如:

    #string & other:
    select username from User where name = 1;

    SQL注入风险

    ${param}会存在SQL注入的风险

    $的作用

    获取动态表动态列的时候需要使用${table_name}

    在orderby groupby 语句执行动态查询时使用

    Mybatis的执行流程

    1. 创建SqlSessionFactory
    2. 读取xml文件信息
    3. 根据SqlSessionFatory的openSession方法获取sqlsession对象
    4. opensession的执行流程:DefaultSessionFactory openSessionFromDateSourse方法:创建一个sql执行器Excutor,通过Configuration读取配置信息同时利用责任链形成插件执行链,再通过代理方式对Excutor不断增强
    5. 通过session.getMapper方法获得mapper实现类
    6. 通过动态代理创建一个代理对象,调用proxy方法执行
    7. 执行查询操作,实际上是通过代理对象MapperMethod的excutor方法执行
    8. executeForMany()调用的核心方法是 DefaultSalSession # selectList() 我们可以看到,该方法主要调用的是executor.query()在 Mybatis 中,Executor 有多个实现类,默认使用的是 CachingExecutor 类。
    9. CachingExecutor的执行流程:先去二级缓存查询,如果查询不到就去BaseExecutor中查询
    10. BaseExecutor的执行流程:先去一级缓存中查询,如果查询不到结果则进行数据库查询queryFromdatebase()
    11. queryfromDatabase()调用的主要方法是 PreparedStatementHandler # query()方法。它的主要作用是使用 PreparedStatement执行 SQL,并将执行结果交给 ResultSetHandler 对象进行处理,最后将 ResultSetHandler 处理后的对象进行返回。

    Mysql的缓存机制

    一级缓存:

    实际就是一个hashmap

    在同一个sqlsession中,sql语句的执行结果会存放到一级缓存中,当执行增删改操作时会失效,在分布式环境下,多个sqlsession感知不到,可能会产生脏数据

    二级缓存:

    基于mapper配置文件的缓存

    不同于一级缓存,二级缓存是在事务提交,即 SalSession 关闭时才会被缓存。另外,虽说二级缓存本质上也是一个 Map,但是它的存储要比一级缓存更复杂,它的 Map 的 key 是 Mapper 配置文件的命名空间,value 是一个 Map。

    动态SQL

    IF

    <select id="findActiveBlogLike" resultType="'Blog">
      SELECT * FROM BLOG WHERE state ='ACTIVE
      <if test='title != null'>
        AND title like #{title}
      </if>
      <if test='author != null and author,name != null'>
        AND author name like #{author.name}
      </if>
    </select>

    CHOOSE WHEN

    类似于 if else if

    <select id="findActiveBlogLike" resultType="Blog">
        SELECT * FROM BLOG WHERE state = 'ACTIVE'
        <choose>
            <when test="title!= null">
                AND title like #{title}
            </when>
            <when test="author!= null and author.name!= null">
                AND author.name like #{author.name}
            </when>
            <otherwise>
                AND featured = 1
            </otherwise>
        </choose>
    </select>

    Trim

    <trim prefix="'WHERE" prefix0verrides="AND OR>
    ...  
    </trim>

    WHERE

    where 标签中的元素只会在满足条件的情况下才会插入到 where 子句中;而且,若 where 第一个子句的开头为 AND 或 OR,where 标签也会将它们去除。比如:

    <select id="findActiveBlogLike" resultType="Blog">
        SELECT * FROM BLOG
        <where>
            <if test="state!= null">
                state = #{state}
            </if>
            <if test="title!= null">
                AND title like #{title}
            </if>
            <if test="author!= null and author.name!= null">
                AND author.name like #{author.name}
            </if>
        </where>
    </select>

    SET

    set 标签可以用于动态包含需要更新的列,而忽略其它不更新的列。比如:

    <update id="updateAuthorIfNecessary">
        update Author
        <set>
            <if test="username!= null">
                username = #{username},
            </if>
            <if test="password!= null">
                password = #{password},
            </if>
            <if test="email!= null">
                email = #{email},
            </if>
            <if test="bio!= null">
                bio = #{bio}
            </if>
        </set>
        where id = #{id}
    </update>

    FOREACH

    <select id="selectPostIn" resultType="domain.blog.Post">
        SELECT * FROM POST P
        <where>
            <foreach item="item" index="index" collection="list" open="ID in (" separator=" " close=")" nullable="true">
                #{item}
            </foreach>
        </where>
    </select>

    分页查询

    分页实现

    1.MvBatis 启动阶段、会解析配置文件,并将解析后的结果保存到一个 Configuration 对象中。其中,我们配置的插件,会被解析并保存到 Configuration 对象的 InterceptorChain 对象中,从名字上看,此处使用了责任链的设计模式。InterceptorChain 对象本质上就是由一个 interceptors 的集合及几个操作 interceptor 的方法组成。此处设计的核心源码包括:XMLConfiaBuilder #pluginElement()以及 InterceptorChain 对象,

    2.接下来让我们看下 interceptorChain.pluginAll(Object target)方法。它的主要作用是遍历所有的拦截器,如果拦截器上配置了Executor 类,那么就会为目标对象也就是当脑的 executor 对象创建一个 DK 的动态代理对象并返回,如果有多个满足条件的拦截器,那么将在现有的代理对象的基础上继续生成代理对象,最终返回的是一个被层层代理的 executor 对象。该方法使用了责任链+装饰器 +动态代理的设计模式。代理对象的实现逻辑在 Plugin #invoke()中。此处涉及到的核心源码包含:interceptorChain # pluginAl()、Plugin # wrap()

    3.通俗来说就是通过责任链模式加载插件,再通过代理模式对Excutor对象进行层层代理

    PageHelper插件

    在原始sql尾部添加limit关键字 属于物理分页

    将分页参数封装成Page参数,并且存放到ThreadLocal中

  • Go-redis-api

    String API

    //给数据库中名称为key的string赋予值value,并设置失效时间,0为永久有效
    Set(key string, value interface{}, expiration time.Duration) *StatusCmd
    //查询数据库中名称为key的value值
    Get(key string) *StringCmd
    //设置一个key的值,并返回这个key的旧值
    GetSet(key string, value interface{}) *StringCmd
    //如果key不存在,则设置这个key的值,并设置key的失效时间。如果key存在,则设置不生效
    SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd
    //批量查询key的值。比如redisDb.MGet("name1","name2","name3")
    MGet(keys ...string) *SliceCmd
    //批量设置key的值。redisDb.MSet("key1", "value1", "key2", "value2", "key3", "value3")
    MSet(pairs ...interface{}) *StatusCmd
    //Incr函数每次加一,key对应的值必须是整数或nil
    //否则会报错incr key1: ERR value is not an integer or out of range
    Incr(key string) *IntCmd
    // IncrBy函数,可以指定每次递增多少,key对应的值必须是整数或nil
    IncrBy(key string, value int64) *IntCmd
    // IncrByFloat函数,可以指定每次递增多少,跟IncrBy的区别是累加的是浮点数
    IncrByFloat(key string, value float64) *FloatCmd
    // Decr函数每次减一,key对应的值必须是整数或nil.否则会报错
    Decr(key string) *IntCmd
    //DecrBy,可以指定每次递减多少,key对应的值必须是整数或nil
    DecrBy(key string, decrement int64) *IntCmd
    //删除key操作,支持批量删除    redisDb.Del("key1","key2","key3")
    Del(keys ...string) *IntCmd
    //设置key的过期时间,单位秒
    Expire(key string, expiration time.Duration) *BoolCmd
    //给数据库中名称为key的string值追加value
    Append(key, value string) *IntCmd

    List API

     //从列表左边插入数据,list不存在则新建一个继续插入数据
    LPush(key string, values ...interface{}) *IntCmd
    //跟LPush的区别是,仅当列表存在的时候才插入数据
    LPushX(key string, value interface{}) *IntCmd
    //返回名称为 key 的 list 中 start 至 end 之间的元素
    //返回从0开始到-1位置之间的数据,意思就是返回全部数据
    LRange(key string, start, stop int64) *StringSliceCmd
    //返回列表的长度大小
    LLen(key string) *IntCmd
    //截取名称为key的list的数据,list的数据为截取后的值
    LTrim(key string, start, stop int64) *StatusCmd
    //根据索引坐标,查询列表中的数据
    LIndex(key string, index int64) *StringCmd
    //给名称为key的list中index位置的元素赋值
    LSet(key string, index int64, value interface{}) *StatusCmd
    //在指定位置插入数据。op为"after或者before"
    LInsert(key, op string, pivot, value interface{}) *IntCmd
    //在指定位置前面插入数据
    LInsertBefore(key string, pivot, value interface{}) *IntCmd
    //在指定位置后面插入数据
    LInsertAfter(key string, pivot, value interface{}) *IntCmd
    //从列表左边删除第一个数据,并返回删除的数据
    LPop(key string) *StringCmd
    //删除列表中的数据。删除count个key的list中值为value 的元素。
    LRem(key string, count int64, value interface{}) *IntCmd

    Set API

    //向名称为key的set中添加元素member
    SAdd(key string, members ...interface{}) *IntCmd
    //获取集合set元素个数
    SCard(key string) *IntCmd
    //判断元素member是否在集合set中
    SIsMember(key string, member interface{}) *BoolCmd
    //返回名称为 key 的 set 的所有元素
    SMembers(key string) *StringSliceCmd
    //求差集
    SDiff(keys ...string) *StringSliceCmd
    //求差集并将差集保存到 destination 的集合
    SDiffStore(destination string, keys ...string) *IntCmd
    //求交集
    SInter(keys ...string) *StringSliceCmd
    //求交集并将交集保存到 destination 的集合
    SInterStore(destination string, keys ...string) *IntCmd
    //求并集
    SUnion(keys ...string) *StringSliceCmd
    //求并集并将并集保存到 destination 的集合
    SUnionStore(destination string, keys ...string) *IntCmd
    //随机返回集合中的一个元素,并且删除这个元素
    SPop(key string) *StringCmd
    // 随机返回集合中的count个元素,并且删除这些元素
    SPopN(key string, count int64) *StringSliceCmd
    //删除名称为 key 的 set 中的元素 member,并返回删除的元素个数
    SRem(key string, members ...interface{}) *IntCmd
    //随机返回名称为 key 的 set 的一个元素
    SRandMember(key string) *StringCmd
    //随机返回名称为 key 的 set 的count个元素
    SRandMemberN(key string, count int64) *StringSliceCmd
    //把集合里的元素转换成map的key
    SMembersMap(key string) *StringStructMapCmd
    //移动集合source中的一个member元素到集合destination中去
    SMove(source, destination string, member interface{}) *BoolCmd

    HASH API

    //根据key和字段名,删除hash字段,支持批量删除hash字段
    HDel(key string, fields ...string) *IntCmd
    //检测hash字段名是否存在。
    HExists(key, field string) *BoolCmd
    //根据key和field字段,查询field字段的值
    HGet(key, field string) *StringCmd
    //根据key查询所有字段和值
    HGetAll(key string) *StringStringMapCmd
    //根据key和field字段,累加数值。
    HIncrBy(key, field string, incr int64) *IntCmd
    //根据key和field字段,累加数值。
    HIncrByFloat(key, field string, incr float64) *FloatCmd
    //根据key返回所有字段名
    HKeys(key string) *StringSliceCmd
    //根据key,查询hash的字段数量
    HLen(key string) *IntCmd
    //根据key和多个字段名,批量查询多个hash字段值
    HMGet(key string, fields ...string) *SliceCmd
    //根据key和多个字段名和字段值,批量设置hash字段值
    HMSet(key string, fields map[string]interface{}) *StatusCmd
    //根据key和field字段设置,field字段的值
    HSet(key, field string, value interface{}) *BoolCmd
    //根据key和field字段,查询field字段的值
    HSetNX(key, field string, value interface{}) *BoolCmd

    ZSet API

     // 添加一个或者多个元素到集合,如果元素已经存在则更新分数
    ZAdd(key string, members ...Z) *IntCmd
    ZAddNX(key string, members ...Z) *IntCmd
    ZAddXX(key string, members ...Z) *IntCmd
    ZAddCh(key string, members ...Z) *IntCmd
    ZAddNXCh(key string, members ...Z) *IntCmd
    // 添加一个或者多个元素到集合,如果元素已经存在则更新分数
    ZAddXXCh(key string, members ...Z) *IntCmd
    //增加元素的分数
    ZIncr(key string, member Z) *FloatCmd
    ZIncrNX(key string, member Z) *FloatCmd
    ZIncrXX(key string, member Z) *FloatCmd
    //增加元素的分数,增加的分数必须是float64类型
    ZIncrBy(key string, increment float64, member string) *FloatCmd
    // 存储增加分数的元素到destination集合
    ZInterStore(destination string, store ZStore, keys ...string) *IntCmd
    //返回集合元素个数
    ZCard(key string) *IntCmd
    //统计某个分数范围内的元素个数
    ZCount(key, min, max string) *IntCmd
    //返回集合中某个索引范围的元素,根据分数从小到大排序
    ZRange(key string, start, stop int64) *StringSliceCmd
    //ZRevRange的结果是按分数从大到小排序。
    ZRevRange(key string, start, stop int64) *StringSliceCmd
    //根据分数范围返回集合元素,元素根据分数从小到大排序,支持分页。
    ZRangeByScore(key string, opt ZRangeBy) *StringSliceCmd
    //根据分数范围返回集合元素,用法类似ZRangeByScore,区别是元素根据分数从大到小排序。
    ZRemRangeByScore(key, min, max string) *IntCmd
    //用法跟ZRangeByScore一样,区别是除了返回集合元素,同时也返回元素对应的分数
    ZRangeWithScores(key string, start, stop int64) *ZSliceCmd
    //根据元素名,查询集合元素在集合中的排名,从0开始算,集合元素按分数从小到大排序
    ZRank(key, member string) *IntCmd
    //ZRevRank的作用跟ZRank一样,区别是ZRevRank是按分数从大到小排序。
    ZRevRank(key, member string) *IntCmd 
    //查询元素对应的分数
    ZScore(key, member string) *FloatCmd
    //删除集合元素
    ZRem(key string, members ...interface{}) *IntCmd
    //根据索引范围删除元素。从最低分到高分的(stop-start)个元素
    ZRemRangeByRank(key string, start, stop int64) *IntCmd
  • Mirros算法实现

    小萱今天面试被面试官问了一道题,面试官说,请你写一个后序遍历二叉树,一看这么简单,以为可用秒了,结果被面试官拷打了,这是什么情况,力扣简单题难道也有坑吗,让我们来细细研究一下

    递归实现

    首先使用递归非常快速的完成这题

    func PostorderTraversal(root *TreeNode) []int {
        result := make([]int, 0)
        var postorder func(*TreeNode)
        postorder = func(node *TreeNode) {
            if node == nil {
                return
            }
            postorder(node.Left)
            postorder(node.Right)
            result = append(result, node.Val)
        }
        postorder(root)
        return result
    }

    小萱毫不费劲光速写完了这道题,面试官看了几秒,说说空间复杂度,小萱回答O(n),面试官说有没有优化思路,要怎么做,难道要对空闲的指针动手?

    您猜怎么着 还真就是

    使用Morris算法实现前中后序遍历

    Morris算法

    首先要知道的事情是,递归遍历的空间复杂度与与数的节点有关,那要怎么优化呢

    我们需要利用创建两个指针,这两个指针分别指向的对象会不断更替,其中创建cur节点不断更替当前节点的位置,p指针用来指向修改一些执政的操作,结合上图,我们来分析下具体的实现流程

    1. 首先cur节点会指向头节点,如果cur节点没有左数,说明有两种情况,要么此时节点走到了叶子节点,要么该节点只有右树
    2. 一直遍历位置直到走到cur的左树的最右节点,即粉色标记cur:5的位置,此时会修改p执政右指针指向的位置,将其修改为cur节点,即粉色标记cur:1的位置,此时蓝色指针1标记完成,当前指针执行完毕,执行continue,在continue之后,cur节点左移,来到粉色标记cur: 2
    3. 此时执行操作2,创建蓝色指针2,cur左移
    4. 此时cur来到粉色标记cur:3,此时判断左树(指针p指向的节点)为null,那么此时cur指针会右移,此时注意,由于右指针被修改了,所以此时cur会回到cur:4位置
    5. p指针会循环查找左树的右节点,此时发现指向不为null,而是节点cur,此时选择释放指针
    6. 同理,最后cur会回到cur: 1节点
    func morris(root *TreeNode) {
        var p1, p2 *TreeNode = root, nil
        for p1 != nil {
            // 当前节点不为空,找到左子树最右节点
            p2 = p1.Left
            if p2 != nil {
                for p2.Right != nil && p2.Right != p1 {
                    p2 = p2.Right
                }
                //此时p2 要么走到p1 要么走到空
                if p2.Right == nil {
                    // 如果是走到空 修改指针指 p1
                    p2.Right = p1
                    p1 = p1.Left
                    continue
                    //进入下一次while循环
                } else {
                    // 否则释放指针
                    p2.Right = nil
                }
            } 
            p1 = p1.Right
        }
        return
    }

    先序遍历

    // preorderTraversal morris方法遍历
    // 没有左树 直接收集 有左数 第一次遍历时收集
    func preorderTraversal(root *TreeNode) (vals []int) {
        var p1, p2 *TreeNode = root, nil
        for p1 != nil {
            // 当前节点不为空,找到左子树最右节点
            p2 = p1.Left
            if p2 != nil {
                for p2.Right != nil && p2.Right != p1 {
                    p2 = p2.Right
                }
                //此时p2 要么走到p1 要么走到空
                if p2.Right == nil { // 此时为第一次到达
                    vals = append(vals, p1.Val)
                    // 如果是走到空 修改指针指 p1
                    p2.Right = p1
                    p1 = p1.Left
                    continue
                    //进入下一次while循环
                } else { // 此时为第二次到达
                    // 否则释放指针
                    p2.Right = nil
                }
            } else {
                vals = append(vals, p1.Val)
            }
            p1 = p1.Right
        }
        return
    }

    中序遍历

    // inorderTraversal morris方法中序遍历
    // 没有左树 直接收集 有左数 第二次遍历时收集
    func inorderTraversal(root *TreeNode) (vals []int) {
        var p1, p2 *TreeNode = root, nil
        for p1 != nil {
            // 当前节点不为空,找到左子树最右节点
            p2 = p1.Left
            if p2 != nil {
                for p2.Right != nil && p2.Right != p1 {
                    p2 = p2.Right
                }
                //此时p2 要么走到p1 要么走到空
                if p2.Right == nil { // 此时为第一次到达
                    // 如果是走到空 修改指针指 p1
                    p2.Right = p1
                    p1 = p1.Left
                    continue
                    //进入下一次while循环
                } else { // 此时为第二次到达
                    // 否则释放指针
                    vals = append(vals, p1.Val)
                    p2.Right = nil
                }
            } else {
                vals = append(vals, p1.Val)
            }
            p1 = p1.Right
        }
        return
    }

    后续遍历

    // 第二次遍历到该节点 倒叙收集左数的右边界
    // 最后一次收集 从头节点的右边界
    // postorderTraversal morris方法后序遍历
    func postorderTraversal(root *TreeNode) (res []int) {
        addPath := func(node *TreeNode) {
            resSize := len(res)
            for ; node != nil; node = node.Right {
                res = append(res, node.Val)
            }
            reverse(res[resSize:])
        }
    
        p1 := root
        for p1 != nil {
            if p2 := p1.Left; p2 != nil {
                for p2.Right != nil && p2.Right != p1 {
                    p2 = p2.Right
                }
                if p2.Right == nil {
                    p2.Right = p1
                    p1 = p1.Left
                    continue
                }
                p2.Right = nil
                addPath(p1.Left)
            }
            p1 = p1.Right
        }
        addPath(root)
        return
    }
    
    func reverse(a []int) {
        for i, n := 0, len(a); i < n/2; i++ {
            a[i], a[n-1-i] = a[n-1-i], a[i]
        }
    }
  • Golang Channel

    Channel的作用

    协程间通信
    数据传递:channel可以让一个goroutine发送数据,另一个goroutine接收数据。例如,在一个网络服务器程序中,一个goroutine可能负责接收客户端连接并将请求数据放入channel,另一个goroutine从channel中取出请求数据进行处理。

    数据结构

    type hchan struct {
        qcount   uint           // total data in the queue 队列中数据总数
        dataqsiz uint           // size of the circular queue  环形队列size大小
        buf      unsafe.Pointer // points to an array of dataqsiz elements (环形队列)
        elemsize uint16 // 元素大小
        closed   uint32 // 关闭
        timer    *timer // timer feeding this chan
        elemtype *_type // element type 元素类型
        sendx    uint   // send index 已发送数据在环形队列的位置
        recvx    uint   // receive index 已接受数据在环形队列的位置
        recvq    waitq  // list of recv waiters 接收者等待队列
        sendq    waitq  // list of send waiters 发送者等待队列
    
        // lock protects all fields in hchan, as well as several
        // fields in sudogs blocked on this channel.
        //
        // Do not change another G's status while holding this lock
        // (in particular, do not ready a G), as this can deadlock
        // with stack shrinking.
        lock mutex
    }
    
    type waitq struct {
        first *sudog
        last  *sudog
    }

    如何保证协程安全

    lock mutex

    无缓冲区的Channel

    reveq:从Channel中接受元素,如果没有元素则会直接阻塞等待

    // Signal to anyone trying to shrink our stack that we're about
    // to park on a channel. The window between when this G's status
    // changes and when we set gp.activeStackChans is not safe for
    // stack shrinking.
    gp.parkingOnChan.Store(true)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)

    sendq:向Channel中发送元素,如果没有接收者则会直接阻塞等待

    goready(gp, skip+1)

    lock 读写锁

    有缓冲区的Channel

    从缓冲区中读取数据,用数组建立的一个环形链表

    不会产生阻塞操作