作者:来自 vivo 互联网存储研发团队-Guo Xiang

本文介绍了TiDB中最基本的PointGet算子在存储层TiKV中的执行流程。

一、背景介绍

TiDB是一款具有HTAP能力(同时支持在线事务处理与在线分析处理 )的融合型分布式数据库产品,具备水平扩容或者缩容等重要特性。TiDB 采用多副本+Multi-Raft 算法的方式将数据调度到不同的机器节点上,具备较高的可靠性和容灾能力。TiDB中的存储层TiKV组件,能够独立于TiDB作为一款分布式KV数据库使用,目前已经捐赠给CNCF并于2020年正式毕业。目前vivo公司内部的磁盘KV产品采用了开源的TiKV作为存储层实现, 目前已经在公司的不同业务产品中有深度实践。

TiKV作为一款KV数据库产品,同时提供了RawAPI和TxnAPI两套接口:

  • RawAPI仅支持最基本的针对单Key操作的Set/Get/Del及Scan语义

  • TxnAPI提供了基于ACID事务标准的接口,支持多Key写入的原子性

TxnAPI采用了分布式事务来保证多Key写入的原子性,其适用的业务场景与RawAPI相比来说更为广泛。本文后续内容将重点对PointGet在TiKV侧的执行流程进行分析,其内容涉及到storage和txn模块。阅读本文后,读者将会深入了解TiKV源码中Get流程的实现细节,包括如何处理读请求、如何进行数据定位和读取、如何实现事务隔离级别等方面,并且能够更好地理解TiKV的内部工作原理和性能优化。

二、PointGet介绍

2.1 TiDB视角中的PointGet

PointGet顾名思义即"点查", 它是TiDB中最为基本的几种算子之一,以下列举了两个常见的PointGet算子的使用场景:

  • 根据主键Id查询

MySQL [test]> explain select * from user where id = 1024;
+-------------+---------+------+-------------------------------+---------------+
| id | estRows | task | access object | operator info |
+-------------+---------+------+-------------------------------+---------------+
| Point_Get_1 | 1.00 | root | table:user, index:PRIMARY(id) | |
+-------------+---------+------+-------------------------------+---------------+
  • 根据唯一索引查询

MySQL [test]> explain select * from users where name = "test";
+-------------+---------+------+-------------------------------+---------------+
| id | estRows | task | access object | operator info |
+-------------+---------+------+-------------------------------+---------------+
| Point_Get_1 | 1.00 | root | table:users, index:name(name) | |
+-------------+---------+------+-------------------------------+---------------+

2.2 纯KV用户视角中的PointGet

部分业务没有完整地使用TiDB组件,而是使用官方提供的client-go/client-rust直接访问PD和TiKV。

func testGet(k []byte) (error) {
txn, err := client.Begin()
if err != nil {
return err
}
v, err := txn.Get(context.TODO(), k)
if err != nil {
return err
}
fmt.Printf("value of key is: %+v", v)
return nil
}

三、PointGet在TiDB中的实现

TiDB层为计算层,其主要职能为MySQL协议的实现以及SQL优化器和执行器的构建。客户端发起的所有SQL, 都会经过以下生命周期流程:

  1. Lexer/Parser解析后得到AST,并转换为执行计划。

  2. 执行计划经过RBO/CBO后得到优化过后的执行计划。

  3. 基于执行计划构建执行器,其本质是不同的算子"套娃",整体构成一个树型结构。

TiDB的执行器基于"火山模型"构建,不同的操作算子具有不同的Executor实现:

type Executor interface {
base() *baseExecutor
Open(context.Context) error
Next(ctx context.Context, req *chunk.Chunk) error
Close() error
Schema() *expression.Schema
}

Executor中最为核心的是三个函数分别是Open/Next/Close,分别对应算子的初始化、迭代以及收尾逻辑。本文涉及的PointGet算子由PointGetExector实现,其核心的查询逻辑位于PointGetExector::Next()函数中。由于相关逻辑耦合了悲观事务,以及tikv/client-go中部分Percolator的实现,且不属于本文重点分析的主要内容,这里不展开描述,感兴趣的读者可以自行阅读。

四、PointGet在TiKV中的实现

4.1 PointGet接口定义

TiKV和TiDB使用gRPC进行通信,其接口契约定义采用了protobuf,我们可以在pingcap/kvproto项目中找到与PointGet相关的接口定义KvGet如下:

