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  }