Mysql8关于hashjoin的代码处理方式

1 表的Schema如下所示:

create table t1(
c1 int primary key,
c2 int
); create table t2(
d1 int primary key,
d2 int
); insert into t1 values(1,1);
insert into t1 values(2,2);
insert into t1 values(3,3);
insert into t1 values(4,4);
insert into t1 values(5,5);
insert into t1 values(6,6);
insert into t1 values(7,7); insert into t2 values(1,1);
insert into t2 values(2,2);
insert into t2 values(3,3);
insert into t2 values(4,4);
insert into t2 values(5,5);
insert into t2 values(6,6);
insert into t2 values(7,7);

在Mysql8.0.20之后(包含),Mysql在实际执行过程中大量的使用了HashJoin,只要是以前能够用到BNL(Blocked Nested Loop Join)的地方,都会选择HashJoin的方式来代替,众所周知,HashJoin只能适用于等值查询,譬如下面的语句:

explain format=tree SELECT *  FROM t1 JOIN t2 ON t1.c2=t2.d2;
/*explain的结果如下*/
| EXPLAIN |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Inner hash join (t2.d2 = t1.c2) (cost=6.10 rows=7)
-> Table scan on t2 (cost=0.05 rows=7)
-> Hash
-> Table scan on t1 (cost=0.95 rows=7)
|

因为上面join列都没有使用到索引,所以对于这个语句,Mysql会优先使用HashJoin,但是如果使用到了索引,那么就依旧会采用Nested Loop Join的方式,比如下面的语句:

explain format=tree SELECT * FROM t1 JOIN t2 ON t1.c1=t2.d2;
| EXPLAIN |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=3.40 rows=7)
-> Filter: (t2.d2 is not null) (cost=0.95 rows=7)
-> Table scan on t2 (cost=0.95 rows=7)
-> Single-row index lookup on t1 using PRIMARY (c1=t2.d2) (cost=0.26 rows=1)
|

重点来了,在Mysql中,即使join条件没有采用等值的方式,那么也会采用HashJoin的方式,比如下面的语句:

 explain format=tree SELECT * FROM t1 JOIN t2 ON t1.c2 > t2.d2;
----------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------+
/*可以看到,join条件变成了filter的方式,但是join方式还是采用了HashJoin的方式*/
| -> Filter: (t1.c2 > t2.d2) (cost=6.10 rows=16)
-> Inner hash join (no condition) (cost=6.10 rows=16)
-> Table scan on t2 (cost=0.07 rows=7)
-> Hash
-> Table scan on t1 (cost=0.95 rows=7)
|

可以看到,join条件变成了filter的方式,但是join方式还是采用了HashJoin的方式,那么Mysql是怎么实现非等值情况下的HashJoin的呢?这里我们分析下代码。

2 HashJoin代码实现

本示例代码采用如下的sql语句来进行示例:

SELECT * FROM t1 JOIN t2 ON t1.c1=t2.d2

上述语句在Mysql里面使用到的Iterator如下:

HashJoinIterator
TableScanIterator
TableScanIterator

在HashJoin中,主要是流程如下:

左表扫描数据 -> 构建Hash表 -> 右表扫描数据 -> 在Hash进行probe操作 -> 将结果返回给用户