// Key/value store API for TiKV.
service Tikv {
// Commands using a transactional interface.
rpc KvGet(kvrpcpb.GetRequest) returns (kvrpcpb.GetResponse) {}
// ... other api definations ...
}

其中入参GetRequest定义如下代码片段,我们可以看到,TiKV的点查接口除了key之外,还额外需要一个名为version的参数,即当前事务的start_ts(事务开始时间戳),这个时间戳是由TiDB在启动事务时从Pd组件申请而来。与很多数据库类似,TiKV也采用了MVCC机制,即同一个key在底层的存储中在不同时刻拥有不同的值,因此要想进行查询,除了key之外,还需要带上版本。

// A transactional get command. Lookup a value for `key` in the transaction with
// starting timestamp = `version`.
message GetRequest {
Context context = 1;
bytes key = 2;
uint64 version = 3;
}

4.2 TiKV侧调用堆栈

TiKV作为gRPC的Server端,提供了KvGet接口的实现,相关调用堆栈为:

+TiKV::kv_get (grpc-poll-thread)
+future_get
+Storage::get
+Storage::snapshot (readpool-thread)
+SnapshotStore::get
+PointGetterBuilder::build
+PointGetter::get

在一次KvGet调用中,函数执行流程会在grpc-poll-thread和readpool-thread中切换,其中前者为gRPC的poll thread,请求在被路由到Storage层后,会根据读写属性路由到不同的线程池中,只读语义的Get/Scan请求都会被路由到ReadPool中执行,这是一个特定用于处理只读请求的线程池。

4.3 Read through locks介绍

在分析后续逻辑之前,我们需要对Read through locks机制先做个简单介绍。TiKV使用Percoaltor模型来实现分布式事务,同时也引入了MVCC机制。然而其实现和传统的MVCC实现略有差异:TiKV的读取过程中若遇到其他事务提交时写入的Lock, 则需要等待或者尝试解锁,这会阻塞读取直到事务状态确定,一定程度上会损失并发性能。

然而在一些场景(如SecondaryLocks),在Key对应的锁仍然存在的情况下,我们已经知道相关事务的最终状态(提交或回滚)。如果我们将这些事务的最终状态与查询请求一起发送给TiKV, 那么TiKV可以根据这些事务状态来确定能否在有Lock的情况下安全读取,避免不必要的等待, 即本小节提到的Read through lock机制。

Context是所有的TiKV请求都会携带的上下文信息,为了实现Read through lock, https://github.com/pingcap/kvproto/pull/833 这个PR在Context中添加了如下字段:

message Context {
// Read requests can ignore locks belonging to these transactions because either
// these transactions are rolled back or theirs commit_ts > read request's start_ts.
repeated uint64 resolved_locks = 13; // Read request should read through locks belonging to these transactions because these
// transactions are committed and theirs commit_ts <= read request's start_ts.
repeated uint64 committed_locks = 22;
}

其中resolved_locks用于记录读取时可以忽略的锁,这些锁对应的事务可能已被回滚,或者已成功提交但CommitTS大于当前的读StartTS,直接忽略这些锁也不影响快照一致性。

其中committed_locks则用于记录逻辑上已被正确提交但物理上Lock还未被清理的、且CommitTS小于当前读取使用的StartTS的事务。由于事务本质上已经被提交,因此读取时可以不需要返回等待,只需要通过Lock查询DefaultCF中的数据即可。

通过Read through lock机制,TiKV可以在一些Lock尚未被清理的情况下直接返回正确的结果,避免了客户端层面的Wait和ResolveLock,其具体实现在后续小节会涉及到。

4.4 Storage::get流程分析

下方代码块是经过精简过后的伪代码,主要标注了get流程中一些比较关键的步骤。

pub fn get(&self, mut ctx: Context, key: Key, start_ts: TimeStamp) -> impl Future<Output = ... >> {
self.read_pool.spawn_handle(async move { // 1. 创建创建快照需要的上下文
let snap_ctx = prepare_snap_ctx(...); // 2. 申请一个快照
let snapshot = Self::with_tls_engine(|engine| Self::snapshot(engine, snap_ctx)).await?; // 3. 创建SnapshotStore对象并执行查询
let snap_store = SnapshotStore::new(...);
let result = snap_store.get(key); // 4. 更新Metrics和Stats统计信息
});
}

4.4.1 准备快照上下文

prepare_snap_ctx顾名思义即准备用于创建快照所需要的上下文对象,即SnapContext对象,其完整定义如下:

