Redis事务和锁
> 本文档整理自教程: 1. Redis官方文档:[Transactions](https://redis.io/topics/transactions) 2. 尚硅谷视频:[尚硅谷_Redis6](http://www.atguigu.com/download_detail.shtml?v=323) # 简介 `MULTI`、`EXEC`、`DISCARD`和`WATCH`是Redis事务的基础。它们允许在单个步骤中执行一组命令,并具有如下两个重要保证: - 事务中的所有命令都被序列化并按顺序执行。在Redis事务的执行过程中,另一个客户端发出的请求永远不会被服务。这保证了命令作为一个单独的操作执行。 - 要么所有的命令都被处理,要么没有,所以Redis事务也是原子性的。 # 事务 ## 事务的执行 - `MULTI`:开始事务 - `EXEC`:执行事务 - `DISCARD`:取消事务 Redis事务使用`MULTI`命令开始。之后,用户可以发出多个命令。Redis不会执行这些命令,而是将它们排成队列。所有命令都在调用`EXEC`后执行。而调用`DISCARD`将刷新事务队列并退出事务。  > 案例:正常组队执行 ```bash 127.0.0.1:6379> SET a 100 OK 127.0.0.1:6379> SET b 100 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> INCRBY a 10 QUEUED 127.0.0.1:6379(TX)> DECRBY b 10 QUEUED 127.0.0.1:6379(TX)> EXEC 1) (integer) 110 2) (integer) 90 ``` ## 事务的错误 在事务过程中,可能会遇到两种命令错误: - `EXEC`之前:组队中某个命令出现了错误,执行时整个队列都会被取消。例如:命令语法错误。 - `EXEC`之后:如果执行阶段某个命令出现了错误,则只有报错的命令不会被执行,而其他的命令都会执行,**不会回滚**。 > 案例1:在`EXEC`之前错误 ``` 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> SET a a QUEUED 127.0.0.1:6379(TX)> SET b (error) ERR wrong number of arguments for 'set' command 127.0.0.1:6379(TX)> SET c c QUEUED 127.0.0.1:6379(TX)> EXEC (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> GET a (nil) 127.0.0.1:6379> GET c (nil) ``` > 案例2:在`EXEC`之后错误 ```bash 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> SET a a QUEUED 127.0.0.1:6379(TX)> LPOP a QUEUED 127.0.0.1:6379(TX)> SET b b QUEUED 127.0.0.1:6379(TX)> EXEC 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) OK 127.0.0.1:6379> GET a "a" 127.0.0.1:6379> GET b "b" ``` ## 事务的取消 可以使用`DISCARD`来中止事务。此时不执行任何命令,连接状态恢复正常。 > 示例:取消事务 ``` 127.0.0.1:6379> SET a 1 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> INCR a QUEUED 127.0.0.1:6379(TX)> DISCARD OK 127.0.0.1:6379> GET a "1" ``` # 锁 ## 乐观锁与悲观锁简介 > 情景 多线程并发操作下,执行`i=i+1`,大概率会出现结果不为`100`的情况。例如,线程A和线程B读取的值均为10。该值将由两个线程均增加到11,最后线程C读到的值为11。 ```java public class Demo { static int i = 0; public static void main(String[] args) { for (int j = 0; j < 100; j++) { new Thread(() -> i = i + 1).start(); } System.out.println(i); } } ``` > 悲观锁(Pessimistic Lock) 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 > 乐观锁(Optimistic Lock) 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种`check-and-set`机制实现事务的。 ## Redis乐观锁 - `WATCH`:开启锁 - `UNWATCH`:取消锁 `WATCH`命令用于为Redis事务提供乐观锁(CAS)。 被执行`WATCH`的键受到监控以检测其更改。如果在`EXEC`命令之前至少修改了一个被监控键,则整个事务将中止,`EXEC`返回一个`Null`来通知事务失败。当`EXEC`被调用时,所有键都被`UNWATCH`,不管事务是否中止。同样,当客户端连接关闭时,所有内容都会被`UNWATCH`。也可以使用`UNWATCH`命令(不带参数)来刷新所被监控的键。 > 示例 准备数据 ``` 127.0.0.1:6379> SET a 5 OK 127.0.0.1:6379> SET b 20 OK ``` 使用`WATCH`监控一个键,之后开启一个事务并执行修改数据但是不提交 ``` 127.0.0.1:6379> WATCH a OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> INCR a QUEUED 127.0.0.1:6379(TX)> INCR b QUEUED ``` 使用另一个客户端修改数据 ``` 127.0.0.1:6379> INCR a (integer) 6 ``` 提交事务,因`a`被修改,事务不执行 ``` 127.0.0.1:6379(TX)> EXEC (nil) 127.0.0.1:6379> GET a "6" 127.0.0.1:6379> GET b "20" ``` # Redis事务三特性 - `单独的隔离操作`:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 - `没有隔离级别的概念`:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行 - `不保证原子性`:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