事务概念

一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他命令插入,不许加塞。

事务的作用

一个队列中,一次性、顺序性、排他性的执行一系列命令。

事务常用命令

multi

标记一个事务块的开始

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379(TX)> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379(TX)> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379(TX)> get k1
  8. QUEUED
  9. 127.0.0.1:6379(TX)> exec
  10. 1) OK
  11. 2) OK
  12. 3) "v1"

discard

取消事务,放弃执行事务块内的所有命令

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379(TX)> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379(TX)> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379(TX)> discard
  8. OK
  9. 127.0.0.1:6379> get k1
  10. (nil)

exec

执行所有事务块内的命令

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379(TX)> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379(TX)> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379(TX)> get k1
  8. QUEUED
  9. 127.0.0.1:6379(TX)> exec
  10. 1) OK
  11. 2) OK
  12. 3) "v1"

事务的原子性

Redis单条命令是保证原子性的,但是Redis事务并不能保证原子性;所有的命令在事务中并不会立即执行,只会在执行事务的时候才会执行,所以Redis事务没有事务隔离级别的概念

编译时异常

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379(TX)> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379(TX)> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379(TX)> set k3 v3
  8. QUEUED
  9. # 随便写的,此时会报错误不存在这个命令;但是并没有说事务停止了
  10. 127.0.0.1:6379(TX)> helloworld
  11. (error) ERR unknown command `helloworld`, with args beginning with:
  12. 127.0.0.1:6379(TX)> set k4 v4\
  13. QUEUED
  14. 127.0.0.1:6379(TX)> exec
  15. (error) EXECABORT Transaction discarded because of previous errors.
  16. 127.0.0.1:6379> get k1
  17. (nil)

结果可知:事务中所有命令都不会被执行

运行时异常

  1. 127.0.0.1:6379> multi
  2. OK
  3. 127.0.0.1:6379(TX)> set k1 v1
  4. QUEUED
  5. 127.0.0.1:6379(TX)> set k2 v2
  6. QUEUED
  7. 127.0.0.1:6379(TX)> set k3 v3
  8. QUEUED
  9. # k1值是字符串类型所以无法自增,但是并没有提示错误
  10. 127.0.0.1:6379(TX)> incr k1
  11. QUEUED
  12. 127.0.0.1:6379(TX)> exec
  13. 1) OK
  14. 2) OK
  15. 3) OK
  16. 4) (error) ERR value is not an integer or out of range
  17. 127.0.0.1:6379> get k1
  18. "v1"
  19. 127.0.0.1:6379> get k2
  20. "v2"
  21. 127.0.0.1:6379> get k3
  22. "v3"

结果可知:出错的命令不会被执行,正常的命令还是会被执行

基于编译时异常和运行时异常的区别,可以更好的理解为什么说Redis单条命令是保证原子性的,但是Redis事务是不保证原子性的

Watch监控

  • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量
  • 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

单线程操作

  1. # 余额
  2. 127.0.0.1:6379> set money 100
  3. OK
  4. # 花出去的钱
  5. 127.0.0.1:6379> set out 0
  6. OK
  7. # 监视money
  8. 127.0.0.1:6379> watch money
  9. OK
  10. # 启动事务
  11. 127.0.0.1:6379> multi
  12. OK
  13. # 余额-20
  14. 127.0.0.1:6379(TX)> DECRby money 20
  15. QUEUED
  16. # out+20
  17. 127.0.0.1:6379(TX)> INCRBY out 20
  18. QUEUED
  19. # 执行事务
  20. 127.0.0.1:6379(TX)> EXEC
  21. 1) (integer) 80
  22. 2) (integer) 20

结果正常

多线程操作

客户端1

  1. # 余额
  2. 127.0.0.1:6379> set money 100
  3. OK
  4. # 花出去的钱
  5. 127.0.0.1:6379> set out 0
  6. OK
  7. # 监视money
  8. 127.0.0.1:6379> watch money
  9. OK
  10. # 启动事务
  11. 127.0.0.1:6379> multi
  12. OK
  13. # 余额-20
  14. 127.0.0.1:6379(TX)> DECRby money 20
  15. QUEUED
  16. # out+20
  17. 127.0.0.1:6379(TX)> INCRBY out 20
  18. QUEUED

注意:这个时候并没有执行事务

客户端2

  1. 127.0.0.1:6379> WATCH money
  2. OK
  3. 127.0.0.1:6379> incrby money 100
  4. (integer) 200

回到客户端1

  1. 127.0.0.1:6379(TX)> exec
  2. (nil)

结果可知:使用watch可以实现乐观锁的功能

UnWatch

接着上面的watch讲解,先解锁再获取最新的值进行操作

  1. # 解锁
  2. 127.0.0.1:6379> unwatch
  3. OK
  4. 127.0.0.1:6379> multi
  5. OK
  6. 127.0.0.1:6379(TX)> decrby money 20
  7. QUEUED
  8. 127.0.0.1:6379(TX)> INCRBY out 20
  9. QUEUED
  10. 127.0.0.1:6379(TX)> exec
  11. 1) (integer) 180
  12. 2) (integer) 20