pub struct SnapContext<'a> {
pub pb_ctx: &'a Context,
pub read_id: Option<ThreadReadId>,
// When start_ts is None and `stale_read` is true, it means acquire a snapshot without any
// consistency guarantee.
pub start_ts: Option<TimeStamp>,
// `key_ranges` is used in replica read. It will send to
// the leader via raft "read index" to check memory locks.
pub key_ranges: Vec<KeyRange>,
// Marks that this snapshot request is allowed in the flashback state.
pub allowed_in_flashback: bool,
} fn prepare_snap_ctx<'a>(...) -> Result<SnapContext<'a>> {
if !pb_ctx.get_stale_read() {
concurrency_manager.update_max_ts(start_ts);
}
if need_check_locks(isolation_level) {
concurrency_manager.read_key_check(...)
}
let mut snap_ctx = SnapContext {...};
if need_check_locks_in_replica_read(pb_ctx) {
snap_ctx.key_ranges = ...
}
}

prepare_snap_ctx只需要创建一个SnapContext对象,但目前实现中多出了如下判断或操作,绝大部分都源于TiKV5.0中的AsyncCommit特性所需。

1.当本次读取非StaleRead时,需要将当前读取请求的start_ts与CurrencyManager中的max_ts进行比较,并将二者中的最大值更新为全局max_ts。这一操作用于保证异步提交事务计算出来的MinCommitTs不会破坏快照一致性。

2. 若当前的隔离级别是SnapshotIsolation或者RcCheckTs时, 则需要额外检查CurrencyManager中的内存锁。如果存在锁且当前start_ts大于锁中的MinCommitTs,TiKV会直接拒绝本次读取请求。其原因在于AsyncCommit事务Prewrite结束之前需要暂时阻止使用更新的start_ts发起的快照读,否则会导致正在异步提交的事务计算出的MinCommitTS无法满足快照一致性。

4.4.2 向Engine申请Snapshot

Engine是TiKV中对上层存储组件的一次抽象,所有实现了Engine Trait的具体实现都可以作为TiKV中的存储层组件。目前TiKV中已经实现了BTreeEngine/MockEngine/RocksEngine/RaftKV等多个实现。

pub trait Engine: Send + Clone + 'static {
// 获取用于查询的快照
fn async_snapshot(&mut self, ctx: SnapContext<'_>) -> Self::SnapshotRes; // 提交写入的Mutation
fn async_write(&self,ctx: &Context,batch: WriteData,subscribed: u8, on_applied: Option<OnAppliedCb>) -> Self::WriteRes; // 其他接口...
}

Engine的接口定义中与读写相关的接口分别是async_snapshot和async_write。目前TiKV中的默认Engine实现为RaftKV,即一个基于Raftstore的实现。在RaftKV中,所有的写入都会通过Raft状态机进行propose/commit/apply流程,用户可以基于订阅机制获得这3个事件的通知从而做出不同处理,默认情况下,TiKV会在一次写入请求被RaftLeader apply成功后返回用户。而读取操作则需要遵循先行一致性读取,在早期版本中,一次读取需要通过Raft状态机进行一次ReadIndex才能进行,在新版中TiKV实现了基于租约的LeaseRead, 简化了读取流程。本次介绍的PointGet读取流程中,会涉及到使用async_snapshot获取一个Engine在当前时刻的快照,并基于快照进行读取。

TiKV按照KeyRange将Key拆分为不同的Region, 每个Region都是一个RaftGroup,且拥有独立的状态机推进运转。因此,RaftKV-Engine中async_snapshot返回的是一个名为RegionSnapshot的对象,其定义如下:

pub struct RegionSnapshot<S: Snapshot> {
snap: Arc<S>,
region: Arc<Region>,
apply_index: Arc<AtomicU64>,
pub term: Option<NonZeroU64>,
pub txn_extra_op: TxnExtraOp,
// `None` means the snapshot does not provide peer related transaction extensions.
pub txn_ext: Option<Arc<TxnExt>>,
pub bucket_meta: Option<Arc<BucketMeta>>,
}

RegionSnapshot本质是对底层的KV引擎RocksDB层面的快照的封装,其逻辑视图如下:

4.4.3 MVCC实现和快照隔离级别实现

前文提到的Engine::async_snapshot接口返回的快照本质是Engine在当下时刻的快照,并不等于事务层面的MVCC快照,因此在具体查询时,需要配合StartTS进行使用。TiKV中封装了一个SnapshotStore用于辅助MVCC层面的查询。其定义如下:

