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

[复制链接]
发表于 2025-12-12 14:17:06 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
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原理:
  1. stm if xx then yy else zz
  2. 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代码使用样例(抄自网络):
  1. func getEtcdClient() *v3.Client {
  2.         // 配置 etcd 客户端
  3.         cfg := v3.Config{
  4.                 Endpoints:   []string{"30.128.101.96:2379"}, // 集群中的任意一个节点
  5.                 DialTimeout: 5 * time.Second,
  6.         }
  7.         // 创建 etcd 客户端
  8.         cli, err := v3.New(cfg)
  9.         if err != nil {
  10.                 log.Fatal(err)
  11.         }
  12.         return cli
  13. }
  14. func stm() {
  15.         client := getEtcdClient()
  16.         txnTransfer(client, "k1", "k2")
  17. }
  18. func txnTransfer(etcd *v3.Client, sender, receiver string) error {
  19.         // 失败重试
  20.         for {
  21.                 if ok, err := doTxn(etcd, sender, receiver); err != nil {
  22.                         fmt.Println("dotxn failed:", err.Error())
  23.                 } else if ok {
  24.                         fmt.Println("dotxn success")
  25.                 }
  26.         }
  27. }
  28. func doTxn(etcd *v3.Client, sender, receiver string) (bool, error) {
  29.         fmt.Println("\n开始事务前的读取操作:")
  30.         getresp, err := etcd.Txn(context.TODO()).Then(v3.OpGet(sender), v3.OpGet(receiver)).Commit()
  31.         if err != nil {
  32.                 return false, err
  33.         }
  34.         senderKV := getresp.Responses[0].GetResponseRange().Kvs[0]
  35.         receiverKV := getresp.Responses[0].GetResponseRange().Kvs[0]
  36.         // 发起转账事务,冲突判断 ModRevision 是否发生变化
  37.         var s string
  38.         fmt.Println("senderKv: revision=" + strconv.FormatInt(getresp.Header.Revision, 10) +
  39.                 " key=" + string(senderKV.Key) + "  value=" + string(senderKV.Value))
  40.         fmt.Println("senderKv: revision=" + strconv.FormatInt(getresp.Header.Revision, 10) +
  41.                 " key=" + string(receiverKV.Key) + "  value=" + string(receiverKV.Value))
  42.         fmt.Println("请输入newValue:")
  43.         _, err = fmt.Scan(&s)
  44.         if err != nil {
  45.                 fmt.Println("scan error:", err.Error())
  46.                 return false, err
  47.         }
  48.         fmt.Println("开始执行事务:修改key的值为newValue......")
  49.         txn := etcd.Txn(context.TODO()).If(                                     #etcd stm是通过if来判断是否可以执行
  50.                                                                             #一旦if里面设置的条件通过了
  51.                                                                             #那么就说明该事物没有发生冲突可以执行
  52.                                                                             #这是if的两个compare即两个比较器
  53.                 v3.Compare(v3.ModRevision(sender), "=", senderKV.ModRevision),      #compare1: key=k1,比较字段=ModRevision,result="="
  54.                                                                             #result表示我们用什么比较方式
  55.                                                                             #即比较看k1这个key的ModRevision这个字段
  56.                                                                             #在事务执行时是否和旧的ModRevision相等
  57.                                                                             #如果不相等则说明在这期间被其他人编辑了,
  58.                                                                             #就说明if失败即事务失败
  59.                 v3.Compare(v3.ModRevision(receiver), "=", receiverKV.ModRevision))  #compare2: key=k2,比较字段=ModRevision,result="="
  60.                                                                             #即比较看k2这个key的ModRevision这个字段
  61.                                                                             #在事务执行时是否和旧的ModRevision相等
  62.                                                                             #如果不相等则说明在这期间被其他人编辑了
  63.                                                                             #!!!比较哪些key是任意的,是自己选的
  64.                                                                             #!!!也支持一次比较多个key  
  65.                                                                             #!!!当然还支持比较key的其他字段比如xx
  66.                                                                             #!!!当然也支持其他result即>、<、=、!=等
  67.                                                                             #!!!我们这里只比较ModRevision且用的是=
  68.     txn = txn.Then(                                                         #path then里面又有两个op,每个op又是一个put操作
  69.                 v3.OpPut(sender, s),   
  70.                 v3.OpPut(receiver, s),
  71.         ).Else(                                                                 #path else里面又有两个op,每个op又是一个put操作
  72.                 v3.OpPut(sender, "faild to set"),
  73.                 v3.OpPut(receiver, "faild to set"),
  74.         )  
  75.         resp, err := txn.Commit()
  76.         fmt.Println("事务执行完成")
  77.         if err != nil {
  78.                 return false, err
  79.         }
  80.         return resp.Succeeded, nil
  81. }
