Redis事务机制

1.概述

Redis 事务能够一次执行多个命令, 而且带有如下两个重要的保证:html

  • 批量操做在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其他的命令依然被执行。
  • 在事务执行过程,其余客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历如下三个阶段:java

  • 开始事务。
  • 命令入队。
  • 执行事务。

2.Redis事务命令

命令

命令及描述

MULTI redis

标记一个事务块的开始。其后执行的命令都将被存入命令队列,直到执行EXEC时,这些命令才会被原子执行。数据库

EXEC 缓存

执行全部事务块内的命令,同时将当前链接的状态恢复为正常状态,即非事务状态。若是在事务中执行了WATCH命令,那么只有当WATCH所监控的keys没有被修改的前提下,EXEC命令才能执行事务队列中的全部命令,不然EXEC将放弃当前事务中的全部命令。数据结构

DISCARD 多线程

取消事务,放弃执行事务块内的全部命令。同时再将当前链接的状态恢复为正常状态,即非事务状态。如 果WATCH命令被使用,该命令将UNWATCH全部的keys.app

WATCH key [key ...] 分布式

监视一个(或多个) key ,若是在事务执行以前这个(或这些) key 被其余命令所改动,那么事务将被打断。学习

UNWATCH 

取消 WATCH 命令对全部 key 的监视。若是执行了EXEC或DISCARD命令,则无需再手工执行  该命令了,由于在此以后,事务中全部的keys都将自动取消,

 

3.事务的使用

Redis提供了一个 multi 命令开启事务,exec 命令提交事务,在它们之间的命令是在一个事务内的,能保证原子性。

127.0.0.1:6379>multi
"OK"
127.0.0.1:6379>set tran1 hello
"QUEUED"
127.0.0.1:6379>set tran2 world
"QUEUED"
127.0.0.1:6379>exec
 1)  "OK"
 2)  "OK"

 经过上面的命令能够看到使用 multi 命令开启事务以后,执行的Redis命令返回结果 QUEUED,表示命令并无执行,而是暂时保存在Redis事务中,直到执行 exec 命令后才会执行上面的命令而且返回结果。

此外咱们可使用DISCARD取消事务。当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 而且客户端会从事务状态中退出。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name2 "lisi"
QUEUED
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379>

 下面这个是Jedis客户端执行事务的代码:

public static void testTran() {
    // 开启事务
    Transaction transaction = jedis.multi();
    // 执行事务内的Redis命令
    transaction.set("tran1", "hello");
    transaction.set("tran2", "world");
    // 提交事务
    List<Object> exec = transaction.exec();
    // 事务中每条命令的执行结果
    System.out.println(exec);
}

须要注意的是开启事务以后,执行命令的对象不是Jedis对象,而是Transaction对象,不然会抛出下面的异常:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: 
Cannot use Jedis when in Multi. 
Please use Transation or reset jedis state.

4.事务对异常的处理机制

 Redis执行命令的错误主要分为两种: 

- 1.命令错误:执行命令语法错误,好比说将 set 命令写成 sett 
- 2.运行时错误:命令语法正确,可是执行错误,好比说对 List 集合执行 sadd 命令

 Redis事务中若是发生上面两种错误,处理机制也是不一样的。

(1)命令错误处理机制

这种状况须要区别Redis版本,Redis2.65以前的版本会忽略错误的命令,执行其余正确的命令,2.65以后的版本会忽略这个事务中的全部命令,都不执行,就好比上面的例子(使用的Redis版本是2.8的);

127.0.0.1:6379>multi
"OK"
127.0.0.1:6379>set a1 a
"QUEUED"
127.0.0.1:6379>sett a2 b
"ERR unknown command 'sett'"
127.0.0.1:6379>exec
"EXECABORT Transaction discarded because of previous errors."
127.0.0.1:6379>get a1
null

 上面案例中,开启事务后第一条命令添加返回QUEUED,第二条命令语法错误,最后提交事务。

能够看到,事务提交后 get a1 返回值是null,因此第二条命令的语法错误致使整个事务中的命令都不会执行。

(2)运行时错误处理机制

运行错误表示命令执行过程当中出现错误,就好比用GET命令去获取一个散列表类型的键值。

这种错误在命令执行以前Redis是没法发现的,因此在事务里这样的命令都会被Redis接受并执行.若是事务里有一条命令执行错误,Redis不只不会回滚事务,还会跳过这个运行时错误,其余命令依旧会执行(包括出错后的命令)。

127.0.0.1:6379>lpush l1 a
"1"
127.0.0.1:6379>lpush l2 b
"1"
127.0.0.1:6379>lpush l3 c
"1"
127.0.0.1:6379>multi 
"OK"
127.0.0.1:6379>lpush l1 aa
"QUEUED"
127.0.0.1:6379>sadd l2 bb
"QUEUED"
127.0.0.1:6379>lpush l3 cc
"QUEUED"
127.0.0.1:6379>exec
1) "2"
2) "WRONGTYPE Operation against a key holding the wrong kind of value"
3) "2"

