redis的事务、管道和脚本

参考
Redis的事务、管道和脚本

1、事务

1.1 ACID/CAP/BASE

ACID:是指在数据库管理系统(DBMS)中,事务(transaction)所具有的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性限制没有被破坏
  • 隔离性:当两个或者多个事务并发访问(此处访问指查询和修改的操作)数据库的同一数据时所表现出的相互关系。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • 持久性:在事务完成以后,该事务对数据库所作的更改便持久地保存在数据库之中,并且是完全的。

CAP

BASE:BASE全称是BasicallyAvailable(基本可用), Soft-state(软状态/柔性事务), Eventually Consistent(最终一致性)。BASE模型在理论逻辑上是相反于ACID(原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability)模型的概念,它牺牲高一致性,获得可用性和分区容忍性。

  • 最终一致性 (Eventually Consistent)。最终一致性是指:经过一段时间以后,更新的数据会到达系统中的所有相关节点。这段时间就被称之为最终一致性的时间窗口
  • 软状态(Soft-state)。软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。-
  • 基本可用(Basically Available)。基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

1.2 Redis事务的定义

Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。事务的原理是先将属于一个事务的命令发送给Redis,然后再让Redis依次执行这些命令。

Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。

除此之外,Redis的事务还能保证一个事务内的命令依次执行而不被其他命令插入。试想客户端A需要执行几条命令,同时客户端B发送了一条命令,如果不使用事务,则客户端B的命令可能会插入到客户端A的几条命令中执行。如果不希望发生这种情况,也可以使用事务。

1.3 Redis事务的错误和回滚

Redis的事务没有隔离级别的概念(ACID中的I),在事务执行前所有的命令都未执行。对于执行过程中的错误按照类型分为两种。

1、语法错误
语法错误指命令不存在或者命令参数的个数不对,这个命令可能会有语法错误(参数的数量错误、命令名称错误,等等),或者可能会有某些临界条件(例如:如果使用maxmemory指令,为Redis服务器配置内存限制,那么就可能会有内存溢出条件)。

可用Redis客户端检测第一种类型的错误,在调用EXEC命令之前,这些客户端可以检查被放入队列的命令的返回值:如果命令的返回值是QUEUE字符串,那么就表示已经正确地将这个命令放入队列;否则,Redis将返回一个错误。如果将某个命令放入队列时发生错误,那么大多数客户端将会中止事务,并且丢弃这个事务

在Redis 2.6.5版本之前,如果发生了上述的错误,那么在客户端调用了EXEC命令之后,Redis还是会运行这个出错的事务,执行已经成功放入事务队列的命令,而不会关心先前发生的错误。从2.6.5版本开始,Redis在遭遇上述错误时,服务器会记住事务积累命令期间发生的错误。然后,Redis会拒绝执行这个事务,在运行EXEC命令之后,便会返回一个错误消息。最后,Redis会自动丢弃这个事务,这样便能轻松地混合使用事务和管道。在这种情况下,客户端可以一次性地将整个事务发送至Redis服务器,稍后再一次性地读取所有的返回值

2、运行错误
运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键,这种错误在实际执行之前Redis是无法发现的,所以在事务里这样的命令是会被Redis接受并执行的。如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行(包括出错命令之后的命令)。

Redis的回滚机制
Redis的事务没有关系数据库事务提供的回滚(rollback)功能。为此开发者必须在事务执行出错后自己收拾剩下的摊子(将数据库复原回事务执行前的状态等,这里我们一般采取日志记录然后业务补偿的方式来处理,但是一般情况下,在redis做的操作不应该有这种强一致性要求的需求,我们认为这种需求为不合理的设计)。

multi使用如下: 使用multi 打开事务。执行完所有数据操作后,使用 exec 执行生效。

> multi
OK
set p1 1
QUEUED
> set p2 2
QUEUED
> set p3 3
QUEUED
> exec
1) OK
2) OK
3) OK
> keys p*
1) "p1"
2) "p3"
3) "p2"

使用discard 取消事务

> multi
OK
> set p4 4
QUEUED
> set p5 5
QUEUED
> discard
OK
127.0.0.1:6379> keys p*
1) "p1"
2) "p3"
3) "p2"

注意。目前redis还不能保证操作的原子性,可能部分回滚失败,比如事务中间出现报错的时候。

> get name
"mawenxia"
> get age
"19"
> multi    //开启事务
OK
> incr age    //年龄+1
QUEUED
> incr name    //名字+1
QUEUED
> exec      //执行
1) (integer) 20    //年龄+1成功
2) (error) ERR value is not an integer or out of range  //名字+1报错
> get age
"20"    //年龄+1成功。说明事务的回滚失败

1.4 Redis的乐观锁和Watch

Watch命令描述:
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值

在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能,且是基于乐观锁的思想。假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。例如,我们再次假设Redis中并未提供incr命令来完成键值的原子性递增,如果要实现该功能,我们只能自行编写相应的代码。其伪码如下:

  val = GET mykey
  val = val + 1
  SET mykey $val

