etcd-v3.5release-(2)-STM

a.b.c表示a文件里的b类的方法c,注意a不一定是包名,因为文件名不一定等于包名

!!!!etcd在put的过程中使用的batchTxBuffered,这个事务是写bbolt数据库使用的事务,是对bbolt.Tx的一个封装,就是攒一批事务作为一个大事务,一次性提交到bbolt。etcd对外服务得像一个数据库,但是外部看到的不是bbolt,也就是说外部对etcd提交的事务和etcd用来写bbolt时使用的事务完全是两个东西,etcd使用的事务是STM(软件事务内存)

!!!etcd smt在单个事务中对同一key的修改在提交的时候只有最终的修改有效,因为他用了一个rset/wset分别表示客户端在事务中要读取哪些key和修改哪些key,在一条path中对某个key只允许有一个put操作,如果同一条path里有对同一个key的两个put操作,那么clientsdk就会报错

STM、ReadView

!!!!总的来说,etcd的stm事务模型是在客户端实现的,但事务的执行是在etcd集群中的成员节点上完成的。

apply的时候是一条一条的应用的,但是如果要原子的同时apply一批日志,那么就必须使用STM

STM内部包含一个wset和一个rset,保存了本次事务访问到的数据和版本,然后在提交的时候如果发现事务变了,那么就会报错

commited时日志的索引顺序和apply时revision顺序是一致的

!!!wal虽然是顺序写入的,apply的时候也是顺序提交到bbolt的,apply的时候他是会把从chan收到的数据包装成一个job,然后丢到一个fifo队列,这个fifo队列会等当前任务运行完毕才会运行下一个job,也就是apply的时候也必定是按顺序apply的,也就是说只有上一个事务提交到bbolt才会开始提交下一个事务(看了源码),这样在开始x事务的时候x以前的日志都已经写完了,也就是说如果x事务读取的数据被x-k版本的事务修改了,那么x事务是可以检测出来的,如果检测到冲突,那么就不会把该数据写入数据库,也即是说即使日志已经写入了磁盘,但是数据是不一定会写入数据库的。举个例子:一个事务t1,先读取key a,此时a的版本(即revision)为v1,然后事务t1在本地执行了很久才提交事务,然后t1提交到etcd的时候raft日志索引为index1,然后apply的时候对应的revision为 rev1,然后执行的时候检测到key a此时的版本是v1+x,但是他读的时候a的版本为v1,所以此时就检测到了冲突(从treeIndex检测,因为treeIndex包含所有key和key对应的revision,比较一下revision就行),那么事务就会执行失败,虽然事务执行失败,但是日志是apply成功的,也就是说事务执行的结果就是数据不写入数据库。再举个例子,假设事务t1对应的raft日志已经写入磁盘,然后apply但是还没apply完成的时候系统崩溃了,这个没关系,重做就是了,当再次重做到事务t1对应的日志的时候,因为t1以前的日志必定已经重做完了,所以此时还是可以再次检测到冲突的,所以只要wal日志已经持久化了,那么崩溃就对这些已经持久化的日志无影响。

!!!区分revision和applyIndex:etcd的修改操作比如txn或者put,底层都是提交一个事务到bbolt,但是底层是batchTxBuffered即批量提交的,所以就是说上层执行完一个事务后,事务缓存在batchTxBuffered中并没有提交,但是只要把事务提交到了batchTxBuffered,s.currentRev就会+1,也就是说下一次put或者writetxn事务分配的revision就是s.currentRev了,但是!!!在底层batchTxBuffered提交事务前,新增的数据对外部是不可见的,假设batchTxBuffered提交时,本批次最大的revision为x,一旦提交以后,那么rev<=x的素有key都对外可见了,但是x<rev<=s.currentRev之间的而所有key此时对外是不可见的,也就是说可见的数据是落后于已做的数据(因为底层没有提交,所以这些数据对外不可见),这与applyIndex落后于commiteIndex原理是一致的,已经apply的数据对外可见,但是已经commite但是还没有apply的数据不可见

STM内对同一个值多次修改,那么只有最后一次写会成功,因为他是先在本地执行函数,然后得到一个最终的结果,再在commited时候直接提交最终的结果

!!mvcc写,也就是说多个版本

Server上Txn请求流程,分Read和Write