上面这个案例中,先建立了三个List类型 l一、l二、l3,而后开启事务,第一条命令往l1中插入元素,第二条命令使用 sadd 命令往List类型的l2中添加元素,第三天命令往l2中插入元素,最后提交事务。

能够看到最后事务的执行结果是第一条和第三条命令执行成功,第二条命令执行失败,因此第二条命令的执行失败不只没有回滚事务并且还不会影响后续第三条命令的执行。

 5.Watch命令(乐观锁的实现)

 WATCH 对key值进行锁操做。 在 WATCH 执行以后, EXEC 执行以前, 有其余客户端修改了 key 的值, 那么当前客户端的事务就会失败。以下:

 Client1开启watch name并在事务中修改name,可是没有执行exec

127.0.0.1:6379> get name

"huangliu"

127.0.0.1:6379> watch name

OK

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set name lisi

QUEUED

Client2 修改name

127.0.0.1:6379> get name

"huangliu"

127.0.0.1:6379> set name "wangwu"

OK

127.0.0.1:6379> get name

"wangwu"

127.0.0.1:6379>

Client1执行exec

127.0.0.1:6379> exec

(nil)

127.0.0.1:6379>

 可见,因为被watch的name已经被Client2 修改,因此Client1的事务执行失败,程序须要作的, 就是不断重试这个操做, 直到没有发生碰撞(Crash)为止。对key进行加锁监视的机制相似Java多线程中的锁(synchronized中的监视器对象),被称做乐观锁。乐观是一种很是强大的锁机制,后面咱们会进一步学习redis的分布式锁。下面咱们来看一下watch在java的操做:

@Test
public void testWatch() {
    JedisPool jedisPool = new JedisPool("192.168.1.4");
    // 设定 nowatch 的初始值为 hello
    Jedis jedis = jedisPool.getResource();
    jedis.set("watchtest", "hello");
    // 开启事务
    Transaction multi = jedis.multi();
    // 另外一个jedis客户端对 watchtest进行append操做
    jedisPool.getResource().append("watchtest", " xxx");
    // 事务内部对watchtest进行append操做
    multi.append("watchtest", " world");
    // 提交事务
    multi.exec();
    // 打印watchtest对应的value
    System.out.println(jedis.get("watchtest"));
}

 上面这个案例,watchtest的初始值是”hello”,开启了一个事务,而且往watchtest中append ” world”,咱们预期的结果是”hello world”,可是在事务执行过程当中有另外一个jedis客户端往watchtest中append ” xxx”,因此上面这段代码会在控制台打印:

hello xxx world

 

咱们每每但愿当前事务的执行不会受到其余事务的影响,因此这个结果明显不是咱们所预期的。

Redis提供了一个 watch 命令来帮咱们解决上面描述的这个问题,在 multi 命令以前咱们可使用 watch 命令来”观察”一个或多个key,在事务提交以前Redis会确保被”观察”的key有没有被修改过,没有被修改过才会执行事务中的命令,若是存在key被修改过,那么整个事务中的命令都不会执行,有点相似于乐观锁的机制。

仍是上面的案例,若是在开启事务那一行上面添加 watch 命令:

// 使用 watch 命令watch "watchtest"
jedis.watch("watchtest");
// 开启事务
Transaction multi = jedis.multi();

 最终控制台打印结果会变成:

hello xxx

 能够看出,使用 watch 命令以后,因为watchtest被其余客户端修改过,因此事务中append " world" 的命令就不会执行,因此最终会打印 "hello xxx"。

通常乐观锁都须要配合重试机制来实现,因此这里 watch 命令也能够配合重试机制来实现:

public void incr(String key) {
    jedis.watch(key);
    Integer num = Integer.valueOf(jedis.get(key));
    Transaction multi = jedis.multi();
    multi.set(key, String.valueOf(num + 1));
    List<Object> exec = multi.exec();
    // exec为空表示事务没有执行,在这里添加剧试机制
    if (exec.isEmpty()) {
        incr(key);
    }
}

 上面这段代码是使用 watch 命令实现了Redis中的incr命令,这里为了演示 watch 命令配合重试的机制,就不去校验key对应的数据结构是不是int类型。

综上所述,在这里引出乐观锁,针对乐观锁和悲观锁作一解释:

乐观锁和共享锁
乐观锁(Optimistic Lock)又叫作共享锁,每次别人拿数据的时候都认为别人不会修改数据,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。乐观锁适用于多读得应用类型,这样会提升吞吐量。

悲观锁(Pessimistic Lock)又叫作排它锁(x锁),每次拿刀数据的时候都认为别人会修改数据,因此每次在拿到数据的时候都会上锁,这样别人想拿到这个数据就会block直到
它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁,都是在操做以前先上锁。

6.参考资料

http://www.runoob.com/redis/redis-transactions.html

http://www.sohu.com/a/282419876_179850

https://blog.csdn.net/Hqs_1020417504/article/details/79908264

https://www.cnblogs.com/laojiao/p/9580653.html

https://blog.csdn.net/Leon_cx/article/details/82345054

https://www.cnblogs.com/hjwublog/p/5660578.html

相关文章
相关标签/搜索