2025年1月

前言

EasySQLite是一个.NET 8操作SQLite入门到实战的详细教程,主要是对学校班级,学生信息进行管理维护。今天咱们的主要内容是将EasySQLite从.NET 8升级到.NET 9。

选型、开发详细教程

.NET 9介绍

.NET 9是微软于2024年11月13日推出的一个重大版本,被誉为迄今为止最高效、最现代、最安全、最智能、性能最高的.NET版本。.NET 9是标准期限支持 (STS) 版本,它将在18个月内受到支持。

.NET 9 环境准备

安装 .NET 9 环境,安装 .NET 9 SDK 并且Visual Studio 2022需要更新至17.12版本。

将目标框架切换到.NET 9

我们先直接把.NET 8的目标框架切换至.NET 9,假如项目能够正常运行那就说明没有什么需要调整的,有问题在具体问题具体分析。

升级方式

选中对应项目右键属性切换对应目标框架版本:

WebApi升级

运行效果查看:

运行下来一切正常!

WebUI升级

运行查看效果:

项目运行异常:

(stack=   at Microsoft.WebAssembly.Diagnostics.MonoSDBHelper.SendDebuggerAgentCommand[T](T command, MonoBinaryWriter arguments, CancellationToken token, Boolean throwOnError)
         at Microsoft.WebAssembly.Diagnostics.MonoSDBHelper.HasDebugInfoLoadedByRuntimeAsync(String assemblyName, CancellationToken token)
         at Microsoft.WebAssembly.Diagnostics.MonoSDBHelper.GetDataFromAssemblyAndPdbAsync(String assemblyName, Boolean ignoreJMC, CancellationToken token)
         at Microsoft.WebAssembly.Diagnostics.DebugStore.Load(SessionId id, String[] loaded_files, ExecutionContext context, Boolean useDebuggerProtocol, CancellationToken token)+MoveNext())

升级WebAssembly相关包版本:

运行成功:

其他类库升级

升级前后端运行效果

设置多项目运行:

问题

在 MySQL 中,查询全局状态变量的方式一般有两种:
SHOW GLOBAL STATUS

performance_schema.global_status

但不知道大家注意到没有,performance_schema.global_status 返回的状态变量数要远远少于 SHOW GLOBAL STATUS 。

具体来说,

  • 在 MySQL 8.4.2 中,SHOW GLOBAL STATUS 返回了 503 个变量,而 performance_schema.global_status 只返回了 336 个。

  • 在 MySQL 5.7.44 中,SHOW GLOBAL STATUS 返回了 354 个变量,而 performance_schema.global_status 只返回了 207 个。

有的童鞋可能会认为这两者的实现方式不一样,但事实上,从 MySQL 5.7 开始,当执行 SHOW GLOBAL STATUS 时,MySQL 并不是直接从内存中的状态变量获取数据,而是通过查询 performance_schema.global_status 表来间接获取。

既然两者的实现方式是一样的,为什么返回的变量数会不一样?

带着这个问题,接下来我们具体分析下 SHOW GLOBAL STATUS 的实现原理。本文主要包括以下几个部分:

  • 状态变量是在哪里定义的?
  • 状态变量值的来源。
  • SHOW GLOBAL STATUS 的实现原理。
  • performance_schema.global_status 的实现原理。
  • 为什么 performance_schema.global_status 返回的变量数比 SHOW GLOBAL STATUS 少。

状态变量是在哪里定义的?