pub struct SnapshotStore<S: Snapshot> {
snapshot: S,
start_ts: TimeStamp,
isolation_level: IsolationLevel,
fill_cache: bool,
bypass_locks: TsSet,
access_locks: TsSet,
check_has_newer_ts_data: bool,
point_getter_cache: Option<PointGetter<S>>,
}

SnapshotStore中集合了从Engine获取的快照和客户端请求附带的StartTS, 因此可以被认为是一个MVCC层面的快照。用户对SnapshotStore发起的点查会被委托给内部的PointGetter。

// PointGetter::get
pub fn get(&mut self, user_key: &Key) -> Result<Option<Value>> {
fail_point!("point_getter_get"); // 根据当前请求使用的隔离级别判定是否需要检查锁
if need_check_locks(self.isolation_level) {
// 如果需要检查锁且锁存在,则需要根据判定锁
if let Some(lock) = self.load_and_check_lock(user_key)? {
return self.load_data_from_lock(user_key, lock);
}
} // Percoaltor正常读取流程:从WriteCF中找到<=start_ts中最大的commit_ts,并基于其存储的start_ts到DefaultCF中读取
self.load_data(user_key)
}

在执行查询前,TiKV需要根据当前请求的隔离级别判定是否需要检查锁。

pub fn need_check_locks(iso_level: IsolationLevel) -> bool {
matches!(iso_level, IsolationLevel::Si | IsolationLevel::RcCheckTs)
}

TiKV支持SnapshotIsolation/ReadCommitted/ReadCommittedCheckTs三种隔离级别,其中前两种需要检查锁。其原因在于LockCf中的锁是由于事务在2PC的第一阶段提交阶段写入的,事务的最终状态无法确定,如果不检查锁直接读取,那么可能导致快照读取被破坏。