重要函数调用的流程图大体如下:

  • 左表扫描数据并构建Hash表的流程

    • sql_union.cc里面的ExecuteIteratorQuery函数,会在执行前对iterator进行如下操作:
      if (m_root_iterator->Init()) {
    return true;
    }
    • 对于HashJoinIteratorInit函数介绍如下:
      // Prepare to read the build input into the hash map.
    // m_build_input:为做表的对象,这里也就是t1表的TableScanIterator,先进行init操作,里面其实就是准备扫描表相关的准备工作
    if (m_build_input->Init()) {
    DBUG_ASSERT(thd()->is_error()); // my_error should have been called.
    return true;
    }
    // We always start out by doing everything in memory.
    m_hash_join_type = HashJoinType::IN_MEMORY;
    m_write_to_probe_row_saving = false;
    //省略部分代码
    // Build the hash table
    // 扫描左表,第一次进行构建Hash表的操作,这是一个比较重要的函数
    if (BuildHashTable()) {
    DBUG_ASSERT(thd()->is_error()); // my_error should have been called.
    return true;
    }
    if (m_state == State::END_OF_ROWS) {
    // BuildHashTable() decided that the join is done (the build input is
    // empty, and we are in an inner-/semijoin. Anti-/outer join must output
    // NULL-complemented rows from the probe input).
    return false;
    }
    // 进行右表的准备工作。
    return InitProbeIterator();
    • BuildHashTable()函数如下:
      if (!m_build_iterator_has_more_rows) {
    m_state = State::END_OF_ROWS;
    return false;
    }
    //省略部分代码
    // 初始化m_row_buffer成员变量,该变量本质上存储了一个std::unordered_multimap对象
    if (InitRowBuffer()) {
    return true;
    }
    const bool reject_duplicate_keys = RejectDuplicateKeys();
    const bool store_rows_with_null_in_join_key = m_join_type == JoinType::OUTER;
    // If Init() is called multiple times (e.g., if hash join is inside an
    // dependent subquery), we must clear the NULL row flag, as it may have been
    // set by the previous executing of this hash join.
    m_build_input->SetNullRowFlag(/*is_null_row=*/false);
    PFSBatchMode batch_mode(m_build_input.get());
    //在Init阶段,Mysql会尽可能的多从底层读取数据构建Hash表。
    for (;;) { // Termination condition within loop.
    int res = m_build_input->Read();
    if (res == 1) {
    DBUG_ASSERT(thd()->is_error()); // my_error should have been called.
    return true;
    }
    //省略部分代码
    //将底层扫描的数据放在m_row_buffer map里面,该函数下面会详细介绍。
    const hash_join_buffer::StoreRowResult store_row_result =
    m_row_buffer.StoreRow(thd(), reject_duplicate_keys,
    store_rows_with_null_in_join_key);
    //校验返回的结果,分别为:存储成功,buffer满了需要下盘和致命错误三种情况。
    switch (store_row_result) {
    case hash_join_buffer::StoreRowResult::ROW_STORED:
    break;
    case hash_join_buffer::StoreRowResult::BUFFER_FULL: {
    // The row buffer is full, so start spilling to disk (if allowed). Note
    // that the row buffer checks for OOM _after_ the row was inserted, so
    // we should always manage to insert at least one row.
    DBUG_ASSERT(!m_row_buffer.empty());
    // If we are not allowed to spill to disk, just go on to reading from
    // the probe iterator.
    if (!m_allow_spill_to_disk) {
    if (m_join_type != JoinType::INNER) {
    // Enable probe row saving, so that unmatched probe rows are written
    // to the probe row saving file. After the next refill of the hash
    // table, we will read rows from the probe row saving file, ensuring
    // that we only read unmatched probe rows.
    InitWritingToProbeRowSavingFile();
    }
    SetReadingProbeRowState();
    return false;
    }
    // Ideally, we would use the estimated row count from the iterator. But
    // not all iterators has the row count available (i.e.
    // RemoveDuplicatesIterator), so get the row count directly from the
    // QEP_TAB.
    const QEP_TAB *last_table_in_join =
    m_build_input_tables.tables().back().qep_tab;
    if (InitializeChunkFiles(
    last_table_in_join->position()->prefix_rowcount,
    m_row_buffer.size(), kMaxChunks, m_probe_input_tables,
    m_build_input_tables,
    /*include_match_flag_for_probe=*/m_join_type == JoinType::OUTER,
    &m_chunk_files_on_disk)) {
    DBUG_ASSERT(thd()->is_error()); // my_error should have been called.
    return true;
    }
    // Write out the remaining rows from the build input out to chunk files.
    // The probe input will be written out to chunk files later; we will do
    // it _after_ we have checked the probe input for matches against the
    // rows that are already written to the hash table. An alternative
    // approach would be to write out the remaining rows from the build
    // _and_ the rows that already are in the hash table. In that case, we
    // could also write out the entire probe input to disk here as well. But
    // we don not want to waste the rows that we already have stored in
    // memory.
    //
    // We never write out rows with NULL in condition for the build/right
    // input, as these rows will never match in a join condition.
    if (WriteRowsToChunks(thd(), m_build_input.get(), m_build_input_tables,
    m_join_conditions, kChunkPartitioningHashSeed,
    &m_chunk_files_on_disk,
    true /* write_to_build_chunks */,
    false /* write_rows_with_null_in_join_key */,
    &m_temporary_row_and_join_key_buffer)) {
    DBUG_ASSERT(thd()->is_error()); // my_error should have been called.
    return true;
    }
    // Flush and position all chunk files from the build input at the
    // beginning.
    for (ChunkPair &chunk_pair : m_chunk_files_on_disk) {
    if (chunk_pair.build_chunk.Rewind()) {
    DBUG_ASSERT(
    thd()->is_error()); // my_error should have been called.
    return true;
    }
    }
    SetReadingProbeRowState();
    return false;
    }
    case hash_join_buffer::StoreRowResult::FATAL_ERROR:
    // An unrecoverable error. Most likely, malloc failed, so report OOM.
    // Note that we cannot say for sure how much memory we tried to allocate
    // when failing, so just report 'join_buffer_size' as the amount of
    // memory we tried to allocate.
    my_error(ER_OUTOFMEMORY, MYF(ME_FATALERROR),
    thd()->variables.join_buff_size);
    return true;
    }
    }
    • HashJoinRowBuffer::StoreRow函数

      StoreRowResult HashJoinRowBuffer::StoreRow(
      THD *thd, bool reject_duplicate_keys,
      bool store_rows_with_null_in_condition) {
      // Make the key from the join conditions.
      m_buffer.length(0);
      //所有的condition都是equal的形式,append_join_key_for_hash_join函数里面会根据不同的数据类型,将数据拷贝到m_buffer里面。
      for (const HashJoinCondition &hash_join_condition : m_join_conditions) {
      bool null_in_join_condition =
      hash_join_condition.join_condition()->append_join_key_for_hash_join(
      thd, m_tables.tables_bitmap(), hash_join_condition, &m_buffer);
      if (null_in_join_condition && !store_rows_with_null_in_condition) {
      // SQL NULL values will never match in an inner join or semijoin, so skip
      // the row.
      return StoreRowResult::ROW_STORED;
      }
      }
      // TODO(efroseth): We should probably use an unordered_map instead of multimap
      // for these cases so we do not have to hash and lookup twice.
      //有的语句是会reject 重复key的,所以这里专门做一个检查。
      if (reject_duplicate_keys &&
      contains(Key(pointer_cast<const uchar *>(m_buffer.ptr()),
      m_buffer.length()))) {
      return StoreRowResult::ROW_STORED;
      }
      // Allocate the join key on the same MEM_ROOT that the hash table is
      // allocated on, so it has the same lifetime as the rest of the contents in
      // the hash map (until Clear() is called on the HashJoinBuffer).
      //从m_buffer中拷贝join key到join_key_data中,方便后续放入map中的操作
      const size_t join_key_size = m_buffer.length();
      uchar *join_key_data = nullptr;
      if (join_key_size > 0) {
      join_key_data = m_mem_root.ArrayAlloc<uchar>(join_key_size);
      if (join_key_data == nullptr) {
      return StoreRowResult::FATAL_ERROR;
      }
      memcpy(join_key_data, m_buffer.ptr(), join_key_size);
      }
      //将行中的具体内容拷贝m_buffer中
      // Save the contents of all columns marked for reading.
      if (StoreFromTableBuffers(m_tables, &m_buffer)) {
      return StoreRowResult::FATAL_ERROR;
      }
      // Give the row the same lifetime as the hash map, by allocating it on the
      // same MEM_ROOT as the hash map is allocated on.
      //从m_buffer中取出具体的数据内容,然后放在row对象中,方便后面插入到map中,目前的版本中,BufferRow对象只是Key的一个重命名,本质上两者是同一个类对象。
      const size_t row_size = m_buffer.length();
      uchar *row = nullptr;
      if (row_size > 0) {
      row = m_mem_root.ArrayAlloc<uchar>(row_size);
      if (row == nullptr) {
      return StoreRowResult::FATAL_ERROR;
      }
      memcpy(row, m_buffer.ptr(), row_size);
      }
      //将查询到的数据和join key封装为Key和BufferRow对象,放入到map中
      m_last_row_stored = m_hash_map->emplace(Key(join_key_data, join_key_size),
      BufferRow(row, row_size));
      if (m_mem_root.allocated_size() > m_max_mem_available) {
      return StoreRowResult::BUFFER_FULL;
      }
      return StoreRowResult::ROW_STORED;
      }
    • InitProbeIterator函数

      bool HashJoinIterator::InitProbeIterator() {
      DBUG_ASSERT(m_state == State::READING_ROW_FROM_PROBE_ITERATOR);
      //本质上是一个TableScanIterator,因此这里也只是简单的Init操作,并没有进行真正的probe操作
      if (m_probe_input->Init()) {
      return true;
      }
      if (m_probe_input_batch_mode) {
      m_probe_input->StartPSIBatchMode();
      }
      return false;
      }
  • 右表扫描数据并进行probe操作。

    • 首先绝大多数情况下,probe操作是在Read阶段执行的,如下ExecuteIteratorQuery函数里面的Read循环。

          for (;;) {
      //在这个例子里面,就是HashJoinIterator进行Read操作。
      int error = m_root_iterator->Read();
      DBUG_EXECUTE_IF("bug13822652_1", thd->killed = THD::KILL_QUERY;);
      if (error > 0 || thd->is_error()) // Fatal error
      return true;
      else if (error < 0)
      break;
      else if (thd->killed) // Aborted by user
      {
      thd->send_kill_message();
      return true;
      }
      ++*send_records_ptr;
      if (query_result->send_data(thd, *fields)) {
      return true;
      }
      thd->get_stmt_da()->inc_current_row_for_condition();
      }
    • HashJoinIterator::Read函数如下:

      int HashJoinIterator::Read() {
      for (;;) {
      if (thd()->killed) { // Aborted by user.
      thd()->send_kill_message();
      return 1;
      }
      switch (m_state) {
      case State::LOADING_NEXT_CHUNK_PAIR:
      if (ReadNextHashJoinChunk()) {
      return 1;
      }
      break;
      //大多数情况下,会进入到该分支。
      case State::READING_ROW_FROM_PROBE_ITERATOR:
      if (ReadRowFromProbeIterator()) {
      return 1;
      }
      break;
      //省略部分代码,省略的case大部分都是内存无法存下,需要下盘的情况。
      case State::READING_FIRST_ROW_FROM_HASH_TABLE:
      //开始从hashtable中获取数据,准备返回给用户
      case State::READING_FROM_HASH_TABLE: {
      //这里的操作是将HashTable里面的数据重新复制给qep_tab->table()里面的相关的成员变量中,具体的行数据存储在qep_tab->table()->record数组中。该函数下面的会介绍
      const int res = ReadNextJoinedRowFromHashTable();
      if (res == 0) {
      // A joined row is ready, so send it to the client.
      return 0;
      }
      if (res == -1) {
      // No more matching rows in the hash table, or antijoin found a
      // matching row. Read a new row from the probe input.
      continue;
      }
      // An error occured, so abort the join.
      DBUG_ASSERT(res == 1);
      return res;
      }
      case State::END_OF_ROWS:
      return -1;
      }
      }
      // Unreachable.
      DBUG_ASSERT(false);
      return 1;
      }
    • HashJoinIterator::ReadRowFromProbeIterator函数如下:

      bool HashJoinIterator::ReadRowFromProbeIterator() {
      DBUG_ASSERT(m_current_chunk == -1);
      //从右表中读取具体的数据
      int result = m_probe_input->Read();
      if (result == 1) {
      DBUG_ASSERT(thd()->is_error()); // my_error should have been called.
      return true;
      }
      if (result == 0) {
      RequestRowId(m_probe_input_tables.tables());
      // A row from the probe iterator is ready.
      //开始从hash表中进行probe操作
      LookupProbeRowInHashTable();
      return false;
      }
      //下面的代码省略掉,这里只介绍最简单的场景
      }
    • void HashJoinIterator::LookupProbeRowInHashTable()函数如下:

      void HashJoinIterator::LookupProbeRowInHashTable() {
      if (m_join_conditions.empty()) {
      // Skip the call to equal_range in case we don't have any join conditions.
      // This can save up to 20% in case of multi-table joins.
      m_hash_map_iterator = m_row_buffer.begin();
      m_hash_map_end = m_row_buffer.end();
      //因为没有join condition(本质上是一个笛卡尔积),所以这里不进行probe操作,直接更新state,下一次循环就可以直接从hashJoin中读取到join后的行
      m_state = State::READING_FIRST_ROW_FROM_HASH_TABLE;
      return;
      }
      // Extract the join key from the probe input, and use that key as the lookup
      // key in the hash table.
      //类似于HashJoinRowBuffer::StoreRow里面的操作,将probe里面的行构建成hashMap可以用的Key
      bool null_in_join_key = ConstructJoinKey(
      thd(), m_join_conditions, m_probe_input_tables.tables_bitmap(),
      &m_temporary_row_and_join_key_buffer);
      if (null_in_join_key) {
      if (m_join_type == JoinType::ANTI || m_join_type == JoinType::OUTER) {
      // SQL NULL was found, and we will never find a matching row in the hash
      // table. Let us indicate that, so that a null-complemented row is
      // returned.
      m_hash_map_iterator = m_row_buffer.end();
      m_hash_map_end = m_row_buffer.end();
      m_state = State::READING_FIRST_ROW_FROM_HASH_TABLE;
      } else {
      SetReadingProbeRowState();
      }
      return;
      }
      hash_join_buffer::Key key(
      pointer_cast<const uchar *>(m_temporary_row_and_join_key_buffer.ptr()),
      m_temporary_row_and_join_key_buffer.length());
      //这里是一个简单的trick,如果只关心第一行的数据,那么直接使用unordered_multimap的find函数可以更低的时间复杂度,否则使用equal_range函数
      if ((m_join_type == JoinType::SEMI || m_join_type == JoinType::ANTI) &&
      m_extra_condition == nullptr) {
      // find() has a better average complexity than equal_range() (constant vs.
      // linear in the number of matching elements). And for semijoins, we are
      // only interested in the first match anyways, so this may give a nice
      // speedup. An exception to this is if we have any "extra" conditions that
      // needs to be evaluated after the hash table lookup, but before the row is
      // returned; we may need to read through the entire hash table to find a row
      // that satisfies the extra condition(s).
      m_hash_map_iterator = m_row_buffer.find(key);
      m_hash_map_end = m_row_buffer.end();
      } else {
      auto range = m_row_buffer.equal_range(key);
      m_hash_map_iterator = range.first;
      m_hash_map_end = range.second;
      }
      //ok,join后的数据已经准备好了,设置状态为,下一次循环直接将结果返回给用户
      m_state = State::READING_FIRST_ROW_FROM_HASH_TABLE;
      }
    • 关于HashJoinIterator::ReadNextJoinedRowFromHashTable()函数

      int HashJoinIterator::ReadNextJoinedRowFromHashTable() {
      int res;
      bool passes_extra_conditions = false;
      do {
      //将jointable里面的行读取出来,拷贝qep_tab()->table()->records数组里。注意的是,之前存储的时候,采用的是pack的方式存储的,现在还原出来需要unpack。至于probe表的数据,因为已经在record数组里面的,所以这里也就不需要任何额外的操作了。
      res = ReadJoinedRow();
      // ReadJoinedRow() can only return 0 (row is ready) or -1 (EOF).
      DBUG_ASSERT(res == 0 || res == -1);
      // Evaluate any extra conditions that are attached to this iterator before
      // we return a row.
      if (res == 0) {
      //一些额外的condition evaluate,不是filter,filter会专门创建iterator来执行。
      passes_extra_conditions = JoinedRowPassesExtraConditions();
      if (thd()->is_error()) {
      // Evaluation of extra conditions raised an error, so abort the join.
      return 1;
      }
      if (!passes_extra_conditions) {
      // Advance to the next matching row in the hash table. Note that the
      // iterator stays in the state READING_FIRST_ROW_FROM_HASH_TABLE even
      // though we are not actually reading the first row anymore. This is
      // because WriteProbeRowToDiskIfApplicable() needs to know if this is
      // the first row that matches both the join condition and any extra
      // conditions; only unmatched rows will be written to disk.
      ++m_hash_map_iterator;
      }
      }
      } while (res == 0 && !passes_extra_conditions);
      // The row passed all extra conditions (or we are out of rows in the hash
      // table), so we can now write the row to disk.
      // Inner and outer joins: Write out all rows from the probe input (given that
      // we have degraded into on-disk hash join).
      // Semijoin and antijoin: Write out rows that do not have any matching row in
      // the hash table.
      //检查是否需要下盘
      if (WriteProbeRowToDiskIfApplicable()) {
      return 1;
      }
      //外查询的null处理
      if (res == -1) {
      // If we did not find a matching row in the hash table, antijoin and outer
      // join should ouput the last row read from the probe input together with a
      // NULL-complemented row from the build input. However, in case of on-disk
      // antijoin, a row from the probe input can match a row from the build input
      // that has already been written out to disk. So for on-disk antijoin, we
      // cannot output any rows until we have started reading from chunk files.
      //
      // On-disk outer join is a bit more tricky; we can only output a
      // NULL-complemented row if the probe row did not match anything from the
      // build input while doing any of the probe phases. We can have multiple
      // probe phases if e.g. a build chunk file is too big to fit in memory; we
      // would have to read the build chunk in multiple smaller chunks while doing
      // a probe phase for each of these smaller chunks. To keep track of this,
      // each probe row is prefixed with a match flag in the chunk files.
      bool return_null_complemented_row = false;
      if ((on_disk_hash_join() && m_current_chunk == -1) ||
      m_write_to_probe_row_saving) {
      return_null_complemented_row = false;
      } else if (m_join_type == JoinType::ANTI) {
      return_null_complemented_row = true;
      } else if (m_join_type == JoinType::OUTER &&
      m_state == State::READING_FIRST_ROW_FROM_HASH_TABLE &&
      !m_probe_row_match_flag) {
      return_null_complemented_row = true;
      }
      SetReadingProbeRowState();
      if (return_null_complemented_row) {
      m_build_input->SetNullRowFlag(true);
      return 0;
      }
      return -1;
      }
      // 省略部分代码
      ++m_hash_map_iterator;
      return 0;
      }

