github.com/hdt3213/godis@v1.2.9/database/transaction.go (about)

     1  package database
     2  
     3  import (
     4  	"github.com/hdt3213/godis/interface/redis"
     5  	"github.com/hdt3213/godis/redis/protocol"
     6  	"strings"
     7  )
     8  
     9  // Watch set watching keys
    10  func Watch(db *DB, conn redis.Connection, args [][]byte) redis.Reply {
    11  	watching := conn.GetWatching()
    12  	for _, bkey := range args {
    13  		key := string(bkey)
    14  		watching[key] = db.GetVersion(key)
    15  	}
    16  	return protocol.MakeOkReply()
    17  }
    18  
    19  func execGetVersion(db *DB, args [][]byte) redis.Reply {
    20  	key := string(args[0])
    21  	ver := db.GetVersion(key)
    22  	return protocol.MakeIntReply(int64(ver))
    23  }
    24  
    25  func init() {
    26  	registerCommand("GetVer", execGetVersion, readAllKeys, nil, 2, flagReadOnly)
    27  }
    28  
    29  // invoker should lock watching keys
    30  func isWatchingChanged(db *DB, watching map[string]uint32) bool {
    31  	for key, ver := range watching {
    32  		currentVersion := db.GetVersion(key)
    33  		if ver != currentVersion {
    34  			return true
    35  		}
    36  	}
    37  	return false
    38  }
    39  
    40  // StartMulti starts multi-command-transaction
    41  func StartMulti(conn redis.Connection) redis.Reply {
    42  	if conn.InMultiState() {
    43  		return protocol.MakeErrReply("ERR MULTI calls can not be nested")
    44  	}
    45  	conn.SetMultiState(true)
    46  	return protocol.MakeOkReply()
    47  }
    48  
    49  // EnqueueCmd puts command line into `multi` pending queue
    50  func EnqueueCmd(conn redis.Connection, cmdLine [][]byte) redis.Reply {
    51  	cmdName := strings.ToLower(string(cmdLine[0]))
    52  	cmd, ok := cmdTable[cmdName]
    53  	if !ok {
    54  		err := protocol.MakeErrReply("ERR unknown command '" + cmdName + "'")
    55  		conn.AddTxError(err)
    56  		return err
    57  	}
    58  	if cmd.prepare == nil {
    59  		err := protocol.MakeErrReply("ERR command '" + cmdName + "' cannot be used in MULTI")
    60  		conn.AddTxError(err)
    61  		return err
    62  	}
    63  	if !validateArity(cmd.arity, cmdLine) {
    64  		err := protocol.MakeArgNumErrReply(cmdName)
    65  		conn.AddTxError(err)
    66  		return err
    67  	}
    68  	conn.EnqueueCmd(cmdLine)
    69  	return protocol.MakeQueuedReply()
    70  }
    71  
    72  func execMulti(db *DB, conn redis.Connection) redis.Reply {
    73  	if !conn.InMultiState() {
    74  		return protocol.MakeErrReply("ERR EXEC without MULTI")
    75  	}
    76  	defer conn.SetMultiState(false)
    77  	if len(conn.GetTxErrors()) > 0 {
    78  		return protocol.MakeErrReply("EXECABORT Transaction discarded because of previous errors.")
    79  	}
    80  	cmdLines := conn.GetQueuedCmdLine()
    81  	return db.ExecMulti(conn, conn.GetWatching(), cmdLines)
    82  }
    83  
    84  // ExecMulti executes multi commands transaction Atomically and Isolated
    85  func (db *DB) ExecMulti(conn redis.Connection, watching map[string]uint32, cmdLines []CmdLine) redis.Reply {
    86  	// prepare
    87  	writeKeys := make([]string, 0) // may contains duplicate
    88  	readKeys := make([]string, 0)
    89  	for _, cmdLine := range cmdLines {
    90  		cmdName := strings.ToLower(string(cmdLine[0]))
    91  		cmd := cmdTable[cmdName]
    92  		prepare := cmd.prepare
    93  		write, read := prepare(cmdLine[1:])
    94  		writeKeys = append(writeKeys, write...)
    95  		readKeys = append(readKeys, read...)
    96  	}
    97  	// set watch
    98  	watchingKeys := make([]string, 0, len(watching))
    99  	for key := range watching {
   100  		watchingKeys = append(watchingKeys, key)
   101  	}
   102  	readKeys = append(readKeys, watchingKeys...)
   103  	db.RWLocks(writeKeys, readKeys)
   104  	defer db.RWUnLocks(writeKeys, readKeys)
   105  
   106  	if isWatchingChanged(db, watching) { // watching keys changed, abort
   107  		return protocol.MakeEmptyMultiBulkReply()
   108  	}
   109  	// execute
   110  	results := make([]redis.Reply, 0, len(cmdLines))
   111  	aborted := false
   112  	undoCmdLines := make([][]CmdLine, 0, len(cmdLines))
   113  	for _, cmdLine := range cmdLines {
   114  		undoCmdLines = append(undoCmdLines, db.GetUndoLogs(cmdLine))
   115  		result := db.execWithLock(cmdLine)
   116  		if protocol.IsErrorReply(result) {
   117  			aborted = true
   118  			// don't rollback failed commands
   119  			undoCmdLines = undoCmdLines[:len(undoCmdLines)-1]
   120  			break
   121  		}
   122  		results = append(results, result)
   123  	}
   124  	if !aborted { //success
   125  		db.addVersion(writeKeys...)
   126  		return protocol.MakeMultiRawReply(results)
   127  	}
   128  	// undo if aborted
   129  	size := len(undoCmdLines)
   130  	for i := size - 1; i >= 0; i-- {
   131  		curCmdLines := undoCmdLines[i]
   132  		if len(curCmdLines) == 0 {
   133  			continue
   134  		}
   135  		for _, cmdLine := range curCmdLines {
   136  			db.execWithLock(cmdLine)
   137  		}
   138  	}
   139  	return protocol.MakeErrReply("EXECABORT Transaction discarded because of previous errors.")
   140  }
   141  
   142  // DiscardMulti drops MULTI pending commands
   143  func DiscardMulti(conn redis.Connection) redis.Reply {
   144  	if !conn.InMultiState() {
   145  		return protocol.MakeErrReply("ERR DISCARD without MULTI")
   146  	}
   147  	conn.ClearQueuedCmds()
   148  	conn.SetMultiState(false)
   149  	return protocol.MakeOkReply()
   150  }
   151  
   152  // GetUndoLogs return rollback commands
   153  func (db *DB) GetUndoLogs(cmdLine [][]byte) []CmdLine {
   154  	cmdName := strings.ToLower(string(cmdLine[0]))
   155  	cmd, ok := cmdTable[cmdName]
   156  	if !ok {
   157  		return nil
   158  	}
   159  	undo := cmd.undo
   160  	if undo == nil {
   161  		return nil
   162  	}
   163  	return undo(db, cmdLine[1:])
   164  }
   165  
   166  // GetRelatedKeys analysis related keys
   167  func GetRelatedKeys(cmdLine [][]byte) ([]string, []string) {
   168  	cmdName := strings.ToLower(string(cmdLine[0]))
   169  	cmd, ok := cmdTable[cmdName]
   170  	if !ok {
   171  		return nil, nil
   172  	}
   173  	prepare := cmd.prepare
   174  	if prepare == nil {
   175  		return nil, nil
   176  	}
   177  	return prepare(cmdLine[1:])
   178  }