fn load_and_check_lock(&mut self, user_key: &Key) -> Result<Option<Lock>> {
// 从LockCf查询该Key的锁信息
let lock_value = self.snapshot.get_cf(CF_LOCK, user_key)?; if let Some(ref lock_value) = lock_value {
let lock = Lock::parse(lock_value)?;
// 如果存在锁则检查锁是否冲突
if let Err(e) = Lock::check_ts_conflict(
Cow::Borrowed(&lock),
user_key,
self.ts,
&self.bypass_locks,
self.isolation_level,
)
// ...
}

其中Lock::check_ts_conflict的实现中会根据当前的事务隔离级别进行判定,不同的隔离级别的判定逻辑略有差异。由于本文篇幅有限,这里只分析我们常用的快照隔离级别的实现。

fn check_ts_conflict_si(lock: Cow<'_, Self>, key: &Key, ts: TimeStamp, bypass_locks: &TsSet ) -> Result<()> {
if lock.ts > ts
|| lock.lock_type == LockType::Lock
|| lock.lock_type == LockType::Pessimistic
{
return Ok(());
} if lock.min_commit_ts > ts {
// Ignore lock when min_commit_ts > ts
return Ok(());
} if bypass_locks.contains(lock.ts) {
return Ok(());
} let raw_key = key.to_raw()?; if ts == TimeStamp::max() && raw_key == lock.primary && !lock.use_async_commit {
// When `ts == TimeStamp::max()` (which means to get latest committed version
// for primary key), and current key is the primary key, we ignore
// this lock.
return Ok(());
} // There is a pending lock. Client should wait or clean it.
Err(Error::from(ErrorInner::KeyIsLocked(
lock.into_owned().into_lock_info(raw_key),
)))
}
  • 当lock.ts > ts时,当前查询请求可以直接忽略这个锁。其原因在于当前的lock是由具有更高start_ts的事务写入,因此即便这个事务后续被提交,其commit_ts一定大于当前的start_ts,其新写入的数据是不可见的,不会破坏快照一致性。

  • 当lock_type==Lock时,也可以直接忽略这个锁突, 其原因在于LockType::Lock是由于创建索引产生,它只用于指示被锁定但不会修改数据,因此也可以直接被忽略。

  • 当lock_type==Pessistics时,也可以直接忽略这个锁突,LockType::Pessistics是由于悲观事务执行DML时写入,并未进行到事务提交阶段,即使这个事务很快被提交,由于其commit_ts也一定大于当前读取的start_ts, 直接忽略并不会影响快照一致性。

  • 当lock.min_commit_ts > ts时,也可以直接忽略这个锁,其原因在于它能保证这个AsyncCommit事务的最终计算出的commit_ts一定大于ts,即使这个事务会被提交,也不会破坏快照一致性。

  • 当bypass_locks中包含了当前锁的start_ts时, 也可以直接忽略这个锁。bypass_locks即前面Read through locks小节中提到了resloved_locks,这些锁虽然存在,但它们对应事务要么已经被回滚,要么使用了大于当前读取start_ts的commit_ts进行提交,无论是哪种情况都不会破坏快照一致性。

  • 其他情况则需要返回KeyIsLocked错误给客户端,客户端收到这个错误后则会检查这个锁的过期时间,如果锁尚未过期则需要做wait,否则会尝试进行解锁恢复这个事务的状态。

若check_ts_conflict_si返回KeyIsLocked或其他错误后,TiKV会额外检查access_locks里是否包含该锁,如果该锁存在,则KeyIsLocked错误则会被忽略,同时锁会被直接返回,外层函数可以通过锁找到start_ts从而直接读取DefaultCF中的数据。这里的access_locks即Read through locks中的committed_locks,即已经知晓被提交的且commit_ts小于当前快照读start_ts的事务,在这种情况下,直接读取DefaultCF是一个超前但安全的操作,原因在在于一旦这个Lock被Resolve,用户通过新的commit_ts可以定位到同一个start_ts。

if let Err(e) = Lock::check_ts_conflict(Cow::Borrowed(&lock),user_key,self.ts,&self.bypass_locks,self.isolation_level) {
if self.access_locks.contains(lock.ts) {
return Ok(Some(lock));
}
Err(e.into())
}

在不存在Key被锁定或冲突,且没有使用Read through locks读取后,TiKV则会进行正常的Percolator读取流程,即从WriteCF中找到<=start_ts中最大的commit_ts,并基于其存储的start_ts到DefaultCF中读取。

4.4.4 RegionSnapshot的Get实现

RegionSnapshot::get的实现相对比较简单,逻辑如下:

fn get_value_cf_opt(&self, opts: &ReadOptions, cf: &str, key: &[u8]) -> EngineResult<Option<Self::DbVector>> {
// 1. 检查查询的key是否在Region的范围内, 如果不在则直接返回错误。
check_key_in_range(key,self.region.get_id(),self.region.get_start_key(),self.region.get_end_key()).map_err(|e| EngineError::Other(box_err!(e)))?; // 2. 基于查询的key拼接出raftstore层面的DataKey (raftstore在写入时会给用户key前添加一个前缀'z')。
let data_key = keys::data_key(key); // 3. 使用内部的RocksSnapshot查询RocksDB获取key对应的值。
self.snap.get_value_cf_opt(opts, cf, &data_key).map_err(|e| self.handle_get_value_error(e, cf, key))
}

4.4.5 RocksDB/Titan的Get实现

TiKV使用rust-rocksdb库使用FFI实现与RocksDB C-API的交互,RocksSnapshot::get会通过crocksdb_get_pinned_cf将查询接口委托给底层的RocksDB。值得注意的是,TiKV使用的并不是官方的RocksDB,而是自行维护的一个整合了Titan插件的版本。Titan是一个受WiscKey论文启发而创建的项目,其主要目的是将存入RocksDB的大Value从LSM-Tree中分离出来,存储到额外的Blob文件中,从而达到减小写放大的目的。

本小节我们着重分析一下TitanDB中一次查询的实现过程(做过大量精简):

Status TitanDBImpl::GetImpl(const ReadOptions& options,
ColumnFamilyHandle* handle, const Slice& key,
PinnableSlice* value) { // 先查询RocksDB
s = db_impl_->GetImpl(options, key, gopts); // 如果Key的Value不存在或者不是BlobIndex, 则直接返回
if (!s.ok() || !is_blob_index) return s; // Value是BlobIndex,说明这是一个索引,还需要额外查询BlobStorage
BlobIndex index;
s = index.DecodeFrom(value);
assert(s.ok());
if (!s.ok()) return s; BlobRecord record;
PinnableSlice buffer; mutex_.Lock();
// 根据索引查询BlobStorage
auto storage = blob_file_set_->GetBlobStorage(handle->GetID()).lock();
mutex_.Unlock(); if (s.ok()) {
value->Reset();
value->PinSelf(record.value);
}
return s;
}

五、总结

  1. TiKV对数据存储层的职能进行了非常合理的抽象,通过Engine/Snapshot/Iterator等trait定义实现了存储层与上层的解耦。

  2. TiKV在RocksDB提供的多列族原子性写入能力之上实现了Percolator模型,提供了分布式事务和MVCC等能力,并实现了AsyncCommit和1PC等改善了事务提交延迟。

  3. TiKV实现了一个基于RocksDB的KV分离插件titan, 借鉴了Wisckey的思想将大Value从LSM-Tree中分离,在大Value的业务场景下能够通过降低写放大改善性能。

  4. 从PointGet的实现我们可以看到在使用了MVCC的情况下,查询时遇到前一事务Prewrite产生的Lock仍然需要等待Resolve, 因此在AsyncCommit开启的前提下,业务开发需要尽量避免设计事务提交后即刻发起查询的场景,此外也要尽量避免由于大事务提交延迟高影响相关的查询。

参考资料:

TiKV 源码分析之 PointGet的更多相关文章

  1. TiDB show processlist命令源码分析

    背景 因为丰巢自去年年底开始在推送平台上尝试了TiDB,最近又要将承接丰巢所有交易的支付平台切到TiDB上.我本人一直没有抽出时间对TiDB的源码进行学习,最近准备开始一系列的学习和分享.由于我本人没 ...

  2. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  3. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  4. nginx源码分析之网络初始化

    nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...

  5. zookeeper源码分析之五服务端(集群leader)处理请求流程

    leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...

  6. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

  7. zookeeper源码分析之三客户端发送请求流程

    znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...

  8. java使用websocket,并且获取HttpSession,源码分析

    转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...

  9. ABP源码分析二:ABP中配置的注册和初始化

    一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...

  10. ABP源码分析三:ABP Module

    Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...

随机推荐

  1. 阿里集团业务驱动的升级 —— 聊一聊Dubbo 3.0 的演进思路

    简介: 阿里云在 2020年底提出了"三位一体"理念,目标是希望将"自研技术"."开源项目"."商业产品"形成统一的技术 ...

  2. 阿里云混合云Apsara Stack 2.0发布 加速政企数智创新

    ​简介: 2021年10月21日,杭州 – 今日,阿里云于云栖大会正式发布Apsara Stack 2.0,从面向单一私有云场景,升级为服务大型集团云&行业云场景.新一代Apsara Stac ...

  3. dotnet 警惕 async void 线程顶层异常

    在应用程序设计里面,不单是 dotnet 应用程序,绝大部分都会遵循让应用在出现未处理异常状态时终结的原则.在 dotnet 应用里面,如果一个线程顶层出现未捕获异常,则应用进程将会被认为出现异常状态 ...

  4. Mobius 一个运行在 .NET Core 上的 .NET 运行时

    一个 .NET 应用仅仅只是一块在 .NET 运行时上面运行的二进制代码.而 .NET 运行时只是一个能执行这项任务的程序.当前的 .NET Framework 和 .NET Core 运行时采用 C ...

  5. Ingress-Controller高可用方案及多租户场景(21)

    一.Ingress-controller高可用 Ingress Controller 是集群流量的接入层,对它做高可用非常重要,可以基于 keepalive 实现 nginx-ingress-cont ...

  6. Ansible的yaml文件

    ansible提供的脚本,遵循规范yaml(一般用于写配置文件) 可用于配制文件的语言:yaml.xml.json - 冒号后面必须有空格 - 横线后面必须要空格 - 严格保持对齐 - 等号前面不能有 ...

  7. 利用Navicat的历史日志查询表的索引信息(还可以查询很多系统级别的信息)

    1.使用前提 所有的能用Navicat连接的数据库都可以使用这个方法 DDL/DML语句都有 2.Navicat中的历史日志 3.比如查询mysql的表的索引 先打开"历史记录" ...

  8. LLaMA 3 源码解读-大语言模型5

    本来不是很想写这一篇,因为网上的文章真的烂大街了,我写的真的很有可能没别人写得好.但是想了想,创建这个博客就是想通过对外输出知识的方式来提高自身水平,而不是说我每篇都能写得有多好多好然后吸引别人来看. ...

  9. LMDeploy量化部署LLM&LVM实操-书生浦语大模型实战营第二期第5节作业

    书生浦语大模型实战营第二期第5节作业 本页面包括实战营第二期第五节作业的全部操作步骤.如果需要知道模型量化部署的相关知识请访问学习笔记. 作业要求 基础作业 完成以下任务,并将实现过程记录截图: 配置 ...

  10. docker 安装 kafka+zookeeper,golang操作kafka

    目录 docker-compose.yml go操作kafka producer 消费者 consumer 消费者 结合gin框架操作kafka go-queue操作kafka 环境: centos8 ...