3 总结

可以看到,在Mysql的处理中,如果join条件里面没有等值查询的话,直接的表现就是HashJoinIterator里面的m_join_conditions值为空,那么在往HashTable里面emplace的时候,创建的Key对象,代码如下:

    //将查询到的数据和join key封装为Key和BufferRow对象,放入到map中
m_last_row_stored = m_hash_map->emplace(Key(join_key_data, join_key_size),
BufferRow(row, row_size));

此时的join_key_data固定为NULL,而join_key_size固定为0,也就是说,这个map里面的key只有一个,与之对应的value则有多个。实际上表现出来的结果就是做了一个简单的笛卡尔积,然后在进行条件过滤,本质上并没有进行传统HashJoinprobe操作。

Mysql8关于hashjoin的代码处理方式的更多相关文章

  1. 最新的JavaScript核心语言标准——ES6,彻底改变你编写JS代码的方式!【转载+整理】

    原文地址 本文内容 ECMAScript 发生了什么变化? 新标准 版本号6 兑现承诺 迭代器和for-of循环 生成器 Generators 模板字符串 不定参数和默认参数 解构 Destructu ...

  2. 通过代码的方式完成WCF服务的寄宿工作

    使用纯代码的方式进行服务寄宿 服务寄宿的目的是为了开启一个进程,为WCF服务提供一个运行的环境.通过为服务添加一个或者多个终结点,使之暴露给潜在的服务消费,服务消费者通过匹配的终结点对该服务进行调用, ...

  3. 编写css代码的方式

            css(层叠样式表) 在一个网页中主要负责了页面的数据样式.       编写css代码的方式:       第一种: 在style标签中编写css代码. 只能用于本页面中,复用性不强 ...

  4. 32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式

    32位汇编第三讲,RadAsm,IDE的配置和使用,以及汇编代码注入方式 一丶RadAsm的配置和使用 用了怎么长时间的命令行方式,我们发现了几个问题 1.没有代码提醒功能 2.编写代码很慢,记不住各 ...

  5. WCF使用纯代码的方式进行服务寄宿

    服务寄宿的目的是为了开启一个进程,为WCF服务提供一个运行的环境.通过为服务添加一个或者多个终结点,使之暴露给潜在的服务消费,服务消费者通过匹配的终结点对该服务进行调用,除去上面的两种寄宿方式,还可以 ...

  6. Java 10 的 10 个新特性,将彻底改变你写代码的方式!

    Java 9才发布几个月,很多玩意都没整明白,现在Java 10又快要来了.. 这时候我真尼玛想说:线上用的JDK 7 甚至JDK 6,JDK 8 还没用熟,JDK 9 才发布不久不知道啥玩意,JDK ...

  7. 21_Android中常见对话框,光传感器,通过重力感应器编写出指南针应用,帧动画,通过Jav代码的方式编写补间动画,通过XML的方式编写补间动画

     1 关于常见的对话框,主要有: 常见的对话框,单选对话框,多选对话框,进度条对话框(转圈类型的),带进度条的对话框. 案例结构: 完成如下结构的案例,将所有的案例都测试一下: 2 编写MainA ...

  8. 如何用代码的方式取出SAP C4C销售订单创建后所有业务伙伴的数据

    比如我创建了一个Sales Order(销售订单)后,如何用代码的方式取出这些通过SAP Partner determination自动填充的Involved Parties信息呢? 一种方法可以使用 ...

  9. 无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护

    golang/goroutine 和 swoole/coroutine 协程性能测试对比 - Go语言中文网 - Golang中文社区 https://studygolang.com/articles ...

