一、Redis事务简介
1.1 什么是 Redis事务?
Redis 事务的本质就是一组命令的集合。是指一个单独的隔离操作
:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
总的来说:
Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
1.2 为什么要使用事务?
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
注意:
- 在 Redis 事务中没有隔离级别的概念;
- 在 Redis 单条命令是保证原子性的,但是事务不保证原子性;
- Redis 是不支持事务回滚的。
二、Redis事务的相关命令
从输入 multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行。直到输入 exec 后,Redis会将之前的命令队列中的命令以此执行。组队的过程中可以通过 discard 来放弃组队。
2.1 MULTI—标记事务块的开始
用于标记事务块的开始。
Redis 会将后续的命令逐个放入队列中,然后才能使用 EXEC 命令原子化地执行这个命令序列。
# 标记事务块的开始
multi
此命令的返回值是一个字符串—OK。
2.2 EXEC—执行所有事务块内的命令
执行所有事务块内的命令。
在一个事务中执行所有先前放入队列的命令,然后回复正常的连接状态。
当使用 WATCH 命令时,只有当受监视的键没有被修改时,EXEC 命令才会执行事务中的命令,这种方式利用了检查再设置(CAS)的机制。
# 执行所有事务块内的命令
exec
此命令的返回值是一个数组,其中的没歌元素分别是原子化事务中的每个命令的返回值。当时用 WATCH 命令时,如果事务执行中止,exec 命令就会返回一个 null 值。
2.3 DISCARD—取消事务
取消事务,放弃执行事务块内的所有命令。
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
如果使用了 WATCH 命令,那么 DISCARD 命令就会将当前连接监控的所有键取消监控。
# 取消事务
discard
此命令的返回值是一个简单的字符传——OK。
2.4 WATCH—监视一个(或多个key)
Redis中使用WATCH实现乐观锁!
监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令(其他线程)所改动,那么事务将被打断。
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到 exec命令(事务中的命令是在 exec之后才执行的,所以在 multi命令后可以修改 watch监控的键值)。假设我们通过 watch命令再事务执行之前监控了多个 keys,倘若在 watch之后有任何 key的值发生了变化,exec命令执行的事务都将放弃,同时会应答调用者事务执行失败。
watch key [key ...]
注意:
这个监视是指在多线程的情况下
,别的线程来修改自己线程中的值时,exec命令执行的事务都将被抛弃,表示事务执行失败!
2.5 UNWATCH—取消WATCH命令对所有key的监视
取消WATCH命令对所有 key的监视。
如果调用了 EXEC或 DISCARD命令,那么就不需要手动调用 UNEATCH命令。
unwatch
此命令的返回值是一个简单的字符串——OK。
三、Redis事务的错误处理
在一个事务的运行期间,可能会遇到两种类型的命令错误:命令被放入队列时失败
和调用exec命令后事务的某个命令执行失败
。
3.1 命令被放入队列时失败
在调用 exec 命令之前,这些客户端可以检查被放入队列的命令的返回值:如果命令的返回值是QUEUE字符串,那么就表示已经正确地将这个命令放入队列;否则,Redis将返回一个错误。如果将某个命令放入队列时发生错误,那么大多数客户端将会中止事务,并且丢弃这个事务。
but,从Redis 2.6.5版本开始,服务器会记住事务累积命令期间发生的错误,然后 Redis会拒绝执行这个事务,在运行EXEC命令之后便会返回一个错误消息。最后 Redis会自动丢弃这个事务。
在Redis 2.6.5版本之前,如果发生了上述的错误,那么在客户端调用EXEC命令之后,Redis还是会运行这个出错的事务,执行已经成功放入事务队列的命令,而不会关心先前发生的错误。从2.6.5版本开始,Redis在遭遇上述错误时,会采用先前描述的新行为,这样便能轻松地混合使用事务和管道。在这种情况下,客户端可以一次性地将整个事务发送至Redis服务器,稍后再一次性地读取所有的返回值。
3.2 调用exec命令后事务的某个命令执行失败
在调用EXEC命令之后发生的事务错误,Redis不会进行任何特殊处理:在事务运行期间,即使某个命令运行失败,所有其他的命令也会继续执行。
四、Redis事务三特性
4.1 单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中不会被其他客户端发送来的命令请求所打断。
4.2 没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
4.3 不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
为什么Redis不支持回滚?
在事务运行期间,虽然Redis命令可能会执行失败,但是Redis仍然会执行事务中余下的其他命令,而不会执行回滚操作,你可能会觉得这种行为很奇怪。但是也有其合理之处:
只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作。实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。
Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力。
对于Redis事务的这种行为,有一个普遍的反对观点,那就是程序有可能会有bug。但是事务回滚并不能解决任何程序错误——没有人能解决程序猿自己的错误,这种错误可能会导致Redis命令执行失败。
正因为这些程序错误不大可能会进入生产环境,所以我们在开发Redis时选用更加简单和快速的方法,没有实现错误的回滚的功能。
五、悲观锁和乐观锁
5.1 悲观锁
5.1.1 简介
通俗来讲,悲观锁就是很悲观。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁
,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等都是在操作之前先上锁让别人无法操作该数据。
5.1.2 使用场景
比较适合写入操作比较频繁的场景
,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。
5.2 乐观锁
5.2.1 简介
通俗来讲,乐观锁就是很乐观。每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据库中的数据时需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
一般使用版本号机制进行判断。
乐观锁大多数情况是基于数据版本号(version)的机制实现的。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表添加一个“version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据,不予更新。
5.2.2 适用场景
比较适合读取操作比较频繁的场景
,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
乐观锁则适用于读多写少,并发冲突少的场景。
5.2.3 Redis使用WATCH实现乐观锁
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set cost 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 30
QUEUED
127.0.0.1:6379(TX)> incrby cost 30
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 70
2) (integer) 30
六、Redis脚本和事务
根据定义,Redis脚本也是事务型的。因此,可以通过Redis事务实现的功能,同样也可以通过Redis脚本来实现,而且通常脚本更简单、更快速。
由于Redis从2.6版本才开始引入脚本特性,而事务特性是很久以前就已经存在的,所以目前的版本才有两个看起来重复的特性。但是,我们不太可能在短时间内移除对事务特性的支持。因为,即使不用求助于Redis脚本,用户仍然能够规避竞争状态,这从语义上来看是适宜的。还有另一个更重要的原因,Redis事务特性的实现复杂度是最小的。
但是,在相当长的一段时间之内,我们不大可能看到整个用户群体都只使用Redis脚本。如果发生这种情况,那么我们可能会废弃,甚至最终移除Redis事务。