复制代码
  1. //txn事务有两种,一种是读事务,一种是写事务,通通都是通过这个函数来处理,读事务比较简单,重点在于后半段的写事务
  2. //!!!!etcd只读事务不走raft流程,直接本地读
  3. //!!!!个人疑惑:只读事务里面读不需要readIndex吗?
  4. key.quotaKVServer.Txn
  5.   key.kvServer.Txn
  6.     if isTxnReadOnly                                  #如果是只读事务,那么不需要走raft请求,直接事务读就行
  7.       if !isTxnSerializable(r):                       #判断是不是线性读,!isTxnSerializable表示线性读
  8.         s.linearizableReadNotify(ctx)                 #如果需要线性读则阻塞等待直到读的条件满足
  9.                                                       #就是记录当前的commitedIndex为cx,然后等待,直到applyIndex>=cx
  10.       s.doSerialize                                   #此时applyIndex>=cx,所以可以执行序列化读
  11.         apply.applierV3backend.Txn                    #事务读也是一个事务操作,只不过这个事务执行的是range读操作
  12.                                                       #所以这里的具体操作就是读取操作
  13.            ...txn函数在下面详解
  14.     else:                       
  15.       etcdserver.EtcdServer.raftRequest               #写事务因为对数据库有修改,所以必须先走一遍raft日志流程
  16.                                                       #也就是先写一条raft日志,然后再apply这条日志
  17.                                                       #是的,所有对数据库有修改的操作都叫apply,
  18.                                                       #只不过apply可以进一步细分为put/txn等很多操作
  19.         ...raft写日志流程略...                          #1:raft写日志流程,略
  20.         etcdserver.apply                              #2:apply日志,这里就是执行写事务
  21.           ......
  22.             apply.applierV3backend.Apply              #所有修改数据库的操作都可以叫做apply
  23.               case r.Txn != nil:                      #apply可以进一步细分,这里是实务操作,即txn
  24.                         op = "Txn"  
  25.                         apply_auth.authApplierV3.Txn          #鉴权准入
  26.                   apply.quotaApplierV3.Txn            #配额准入
  27.                     apply.applierV3backend.Txn        #执行事务
  28.                       isWrite := !isTxnReadonly(rt)   #判断是不是写事务
  29.                       if isWrite && a.s.Cfg.ExperimentalTxnModeWriteWithSharedBuffer:
  30.                         readTx=kvstore_txn.store.Read(mvcc.SharedBufReadTxMode) #共享buf读事务,可以避免复制buffer
  31.                                                                     #因为是共享buf,所以会对buf加读锁
  32.                           s.mu.RLock()                              #kvstore代表数据库,kvstore是一个watchablestore对象
  33.                                                                     #watchablestore对象内部又含有一个store对象
  34.                                                                     #watchablestore和store对象都有一个mu对象
  35.                                                                     #但是watchablestore对象没有Read函数而store对象有
  36.                                                                     #所以go这里kvstore.Read代表的就是store的Read函数
  37.                                                                     #所以store.Read函数里使用的肯定就是store.mu了
  38.                                                                     #操蛋的go语法,刚开始没了解数据结构,迷惑了好久
  39.                                                                     #!!!这里是给store.mu加读锁即给数据库加读锁
  40.                                                                     #!!!加读锁后会阻止数据库写操作
  41.                                                                     #!!!也就是说虽然读写会并发进行
  42.                                                                     #!!!但是读的时候会阻止写
  43.                                                                     #!!!注意:这里是store.mu后面还有把watchable.mu锁
  44.    
  45.                                                                     #!!!简单粗暴,s.mu.RLock会在txn.End中释放
  46.                           s.revMu.RLock()                           #临时给revMu加读锁,
  47.                                                                     #因为我们要读取curretnRev和compactMainRev
  48.                           var tx backend.ReadTx                     #backend.ReadTx是对bolt readtx的一个封装
  49.                           if mode == ConcurrentReadTxMode:          #根据mode参数创建对应的txn
  50.                             tx = s.b.ConcurrentReadTx()             #conncurrentReadTx创建一个新的readTx,并且复制buf
  51.                                                                     #readBuf中保存的是最新commit的数据
  52.                               backend.readTx.RLock()                #给readBuf加读锁,因为事务提交的时候会把writebuf写回到readbuf
  53.                               backend.readTx.txWg.Add(1)            #???不太懂,bbolt回滚和事务机制
  54.                                                       
  55.                                                          
  56.                               curBuf := b.readTx.buf.unsafeCopy()   #复制readBuf,readTx.buf就是readBuf
  57.                                                                     #提交事务的时候会把batchTxBuffered的writeBuffer
  58.                                                                     #中的内容写到readTx.buf
  59.                                                                     #查询操作先查readBuf,然后再查bbolt数据库
  60.                               backend.readTx.RUnlock()              #!!!操作完后解锁
  61.                                                                     #!!!也就是说复制完readBuf后就会释放事务读锁了
  62.                                                                     #!!!后续读操作就是读readBuf的副本了
  63.                                                                     #!!!所以不用担心commite的时候会用writebuf覆盖readBuf
  64.                                                                     #!!!虽然concurrentRead创建后就会释放readTx的读锁
  65.                                                                     #!!!但是创建concurrentReadTx的时候需要对readTx加读锁
  66.                                                                     #!!!所以如果有地方已经加了写锁,比如提交事务的时候
  67.                                                                     #!!!那么这里也会等到写事务完成后才能成功加读锁
  68.                           else:
  69.                             tx = backend.backend.ReadTx()           #如果是sharedBuff模式
  70.                               return b.readTx                       #则直接返回backend内部的readTx而不是新创一个readTx
  71.                           tx.RLock()                                #因为如果是sharedbuff模式则是共用一个readTx
  72.                             tx.mu.RLock                             #所以tx.RLock是一个mutex.RLock操作
  73.                                                                     #如果是concurrentReadTx则tx.RLock是一个空操作
  74.                                                                     #!!!etcd自己包装了bbolt.tx,即包装出一个readTx
  75.                                                                     #!!!这个readTx主要同步读事务和写事务的
  76.                                                                     #!!!etcd读流程会先从readbuf读,读不到再去bbolt读
  77.                                                                     #!!!sharedBuff模式下所有请求都使用同一个readTx
  78.                                                                     #!!!即都会访问同一个readTx.buf,所以读写需要加锁
  79.                                                                     #!!!对这个readTx加读写锁来表示是否有读写事务正在运行
  80.                                                                     #!!!如果是读操作那么就对这个readTx加读锁
  81.                                                                     #!!!比如这里compare操作需要加读锁阻止修改
  82.                                                                     #!!!如果是写操作则对这个readTx加写锁
  83.                                                                     #!!!比如commit的时候就要对readTx加写锁阻止后续读
  84.                                                                     #!!!直到写操作完成
  85.                                                                     #总结一下两种模式:
  86.                                                                     #concurrent模式:加读锁-复制-解读锁-读,加写锁-写-解写锁,
  87.                                                                     #因为锁住事件断,这样就可以读、写交替,但是有复制的开销
  88.                                                                     #shared模式:加锁-读-读-读-读-解锁-写
  89.                                                                     #无复制,但是这样就必须全部读事务都读完写事务才能写,
  90.                                                                     #这样就会导致写阻塞的时间可能偏长
  91.               
  92.       
  93.                           firstRev, rev := s.compactMainRev, s.currentRev    #读取共享变量 compactMainRev/currentRev
  94.                                                                              #因为会有其他goroute并发修改,所以读之前需要加锁
  95.                                                                              #currentRev就是当前的revision
  96.                           s.revMu.RUnlock()                                  #释放revMu读锁
  97.                           return newMetricsTxnRead(&storeTxnRead{s, tx, firstRev, rev, trace}) #创建事务对象
  98.                         txn = mvcc.NewReadOnlyTxnWrite(readTx)   
  99.                       else:  
  100.                         readTx=kvstore_txn.store.Read(mvcc.ConcurrentReadTxMode)#并发读事务,创建时会复制buffer
  101.                                                                                 #所以读取的时候不会加锁
  102.                         txn = mvcc.NewReadOnlyTxnWrite(readTx) #并发读
  103.                                                       #!!!stm事务在执行比较前会创建一个只读视图用来确保视图一致性
  104.                                                       #!!!即compare期间看到的数据是不变的
  105.                                                       #!!!虽然apply是串行apply的即写是串行的但是读与写却不是串行的而是并发的
  106.                                                       #!!!即可能同时进行读写操作,因为compare操作需要读数据库
  107.                                                       #!!!所有compare操作时必须通过一个只读视图来compare
  108.                                                       #!!!(个人猜测):换句话说bbolt只能读或者写不能同时读写因为会加读写锁
  109.                               
  110.                         return &txnReadWrite{txn}
  111.                       txnPath := make([]bool, 1)
  112.                       txnPath = apply.compareToPath(txn, rt)             #!!!执行比较,stm事务的核心就是执行比较
  113.                                                                          #!!!rt就代表一个事务if-then-else
  114.                                                                          #!!!这里会递归遍历所有事务及子事务
  115.                                                                          #!!!所有事务的比较结果都会顺序存放到txnPath数组里面
  116.                                                                          #!!!也就是说compareToPath这个函数
  117.                                                                          #!!!就是用来确定所有事务该走哪条path true还是false
  118.                                                                          #!!!也就是说不管有多少个嵌套事务,最终的路径一定是确定的
  119.                                                                          #!!!所以txnPath[i]是事务i,
  120.                                                                          #!!!那么txnPath[i+1]必定是执行路径上的下一个事务的compare结果
  121.                                                                          #!!!也就是说不管有多少个嵌套事务,最终的路径一定是确定的
  122.                         txnPath[0] = apply.applyCompares(rv, rt.Compare) #rt.Compare即if块,一个if可以包含很多个比较器
  123.                           for _, c := range rt.Compare:                  #c代表compare中的一个比较器,这里遍历所有比较器
  124.                                                                          #我们的例子中含有两个比较器
  125.                                                                          #compare1: key=k1,比较字段=ModRevision,result="="
  126.                                                                          #compare2: key=k2,比较字段=ModRevision,result="="
  127.                             ok2=apply.applyCompare(rv, c):               #这里以compare1为例
  128.                               metrics_txn.metricsTxnWrite.Range          #读取比较器中的key
  129.                               for _, kv := range rr.KVs:                 #遍历compare1中的所有的key,我们这里只有一个key,即k1
  130.                                         ok3=apply.compareKV(c, kv):              #对该key应用比较器,这里是对key k1应用比较器compare1
  131.                                   switch c.Target:                       #target表示要比较哪个字段,它支持很多字段
  132.                                                                          #比如value/createVersion/ModVersion......
  133.                                                                          #我们这里是用的ModVersion这个字段
  134.                                   case pb.Compare_VALUE:
  135.                                             tv=c.TargetUnion.(*pb.Compare_Value)
  136.                                     v=tv.Value
  137.                                             result = bytes.Compare(ckv.Value, v)
  138.                                       case pb.Compare_CREATE:
  139.                                             tv=c.TargetUnion.(*pb.Compare_CreateRevision)
  140.                                             rev = tv.CreateRevision
  141.                                             result = compareInt64(ckv.CreateRevision, rev)
  142.                                       case pb.Compare_MOD:                           #我们用的是ModVersion这个字段
  143.                                             tv= c.TargetUnion.(*pb.Compare_ModRevision)  #TargetUnion是一个联合体,
  144.                                             rev = tv.ModRevision                         #表示此时请求中的字段表示的含义是ModRevision
  145.                                             result = compareInt64(ckv.ModRevision, rev)  #执行比较,然后得到一个结果-1(小于),0(等于),1(大于)
  146.                                       case pb.Compare_VERSION:                       #version是每个key独有的,
  147.                                             tv= c.TargetUnion.(*pb.Compare_Version)
  148.                                             rev = tv.Version
  149.                                             result = compareInt64(ckv.Version, rev)
  150.                                       case pb.Compare_LEASE:
  151.                                             tv= c.TargetUnion.(*pb.Compare_Lease)
  152.                                             rev = tv.Lease
  153.                                             result = compareInt64(ckv.Lease, rev)
  154.                                       switch c.Result:                                 #Result表示我们选择的比较方式是什么
  155.                                                                                   #支持很多,比如=、!=、>、<等
  156.                                       case pb.Compare_EQUAL:                          #我们这里compare1的k1选择的是=比较
  157.                                             return result == 0                            #所以我们返回result是否等于0
  158.                                                                                   #true表示if成功,那么稍后执行的就是then路径
  159.                                                                                   #false表示if失败,那么稍后执行的就行else路径
  160.                                       case pb.Compare_NOT_EQUAL:
  161.                                             return result != 0
  162.                                       case pb.Compare_GREATER:
  163.                                             return result > 0
  164.                                       case pb.Compare_LESS:
  165.                                             return result < 0
  166.                                         if !ok3:                                            #只要有任意一个key比较失败,就返回false
  167.                                   return false                                      #就返回false
  168.                               return true
  169.   
  170.                             if !ok2                                                #只要有任意一个比较器返回false即比较失败
  171.                               return false                                         #那么就立即返回false
  172.                           return true
  173.                         ops := rt.Success                           
  174.                         if !txnPath[0]:                                            #如果比较失败就执行false路径,否则执行success路径
  175.                                   ops = rt.Failure                                         #success对应then,failure对应else
  176.                         for _, op := range ops {                                   #选择好执行路径后就遍历该路径下的所有op
  177.                           tv, ok := op.Request.(*pb.RequestOp_RequestTxn)
  178.                           if !ok || tv.RequestTxn == nil:                          #compareToPath只是为了确定事务及其子事务该走哪条path
  179.                             continue                                               #所以如果该op里不包含事务,就跳过
  180.                           txnPath = append(txnPath, compareToPath(rv, tv.RequestTxn)...) #递归调用compareToPath来确定所有事务的路径
  181.                       if isWrite:
  182.                         apply.checkRequests(txn, rt, txnPath, a.checkPut)        #检查写请求正确性
  183.                       apply.checkRequests(txn, rt, txnPath, a.checkRange)        #检查range请求正确性
  184.                       if isWrite:  
  185.                         metrics_txn.metricsTxnWrite.End                   #结束我们为了compare而创建的只读事务
  186.                          kvstore_txn.storeTxnRead.End
  187.                            read_tx.readTx.RUnlock                         #释放backend对应的事务的读锁
  188.                                                                           #一个backend对应一个readTx
  189.                            store.mu.RUnlock                               #释放store.mu的读锁
  190.                         txn = watchable_store_txn.watchableStore.Write    #compare完毕,可以写了,所以这里创建写事务
  191.                           txn=kvstore_txn.store.Write  
  192.                             s.mu.RLock()                                  #store代表kvstore,写事务会先加读锁
  193.                                                                           #等所有读事务完成后释放读锁并加写锁                  
  194.                             tx := s.b.BatchTx()                           #获取底层的batchtx
  195.                                                                           #底层数据结构:store含有一个backend
  196.                                                                           #backend内部有一个batchTxBuffered
  197.                                                                           #batchTxBuffered内嵌一个batchTx
  198.                                                                           #所以调用batchTxBuffered.LockInsideApply
  199.                                                                           #实际就是调用内嵌的batchTx的LockInsideApply
  200.                                                                           #!!!batchTx对应批量提交,内部含有一个bbolt.tx对象
  201.                                                                           #!!!所有事务都是直接写这个bblot.tx
  202.                                                                           #!!!写完以后这个bbolt.tx不会提交
  203.                                                                           #!!!对于上层来说事务只要写到bbolt.tx就会返回
  204.                                                                           #!!!上层会等到bbolt.tx提交以后才会返回结果给client
  205.                                                                           #!!!也就是异步+batch
  206.                                                                           #!!!这个bbolt.tx可以写很多次都不提交
  207.                                                                           #!!!对于上层来说,这个bbolt.tx里包含了一批事务
  208.                                                                           #!!!但是对于底层bbolt来说自始至终都是一个事务
  209.                                                                           #!!!即上层看到的批量事务在底层实际就是一个事务
  210.                                                                           #!!!所以只需要提交一个事务就行,即batchTx.bbolt.tx
  211.                             tx.LockInsideApply()                          #batchtx加写锁,
  212.                             tw := &storeTxnWrite{
  213.                               storeTxnRead: storeTxnRead{s, tx, 0, 0, trace},
  214.                               tx:           tx,
  215.                               beginRev:     s.currentRev,                        #!!!beginRev记录的是当前最后一次操作后的revision
  216.                                                                                  #!!!执行事务里面的操作比如put的时候
  217.                                                                                  #!!!用的revision是beginRev+1
  218.                               changes:      make([]mvccpb.KeyValue, 0, 4),
  219.                             return newMetricsTxnWrite(tw)
  220.                           return &watchableStoreTxnWrite{s.store.Write(trace), s}
  221.                       apply.applierV3backend.applyTxn                          #执行事务
  222.                         reqs := rt.Success                                     #这里判断是走success路径
  223.                         if !txnPath[0]:
  224.                           reqs = rt.Failure                                    #还是走failure路径
  225.                                                                                #!!!stm中不存在事务冲突
  226.                                                                                #!!!stm中只有path  
  227.                                                                                #!!!如果没有发生冲突即比较成功
  228.                                                                                #!!!那么就走success路径
  229.                                                                                #!!!如果发生冲突,那就走failure路径
  230.                                                                                #!!!即不管冲突还是没有冲突,都有path可走
  231.                                                                                #!!!当然,如果走success路径,
  232.                                                                                #!!!但我们没有设置对应的语句,
  233.                                                                                #!!!那么就相当于一个空操作,什么也不做
  234.                         for i, req := range reqs:                              #遍历该path中的所有op
  235.                           switch tv := req.Request.(type):                     #该op有很多类型
  236.                             case *pb.RequestOp_RequestRange:                   #如果是range读取操作,那么就执行range读取操作
  237.                               apply.applierV3backend.Range(txn, tv.RequestRange)
  238.                             case *pb.RequestOp_RequestPut:                     #如果是put操作,那么就执行put操作,这里以put为例
  239.                               apply.applierV3backend.Put(txn, tv.RequestPut)   #!!!那么就直接执行普通的put流程
  240.                                                                                #!!!即使是stm事务,本质上也算一个操作
  241.                                                                                #!!!也就是说每个stm事务都对应一个版本号
  242.                                                                                #!!!也就是说客户端一次stm事务操作和客户端一个put操作
  243.                                                                                #!!!本质上没有任何区别,都是一个修改数据库的操作
  244.                                                                                #!!!都是先写一条日志,然后apply执行日志完成修改
  245.                                                                                #!!!注意,这里的一次stm事务是指一次完整的事务操作
  246.                                                                                #!!!stm事务以及他嵌套的所有子事务用的是同一个版本号
  247.                                                                                #!!!换句话说,不管是子事务还是put等操作
  248.                                                                                #!!!同一个事务中的所有操作都用同一个版本号
  249.                                                                   
  250.                             case *pb.RequestOp_RequestDeleteRange:             #如果是deleteRange操作,那么就执行deleteRange操作
  251.                               apply.applierV3backend.DeleteRange(txn, tv.RequestDeleteRange)  
  252.                             case *pb.RequestOp_RequestTxn:                     #如果是子事务,就递归执行applyTxn
  253.                                                                                #!!!子事务也是使用txn这个事务对象
  254.                                                                                #!!!也就是说子事务的版本号和父事务是同一个
  255.                               apply.applierV3backend.applyTxn(txn, tv.RequestTxn, txnPath[1:], resp)
  256.                                              txns += applyTxns + 1
  257.                                           txnPath = txnPath[applyTxns+1:]                   #txnpath保存了执行路径上所有事物的path
  258.                                                                                 #所以txnPath[applyTxns+1]就表示下一个事务的path索引
  259.                             default:                                            #如果是非预期的操作类型,则啥也不干
  260.                                           no-op
  261.                       rev := txn.Rev()                                #获取txn开始时最新的revision
  262.                       if len(txn.Changes()) != 0:                     #每次修改操作都会改变revision,读操作不会
  263.                         rev++
  264.                       watchable_store_txn.watchableStoreTxnWrite.End  #结束事务,主要是释放锁和提交事务
  265.                             tw.s.mu.Lock()                                #!!!我TM真找不到在哪释放的读锁,难道不是同一个store?
  266.                                                                       #!!!破案了:tw.s代表watchable,tw.s.mu是另一把锁
  267.                                                                       #!!!tw.s.store才表示kvstore,tw.s.store.mu才是数据库锁
  268.                                                                       #!!!kvstore是一个watchablestore对象,就表示会有watch
  269.                                                                       #!!!也就是说这边在end的时候那边可能会有一个watch在不断处理
  270.                                                                       #!!!(不知道compaction会不会也需要访问这把锁)
  271.                                                                       #!!!所以就需要临时锁一下kvstore,注意,这是watchablestore.mu
  272.                                                                       #!!!etcdserver是一条一条apply日志的
  273.                                                                       #!!!也就是说apply是串行执行的
  274.                                                                       #!!!执行一次写事务就执行一次提交操作
  275.                                                                       #!!!但是并不是每次提交都会真的提交
  276.                                                                       #!!!而是每次提交都会把事务放到batchTx buf
  277.                                                                       #!!!当达到一定条数后才会执行commit操作,
  278.                                                                       #!!!由BatchLimit参数指定
  279.                             tw.s.notify(rev, evs)                         #通知watch相关
  280.                             metrics_txn.metricsTxnWrite.End               #更新一些metrics后继续调用内部事务的end
  281.                           kvstore_txn.storeTxnWrite.End
  282.                             tw.s.revMu.Lock()  
  283.                             tw.s.currentRev++                         #更新server.currentRev,
  284.                                                                       #因为一次writeTxn代表一次修改操作                        
  285.                             batch_tx.batchTxBuffered.Unlock()         #解锁batchtx
  286.                                                                       #注意:batchTxBufered加锁是直接调用内部batchTx.lock
  287.                                                                       #但是解锁的时候不能只调用batchTx.Unlock
  288.                                                                       #因为batchTxBufered还需要做其他事,所以得用自己的Unlock函数
  289.                                                                       #batchTxBufered.unlock做三件事:1:把数据从writebuf写入readBuf
  290.                                                                       #2:如果未提交事务数达到batchLimit就提交事务
  291.                                                                       #3:做完上面两条后才能释放batchTx.lock
  292.                                                                       #所以加锁用batchTx.lock即可
  293.                                                                       #但是解锁则batchTxBufered需实现自己的Unlock函数
  294.                               if t.pending != 0:
  295.                                 t.backend.readTx.Lock()               #batchTxn的所有读请求都会对这个readTx加读锁,
  296.                                                                       #会等到所有读请求完成并释放锁后才对buf加写锁  
  297.                                 txWriteBuffer.writeback               #把writebuf中的内容写到readbuff
  298.                                 t.backend.readTx.Unlock()             #释放buf锁
  299.                                 if t.pending >= t.backend.batchLimit ||t.pendingDeleteOperations > 0   
  300.                                                                       #batchTxn表示批量事务,如果pending的事务数超过了阈值
  301.                                                                       #那么就提交,默认是1w或者挂起的delete操作数大于0也会提交
  302.                                 {
  303.                                   t.commit(false)                            #提交事务
  304.                                     t.backend.readTx.Lock()                  #要提交事务必须先阻止所有读
  305.                                     batch_tx.batchTxBuffered.unsafeCommit    #提交事务
  306.                                       backend.hooks.OnPreCommitUnsafe(t)     #执行hook
  307.                                       if t.backend.readTx.tx != nil:         #tx是bbolt.tx  
  308.                                                                              #如果bbolt.readTx不为null
  309.                                                                              #则需要等待这些读事务完成才能commit  
  310.                                                                
  311.                                         go func(tx *bolt.Tx, wg *sync.WaitGroup):
  312.                                           wg.Wait()
  313.                                           bbolt.tx.Rollback()
  314.                                           t.backend.readTx.reset()
  315.                                       batch_tx.batchTx.commit
  316.                                                 bbolt.tx.Commit()                    #bbolt的事务提交sdk
  317.                                         t.pending = 0
  318.                                         t.pendingDeleteOperations = 0
  319.                                     t.backend.readTx.Unlock()                #提交完毕,释放锁
  320.                                 }
  321.                                 t.batchTx.Unlock()                           #解锁batchtx
  322.                             tw.s.revMu.Unlock()
  323.                             tw.s.mu.RUnlock()                         #???释放读锁。我TM真找不到在哪加的读锁
  324.                                                                       #下班 20241202 18:35
  325.                                                                       #我靠,根本不是同一把锁,watchableStore有一把mu锁
  326.                                                                       #watchablestore.store也有一把叫做mu的锁
  327.                                                                       #这里是释放store上的读锁,
  328.                                                                       #我们在创建txnWrite的时候会加读锁,在End里释放读锁
  329.                                                                       #!!!store.mu是一把读写锁,因为采用的是mvcc。
  330.                                                                       #!!!任何put/txn/delete等操作对于store来说
  331.                                                                       #!!!都是一个读操作即只会追加数据不会修改旧数据
  332.                                                                       #!!!所以此时加读锁就行了
  333.                                                                       #!!!但是compact会压缩数据也就是修改旧数据
  334.                                                                       #!!!所以此时就必须对store.mu加读锁
  335.                                                                       #!!!捋一下:store.mu是保护compact和mvcc操作
  336.                                                                       #!!!而watchablestore.mu则是保护watch与notify 操作
  337.                                                                       #!!!所以txnwrite的时候只需要对store加读锁就行了
  338.                                                                       #!!!不影响watch线程
  339.                                                         
  340.                               tw.s.mu.Unlock()                            #这里是watchablestore.mu.unlock
  341.                                                                       #此后watch goroute就可以继续访问kvstore了
  342.                       txnResp.Header.Revision = rev                   #返回给客户端的revision
复制代码
干系文章链接:
STM客户端api干系文章


回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表