github.com/hdt3213/godis@v1.2.9/cluster/tcc.go (about) 1 package cluster 2 3 import ( 4 "fmt" 5 "github.com/hdt3213/godis/database" 6 "github.com/hdt3213/godis/interface/redis" 7 "github.com/hdt3213/godis/lib/logger" 8 "github.com/hdt3213/godis/lib/timewheel" 9 "github.com/hdt3213/godis/redis/protocol" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 ) 15 16 // prepareFunc executed after related key locked, and use additional logic to determine whether the transaction can be committed 17 // For example, prepareMSetNX will return error to prevent MSetNx transaction from committing if any related key already exists 18 var prepareFuncMap = make(map[string]CmdFunc) 19 20 func registerPrepareFunc(cmdName string, fn CmdFunc) { 21 prepareFuncMap[strings.ToLower(cmdName)] = fn 22 } 23 24 // Transaction stores state and data for a try-commit-catch distributed transaction 25 type Transaction struct { 26 id string // transaction id 27 cmdLine [][]byte // cmd cmdLine 28 cluster *Cluster 29 conn redis.Connection 30 dbIndex int 31 32 writeKeys []string 33 readKeys []string 34 keysLocked bool 35 undoLog []CmdLine 36 37 status int8 38 mu *sync.Mutex 39 } 40 41 const ( 42 maxLockTime = 3 * time.Second 43 waitBeforeCleanTx = 2 * maxLockTime 44 45 createdStatus = 0 46 preparedStatus = 1 47 committedStatus = 2 48 rolledBackStatus = 3 49 ) 50 51 func genTaskKey(txID string) string { 52 return "tx:" + txID 53 } 54 55 // NewTransaction creates a try-commit-catch distributed transaction 56 func NewTransaction(cluster *Cluster, c redis.Connection, id string, cmdLine [][]byte) *Transaction { 57 return &Transaction{ 58 id: id, 59 cmdLine: cmdLine, 60 cluster: cluster, 61 conn: c, 62 dbIndex: c.GetDBIndex(), 63 status: createdStatus, 64 mu: new(sync.Mutex), 65 } 66 } 67 68 // Reentrant 69 // invoker should hold tx.mu 70 func (tx *Transaction) lockKeys() { 71 if !tx.keysLocked { 72 tx.cluster.db.RWLocks(tx.dbIndex, tx.writeKeys, tx.readKeys) 73 tx.keysLocked = true 74 } 75 } 76 77 func (tx *Transaction) unLockKeys() { 78 if tx.keysLocked { 79 tx.cluster.db.RWUnLocks(tx.dbIndex, tx.writeKeys, tx.readKeys) 80 tx.keysLocked = false 81 } 82 } 83 84 // t should contain Keys and ID field 85 func (tx *Transaction) prepare() error { 86 tx.mu.Lock() 87 defer tx.mu.Unlock() 88 89 tx.writeKeys, tx.readKeys = database.GetRelatedKeys(tx.cmdLine) 90 // lock writeKeys 91 tx.lockKeys() 92 93 // build undoLog 94 tx.undoLog = tx.cluster.db.GetUndoLogs(tx.dbIndex, tx.cmdLine) 95 tx.status = preparedStatus 96 taskKey := genTaskKey(tx.id) 97 timewheel.Delay(maxLockTime, taskKey, func() { 98 if tx.status == preparedStatus { // rollback transaction uncommitted until expire 99 logger.Info("abort transaction: " + tx.id) 100 tx.mu.Lock() 101 defer tx.mu.Unlock() 102 _ = tx.rollbackWithLock() 103 } 104 }) 105 return nil 106 } 107 108 func (tx *Transaction) rollbackWithLock() error { 109 curStatus := tx.status 110 111 if tx.status != curStatus { // ensure status not changed by other goroutine 112 return fmt.Errorf("tx %s status changed", tx.id) 113 } 114 if tx.status == rolledBackStatus { // no need to rollback a rolled-back transaction 115 return nil 116 } 117 tx.lockKeys() 118 for _, cmdLine := range tx.undoLog { 119 tx.cluster.db.ExecWithLock(tx.conn, cmdLine) 120 } 121 tx.unLockKeys() 122 tx.status = rolledBackStatus 123 return nil 124 } 125 126 // cmdLine: Prepare id cmdName args... 127 func execPrepare(cluster *Cluster, c redis.Connection, cmdLine CmdLine) redis.Reply { 128 if len(cmdLine) < 3 { 129 return protocol.MakeErrReply("ERR wrong number of arguments for 'prepare' command") 130 } 131 txID := string(cmdLine[1]) 132 cmdName := strings.ToLower(string(cmdLine[2])) 133 tx := NewTransaction(cluster, c, txID, cmdLine[2:]) 134 cluster.transactions.Put(txID, tx) 135 err := tx.prepare() 136 if err != nil { 137 return protocol.MakeErrReply(err.Error()) 138 } 139 prepareFunc, ok := prepareFuncMap[cmdName] 140 if ok { 141 return prepareFunc(cluster, c, cmdLine[2:]) 142 } 143 return &protocol.OkReply{} 144 } 145 146 // execRollback rollbacks local transaction 147 func execRollback(cluster *Cluster, c redis.Connection, cmdLine CmdLine) redis.Reply { 148 if len(cmdLine) != 2 { 149 return protocol.MakeErrReply("ERR wrong number of arguments for 'rollback' command") 150 } 151 txID := string(cmdLine[1]) 152 raw, ok := cluster.transactions.Get(txID) 153 if !ok { 154 return protocol.MakeIntReply(0) 155 } 156 tx, _ := raw.(*Transaction) 157 158 tx.mu.Lock() 159 defer tx.mu.Unlock() 160 err := tx.rollbackWithLock() 161 if err != nil { 162 return protocol.MakeErrReply(err.Error()) 163 } 164 // clean transaction 165 timewheel.Delay(waitBeforeCleanTx, "", func() { 166 cluster.transactions.Remove(tx.id) 167 }) 168 return protocol.MakeIntReply(1) 169 } 170 171 // execCommit commits local transaction as a worker when receive execCommit command from coordinator 172 func execCommit(cluster *Cluster, c redis.Connection, cmdLine CmdLine) redis.Reply { 173 if len(cmdLine) != 2 { 174 return protocol.MakeErrReply("ERR wrong number of arguments for 'commit' command") 175 } 176 txID := string(cmdLine[1]) 177 raw, ok := cluster.transactions.Get(txID) 178 if !ok { 179 return protocol.MakeIntReply(0) 180 } 181 tx, _ := raw.(*Transaction) 182 183 tx.mu.Lock() 184 defer tx.mu.Unlock() 185 186 result := cluster.db.ExecWithLock(c, tx.cmdLine) 187 188 if protocol.IsErrorReply(result) { 189 // failed 190 err2 := tx.rollbackWithLock() 191 return protocol.MakeErrReply(fmt.Sprintf("err occurs when rollback: %v, origin err: %s", err2, result)) 192 } 193 // after committed 194 tx.unLockKeys() 195 tx.status = committedStatus 196 // clean finished transaction 197 // do not clean immediately, in case rollback 198 timewheel.Delay(waitBeforeCleanTx, "", func() { 199 cluster.transactions.Remove(tx.id) 200 }) 201 return result 202 } 203 204 // requestCommit commands all node to commit transaction as coordinator 205 func requestCommit(cluster *Cluster, c redis.Connection, txID int64, groupMap map[string][]string) ([]redis.Reply, protocol.ErrorReply) { 206 var errReply protocol.ErrorReply 207 txIDStr := strconv.FormatInt(txID, 10) 208 respList := make([]redis.Reply, 0, len(groupMap)) 209 for node := range groupMap { 210 var resp redis.Reply 211 if node == cluster.self { 212 resp = execCommit(cluster, c, makeArgs("commit", txIDStr)) 213 } else { 214 resp = cluster.relay(node, c, makeArgs("commit", txIDStr)) 215 } 216 if protocol.IsErrorReply(resp) { 217 errReply = resp.(protocol.ErrorReply) 218 break 219 } 220 respList = append(respList, resp) 221 } 222 if errReply != nil { 223 requestRollback(cluster, c, txID, groupMap) 224 return nil, errReply 225 } 226 return respList, nil 227 } 228 229 // requestRollback requests all node rollback transaction as coordinator 230 // groupMap: node -> keys 231 func requestRollback(cluster *Cluster, c redis.Connection, txID int64, groupMap map[string][]string) { 232 txIDStr := strconv.FormatInt(txID, 10) 233 for node := range groupMap { 234 if node == cluster.self { 235 execRollback(cluster, c, makeArgs("rollback", txIDStr)) 236 } else { 237 cluster.relay(node, c, makeArgs("rollback", txIDStr)) 238 } 239 } 240 } 241 242 func (cluster *Cluster) relayPrepare(node string, c redis.Connection, cmdLine CmdLine) redis.Reply { 243 if node == cluster.self { 244 return execPrepare(cluster, c, cmdLine) 245 } else { 246 return cluster.relay(node, c, cmdLine) 247 } 248 }