You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

152 lines
4.1 KiB

  1. package redis
  2. import (
  3. "context"
  4. "github.com/redis/go-redis/v9/internal/pool"
  5. "github.com/redis/go-redis/v9/internal/proto"
  6. )
  7. // TxFailedErr transaction redis failed.
  8. const TxFailedErr = proto.RedisError("redis: transaction failed")
  9. // Tx implements Redis transactions as described in
  10. // http://redis.io/topics/transactions. It's NOT safe for concurrent use
  11. // by multiple goroutines, because Exec resets list of watched keys.
  12. //
  13. // If you don't need WATCH, use Pipeline instead.
  14. type Tx struct {
  15. baseClient
  16. cmdable
  17. statefulCmdable
  18. }
  19. func (c *Client) newTx() *Tx {
  20. tx := Tx{
  21. baseClient: baseClient{
  22. opt: c.opt.clone(), // Clone options to avoid sharing mutable state between transaction and parent client
  23. connPool: pool.NewStickyConnPool(c.connPool),
  24. hooksMixin: c.hooksMixin.clone(),
  25. pushProcessor: c.pushProcessor, // Copy push processor from parent client
  26. },
  27. }
  28. tx.init()
  29. return &tx
  30. }
  31. func (c *Tx) init() {
  32. c.cmdable = c.Process
  33. c.statefulCmdable = c.Process
  34. c.initHooks(hooks{
  35. dial: c.baseClient.dial,
  36. process: c.baseClient.process,
  37. pipeline: c.baseClient.processPipeline,
  38. txPipeline: c.baseClient.processTxPipeline,
  39. })
  40. }
  41. func (c *Tx) Process(ctx context.Context, cmd Cmder) error {
  42. err := c.processHook(ctx, cmd)
  43. cmd.SetErr(err)
  44. return err
  45. }
  46. // Watch prepares a transaction and marks the keys to be watched
  47. // for conditional execution if there are any keys.
  48. //
  49. // The transaction is automatically closed when fn exits.
  50. func (c *Client) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
  51. tx := c.newTx()
  52. defer tx.Close(ctx)
  53. if len(keys) > 0 {
  54. if err := tx.Watch(ctx, keys...).Err(); err != nil {
  55. return err
  56. }
  57. }
  58. return fn(tx)
  59. }
  60. // Close closes the transaction, releasing any open resources.
  61. func (c *Tx) Close(ctx context.Context) error {
  62. _ = c.Unwatch(ctx).Err()
  63. return c.baseClient.Close()
  64. }
  65. // Watch marks the keys to be watched for conditional execution
  66. // of a transaction.
  67. func (c *Tx) Watch(ctx context.Context, keys ...string) *StatusCmd {
  68. args := make([]interface{}, 1+len(keys))
  69. args[0] = "watch"
  70. for i, key := range keys {
  71. args[1+i] = key
  72. }
  73. cmd := NewStatusCmd(ctx, args...)
  74. _ = c.Process(ctx, cmd)
  75. return cmd
  76. }
  77. // Unwatch flushes all the previously watched keys for a transaction.
  78. func (c *Tx) Unwatch(ctx context.Context, keys ...string) *StatusCmd {
  79. args := make([]interface{}, 1+len(keys))
  80. args[0] = "unwatch"
  81. for i, key := range keys {
  82. args[1+i] = key
  83. }
  84. cmd := NewStatusCmd(ctx, args...)
  85. _ = c.Process(ctx, cmd)
  86. return cmd
  87. }
  88. // Pipeline creates a pipeline. Usually it is more convenient to use Pipelined.
  89. func (c *Tx) Pipeline() Pipeliner {
  90. pipe := Pipeline{
  91. exec: func(ctx context.Context, cmds []Cmder) error {
  92. return c.processPipelineHook(ctx, cmds)
  93. },
  94. }
  95. pipe.init()
  96. return &pipe
  97. }
  98. // Pipelined executes commands queued in the fn outside of the transaction.
  99. // Use TxPipelined if you need transactional behavior.
  100. func (c *Tx) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
  101. return c.Pipeline().Pipelined(ctx, fn)
  102. }
  103. // TxPipelined executes commands queued in the fn in the transaction.
  104. //
  105. // When using WATCH, EXEC will execute commands only if the watched keys
  106. // were not modified, allowing for a check-and-set mechanism.
  107. //
  108. // Exec always returns list of commands. If transaction fails
  109. // TxFailedErr is returned. Otherwise Exec returns an error of the first
  110. // failed command or nil.
  111. func (c *Tx) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
  112. return c.TxPipeline().Pipelined(ctx, fn)
  113. }
  114. // TxPipeline creates a pipeline. Usually it is more convenient to use TxPipelined.
  115. func (c *Tx) TxPipeline() Pipeliner {
  116. pipe := Pipeline{
  117. exec: func(ctx context.Context, cmds []Cmder) error {
  118. cmds = wrapMultiExec(ctx, cmds)
  119. return c.processTxPipelineHook(ctx, cmds)
  120. },
  121. }
  122. pipe.init()
  123. return &pipe
  124. }
  125. func wrapMultiExec(ctx context.Context, cmds []Cmder) []Cmder {
  126. if len(cmds) == 0 {
  127. panic("not reached")
  128. }
  129. cmdsCopy := make([]Cmder, len(cmds)+2)
  130. cmdsCopy[0] = NewStatusCmd(ctx, "multi")
  131. copy(cmdsCopy[1:], cmds)
  132. cmdsCopy[len(cmdsCopy)-1] = NewSliceCmd(ctx, "exec")
  133. return cmdsCopy
  134. }