随机推荐

  1. 手写Spring MVC框架(一) 实现简易版mvc框架

    前言 前面几篇文章中,我们讲解了Spring MVC执⾏的⼤致原理及关键组件的源码解析,今天,我们来模仿它⼿写⾃⼰的mvc框架. 先梳理一下需要实现的功能点: tomcat加载配置文件web.xml: ...

  2. java基础——数组及其应用

    数组 数组时相同类型数据的有序集合 数组描述的时相同类型的若干数据,按照一个定的先后次序排列组合而成 其中,每一个数据成为数组元素,每个数组元素可以通过一个下标来访问他们 数组的声明&创建 首 ...

  3. kubernetes客户端client-go使用

    下载地址: https://github.com/kubernetes/client-go 官方使用文档参考:https://v1-16.docs.kubernetes.io/docs/referen ...

  4. stressapptest工具

    1.在H桌面V7B04上运行stressapptest工具编译报错.请研发协助! 2.因为stressapptest工具在兆芯和龙芯都能正常运行,所以我怀疑是工具stressapptest未在H桌面V ...

  5. 上,打开SSH服务的配置文件:/etc/ssh/sshd_config 加上如下两行: ClientAliveInterval 120 ClientAliveCountMax 720 第一行,表示每隔120秒向客户端

    SSH的默认过一段时间会超时,有时候正在执行着脚本,出去一会回来就断开了,输出信息都看不到了... 禁止SSH自动超时最简单的办法就是,每隔一段时间在客户端和服务器之间发送一个"空包&quo ...

  6. hugboy源库

    =[个人整理的一些源库,均来自网络]= -[Ubuntu]- #阿里源 Ubuntu 20.04 deb http://mirrors.aliyun.com/ubuntu/ focal main re ...

  7. Lua中的元表与元方法学习总结

    前言 元表对应的英文是metatable,元方法是metamethod.我们都知道,在C++中,两个类是无法直接相加的,但是,如果你重载了"+"符号,就可以进行类的加法运算.在Lu ...

  8. linux系统ifconfig中网卡名和网卡配置文件名称不同的解决办法

    比如我的配置文件, cd /etc/sysconfig/network-scripts/ifcfg-eth1是这个名称,但是我使用ifconfig显示的信息却是 eth0,很明显这不是我配置文件的名称 ...

  9. Win7 64 + mysql5.6.24(.zip) 不知道root密码的情况下重设密码

    解决方式 第一步:在运行(常常在附件中)里输出cmd,右键以系统管理员身份登陆: 第二步:停止mysql服务,命令为:net stop mysql  注意,若不行将当前目录切换到mysql\bin目录 ...

  10. ALD技术,相机去噪,图像传感器

    ALD技术,相机去噪,图像传感器 1. 作为镜片的防反射涂层技术被关注的ALD(atomic layer deposition)的引入趋势. (a)为什么需要一种新的防止反射的涂层技术? ALD被认为 ...