以上代码只有在单连接的情况下才可以保证执行结果是正确的,因为如果在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中经常出现的一种错误场景–竞态争用(race condition)。比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是我们认为的12。为了解决类似的问题,我们需要借助WATCH命令的帮助,见如下代码:

  WATCH mykey
  val = GET mykey
  val = val + 1
  MULTI
  SET mykey $val
  EXEC

和此前代码不同的是,新代码在获取mykey的值之前先通过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的mykey的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功

由于WATCH命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值,所以在一般的情况下我们需要在EXEC执行失败后重新执行整个函数。执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用UNWATCH命令来取消监控

实现一个hsetNX函数
我们实现的hsetNX这个功能是:仅当字段存在时才赋值。为了避免竞态条件我们使用watch和事务来完成这一功能(伪代码):

WATCH key  
isFieldExists = HEXISTS key, field  
if isFieldExists is 1  
MULTI  
HSET key, field, value  
EXEC  
else  
UNWATCH  
return isFieldExists

在代码中会判断要赋值的字段是否存在,如果字段 不存在的话就不执行事务中的命令,但需要使用 UNWATCH命令来保证下一个事务的执行不会受到影响

2、Redis的管道

Redis是一个响应式的服务,当客户端发送一个请求后,就处于阻塞状态等待Redis返回结果。这样一次命令消耗的时间就包括三个部分:请求从客户端到服务器的时间、结果从服务器到客户端的时间和命令真正执行时间,前两个部分消耗的时间总和称为RTT(Round Trip Time),当客户端与服务器存在网络延时时,RTT就可能会很大,这样就会导致性能问题。

管道(Pipeline)就是为了改善这个情况的,利用管道,客户端可以一次性发送多个请求而不用等待服务器的响应待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低RTT时间从而提升性能。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。

pipeline和“事务”是两个完全不同的概念,pipeline只是表达“交互”中操作的传递的方向性,pipeline也可以在事务中运行,也可以不在。无论如何,pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的相应中得到信息;也就是pipeline并不是表达“所有command都一起成功”的语义;但是如果pipeline的操作被封装在事务中,那么将有事务来确保操作的成功与失败。Pipeline的示例代码如下:

private static void usePipeline(int count){
    Jedis jr = null;
    try {
        jr = new Jedis("10.10.224.44", 6379);
        Pipeline pl = jr.pipelined();
        for(int i =0; i<count; i++){
             pl.incr("testKey2");
        }
            pl.sync();
    } catch (Exception e) {
        e.printStackTrace();
    }
    finally{
        if(jr!=null){
            jr.disconnect();
        }
    }
}

3、Redis Lua脚本

Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:

  1. 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延
  2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入
  3. 复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。

在实际工作过程中,可以使用lua脚本来解决一些需要保证原子性的问题,而且lua脚本可以缓存在redis服务器上,势必会增加性能。

Lua语法
Lua语言模型 与 Redis应用中关于Lua语法的详述。

Eval命令
从Redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。EVAL命令的格式如下:

EVAL script numkeys key [key ...] arg [arg ...]  

script参数是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本 不必(也不应该)定义为一个Lua函数。numkeys参数用于指定键名参数的个数。键名参数 key [key …] 从EVAL的第三个参数开始算起,表示在脚本中所用到的那些Redis键(key),这些键名参数可以在 Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。例如

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

其中 “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 是被求值的Lua脚本,数字2指定了键名参数的数量, key1和key2是键名参数,分别使用 KEYS[1] 和 KEYS[2] 访问,而最后的 first 和 second 则是附加参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。

Call和pcall
在 Lua 脚本中,可以使用两个不同函数来执行Redis命令,它们分别是:

redis.call()
redis.pcall()

这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误。当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因,redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误

Jedis调用
Jedis中调用脚本需要以字符串形式给出脚本主体,并遵从EVAL的数据规范,示例代码如下:

String script ="local result={} " + 
                " for i,v in ipairs(KEYS) do " + 
                " result[i] = redis.call('get',v) " + 
                " end " + 
                " return result ";
 
Jedis jedis = new Jedis(ip,port);
 
jedis.eval(script,keyCount,String … params);

注意,不要再Lua脚本中出现死循环和耗时的运算,否则redis将不接受其他的命令,这个redis就挂了,只能script kill,如果有写入的话,只能shutdown nosave。 所以使用时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本。

4、Redis事务、管道和脚本的区别

  1. 事务和脚本从原子性上来说都能满足原子性的要求,其区别在于脚本可借助Lua语言可在服务器端存储的便利性定制和简化操作,但脚本无法处理长耗时的操作

  2. 管道是无状态操作集合,使用管道可能在效率上比使用script要好,但是有的情况下只能使用script。因为在执行后面的命令时,无法得到前面命令的结果,就像事务一样,所以如果需要在后面命令中使用前面命令的value等结果,则只能使用script或者事务+watch

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值