状态变量的来源主要有三个:

  1. Server 层面的状态变量:这些状态变量主要在
    status_vars
    (mysqld.cc)中定义。在 MySQL 8.4 中,共有 321 个状态变量。其中包括了 com_status_vars 中定义的 167 个 Com 相关的变量。
  2. 插件中的状态变量:
  • InnoDB:在
    innodb_status_variables
    (ha_innodb.cc)中定义 ,共 76 个。
  • 半同步复制:在
    semi_sync_master_status_vars
    (semisync_source_plugin.cc)中定义,共 14 个,从库只有 1 个。
  • 组复制:在
    group_replication_status_vars
    (plugin.cc)中定义,共 22 个。
  • performance_schema:在
    pfs_status_vars
    (ha_perfschema.cc)中定义,共 33 个。
  • mysqlx:在
    m_plugin_status_variables
    (status_variables.cc)中定义,共 78 个。
  • Component 中的状态变量:例如,在密码认证插件 validate_password 中定义的状态变量。
  • 这些变量会通过
    add_status_vars
    函数添加到一个数组中 all_status_vars。注意这个数组名,后面讲解原理时会用到。

    // sql/mysqld.cc
    int init_common_variables() {
      ...
      if (add_status_vars(status_vars)) return 1; 
      ...
    }

    // sql/sql_plugin.cc
    static int plugin_initialize(st_plugin_int *plugin) {
      ...
      if (plugin->plugin->status_vars) {
        if (add_status_vars(plugin->plugin->status_vars)) goto err;
      }
      ...
    }

    // sql/server_component/component_status_var_service.cc
    DEFINE_BOOL_METHOD(mysql_status_variable_registration_imp::register_variable,
                       (SHOW_VAR * status_var)) {
      try {
        if (add_status_vars(status_var)) return true;

        return false;
      } catch (...) {
        mysql_components_handle_std_exception(__func__);
      }
      return true;
    }

    // sql/sql_show.cc
    bool add_status_vars(const SHOW_VAR *list) {
    ...
        while (list->name) all_status_vars.push_back(*list++);
    ...
    }

    状态变量值的来源

    状态变量的值是在定义时指定的,以 Server 层面的状态变量为例:

    SHOW_VAR status_vars[] = {
        {"Aborted_clients", (char *)&aborted_threads, SHOW_LONG, SHOW_SCOPE_GLOBAL},
        ...
        {"Bytes_received", (char *)offsetof(System_status_var, bytes_received),
         SHOW_LONGLONG_STATUS, SHOW_SCOPE_ALL},
        ...
        {"Com", (char *)com_status_vars, SHOW_ARRAY, SHOW_SCOPE_ALL},
        ...
        {"Uptime", (char *)&show_starttime, SHOW_FUNC, SHOW_SCOPE_GLOBAL},
        ...
        };

    status_vars 是一个数组,其元素类型是
    SHOW_VAR
    ,每个元素代表一个状态变量。每个元素包含四个字段,依次是:变量名、变量值、变量类型和变量作用范围。所以,通过元素的第二个字段,就可以确定该状态变量值的来源。

    上面列举了四个有代表性的状态变量:

    1. Aborted_clients:变量值来源于全局变量 aborted_threads(
      extern ulong aborted_threads
      )。

    2. Bytes_received:变量值来自于 System_status_var 结构体中 bytes_received 字段的内存偏移量(
      offsetof(System_status_var, bytes_received)
      )。

      System_status_var 常用于以下场景:

    • global_status_var:用于存储全局的状态变量。连接断开后,会通过
      add_to_status
      函数将对应线程的状态变量添加到 global_status_var 中。
    • status_var:用于存储每个线程的状态变量。
    • query_start_status:保存上一个操作结束时线程的状态变量,只在
      log_slow_extra
      为 ON 时使用。
  • Com:变量值来自于
    com_status_vars
    数组(怎么知道它是一个数组呢?实际上看的是第三个字段,SHOW_ARRAY 代表它是一个数组),该数组定义了 Com 相关的状态变量。

  • Uptime:变量值由
    show_starttime
    函数(SHOW_FUNC 代表它是一个函数)生成。下面是该函数的具体实现。

  • static int show_starttime(THD *thd, SHOW_VAR *var, char *buff) {
      var->type = SHOW_LONGLONG;
      var->value = buff;
      *((longlong *)buff) =
          (longlong)(thd->query_start_in_secs() - server_start_time);
      return 0;
    }

    不难看出,Uptime 实际上是通过查询的开始时间(
    thd->set_time()
    中设置的)减去 MySQL 服务器的启动时间得到的。

    SHOW GLOBAL STATUS 的实现原理

    当我们执行 SHOW GLOBAL STATUS 时,实际上查询的是 performance_schema.global_status。

    这一转化操作是在
    build_show_global_status
    函数中实现的。该函数会将表名(table_name,即 global_status 表)、LIKE 子句(wild)和 WHERE 子句(where_cond)传递给
    build_query
    函数,后者会构造对应的 SQL 查询解析树。

    // sql/sql_show_status.cc
    Query_block *build_show_global_status(const POS &pos, THD *thd,
                                          const String *wild, Item *where_cond) {
      static const LEX_CSTRING table_name = {STRING_WITH_LEN("global_status")};

      return build_query(pos, thd, SQLCOM_SHOW_STATUS, table_name, wild,
                         where_cond);
    }

    在不指定任何查询条件的情况下,SHOW GLOBAL STATUS 对应的查询语句如下:

    SELECT * FROM
             (SELECT VARIABLE_NAME as Variable_name, VARIABLE_VALUE as Value
              FROM performance_schema.global_status) global_status

    performance_schema.global_status 的实现原理

    查询 performance_schema.global_status 时,MySQL 会通过调用
    MaterializeIterator<Profiler>::MaterializeOperand
    函数实现数据的物化(即构造查询结果),除此之外,这个函数还会逐行读取数据并将其写入目标表。

    下面是该函数简化后的代码。

    bool MaterializeIterator<Profiler>::MaterializeOperand(const Operand &operand,
                                                           ha_rows *stored_rows) {
      ...
      if (operand.subquery_iterator->Init()) {
        return true;
      }

      PFSBatchMode pfs_batch_mode(operand.subquery_iterator.get());

      while (true) {
        ...
        int error = read_next_row(operand); 
        ...
        error = t->file->ha_write_row(t->record[0]);
        ...
      return false;
    }

    具体来说,

    • operand.subquery_iterator->Init()
      会实现数据的物化(即构造查询结果)。
    • read_next_row(operand)
      会逐行读取数据,并将数据写到 table->record[0] 中,table->record[0] 是当前行的数据缓冲区。
    • t->file->ha_write_row(t->record[0])
      会将 table->record[0] 中的数据写到 performance_schema.global_status 中。

    接下来,我们具体分析下构造查询结果和数据读取这两个步骤的实现逻辑。

    构造查询结果

    下面是
    operand.subquery_iterator->Init()
    调用后的堆栈信息。

    #0  PFS_status_variable_cache::do_materialize_global() at /usr/src/mysql-8.4.2/storage/perfschema/pfs_variable.cc:1178
    #1  PFS_variable_cache<Status_variable>::materialize_global() at /usr/src/mysql-8.4.2/storage/perfschema/pfs_variable.h:526
    #2  table_global_status::rnd_init() at /usr/src/mysql-8.4.2/storage/perfschema/table_global_status.cc:123
    #3  ha_perfschema::rnd_init() at /usr/src/mysql-8.4.2/storage/perfschema/ha_perfschema.cc:1733
    #4  handler::ha_rnd_init() at /usr/src/mysql-8.4.2/sql/handler.cc:2961
    #5  TableScanIterator::Init() at /usr/src/mysql-8.4.2/sql/iterators/basic_row_iterators.cc:260
    #6  MaterializeIterator::MaterializeOperand() at /usr/src/mysql-8.4.2/sql/iterators/composite_iterators.cc:2759

    查询结果的构造主要是在
    PFS_status_variable_cache::do_materialize_global()
    函数中实现的。

    下面我们看看这个函数的具体实现细节。

    int PFS_status_variable_cache::do_materialize_global() {
      // 这个变量用来汇总全局的状态变量(global_status_var)和当前所有线程的状态变量(status_var)
      System_status_var status_totals;
      ...
      if (!m_external_init) {
        // 基于 all_status_vars 构建一个满足条件的状态变量数组 m_show_var_array
        init_show_var_array(OPT_GLOBAL, true);
      }

      // 初始化 PFS_connection_status_visitor,将 status_vars 赋值给 m_status_vars
      PFS_connection_status_visitor visitor(&status_totals);
      // 这个函数非常关键,它会将全局的状态变量(global_status_var)和当前所有线程的状态变量(status_var)累加汇总到 m_status_vars 中。
      PFS_connection_iterator::visit_global(false, /* hosts */
                                            false, /* users */
                                            false, /* accounts */
                                            false, /* threads */
                                            true,  /* THDs */
                                            &visitor);
      // 这个函数也非常关键,它会遍历 m_show_var_array 中的状态变量,获取其值并进行格式化处理,最终将处理后的结果缓存到 m_cache 中。
      manifest(m_current_thd, m_show_var_array.begin(), &status_totals, "", false,
               true);
      ...
      return 0;
    }

    该函数的处理流程如下:

    1. 基于 all_status_vars 构建一个满足条件的状态变量数组 m_show_var_array。至于需要满足什么条件,后面会详细说明。
    2. 将全局的状态变量(global_status_var)和当前所有线程的状态变量(status_var)累加汇总到 status_totals 中。
    3. 遍历 m_show_var_array 中的状态变量,根据变量的类型(如 SHOW_FUNC、SHOW_ARRAY 等)进行不同的处理,并将处理后的状态变量存储到 m_cache 缓存中。具体处理逻辑如下:
    • 对于 SHOW_FUNC 类型的变量,
      manifest
      会递归执行函数来计算变量的最终值。
    • 对于 SHOW_ARRAY 类型的变量,函数会递归调用
      manifest
      ,以展开数组中的每一个状态变量。
    • 状态变量添加到 m_cache 之前,会先转换为 Status_variable 类型。

    读取数据

    下面是
    read_next_row(operand)
    调用后的堆栈信息。

    #0  PFS_variable_cache<Status_variable>::get() at /usr/src/mysql-8.4.2/storage/perfschema/pfs_variable.h:382
    #1  table_global_status::rnd_next() at /usr/src/mysql-8.4.2/storage/perfschema/table_global_status.cc:131
    #2  ha_perfschema::rnd_next() at /usr/src/mysql-8.4.2/storage/perfschema/ha_perfschema.cc:1757
    #3  handler::ha_rnd_next() at /usr/src/mysql-8.4.2/sql/handler.cc:3006
    #4  TableScanIterator::Read() at /usr/src/mysql-8.4.2/sql/iterators/basic_row_iterators.cc:278
    #5  MaterializeIterator::read_next_row() at /usr/src/mysql-8.4.2/sql/iterators/composite_iterators.cc:2278
    #6  MaterializeIterator::MaterializeOperand() at /usr/src/mysql-8.4.2/sql/iterators/composite_iterators.cc:2771

    read_next_row(operand)
    最后会调用
    PFS_variable_cache<Status_variable>::get()
    ,而这个函数实际上读取的就是 m_cache 中的元素。

    // storage/perfschema/pfs_variable.h:382
      const Var_type *get(uint index = 0) const {
        if (index >= m_cache.size()) {
          return nullptr;
        }

        const Var_type *p = &m_cache.at(index);
        return p;
      }

    为什么 performance_schema.global_status 返回的变量数比 SHOW GLOBAL STATUS 少?

    前面我们提到过,在构造查询结果时,会先基于 all_status_vars 构建一个满足条件的状态变量数组 m_show_var_array。

    具体需要满足什么条件,是在
    PFS_status_variable_cache::filter_show_var
    函数中定义的。

    bool PFS_status_variable_cache::filter_show_var(const SHOW_VAR *show_var,
                                                    bool strict) {
      if (!match_scope(show_var->scope, strict)) {
        return true;
      }

      if (filter_by_name(show_var)) {
        return true;
      }

      if (m_aggregate && !can_aggregate(show_var->type)) {
        return true;
      }

      return false;
    }

    bool PFS_status_variable_cache::filter_by_name(const SHOW_VAR *show_var) {
      assert(show_var);
      assert(show_var->name);

      if (show_var->type == SHOW_ARRAY) {
        /* The SHOW_ARRAY name is the prefix for the variables in the sub array. */
        const char *prefix = show_var->name;
        /* Exclude COM counters if not a SHOW STATUS command. */
        if (!my_strcasecmp(system_charset_info, prefix, "Com") && !m_show_command) {
          return true;
        }
      }
      return false;
    }

    从代码中可以看到,需要判断的条件有三个:

    • 变量作用范围:因为
      init_show_var_array(OPT_GLOBAL, true)
      中指定了 OPT_GLOBAL,所以这里会过滤掉变量作用范围为 SHOW_SCOPE_SESSION 的状态变量。在 Server 层面的状态变量中,这样的变量有 6 个:Compression、Compression_algorithm、Compression_level、Last_query_cost、Last_query_partial_plans、Tls_sni_server_name。
    • 对于非 m_show_command 类的查询(其实就是指的是直接查询 performance_schema.global_status 这种方式),还会剔除
      com_status_vars
      数组中 Com 相关的状态变量。这也就是为什么 performance_schema.global_status 返回的变量数比 SHOW GLOBAL STATUS 中少。
    • 查询聚合数据:如果查询的是 status_by_account、status_by_host 或 status_by_user 之类的聚合表,还会剔除无法聚合的状态变量。

    总结

    1. 状态变量的来源主要有三个:Server、插件和 Component。
    2. 如果想查看某个状态变量值的来源,直接查看定义部分对应元素的第二个字段即可。
    3. 当我们执行 SHOW GLOBAL STATUS 时,实际上查询的是 performance_schema.global_status。
    4. performance_schema.global_status 在实现上主要分为两步:1. 构造查询结果,将所有变量的值存储到一个缓存(m_cache)中;2. 数据读取,直接从缓存中读取变量值。
    5. 之所以 performance_schema.global_status 返回的变量数比 SHOW GLOBAL STATUS 中少,主要是
      PFS_status_variable_cache::filter_by_name
      中的限制。
    6. 需要注意的是,如果查询中指定了过滤条件,过滤操作会发生在数据读取阶段,而不是查询结果构造阶段。

    sqlSessionFactory 与 SqlSession

    正如其名,Sqlsession对应着一次数据库会话。由于数据库会话不是永久的,因此Sqlsession的生命周期也不应该是永久的,相反,在你每次访问数据库时都需要创建它(当然并不是说在Sqlsession里只能执行一次sql,你可以执行多次,当一旦关闭了Sqlsession就需要重新创建它)。

    那么咱们就先看看是怎么获取SqlSession的吧:

    首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。源码如下:

     /**
      * 一系列的构造方法最终都会调用本方法(配置文件为Reader时会调用本方法,还有一个InputStream方法与此对应)
      * @param reader
      * @param environment
      * @param properties
      * @return
      */
     public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
       try {
         //通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
         XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
         //这儿创建DefaultSessionFactory对象
         return build(parser.parse());
       } catch (Exception e) {
         throw ExceptionFactory.wrapException("Error building SqlSession.", e);
       } finally {
         ErrorContext.instance().reset();
         try {
           reader.close();
         } catch (IOException e) {
           // Intentionally ignore. Prefer previous error.
         }
       }
     }
    
     public SqlSessionFactory build(Configuration config) {
       return new DefaultSqlSessionFactory(config);
     }
    

    当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。源码如下:

    /**
      * 通常一系列openSession方法最终都会调用本方法
      * @param execType 
      * @param level
      * @param autoCommit
      * @return
      */
     private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
       Transaction tx = null;
       try {
         //通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
         final Environment environment = configuration.getEnvironment();
         final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
         tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
         //之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
         final Executor executor = configuration.newExecutor(tx, execType);
         //关键看这儿,创建了一个DefaultSqlSession对象
         return new DefaultSqlSession(configuration, executor, autoCommit);
       } catch (Exception e) {
         closeTransaction(tx); // may have fetched a connection so lets call close()
         throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
       } finally {
         ErrorContext.instance().reset();
       }
     }
    

    通过以上步骤,咱们已经得到SqlSession对象了。接下来就是该干嘛干嘛去了(话说还能干嘛,当然是执行sql语句咯)。看了上面,咱们也回想一下之前写的Demo:

    SqlSessionFactory sessionFactory = null;  
    String resource = "mybatis-conf.xml";  
    try {
        //SqlSessionFactoryBuilder读取配置文件
       sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(resource));
    } catch (IOException e) {  
       e.printStackTrace();  
    }    
    //通过SqlSessionFactory获取SqlSession
    SqlSession sqlSession = sessionFactory.openSession();
    

    创建Sqlsession的地方只有一个,那就是SqlsessionFactory的openSession方法:

    public SqlSessionopenSession() {  
        return openSessionFromDataSource(configuration.getDefaultExecutorType(),null, false);  
    }
    

    我们可以看到实际创建SqlSession的地方是openSessionFromDataSource,如下:

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {  
     
        Connection connection = null;  
     
        try {  
     
            final Environment environment = configuration.getEnvironment();  
     
            final DataSource dataSource = getDataSourceFromEnvironment(environment);  
            
            // MyBatis对事务的处理相对简单,TransactionIsolationLevel中定义了几种隔离级别,并不支持内嵌事务这样较复杂的场景,同时由于其是持久层的缘故,所以真正在应用开发中会委托Spring来处理事务实现真正的与开发者隔离。分析事务的实现是个入口,借此可以了解不少JDBC规范方面的事情。
            TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);  
     
            connection = dataSource.getConnection();  
     
            if (level != null) {  
                connection.setTransactionIsolation(level.getLevel());
            }  
     
            connection = wrapConnection(connection);  
     
            Transaction tx = transactionFactory.newTransaction(connection,autoCommit);  
     
            Executorexecutor = configuration.newExecutor(tx, execType);  
     
            return newDefaultSqlSession(configuration, executor, autoCommit);  
     
        } catch (Exceptione) {  
            closeConnection(connection);  
            throwExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);  
        } finally {
            ErrorContext.instance().reset();
        }
    }  
    

    可以看出,创建sqlsession经过了以下几个主要步骤:

    • 从配置中获取Environment;
    • 从Environment中取得DataSource;
    • 从Environment中取得TransactionFactory;
    • 从DataSource里获取数据库连接对象Connection;
    • 在取得的数据库连接上创建事务对象Transaction;
    • 创建Executor对象(该对象非常重要,事实上sqlsession的所有操作都是通过它完成的);
    • 创建sqlsession对象。

    SqlSession咱们也拿到了,咱们可以调用SqlSession中一系列的select..., insert..., update..., delete...方法轻松自如的进行CRUD操作了。就这样?那咱配置的映射文件去哪儿了?别急,咱们接着往下看。

    MapperProxy

    在mybatis中,通过MapperProxy动态代理咱们的dao, 也就是说, 当咱们执行自己写的dao里面的方法的时候,其实是对应的mapperProxy在代理。那么,咱们就看看怎么获取MapperProxy对象吧:

    通过SqlSession从Configuration中获取。源码如下:

     /**
      * 什么都不做,直接去configuration中找
      */
     @Override
     public <T> T getMapper(Class<T> type) {
       return configuration.<T>getMapper(type, this);
     }
    

    SqlSession把包袱甩给了Configuration, 接下来就看看Configuration。源码如下:

     public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
       return mapperRegistry.getMapper(type, sqlSession);
     }
    

    接着调用了MapperRegistry,源码如下:

    @SuppressWarnings("unchecked")
     public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
       //交给MapperProxyFactory去做
       final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
       if (mapperProxyFactory == null) {
         throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
       }
       try {
         //关键在这儿
         return mapperProxyFactory.newInstance(sqlSession);
       } catch (Exception e) {
         throw new BindingException("Error getting mapper instance. Cause: " + e, e);
       }
     }
    

    MapperProxyFactory源码:

      @SuppressWarnings("unchecked")
     protected T newInstance(MapperProxy<T> mapperProxy) {
       //动态代理dao接口
       return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
     }
     
     public T newInstance(SqlSession sqlSession) {
       final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
       return newInstance(mapperProxy);
     }
    

    通过以上的动态代理,咱们就可以方便地使用dao接口啦, 就像之前咱们写的demo那样:

    UserDao userMapper = sqlSession.getMapper(UserDao.class);  
    User insertUser = new User();
    

    这下方便多了吧, 呵呵, 貌似mybatis的源码就这么一回事儿啊。具体详细介绍,请参见MyBatis Mapper 接口如何通过JDK动态代理来包装SqlSession 源码分析。别急,还没完, 咱们还没看具体是怎么执行sql语句的呢。

    Excutor

    Executor与Sqlsession的关系就像市长与书记,Sqlsession只是个门面,真正干事的是Executor,Sqlsession对数据库的操作都是通过Executor来完成的。与Sqlsession一样,Executor也是动态创建的:

    • Executor创建的源代码
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {  
    
        executorType = executorType == null ? defaultExecutorType : executorType;  
    
        executorType = executorType == null ?ExecutorType.SIMPLE : executorType;  
    
        Executor executor;  
    
        if(ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this,transaction);
        } else if(ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this,transaction);  
        } else {  
            executor = newSimpleExecutor(this, transaction);
        }
    
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);  
        }
        executor = (Executor) interceptorChain.pluginAll(executor);  
        return executor;  
    }  
    

    可以看出,

    • 如果不开启cache的话,创建的Executor是3种基础类型之一
      • BatchExecutor专门用于执行批量sql操作
      • ReuseExecutor会重用statement执行sql操作
      • SimpleExecutor只是简单执行sql没有什么特别的
    • 开启cache的话(默认是开启的并且没有任何理由去关闭它),就会创建CachingExecutor,它以前面创建的Executor作为唯一参数。CachingExecutor在查询数据库前先查找缓存,若没找到的话调用delegate(就是构造时传入的Executor对象)从数据库查询,并将查询结果存入缓存中。

    Executor对象是可以被插件拦截的,如果定义了针对Executor类型的插件,最终生成的Executor对象是被各个插件插入后的代理对象。

    接下来,去看sql的执行过程。上面,拿到了MapperProxy, 每个MapperProxy对应一个dao接口, 那么在使用的时候,MapperProxy是怎么做的呢?

    • MapperProxy

    我们知道对被代理对象的方法的访问都会落实到代理者的invoke上来,MapperProxy的invoke如下:

      /**
       * MapperProxy在执行时会触发此方法
       */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
          try {
            return method.invoke(this, args);
          } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
          }
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //二话不说,主要交给MapperMethod自己去管
        return mapperMethod.execute(sqlSession, args);
      }
    
    • MapperMethod

    就像是一个分发者,他根据参数和返回值类型选择不同的sqlsession方法来执行。这样mapper对象与sqlsession就真正的关联起来了。

      /**
       * 看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了
       * @param sqlSession
       * @param args
       * @return
       */
      public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        if (SqlCommandType.INSERT == command.getType()) {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.insert(command.getName(), param));
        } else if (SqlCommandType.UPDATE == command.getType()) {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.update(command.getName(), param));
        } else if (SqlCommandType.DELETE == command.getType()) {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.delete(command.getName(), param));
        } else if (SqlCommandType.SELECT == command.getType()) {
          if (method.returnsVoid() && method.hasResultHandler()) {
            executeWithResultHandler(sqlSession, args);
            result = null;
          } else if (method.returnsMany()) {
            result = executeForMany(sqlSession, args);
          } else if (method.returnsMap()) {
            result = executeForMap(sqlSession, args);
          } else {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(command.getName(), param);
          }
        } else {
          throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
          throw new BindingException("Mapper method '" + command.getName() 
              + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
      }
    

    既然又回到SqlSession了,前面提到过,sqlsession只是一个门面,真正发挥作用的是executor,对sqlsession方法的访问最终都会落到executor的相应方法上去。Executor分成两大类,一类是CacheExecutor,另一类是普通Executor。Executor的创建前面已经介绍了,那么咱们就看看SqlSession的CRUD方法了,为了省事,还是就选择其中的一个方法来做分析吧。这儿,咱们选择了selectList方法:

      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
          MappedStatement ms = configuration.getMappedStatement(statement);
          //CRUD实际上是交给Excetor去处理, excutor其实也只是穿了个马甲而已,小样,别以为穿个马甲我就不认识你嘞!
          return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    
    • CacheExecutor

    CacheExecutor有一个重要属性delegate,它保存的是某类普通的Executor,值在构照时传入。执行数据库update操作时,它直接调用delegate的update方法,执行query方法时先尝试从cache中取值,取不到再调用delegate的查询方法,并将查询结果存入cache中。代码如下:

    public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,ResultHandler resultHandler) throws SQLException {  
        if (ms != null) {  
            Cache cache = ms.getCache();  
            if (cache != null) {  
                flushCacheIfRequired(ms);  
                cache.getReadWriteLock().readLock().lock();  
                try {  
                    if (ms.isUseCache() && resultHandler ==null) {  
                        CacheKey key = createCacheKey(ms, parameterObject, rowBounds);  
                        final List cachedList = (List)cache.getObject(key);  
                        if (cachedList != null) {  
                            return cachedList;  
                        } else {  
                            List list = delegate.query(ms,parameterObject, rowBounds, resultHandler);  
                            tcm.putObject(cache,key, list);  
                            return list;  
                        }  
                    } else {  
                        return delegate.query(ms,parameterObject, rowBounds, resultHandler);  
                    }  
                } finally {  
                    cache.getReadWriteLock().readLock().unlock();  
                }
            }  
        }  
        return delegate.query(ms,parameterObject, rowBounds, resultHandler);  
    }
    
    • 普通Executor

    有3类,他们都继承于BaseExecutor

    • BatchExecutor专门用于执行批量sql操作
    • ReuseExecutor会重用statement执行sql操作
    • SimpleExecutor只是简单执行sql没有什么特别的

    下面以SimpleExecutor为例:

    public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler) throws SQLException {  
        Statement stmt = null;  
        try {  
            Configuration configuration = ms.getConfiguration();  
            StatementHandler handler = configuration.newStatementHandler(this, ms,parameter, rowBounds,resultHandler);  
            stmt =prepareStatement(handler);  
            returnhandler.query(stmt, resultHandler);  
        } finally {  
            closeStatement(stmt);  
        }  
    } 
    

    然后,通过一层一层的调用,最终会来到doQuery方法, 这儿咱们就随便找个Excutor看看doQuery方法的实现吧,我这儿选择了SimpleExecutor:

      public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
          Configuration configuration = ms.getConfiguration();
          StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
          stmt = prepareStatement(handler, ms.getStatementLog());
          //StatementHandler封装了Statement, 让 StatementHandler 去处理
          return handler.<E>query(stmt, resultHandler);
        } finally {
          closeStatement(stmt);
        }
      }
    

    Mybatis内置的ExecutorType有3种,默认的是simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句, 并且批量执行所有更新语句,显然batch性能将更优;

    但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的;

    通过走码和研读spring相关文件发现,在同一事务中batch模式和simple模式之间无法转换,由于本项目一开始选择了simple模式,所以碰到需要批量更新时,只能在单独的事务中进行;

    在代码中使用batch模式可以使用以下方式:

    //从spring注入原有的sqlSessionTemplate
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
     
    public void testInsertBatchByTrue() {
        //新获取一个模式为BATCH,自动提交为false的session
        //如果自动提交设置为true,将无法控制提交的条数,改为最后统一提交,可能导致内存溢出
        SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
        //通过新的session获取mapper
        fooMapper = session.getMapper(FooMapper.class);
        int size = 10000;
        try {
            for (int i = 0; i < size; i++) {
                Foo foo = new Foo();
                foo.setName(String.valueOf(System.currentTimeMillis()));
                fooMapper.insert(foo);
                if (i % 1000 == 0 || i == size - 1) {
                    //手动每1000个一提交,提交后无法回滚
                    session.commit();
                    //清理缓存,防止溢出
                    session.clearCache();
                }
            }
        } catch (Exception e) {
            //没有提交的数据可以回滚
            session.rollback();
        } finally {
            session.close();
        }
    }
    

    上述代码没有使用spring的事务,改手动控制,如果和原spring事务一起使用,将无法回滚,必须注意,最好单独使用;

    StatementHandler

    可以看出,Executor本质上也没有进行处理,具体的事情原来是StatementHandler来完成的。当Executor将指挥棒交给StatementHandler后,接下来的工作就是StatementHandler的事了。我们先看看StatementHandler是如何创建的:

    public StatementHandler newStatementHandler(Executor executor, MappedStatementmappedStatement,  
            ObjectparameterObject, RowBounds rowBounds, ResultHandler resultHandler) {  
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement,parameterObject,rowBounds, resultHandler);  
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);  
        return statementHandler;
    }  
    

    可以看到每次创建的StatementHandler都是RoutingStatementHandler,它只是一个分发者,他一个属性delegate用于指定用哪种具体的StatementHandler。可选的StatementHandler有SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler三种。选用哪种在mapper配置文件的每个statement里指定,默认的是PreparedStatementHandler。同时还要注意到StatementHandler是可以被拦截器拦截的,和Executor一样,被拦截器拦截后的对像是一个代理对象。由于mybatis没有实现数据库的物理分页,众多物理分页的实现都是在这个地方使用拦截器实现的,本文作者也实现了一个分页拦截器,在后续的章节会分享给大家,敬请期待。

    StatementHandler创建后需要执行一些初始操作,比如statement的开启和参数设置、对于PreparedStatement还需要执行参数的设置操作等。代码如下:

    private Statement prepareStatement(StatementHandler handler) throws SQLException {  
        Statement stmt;  
        Connection connection = transaction.getConnection();  
        stmt =handler.prepare(connection);  
        handler.parameterize(stmt);  
        return stmt;  
    }
    

    statement的开启和参数设置没什么特别的地方,handler.parameterize倒是可以看看是怎么回事。handler.parameterize通过调用ParameterHandler的setParameters完成参数的设置,ParameterHandler随着StatementHandler的创建而创建,默认的实现是DefaultParameterHandler:

    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {  
       ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,parameterObject,boundSql);  
       parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);  
       return parameterHandler;  
    }
    

    同Executor和StatementHandler一样,ParameterHandler也是可以被拦截的。DefaultParameterHandler里设置参数的代码如下:

    public void setParameters(PreparedStatement ps) throws SQLException {  
        ErrorContext.instance().activity("settingparameters").object(mappedStatement.getParameterMap().getId());  
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  
        if(parameterMappings != null) {  
            MetaObject metaObject = parameterObject == null ? null :configuration.newMetaObject(parameterObject);  
            for (int i = 0; i< parameterMappings.size(); i++) {  
                ParameterMapping parameterMapping = parameterMappings.get(i);  
                if(parameterMapping.getMode() != ParameterMode.OUT) {  
                    Object value;  
                    String propertyName = parameterMapping.getProperty();  
                    PropertyTokenizer prop = newPropertyTokenizer(propertyName);  
                    if (parameterObject == null) {  
                        value = null;  
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())){  
                        value = parameterObject;  
                    } else if (boundSql.hasAdditionalParameter(propertyName)){  
                        value = boundSql.getAdditionalParameter(propertyName);  
                    } else if(propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)  
                            && boundSql.hasAdditionalParameter(prop.getName())){  
                        value = boundSql.getAdditionalParameter(prop.getName());  
                        if (value != null) {  
                            value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));  
                        }  
                    } else {  
                        value = metaObject == null ? null :metaObject.getValue(propertyName);  
                    }  
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();  
                    if (typeHandler == null) {  
                       throw new ExecutorException("Therewas no TypeHandler found for parameter " + propertyName  + " of statement " + mappedStatement.getId());  
                    }  
                    typeHandler.setParameter(ps, i + 1, value,parameterMapping.getJdbcType());  
                }  
      
            }  
      
        }  
    } 
    

    这里面最重要的一句其实就是最后一句代码,它的作用是用合适的TypeHandler完成参数的设置。那么什么是合适的TypeHandler呢,它又是如何决断出来的呢?BaseStatementHandler的构造方法里有这么一句:

    this.boundSql= mappedStatement.getBoundSql(parameterObject);
    

    它触发了sql 的解析,在解析sql的过程中,TypeHandler也被决断出来了,决断的原则就是根据参数的类型和参数对应的JDBC类型决定使用哪个TypeHandler。比如:参数类型是String的话就用StringTypeHandler,参数类型是整数的话就用IntegerTypeHandler等。

    参数设置完毕后,执行数据库操作(update或query)。如果是query最后还有个查询结果的处理过程。

    接下来,咱们看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的:

      public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        // 到此,原形毕露, PreparedStatement, 这个大家都已经滚瓜烂熟了吧
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        // 结果交给了ResultSetHandler 去处理
        return resultSetHandler.<E> handleResultSets(ps);
      }
    

    结果处理使用ResultSetHandler来完成,默认的ResultSetHandler是FastResultSetHandler,它在创建StatementHandler时一起创建,代码如下:

    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement,  
    RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {  
       ResultSetHandler resultSetHandler = mappedStatement.hasNestedResultMaps() ? newNestedResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds): new FastResultSetHandler(executor,mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);  
       resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);  
       return resultSetHandler;  
    } 
    

    可以看出ResultSetHandler也是可以被拦截的,可以编写自己的拦截器改变ResultSetHandler的默认行为。ResultSetHandler内部一条记录一条记录的处理,在处理每条记录的每一列时会调用TypeHandler转换结果,如下:

    protected boolean applyAutomaticMappings(ResultSet rs, List<String> unmappedColumnNames,MetaObject metaObject) throws SQLException {  
        boolean foundValues = false;  
        for (String columnName : unmappedColumnNames) {  
            final String property = metaObject.findProperty(columnName);  
            if (property!= null) {  
                final ClasspropertyType =metaObject.getSetterType(property);  
                if (typeHandlerRegistry.hasTypeHandler(propertyType)) {  
                    final TypeHandler typeHandler = typeHandlerRegistry.getTypeHandler(propertyType);  
                    final Object value = typeHandler.getResult(rs,columnName);  
                    if (value != null) {  
                        metaObject.setValue(property, value);  
                        foundValues = true;  
                    }  
                }  
            }  
        }  
        return foundValues;  
    }
    

    从代码里可以看到,决断TypeHandler使用的是结果参数的属性类型。因此我们在定义作为结果的对象的属性时一定要考虑与数据库字段类型的兼容性。到此, 一次sql的执行流程就完了。

    之前写过一篇介绍
    Python

    dataclass
    的文章:
    《掌握python的dataclass,让你的代码更简洁优雅》

    那篇侧重于介绍
    dataclass
    的使用,今天想探索一下这个有趣的特性是如何实现的。

    表面上看,
    dataclass
    就是一个普通的装饰器,但是它又在
    class
    上实现了很多神奇的功能,

    为我们在
    Python
    中定义和使用
    class
    带来了极大的便利。

    如果你也好奇它在幕后是如何工作的,本篇我们就一同揭开
    Python

    dataclass
    的神秘面纱,

    深入探究一下其内部原理。

    1. dataclass简介

    dataclass
    为我们提供了一种简洁而高效的方式来定义类,特别是那些主要用于存储数据的类。

    它能自动为我们生成一些常用的方法,如
    __init__

    __repr__
    等,大大减少了样板代码的编写。

    例如,我在量化中经常用的一个
    K线
    数据,用dataclass来定义的话,如下所示:

    from dataclasses import dataclass
    from datetime import datetime
    
    @dataclass
    class KLine:
        name: str = "BTC"
        open_price: float = 0.0
        close_price: float = 0.0
        high_price: float = 0.0
        low_price: float = 0.0
        begin_time: datetime = datetime.now()
    
    if __name__ == "__main__":
        kl = KLine()
        print(kl)
    

    这样,我们无需手动编写
    __init__
    方法来初始化对象,就可以轻松创建
    KLine
    类的实例,

    并且直接打印对象也可以得到清晰,易于阅读的输出。

    $  python.exe .\kline.py
    KLine(name='BTC', open_price=0.0, close_price=0.0, 
    high_price=0.0, low_price=0.0, 
    begin_time=datetime.datetime(2025, 1, 2, 17, 45, 53, 44463))
    

    但这背后究竟发生了什么呢?

    2. 核心概念

    dataclass

    Python3.7
    版本开始,已经加入到标准库中了。

    代码就在
    Python
    安装目录中的
    Lib/dataclasses.py
    文件中。

    实现这个装饰器功能的核心有两个:
    __annotations__
    属性和
    exec
    函数。

    2.1. __annotations__属性

    __annotations__

    Python
    中一个隐藏的宝藏,它以字典的形式存储着变量、属性以及函数参数或返回值的类型提示。

    对于
    dataclass
    来说,它就像是一张地图,装饰器通过它来找到用户定义的字段。

    比如,在上面的
    KLine
    类中,
    __annotations__
    会返回字段的相关信息。

    这使得
    dataclass
    装饰器能够清楚地知道类中包含哪些字段以及它们的类型,为后续的操作提供了关键信息。

    if __name__ == "__main__":
        print(KLine.__annotations__)
    
    # 运行结果:
    {'name': <class 'str'>, 'open_price': <class 'float'>, 
    'close_price': <class 'float'>, 'high_price': <class 'float'>, 
    'low_price': <class 'float'>, 'begin_time': <class 'datetime.datetime'>}
    

    2.2. exec 函数

    exec
    函数堪称
    dataclass
    实现的魔法棒,它能够将字符串形式的代码转换为
    Python
    对象。


    dataclass
    的世界里,它被用来创建各种必要的方法。

    我们可以通过构建函数定义的字符串,然后使用
    exec
    将其转化为真正的函数,并添加到类中。

    这就是
    dataclass
    装饰器能够自动生成
    __init__

    __repr__
    等方法的秘密所在。

    下面的代码通过
    exec
    ,将一个字符串代码转换成一个真正可使用的函数。

    # 定义一个存储代码的字符串
    code_string = """
    def greet(name):
        print(f"Hello, {name}!")
    """
    
    # 使用 exec 函数执行代码字符串
    exec(code_string)
    
    # 调用通过 exec 生成的函数
    greet("Alice")
    

    3. 自定义dataclass装饰器

    掌握了上面的核心概念,我们就可以开始尝试实现自己的
    dataclass
    装饰器。

    当然,这里只是简单实现个雏形,目的是为了了解
    Python
    标准库中
    dataclass
    的原理。

    下面主要实现两个功能
    __init__

    __repr__

    通过这两个功能来理解
    dataclass
    的实现原理。

    3.1. 定义架构

    我们首先定义一个
    dataclass
    装饰器,它的结构如下:

    def dataclass(cls=None, init=True, repr=True):
    
        def wrap(cls):
            # 这里将对类进行修改
            return cls
    
        if cls is None:
            return wrap
        return wrap(cls)
    

    接下来,我们在这个装饰器中实现
    __init__

    __repr__

    3.2. 初始化:
    init


    init
    参数为
    True
    时,我们为类添加
    __init__
    方法。

    通过
    _init_fn
    函数来实现,它会根据类的字段生成
    __init__
    方法的函数定义字符串,然后使用
    _create_fn
    函数将其转换为真正的方法并添加到类中。

    def _create_fn(cls, name, fn):
        ns = {}
        exec(fn, None, ns)
        method = ns[name]
        setattr(cls, name, method)
    
    
    def _init_fn(cls, fields):
        args = ", ".join(fields)
    
        lines = [f"self.{field} = {field}" for field in fields]
        body = "\n".join(f"  {line}" for line in lines)
    
        txt = f"def __init__(self, {args}):\n{body}"
    
        _create_fn(cls, "__init__", txt)
    

    3.3. 美化输出:
    repr

    __repr__
    方法让我们能够以一种清晰易读的方式打印出类的实例。

    为了实现这个功能,我们创建
    _repr_fn
    函数,它生成
    __repr__
    方法的定义字符串。

    这个方法会获取实例的
    __dict__
    属性中的所有变量,并使用
    f-string
    进行格式化输出。

    def _repr_fn(cls, fields):
        txt = (
            "def __repr__(self):\n"
            "    fields = [f'{key}={val!r}' for key, val in self.__dict__.items()]\n"
            "    return f'{self.__class__.__name__}({\"\\n \".join(fields)})'"
        )
        _create_fn(cls, "__repr__", txt)
    

    3.4. 合在一起

    最终的代码如下,代码中使用的是自己的
    dataclass
    装饰器,而不是标准库中的
    dataclass

    from datetime import datetime
    
    
    def dataclass(cls=None, init=True, repr=True):
    
        def wrap(cls):
            fields = cls.__annotations__.keys()
    
            if init:
                _init_fn(cls, fields)
    
            if repr:
                _repr_fn(cls, fields)
    
            return cls
    
        if cls is None:  # 如果装饰器带参数
            return wrap
    
        return wrap(cls)
    
    
    def _create_fn(cls, name, fn):
        ns = {}
        exec(fn, None, ns)
        method = ns[name]
        setattr(cls, name, method)
    
    
    def _init_fn(cls, fields):
        args = ", ".join(fields)
    
        lines = [f"self.{field} = {field}" for field in fields]
        body = "\n".join(f"  {line}" for line in lines)
    
        txt = f"def __init__(self, {args}):\n{body}"
    
        _create_fn(cls, "__init__", txt)
    
    
    def _repr_fn(cls, fields):
        txt = (
            "def __repr__(self):\n"
            "    fields = [f'{key}={val!r}' for key, val in self.__dict__.items()]\n"
            "    return f'{self.__class__.__name__}({\"\\n \".join(fields)})'"
        )
        _create_fn(cls, "__repr__", txt)
    
    
    @dataclass
    class KLine:
        name: str = "BTC"
        open_price: float = 0.0
        close_price: float = 0.0
        high_price: float = 0.0
        low_price: float = 0.0
        begin_time: datetime = datetime.now()
    
    
    if __name__ == "__main__":
        kl = KLine(
            name="ETH",
            open_price=1000.5,
            close_price=3200.5,
            high_price=3400,
            low_price=200,
            begin_time=datetime.now(),
        )
        print(kl)
    

    运行的效果如下:

    可以看出,我们自己实现的
    dataclass
    装饰器也可以实现类的初始化和美化输出,这里输出时每个属性占一行。

    4. 总结

    通过自定义
    dataclass
    装饰器的构建过程,我们深入了解了
    Python

    dataclass
    的内部原理。

    利用
    __annotations__
    获取字段信息,借助
    exec
    创建各种方法,从而实现简洁高效的
    dataclass
    定义。

    不过,实际的
    Python
    标准库中的
    dataclass
    还有更多的功能和优化,了解了其原理之后,可以参考它的源码再进一步学习。

    开心一刻

    昨天在幼儿园,领着儿子在办公室跟他班主任聊他的情况
    班主任:皓瑟,你跟我聊天是不是紧张呀
    儿子:是的,老师
    班主任:不用紧张,我虽然是你的班主任,但我也才22岁,你就把我当成班上的女同学
    班主任继续补充道:你平时跟她们怎么聊,就跟我怎么聊,男孩子要果然,想说啥就说啥
    儿子满眼期待的看向我,似乎在征询我的同意,我坚定的点了点头
    儿子:老师,看看腿

    开心一刻

    问题复现

    项目基于 Spring Boot 2.4.2,引入了
    spring-boot-starter-data-redis

    mybatis-plus-boot-starter
    ,完整依赖如下

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    


    RedisTemplate
    进行了自定义配置

    /**
     * @author 青石路
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(factory);
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
            redisTemplate.setKeySerializer(stringRedisSerializer);
            redisTemplate.setValueSerializer(jsonRedisSerializer);
            redisTemplate.setHashKeySerializer(stringRedisSerializer);
            redisTemplate.setHashValueSerializer(jsonRedisSerializer);
            redisTemplate.setEnableDefaultSerializer(true);
            redisTemplate.setDefaultSerializer(jsonRedisSerializer);
            redisTemplate.setEnableTransactionSupport(true);
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    }
    

    需要实现的功能

    保存用户:若用户在缓存(
    Redis
    )中存在,直接返回成功;若用户在缓存中不存在,将用户信息保存到缓存的同时,还要保存到
    MySQL

    功能很简单,实现如下

    /**
     * @author: 青石路
     */
    @Service
    public class UserServiceImpl extends ServiceImpl<UserDao, User> implements IUserService {
    
        private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public String saveNotExist(User user) {
            Object o = redisTemplate.opsForValue().get("dataredis:user:" + user.getUserName());
            if (o != null) {
                LOG.info("用户已存在");
                return "用户已存在";
            }
            redisTemplate.opsForValue().set("dataredis:user:" + user.getUserName(), user);
            this.save(user);
            return "用户保存成功";
        }
    }
    

    结构还是常规的
    Controller
    ->
    Service
    ->
    Dao
    ;启动项目后,我们直接访问接口

    POST http://localhost:8080/user/save
    Content-Type: application/json
    
    {
      "userName": "qsl",
      "password": "123456"
    }
    

    毫无意外,接口 500

    {
      "timestamp": "2024-12-28T05:39:49.577+00:00",
      "status": 500,
      "error": "Internal Server Error",
      "message": "",
      "path": "/user/save"
    }
    

    这么简单的功能,这么完美的实现,为什么也出错?

    早知道不学编程了

    问题排查

    遇到异常我们该如何排查?看
    异常堆栈
    是最直接的方式

    异常堆栈信息

    有两点值得我们好好分析下

    1. RedisConnectionUtils.createConnectionSplittingProxy

      看方法名就知道,这是要创建 Redis Connection 的代理;咱先甭管创建的是什么代理,咱先弄明白为什么要创建代理?


      不就是查 Redis,然后写 Redis,为什么要创建代理?


      怎么弄明白了,看谁调用了这个方法不就清楚了?直接从异常堆栈一眼就可以看出
      RedisConnectionUtils.java:151
      调用了该方法,我们点击跟进看看


      createConnectionSplittingProxy调用处

      所以重点有来到
      bindSynchronization

      isActualNonReadonlyTransactionActive()


      • bindSynchronization 的值

        它的计算逻辑很清楚


        TransactionSynchronizationManager.isActualTransactionActive() && transactionSupport;


        isActualTransactionActive() 注释如下

        /**
         * Return whether there currently is an actual transaction active.
         * This indicates whether the current thread is associated with an actual
         * transaction rather than just with active transaction synchronization.
         * <p>To be called by resource management code that wants to discriminate
         * between active transaction synchronization (with or without backing
         * resource transaction; also on PROPAGATION_SUPPORTS) and an actual
         * transaction being active (with backing resource transaction;
         * on PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW, etc).
         * @see #isSynchronizationActive()
         */
        public static boolean isActualTransactionActive() {
            return (actualTransactionActive.get() != null);
        }
        

        返回当前线程是否是与实际事务相关联;可能你们看的有点迷糊,因为这里还与 Spring 的事务传播机制有关联,结合我给的示例代码来看,可以简单理解成:
        当前线程是否开启事务


        saveNotExist开启事务

        很明显当前线程是开启事务的,所以 TransactionSynchronizationManager.isActualTransactionActive() 的值为
        true

        transactionSupport
        的值则需要继续从上游调用方寻找


        redis_trasactionSupportpng

        跟进
        RedisTemplate.java:209


        RedisTemplate_enableTrascationSupport

        enableTransactionSupport
        是 RedisTemplate 的成员变量,其默认值是
        false


        enableTransactionSupport默认值false

        但我们自定义的时候,将 enableTransactionSupport 设置成了
        true


        enableTransactionSupport自定义成true

        这里为什么设置成 true,我问了当时写这个代码的同事,直接从网上复制的,不是刻意开启的!
        我是不推荐使用 Redis 事务的,至于为什么,后文会有说明


        所以 bindSynchronization 的值为
        true

      • isActualNonReadonlyTransactionActive() 的返回值

        从名称就知道,该方法的作用是判断当前事务是不是
        非只读
        的;其完整代码如下

        private static boolean isActualNonReadonlyTransactionActive() {
            return TransactionSynchronizationManager.isActualTransactionActive()
                    && !TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        }
        

        TransactionSynchronizationManager.isActualTransactionActive() 前面已经分析过,其值是
        true
        ;大家还记得事务设置只读是如何设置的吗?
        @Transactional
        注解是不是有
        readOnly
        配置项?


        @Transactional(rollbackFor = Exception.class, readOnly = true)


        readOnly 的默认值是
        false
        ,而我们的示例代码中又没有将其设置成 true,所以
        !TransactionSynchronizationManager.isCurrentTransactionReadOnly()
        的值就是
        !false
        ,也就是
        true

        所以 isActualNonReadonlyTransactionActive() 的值为
        true


      启用 RedisTemplate 事务的同时,又使用了 @Transactional 使得线程关联了实际事务,并且未启用非只读线程,天时地利人和之下创建了 Redis Connection 代理,通过该代理来实现 Redis 事务


      Spring 对事务的实现是通用的,都是通过代理的方式来实现,不区分是关系型数据库还是Redis,甚至是其他支持事务的数据源!

    2. cannot access its superinterface

      完整信息如下


      java.lang.IllegalAccessError: class org.springframework.data.redis.core.$Proxy82 cannot access its superinterface org.springframework.data.redis.core.RedisConnectionUtils$RedisConnectionProxy


      不合法的访问错误:不能访问父级接口:RedisConnectionUtils$RedisConnectionProxy


      关于 Spring 的代理,我们都知道有两种实现:
      JDK 动态代理

      CGLIB 动态代理
      ,而 Redis 事务则采用的 JDK 动态代理

      Redis事务实现_JDK代理

      JDK 动态代理有哪些限制,你们还记得吗,好好回忆一下


      RedisConnectionUtils$RedisConnectionProxy 都没有实现类,为什么代理会涉及到它?我们看下 RedisConnectionUtils.createConnectionSplittingProxy 的实现就明白了


      createConnectionSplittingProxy具体代码

      我们再看看 RedisConnectionUtils$RedisConnectionProxy 的具体实现


      RedisConnectionProxy_具体代码

      莫非是因为 RedisConnectionProxy 是内部 interface,并且是 package-protected 的,所以导致不能被访问?如何验证了,我们可以进行类似模拟,但我不推荐,我更推荐从官方找答案,因为这个问题肯定不止我们遇到了;从异常堆栈信息可以很明显的看出,这是
      spring-data-redis
      引发的,所以我们直接去其 github 寻找相关 issue


      RedisConnectionProxy_github搜索

      正好有一个,点进去看看,正好有我们想要的答案;推荐大家仔细看看这个 issue,我只强调一下重点


      issue_重点

      1. 将该bug添加到 2.4.7 版本中修复

      2. 将 RedisConnectionProxy 修改成 public

      3. 代码提交版本:503d639


        RedisConnectionProxy_修改记录

      官方 Release 版本也进行了说明


      官方release_2.4.7

    至此,相信你们都清楚问题原因了

    问题修复

    既然问题已经找到,修复方法也就清晰了

    1. 启用只读事务

      这种方式只适用于部分特殊场景,因为它还影响关系型数据库的事务

      不推荐使用

    2. 停用 RedisTemplate 事务

      不设置 enableTransactionSupport,让其保持默认值
      false
      ,或者显示设置成
      false


      redisTemplate.setEnableTransactionSupport(false);


      还记不记得我前面跟你们说过:
      不推荐使用 Redis 事务
      ;至于为什么,我们来看看官网是怎么说明的


      redis事务不支持回滚

      Redis不支持事务回滚,因为支持回滚会对Redis的简单性和性能产生重大影响;Redis 事务只能保证两点


      • 事务中的所有命令都被序列化并按顺序执行。Redis执行事务期间,不会被其它客户端发送的命令打断,事务中的所有命令都作为一个隔离操作顺序执行
      • Redis事务是原子操作,或者执行所有命令或者都不执行。一旦执行命令,即使中间某个命令执行失败,后面的命令仍会继续执行

      另外,官网提到了一个另外一个点


      Redis脚本与事务

      Redis 脚本同样具有事务性。你可以用Redis事务做的一切,你也可以用脚本做,通常脚本会更简单、更快。但有些点我们需要注意,Redis 2.6.0 才引进脚本功能,Lua 脚本具备一定的原子性,可以保证隔离性,而且可以完美的
      支持后面的步骤依赖前面步骤的结果
      ,但同样也
      不支持回滚

      所以如果我们的 Redis 版本满足的话,推荐用 Lua 脚本而非 Redis 事务

      推荐使用

    3. 升级 spring-data-redis 版本

      spring-data-redis 2.4.7 实现了修复,但我们是采用的 starter 的方式引入的依赖,所以升级 spring boot 版本更合适;RedisConnectionUtils$RedisConnectionProxy 是 spring-data-redis 2.4.2 引入的,spring-boot-starter-data-redis 的版本与 spring-boot 版本一致,其 2.4.4、2.4.5 对应的 spring-data-redis 版本是 2.4.6、2.4.8,所以将 spring boot 升级到 2.4.5 或更高即可。如果可以的话,更推荐直接升级到适配 JDK 版本的最新稳定版本

      推荐使用

    总结

    1. 异常堆栈就是发生异常时的调用栈,时间线顺序是
      从下往上
      ,也就是下面一行调用上面一行

    2. 如果Redis版本是2.6.0或更高,不推荐使用其事务功能,用Lua实现事务更合适

      不管是Redis事务,还是Lua脚本,都不支持事务回滚,所以我们要尽量保证Redis命令的正确使用

    3. 不管是使用 spring-data-redis 哪个版本,都推荐关闭 RedisTemplate 的 enableTransactionSupport

      出于两点考虑


      • 你们可以留意下你们项目中的 Redis 版本,肯定都高于 2.6.0,因为版本越高,功能越强大,性能越稳定;言外之意就是可以使用Lua脚本来实现事务
      • 需要用到Redis事务的场景很少,甚至没有;不怕你们笑话,我还没显示的使用过Redis的事务,当然间接用过(Redisson的锁用到了lua脚本)