!!!etcd直接提供的if-then-else不支持嵌套事务,但是不知道提供的NewSTM支不支持嵌套事务,有待确定,但是etcd server是肯定支持嵌套事务的

batchtxn内部有一个batchtx表示最后一个事务

!!!stm原理:

stm if xx then yy else zz
stm执行时先判断if里面的条件是否成立,一旦成立就表示true就执行then 路径,一旦失败就表示false就执行else路径,then和else都叫做txnpath即事务路径。stm是允许嵌套stm的比如if里面有一个子if-then-else或者then里面还有if-then-else或者else里也有子if-then-else,当然,也允许子stm里还有子stm。then-else里面的叫做操作即op,因为可能有很多,所以叫做ops。所以stm对于嵌套事务的执行是这样的:先执行最顶层if,然后根据if结果true或者false选择对应的路径,然后执行路径下的op,每个op可以是子事务也可以是简单的put/get/range等操作,然后就根据op执行对应的操作就行,如果是子事务,就再执行一遍if-then-else流程就行

etcd stm代码使用样例(抄自网络):

func getEtcdClient() *v3.Client {
	// 配置 etcd 客户端
	cfg := v3.Config{
		Endpoints:   []string{"30.128.101.96:2379"}, // 集群中的任意一个节点
		DialTimeout: 5 * time.Second,
	}

	// 创建 etcd 客户端
	cli, err := v3.New(cfg)
	if err != nil {
		log.Fatal(err)
	}
	return cli

}

func stm() {
	client := getEtcdClient()
	txnTransfer(client, "k1", "k2")
}
func txnTransfer(etcd *v3.Client, sender, receiver string) error {
	// 失败重试
	for {
		if ok, err := doTxn(etcd, sender, receiver); err != nil {
			fmt.Println("dotxn failed:", err.Error())
		} else if ok {
			fmt.Println("dotxn success")
		}
	}
}
func doTxn(etcd *v3.Client, sender, receiver string) (bool, error) {
	fmt.Println("\n开始事务前的读取操作:")
	getresp, err := etcd.Txn(context.TODO()).Then(v3.OpGet(sender), v3.OpGet(receiver)).Commit()
	if err != nil {
		return false, err
	}
	senderKV := getresp.Responses[0].GetResponseRange().Kvs[0]
	receiverKV := getresp.Responses[0].GetResponseRange().Kvs[0]
	// 发起转账事务,冲突判断 ModRevision 是否发生变化
	var s string
	fmt.Println("senderKv: revision=" + strconv.FormatInt(getresp.Header.Revision, 10) +
		" key=" + string(senderKV.Key) + "  value=" + string(senderKV.Value))
	fmt.Println("senderKv: revision=" + strconv.FormatInt(getresp.Header.Revision, 10) +
		" key=" + string(receiverKV.Key) + "  value=" + string(receiverKV.Value))
	fmt.Println("请输入newValue:")
	_, err = fmt.Scan(&s)
	if err != nil {
		fmt.Println("scan error:", err.Error())
		return false, err
	}
	fmt.Println("开始执行事务:修改key的值为newValue......")
	txn := etcd.Txn(context.TODO()).If(                                     #etcd stm是通过if来判断是否可以执行
                                                                            #一旦if里面设置的条件通过了
                                                                            #那么就说明该事物没有发生冲突可以执行
                                                                            #这是if的两个compare即两个比较器
		v3.Compare(v3.ModRevision(sender), "=", senderKV.ModRevision),      #compare1: key=k1,比较字段=ModRevision,result="="
                                                                            #result表示我们用什么比较方式
                                                                            #即比较看k1这个key的ModRevision这个字段
                                                                            #在事务执行时是否和旧的ModRevision相等
                                                                            #如果不相等则说明在这期间被其他人编辑了,
                                                                            #就说明if失败即事务失败
		v3.Compare(v3.ModRevision(receiver), "=", receiverKV.ModRevision))  #compare2: key=k2,比较字段=ModRevision,result="="
                                                                            #即比较看k2这个key的ModRevision这个字段
                                                                            #在事务执行时是否和旧的ModRevision相等
                                                                            #如果不相等则说明在这期间被其他人编辑了
                                                                            #!!!比较哪些key是任意的,是自己选的 
                                                                            #!!!也支持一次比较多个key  
                                                                            #!!!当然还支持比较key的其他字段比如xx
                                                                            #!!!当然也支持其他result即>、<、=、!=等
                                                                            #!!!我们这里只比较ModRevision且用的是=

    txn = txn.Then(                                                         #path then里面又有两个op,每个op又是一个put操作
		v3.OpPut(sender, s),   
		v3.OpPut(receiver, s),
	).Else(                                                                 #path else里面又有两个op,每个op又是一个put操作
		v3.OpPut(sender, "faild to set"),
		v3.OpPut(receiver, "faild to set"),
	)  
	resp, err := txn.Commit() 
	fmt.Println("事务执行完成")
	if err != nil {
		return false, err
	}
	return resp.Succeeded, nil
}



//txn事务有两种,一种是读事务,一种是写事务,通通都是通过这个函数来处理,读事务比较简单,重点在于后半段的写事务
//!!!!etcd只读事务不走raft流程,直接本地读
//!!!!个人疑惑:只读事务里面读不需要readIndex吗?
key.quotaKVServer.Txn
  key.kvServer.Txn
    if isTxnReadOnly                                  #如果是只读事务,那么不需要走raft请求,直接事务读就行
      if !isTxnSerializable(r):                       #判断是不是线性读,!isTxnSerializable表示线性读
        s.linearizableReadNotify(ctx)                 #如果需要线性读则阻塞等待直到读的条件满足
                                                      #就是记录当前的commitedIndex为cx,然后等待,直到applyIndex>=cx
      s.doSerialize                                   #此时applyIndex>=cx,所以可以执行序列化读
        apply.applierV3backend.Txn                    #事务读也是一个事务操作,只不过这个事务执行的是range读操作
                                                      #所以这里的具体操作就是读取操作
           ...txn函数在下面详解
    else:                       
      etcdserver.EtcdServer.raftRequest               #写事务因为对数据库有修改,所以必须先走一遍raft日志流程
                                                      #也就是先写一条raft日志,然后再apply这条日志
                                                      #是的,所有对数据库有修改的操作都叫apply,
                                                      #只不过apply可以进一步细分为put/txn等很多操作
        ...raft写日志流程略...                          #1:raft写日志流程,略
        etcdserver.apply                              #2:apply日志,这里就是执行写事务
          ......
            apply.applierV3backend.Apply              #所有修改数据库的操作都可以叫做apply
              case r.Txn != nil:                      #apply可以进一步细分,这里是实务操作,即txn
        		op = "Txn"  
		        apply_auth.authApplierV3.Txn          #鉴权准入
                  apply.quotaApplierV3.Txn            #配额准入
                    apply.applierV3backend.Txn        #执行事务
                      isWrite := !isTxnReadonly(rt)   #判断是不是写事务
                      if isWrite && a.s.Cfg.ExperimentalTxnModeWriteWithSharedBuffer:
                        readTx=kvstore_txn.store.Read(mvcc.SharedBufReadTxMode) #共享buf读事务,可以避免复制buffer
                                                                    #因为是共享buf,所以会对buf加读锁
                          s.mu.RLock()                              #kvstore代表数据库,kvstore是一个watchablestore对象
                                                                    #watchablestore对象内部又含有一个store对象
                                                                    #watchablestore和store对象都有一个mu对象
                                                                    #但是watchablestore对象没有Read函数而store对象有
                                                                    #所以go这里kvstore.Read代表的就是store的Read函数
                                                                    #所以store.Read函数里使用的肯定就是store.mu了
                                                                    #操蛋的go语法,刚开始没了解数据结构,迷惑了好久
                                                                    #!!!这里是给store.mu加读锁即给数据库加读锁
                                                                    #!!!加读锁后会阻止数据库写操作
                                                                    #!!!也就是说虽然读写会并发进行
                                                                    #!!!但是读的时候会阻止写
                                                                    #!!!注意:这里是store.mu后面还有把watchable.mu锁
   
                                                                    #!!!简单粗暴,s.mu.RLock会在txn.End中释放
                          s.revMu.RLock()                           #临时给revMu加读锁,
                                                                    #因为我们要读取curretnRev和compactMainRev
                          var tx backend.ReadTx                     #backend.ReadTx是对bolt readtx的一个封装
                          if mode == ConcurrentReadTxMode:          #根据mode参数创建对应的txn
                            tx = s.b.ConcurrentReadTx()             #conncurrentReadTx创建一个新的readTx,并且复制buf
                                                                    #readBuf中保存的是最新commit的数据
                              backend.readTx.RLock()                #给readBuf加读锁,因为事务提交的时候会把writebuf写回到readbuf
                              backend.readTx.txWg.Add(1)            #???不太懂,bbolt回滚和事务机制
 
                                                      
                                                          
                              curBuf := b.readTx.buf.unsafeCopy()   #复制readBuf,readTx.buf就是readBuf
                                                                    #提交事务的时候会把batchTxBuffered的writeBuffer
                                                                    #中的内容写到readTx.buf
                                                                    #查询操作先查readBuf,然后再查bbolt数据库
                              backend.readTx.RUnlock()              #!!!操作完后解锁
                                                                    #!!!也就是说复制完readBuf后就会释放事务读锁了
                                                                    #!!!后续读操作就是读readBuf的副本了
                                                                    #!!!所以不用担心commite的时候会用writebuf覆盖readBuf
                                                                    #!!!虽然concurrentRead创建后就会释放readTx的读锁
                                                                    #!!!但是创建concurrentReadTx的时候需要对readTx加读锁
                                                                    #!!!所以如果有地方已经加了写锁,比如提交事务的时候
                                                                    #!!!那么这里也会等到写事务完成后才能成功加读锁
                          else:
                            tx = backend.backend.ReadTx()           #如果是sharedBuff模式
                              return b.readTx                       #则直接返回backend内部的readTx而不是新创一个readTx
                          tx.RLock()                                #因为如果是sharedbuff模式则是共用一个readTx
                            tx.mu.RLock                             #所以tx.RLock是一个mutex.RLock操作
                                                                    #如果是concurrentReadTx则tx.RLock是一个空操作
                                                                    #!!!etcd自己包装了bbolt.tx,即包装出一个readTx
                                                                    #!!!这个readTx主要同步读事务和写事务的
                                                                    #!!!etcd读流程会先从readbuf读,读不到再去bbolt读
                                                                    #!!!sharedBuff模式下所有请求都使用同一个readTx
                                                                    #!!!即都会访问同一个readTx.buf,所以读写需要加锁
                                                                    #!!!对这个readTx加读写锁来表示是否有读写事务正在运行
                                                                    #!!!如果是读操作那么就对这个readTx加读锁
                                                                    #!!!比如这里compare操作需要加读锁阻止修改
                                                                    #!!!如果是写操作则对这个readTx加写锁
                                                                    #!!!比如commit的时候就要对readTx加写锁阻止后续读
                                                                    #!!!直到写操作完成
                                                                    #总结一下两种模式:
                                                                    #concurrent模式:加读锁-复制-解读锁-读,加写锁-写-解写锁,
                                                                    #因为锁住事件断,这样就可以读、写交替,但是有复制的开销
                                                                    #shared模式:加锁-读-读-读-读-解锁-写
                                                                    #无复制,但是这样就必须全部读事务都读完写事务才能写,
                                                                    #这样就会导致写阻塞的时间可能偏长
              
 
      
                          firstRev, rev := s.compactMainRev, s.currentRev    #读取共享变量 compactMainRev/currentRev
                                                                             #因为会有其他goroute并发修改,所以读之前需要加锁
                                                                             #currentRev就是当前的revision
                          s.revMu.RUnlock()                                  #释放revMu读锁
                          return newMetricsTxnRead(&storeTxnRead{s, tx, firstRev, rev, trace}) #创建事务对象
                        txn = mvcc.NewReadOnlyTxnWrite(readTx)   
                      else:  
                        readTx=kvstore_txn.store.Read(mvcc.ConcurrentReadTxMode)#并发读事务,创建时会复制buffer
                                                                                #所以读取的时候不会加锁
                        txn = mvcc.NewReadOnlyTxnWrite(readTx) #并发读
                                                      #!!!stm事务在执行比较前会创建一个只读视图用来确保视图一致性
                                                      #!!!即compare期间看到的数据是不变的
                                                      #!!!虽然apply是串行apply的即写是串行的但是读与写却不是串行的而是并发的
                                                      #!!!即可能同时进行读写操作,因为compare操作需要读数据库
                                                      #!!!所有compare操作时必须通过一个只读视图来compare
                                                      #!!!(个人猜测):换句话说bbolt只能读或者写不能同时读写因为会加读写锁
                              
                        return &txnReadWrite{txn} 
                      txnPath := make([]bool, 1)
                      txnPath = apply.compareToPath(txn, rt)             #!!!执行比较,stm事务的核心就是执行比较
                                                                         #!!!rt就代表一个事务if-then-else
                                                                         #!!!这里会递归遍历所有事务及子事务
                                                                         #!!!所有事务的比较结果都会顺序存放到txnPath数组里面
                                                                         #!!!也就是说compareToPath这个函数
                                                                         #!!!就是用来确定所有事务该走哪条path true还是false
                                                                         #!!!也就是说不管有多少个嵌套事务,最终的路径一定是确定的
                                                                         #!!!所以txnPath[i]是事务i,
                                                                         #!!!那么txnPath[i+1]必定是执行路径上的下一个事务的compare结果
                                                                         #!!!也就是说不管有多少个嵌套事务,最终的路径一定是确定的
                        txnPath[0] = apply.applyCompares(rv, rt.Compare) #rt.Compare即if块,一个if可以包含很多个比较器
                          for _, c := range rt.Compare:                  #c代表compare中的一个比较器,这里遍历所有比较器
                                                                         #我们的例子中含有两个比较器
                                                                         #compare1: key=k1,比较字段=ModRevision,result="="
                                                                         #compare2: key=k2,比较字段=ModRevision,result="="
                            ok2=apply.applyCompare(rv, c):               #这里以compare1为例
                              metrics_txn.metricsTxnWrite.Range          #读取比较器中的key 
                              for _, kv := range rr.KVs:                 #遍历compare1中的所有的key,我们这里只有一个key,即k1
		                        ok3=apply.compareKV(c, kv):              #对该key应用比较器,这里是对key k1应用比较器compare1
                                  switch c.Target:                       #target表示要比较哪个字段,它支持很多字段
                                                                         #比如value/createVersion/ModVersion......
                                                                         #我们这里是用的ModVersion这个字段
                                  case pb.Compare_VALUE:
                            		tv=c.TargetUnion.(*pb.Compare_Value)
                                    v=tv.Value
                            		result = bytes.Compare(ckv.Value, v)
                            	  case pb.Compare_CREATE:
                            		tv=c.TargetUnion.(*pb.Compare_CreateRevision)
                            		rev = tv.CreateRevision
                            		result = compareInt64(ckv.CreateRevision, rev)
                            	  case pb.Compare_MOD:                           #我们用的是ModVersion这个字段
                            		tv= c.TargetUnion.(*pb.Compare_ModRevision)  #TargetUnion是一个联合体,
                            		rev = tv.ModRevision                         #表示此时请求中的字段表示的含义是ModRevision
                            		result = compareInt64(ckv.ModRevision, rev)  #执行比较,然后得到一个结果-1(小于),0(等于),1(大于)
                            	  case pb.Compare_VERSION:                       #version是每个key独有的,
                            		tv= c.TargetUnion.(*pb.Compare_Version)
                            		rev = tv.Version
                            		result = compareInt64(ckv.Version, rev)
                            	  case pb.Compare_LEASE:
                            		tv= c.TargetUnion.(*pb.Compare_Lease)
                            		rev = tv.Lease
                            		result = compareInt64(ckv.Lease, rev)

                            	  switch c.Result:                                 #Result表示我们选择的比较方式是什么
                                                                                  #支持很多,比如=、!=、>、<等
                            	  case pb.Compare_EQUAL:                          #我们这里compare1的k1选择的是=比较
                            		return result == 0                            #所以我们返回result是否等于0
                                                                                  #true表示if成功,那么稍后执行的就是then路径
                                                                                  #false表示if失败,那么稍后执行的就行else路径
                            	  case pb.Compare_NOT_EQUAL:
                            		return result != 0
                            	  case pb.Compare_GREATER:
                            		return result > 0
                            	  case pb.Compare_LESS:
                            		return result < 0
		                        if !ok3:                                            #只要有任意一个key比较失败,就返回false
                                  return false                                      #就返回false
                              return true
  
                            if !ok2                                                #只要有任意一个比较器返回false即比较失败
                              return false                                         #那么就立即返回false
                          return true
                        ops := rt.Success                            
                        if !txnPath[0]:                                            #如果比较失败就执行false路径,否则执行success路径
		                  ops = rt.Failure                                         #success对应then,failure对应else
                        for _, op := range ops {                                   #选择好执行路径后就遍历该路径下的所有op
                          tv, ok := op.Request.(*pb.RequestOp_RequestTxn)
                          if !ok || tv.RequestTxn == nil:                          #compareToPath只是为了确定事务及其子事务该走哪条path
                            continue                                               #所以如果该op里不包含事务,就跳过
                          txnPath = append(txnPath, compareToPath(rv, tv.RequestTxn)...) #递归调用compareToPath来确定所有事务的路径


                      if isWrite:
                        apply.checkRequests(txn, rt, txnPath, a.checkPut)        #检查写请求正确性
                      apply.checkRequests(txn, rt, txnPath, a.checkRange)        #检查range请求正确性



                      if isWrite:  
                        metrics_txn.metricsTxnWrite.End                   #结束我们为了compare而创建的只读事务
                         kvstore_txn.storeTxnRead.End
                           read_tx.readTx.RUnlock                         #释放backend对应的事务的读锁
                                                                          #一个backend对应一个readTx
                           store.mu.RUnlock                               #释放store.mu的读锁
                        txn = watchable_store_txn.watchableStore.Write    #compare完毕,可以写了,所以这里创建写事务
                          txn=kvstore_txn.store.Write  
                            s.mu.RLock()                                  #store代表kvstore,写事务会先加读锁
                                                                          #等所有读事务完成后释放读锁并加写锁                  
                            tx := s.b.BatchTx()                           #获取底层的batchtx
                                                                          #底层数据结构:store含有一个backend
                                                                          #backend内部有一个batchTxBuffered
                                                                          #batchTxBuffered内嵌一个batchTx
                                                                          #所以调用batchTxBuffered.LockInsideApply
                                                                          #实际就是调用内嵌的batchTx的LockInsideApply
                                                                          #!!!batchTx对应批量提交,内部含有一个bbolt.tx对象
                                                                          #!!!所有事务都是直接写这个bblot.tx
                                                                          #!!!写完以后这个bbolt.tx不会提交
                                                                          #!!!对于上层来说事务只要写到bbolt.tx就会返回
                                                                          #!!!上层会等到bbolt.tx提交以后才会返回结果给client
                                                                          #!!!也就是异步+batch
                                                                          #!!!这个bbolt.tx可以写很多次都不提交
                                                                          #!!!对于上层来说,这个bbolt.tx里包含了一批事务
                                                                          #!!!但是对于底层bbolt来说自始至终都是一个事务
                                                                          #!!!即上层看到的批量事务在底层实际就是一个事务
                                                                          #!!!所以只需要提交一个事务就行,即batchTx.bbolt.tx
                            tx.LockInsideApply()                          #batchtx加写锁,
                            tw := &storeTxnWrite{
                              storeTxnRead: storeTxnRead{s, tx, 0, 0, trace},
                              tx:           tx,
                              beginRev:     s.currentRev,                        #!!!beginRev记录的是当前最后一次操作后的revision
                                                                                 #!!!执行事务里面的操作比如put的时候
                                                                                 #!!!用的revision是beginRev+1
                              changes:      make([]mvccpb.KeyValue, 0, 4),
                            return newMetricsTxnWrite(tw)
                          return &watchableStoreTxnWrite{s.store.Write(trace), s}



                      apply.applierV3backend.applyTxn                          #执行事务
                        reqs := rt.Success                                     #这里判断是走success路径
                        if !txnPath[0]:
                          reqs = rt.Failure                                    #还是走failure路径
                                                                               #!!!stm中不存在事务冲突
                                                                               #!!!stm中只有path  
                                                                               #!!!如果没有发生冲突即比较成功
                                                                               #!!!那么就走success路径
                                                                               #!!!如果发生冲突,那就走failure路径
                                                                               #!!!即不管冲突还是没有冲突,都有path可走
                                                                               #!!!当然,如果走success路径,
                                                                               #!!!但我们没有设置对应的语句,
                                                                               #!!!那么就相当于一个空操作,什么也不做
                        for i, req := range reqs:                              #遍历该path中的所有op
                          switch tv := req.Request.(type):                     #该op有很多类型
                            case *pb.RequestOp_RequestRange:                   #如果是range读取操作,那么就执行range读取操作
                              apply.applierV3backend.Range(txn, tv.RequestRange)
                            case *pb.RequestOp_RequestPut:                     #如果是put操作,那么就执行put操作,这里以put为例
                              apply.applierV3backend.Put(txn, tv.RequestPut)   #!!!那么就直接执行普通的put流程
                                                                               #!!!即使是stm事务,本质上也算一个操作
                                                                               #!!!也就是说每个stm事务都对应一个版本号
                                                                               #!!!也就是说客户端一次stm事务操作和客户端一个put操作
                                                                               #!!!本质上没有任何区别,都是一个修改数据库的操作
                                                                               #!!!都是先写一条日志,然后apply执行日志完成修改
                                                                               #!!!注意,这里的一次stm事务是指一次完整的事务操作
                                                                               #!!!stm事务以及他嵌套的所有子事务用的是同一个版本号
                                                                               #!!!换句话说,不管是子事务还是put等操作
                                                                               #!!!同一个事务中的所有操作都用同一个版本号
                                                                   
                            case *pb.RequestOp_RequestDeleteRange:             #如果是deleteRange操作,那么就执行deleteRange操作
                              apply.applierV3backend.DeleteRange(txn, tv.RequestDeleteRange)  
                            case *pb.RequestOp_RequestTxn:                     #如果是子事务,就递归执行applyTxn
                                                                               #!!!子事务也是使用txn这个事务对象
                                                                               #!!!也就是说子事务的版本号和父事务是同一个
                              apply.applierV3backend.applyTxn(txn, tv.RequestTxn, txnPath[1:], resp)
                		   	  txns += applyTxns + 1
                			  txnPath = txnPath[applyTxns+1:]                   #txnpath保存了执行路径上所有事物的path
                                                                                #所以txnPath[applyTxns+1]就表示下一个事务的path索引
                            default:                                            #如果是非预期的操作类型,则啥也不干


			                  no-op

                      rev := txn.Rev()                                #获取txn开始时最新的revision
                      if len(txn.Changes()) != 0:                     #每次修改操作都会改变revision,读操作不会
                        rev++
                      watchable_store_txn.watchableStoreTxnWrite.End  #结束事务,主要是释放锁和提交事务
                    	tw.s.mu.Lock()                                #!!!我TM真找不到在哪释放的读锁,难道不是同一个store?
                                                                      #!!!破案了:tw.s代表watchable,tw.s.mu是另一把锁
                                                                      #!!!tw.s.store才表示kvstore,tw.s.store.mu才是数据库锁
                                                                      #!!!kvstore是一个watchablestore对象,就表示会有watch
                                                                      #!!!也就是说这边在end的时候那边可能会有一个watch在不断处理
                                                                      #!!!(不知道compaction会不会也需要访问这把锁)
                                                                      #!!!所以就需要临时锁一下kvstore,注意,这是watchablestore.mu
                                                                      #!!!etcdserver是一条一条apply日志的
                                                                      #!!!也就是说apply是串行执行的
                                                                      #!!!执行一次写事务就执行一次提交操作
                                                                      #!!!但是并不是每次提交都会真的提交 
                                                                      #!!!而是每次提交都会把事务放到batchTx buf
                                                                      #!!!当达到一定条数后才会执行commit操作,
                                                                      #!!!由BatchLimit参数指定
                    	tw.s.notify(rev, evs)                         #通知watch相关
                    	metrics_txn.metricsTxnWrite.End               #更新一些metrics后继续调用内部事务的end
                          kvstore_txn.storeTxnWrite.End
                            tw.s.revMu.Lock()  
                            tw.s.currentRev++                         #更新server.currentRev,
                                                                      #因为一次writeTxn代表一次修改操作                        
                            batch_tx.batchTxBuffered.Unlock()         #解锁batchtx
                                                                      #注意:batchTxBufered加锁是直接调用内部batchTx.lock
                                                                      #但是解锁的时候不能只调用batchTx.Unlock
                                                                      #因为batchTxBufered还需要做其他事,所以得用自己的Unlock函数
                                                                      #batchTxBufered.unlock做三件事:1:把数据从writebuf写入readBuf
                                                                      #2:如果未提交事务数达到batchLimit就提交事务
                                                                      #3:做完上面两条后才能释放batchTx.lock
                                                                      #所以加锁用batchTx.lock即可
                                                                      #但是解锁则batchTxBufered需实现自己的Unlock函数
                              if t.pending != 0:
                                t.backend.readTx.Lock()               #batchTxn的所有读请求都会对这个readTx加读锁,
                                                                      #会等到所有读请求完成并释放锁后才对buf加写锁  
                                txWriteBuffer.writeback               #把writebuf中的内容写到readbuff
                                t.backend.readTx.Unlock()             #释放buf锁
                                if t.pending >= t.backend.batchLimit ||t.pendingDeleteOperations > 0   
                                                                      #batchTxn表示批量事务,如果pending的事务数超过了阈值
                                                                      #那么就提交,默认是1w或者挂起的delete操作数大于0也会提交
                                {
                                  t.commit(false)                            #提交事务
                                    t.backend.readTx.Lock()                  #要提交事务必须先阻止所有读
                                    batch_tx.batchTxBuffered.unsafeCommit    #提交事务
                                      backend.hooks.OnPreCommitUnsafe(t)     #执行hook
                                      if t.backend.readTx.tx != nil:         #tx是bbolt.tx  
                                                                             #如果bbolt.readTx不为null
                                                                             #则需要等待这些读事务完成才能commit  
                                                               
                                        go func(tx *bolt.Tx, wg *sync.WaitGroup):
                                          wg.Wait()
                                          bbolt.tx.Rollback()
                                          t.backend.readTx.reset()
                                      batch_tx.batchTx.commit
                                		bbolt.tx.Commit()                    #bbolt的事务提交sdk
                                        t.pending = 0
	                                t.pendingDeleteOperations = 0
                                    t.backend.readTx.Unlock()                #提交完毕,释放锁
                                }
                                t.batchTx.Unlock()                           #解锁batchtx


                            tw.s.revMu.Unlock()
                            tw.s.mu.RUnlock()                         #???释放读锁。我TM真找不到在哪加的读锁
                                                                      #下班 20241202 18:35
                                                                      #我靠,根本不是同一把锁,watchableStore有一把mu锁
                                                                      #watchablestore.store也有一把叫做mu的锁
                                                                      #这里是释放store上的读锁,
                                                                      #我们在创建txnWrite的时候会加读锁,在End里释放读锁
                                                                      #!!!store.mu是一把读写锁,因为采用的是mvcc。
                                                                      #!!!任何put/txn/delete等操作对于store来说
                                                                      #!!!都是一个读操作即只会追加数据不会修改旧数据
                                                                      #!!!所以此时加读锁就行了
                                                                      #!!!但是compact会压缩数据也就是修改旧数据
                                                                      #!!!所以此时就必须对store.mu加读锁
                                                                      #!!!捋一下:store.mu是保护compact和mvcc操作
                                                                      #!!!而watchablestore.mu则是保护watch与notify 操作
                                                                      #!!!所以txnwrite的时候只需要对store加读锁就行了
                                                                      #!!!不影响watch线程
                                                        

                    	  tw.s.mu.Unlock()                            #这里是watchablestore.mu.unlock
                                                                      #此后watch goroute就可以继续访问kvstore了
                      txnResp.Header.Revision = rev                   #返回给客户端的revision

相关文章链接:

STM客户端api相关文章

https://juejin.cn/post/7134326187064557575

https://juejin.cn/post/7134326187064557575

相关推荐
alden_ygq10 小时前
etcd网关
服务器·数据库·etcd
张声录110 小时前
【ETCD】ETCD Leader 节点写入数据流程概览
数据库·etcd
云川之下21 小时前
【k8s】访问etcd
kubernetes·etcd
张声录121 小时前
【ETCD】【实操篇(四)】etcd常见问题快问快答FAQ
数据库·etcd
怒码ing1 天前
etcd+京东hotkey探测使用
数据库·etcd
Source、1 天前
ETCD备份还原
数据库·etcd
张声录11 天前
【ETCD】【实操篇(二)】如何从源码编译并在window上搭建etcd集群?
数据库·etcd
张声录11 天前
【ETCD】ETCD 的一致性读(Linearizable Read)流程解析
服务器·数据库·etcd
alden_ygq4 天前
etcd常用监控
数据库·etcd
张声录14 天前
【ETCD】【Linearizable Read OR Serializable Read】ETCD 数据读取:强一致性 vs 高性能,选择最适合的读取模式
java·数据库·etcd