如何在Redis中实现事务

事务介绍

事务(Transaction) ,是指做为单个逻辑工做单元执行的一系列操做。事务必须知足ACID原则(原子性、一致性、隔离性和持久性)。
简单来讲,事务可能包括1~N条命令,当这些命令被做为事务处理时,将会顺序执行这些命令直到完成,并返回结果,若是中途有命令失败,则会回滚全部操做。
举个例子:php

  1. 咱们到银行ATM机取一笔钱,咱们的操做多是以下:html

  2. 插卡(输入密码)redis

  3. 输入要取的金额数据库

  4. ATM吐钞
    后台在你的户头上扣掉相应金额服务器

整个操做是一个顺序,不可分割的总体。上一步完成后才会执行下一步,若是ATM没吐钞却扣了用户的钱,银行但是要关门了。网络

Redis中的事务

先来看一下事务相关的命令并发

命令原型 命令描述
MULTI 用于标记事务的开始,其后执行的命令都将被存入命令队列,直到执行EXEC时,这些命令才会被原子的执行。
EXEC 执行在一个事务内命令队列中的全部命令,同时将当前链接的状态恢复为正常状态,即非事务状态。若是在事务中执行了WATCH命令,那么只有当WATCH所监控的Keys没有被修改的前提下,EXEC命令才能执行事务队列中的全部命令,不然EXEC将放弃当前事务中的全部命令。
DISCARD 回滚事务队列中的全部命令,同时再将当前链接的状态恢复为正常状态,即非事务状态。若是WATCH命令被使用,该命令将UNWATCH全部的Keys。
WATCH key [key ...] 在MULTI命令执行以前,能够指定待监控的Keys,然而在执行EXEC以前,若是被监控的Keys发生修改,EXEC将放弃执行该事务队列中的全部命令。
UNWATCH 取消当前事务中指定监控的Keys,若是执行了EXEC或DISCARD命令,则无需再手工执行该命令了,由于在此以后,事务中全部被监控的Keys都将自动取消。

和关系型数据库中的事务相比,在Redis事务中若是有某一条命令执行失败,其后的命令仍然会被继续执行。
咱们能够经过MULTI命令开启一个事务,有关系型数据库开发经验的人能够将其理解为BEGIN TRANSACTION语句。在该语句以后执行的命令都将被视为事务以内的操做,最后咱们能够经过执行EXEC/DISCARD命令来提交/回滚该事务内的全部操做。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句。
在事务开启以前,若是客户端与服务器之间出现通信故障并致使网络断开,其后全部待执行的语句都将不会被服务器执行。然而若是网络中断事件是发生在客户端执行EXEC命令以后,那么该事务中的全部命令都会被服务器执行。
当使用Append-Only模式时,Redis会经过调用系统函数write将该事务内的全部写操做在本次调用中所有写入磁盘。然而若是在写入的过程当中出现系统崩溃,如电源故障致使的宕机,那么此时也许只有部分数据被写入到磁盘,而另一部分数据却已经丢失。Redis服务器会在从新启动时执行一系列必要的一致性检测,一旦发现相似问题,就会当即退出并给出相应的错误提示。此时,咱们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具能够帮助咱们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复以后咱们就能够再次从新启动Redis服务器了。dom

样例

@Test
public void test2Trans() { 
  Jedis jedis = new Jedis("localhost"); 
  long start = System.currentTimeMillis(); 
  Transaction tx = jedis.multi(); 
  for (int i = 0; i < 100000; i++) { 
    tx.set("t" + i, "t" + i); 
  } 
  List<Object> results = tx.exec(); 
  long end = System.currentTimeMillis(); 
  System.out.println("Transaction SET: " + ((end - start)/1000.0) + " seconds"); 
  jedis.disconnect(); 
}

获得事务结果result以后,能够检查当中是否有非OK的返回值,若是存在则说明中间执行错误,可使用DISCARD来回滚执行结果。函数

WATCH命令

WATCHMULTI执行以前的某个Key提供监控(乐观锁)的功能,若是Key的值变化了,就会放弃事务的执行。
当事务EXEC执行完成以后,就会自动UNWATCH工具

Session 1 Session 2
(1)第1步
redis 127.0.0.1:6379> get age
"10"
redis 127.0.0.1:6379> watch age
OK
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379>
(2)第2步
redis 127.0.0.1:6379> set age 30
OK
redis 127.0.0.1:6379> get age
"30"
redis 127.0.0.1:6379>
(3)第3步
redis 127.0.0.1:6379> set age 20
QUEUED
redis 127.0.0.1:6379> exec
(nil)
redis 127.0.0.1:6379> get age
"30"
redis 127.0.0.1:6379>

样例

<?php  
header("content-type:text/html;charset=utf-8");  
$redis = new redis();  
$result = $redis->connect('localhost', 6379);  
$mywatchkey = $redis->get("mywatchkey");  
$rob_total = 100;   //抢购数量  
if($mywatchkey<$rob_total){  
    $redis->watch("mywatchkey");  
    $redis->multi();  
       
    //设置延迟,方便测试效果。  
    sleep(5);  
    //插入抢购数据  
    $redis->hSet("mywatchlist","user_id_".mt_rand(1, 9999),time());  
    $redis->set("mywatchkey",$mywatchkey+1);  
    $rob_result = $redis->exec();  
    if($rob_result){  
        $mywatchlist = $redis->hGetAll("mywatchlist");  
        echo "抢购成功!<br/>";  
        echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";  
        echo "用户列表:<pre>";  
        var_dump($mywatchlist);  
    }else{  
        echo "手气很差,再抢购!";exit;  
    }  
}  
?>

在上例是一个秒杀的场景,该部分抢购的功能会被并行执行。
经过已销售数量(mywatchkey)的监控,达到了控制库存,避免超卖的做用。
WATCH是一个乐观锁,有利于减小并发中的冲突, 提升吞吐量。

乐观锁与悲观锁

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

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

Lua脚本与事务

Lua 以可嵌入,轻量,高效著称,Redis于2.6版本以后,增长了Lua语言解析模块,能够用于一些简单的事务与逻辑运算。

命令原型 命令描述
EVAL script numkeys key[key ...] arg [arg...] 传入并执行一段Lua脚本,script为脚本内容,numkeys表示传入参数数量,key表示脚本要访问的key,arg为传入参数
EVALSHA sha1 经过SHA1序列调用lua_scripts字典预存的脚本
SCRIPT FLUSH 用于清除服务器中lua有关的脚本,释放lua_scripts字典,关闭现有的lua环境,并从新建立
SCRIPT EXISTS sha1 输入SHA1校验和,判断是否存在
SCRIPT LOAD script 与EVAL相同,建立对应的lua函数,存放到字典中
SCRIPT KILL 杀掉正在执行的脚本。正在执行的脚本会中断并返回错误,脚本中的写操做已被执行则不能杀死,由于违反原子性原则。此时只有手动回滚或shutdown nosave来还原数据

应用原理

客户端将Lua脚本做为命令传给服务端,服务端读取并解析后,执行并返回结果

127.0.0.1:6379> eval 'return redis.call("zrange", "name2", 0 , -1);' 0
1) "1"

Redis启动时会建立一个内建的lua_script哈希表,客户端能够将脚本上传到该表,并获得一个SHA1序列。以后能够经过该序列来调用脚本。(相似存储过程)

redis> SCRIPT LOAD "return 'dlrow olleh'"
"d569c48906b1f4fca0469ba4eee89149b5148092"
 
redis> EVALSHA d569c48906b1f4fca0469ba4eee89149b5148092 0
"dlrow olleh"

约束

Redis会把Lua脚本做为一个总体执行,因为Redis是单线程,所以在脚本执行期间,其余脚本或命令是没法插入执行,这个特性符合事务的原子性。
TIP

  1. 表是Lua中的表达式,与不少流行语言不一样。KEYS中的第一个元素是KEYS[1],第二个是KEYS[2](译注:不是0开始)

  2. nil是表的结束符,[1,2,nil,3]将自动变为[1,2],所以在表中不要使用nil。

  3. redis.call会触发Lua中的异常,redis.pcall将自动捕获全部能检测到的错误并以表的形式返回错误内容。

  4. Lua数字都将被转换为整数,发给Redis的小数点会丢失,返回前把它们转换成字符串类型。

  5. 确保在Lua中使用的全部KEY都在KEY表中,不然在未来的Redis版中你的脚本都有不能被很好支持的危险。

  6. 脚本要保持精简,以避免阻塞其余客户端操做

一致性

为了保证脚本执行结果的一致性,重复执行同一段脚本,应该获得相同的结果。Redis作了以下约束:

  • Lua没有访问系统时间或者其余内部状态的命令。

  • Lua脚本在解析阶段,若是发现RANDOMKEYSRANDMEMBERTIME这类返回随机性结果的命令,且脚本中有写指令(SET)类,则会返回错误,不容许执行。

  • Lua脚本中调用返回无序元素的命令时,如SMEMBERS,Redis会在后台将命令的结果排序后传回脚本

  • Lua中的伪随机数生成函数math.randommath.randomseed会被替换为Redis内置的函数来执行,以保证脚本执行时的seed值不变。

样例

private static String getSCRIPT() {
        return "local key = KEYS[1]\n" +
                "local localIp = ARGV[1]\n" +
                "\n" +
                "local gateIp = redis.call(\"HGET\", key, \"gateIp\")\n" +
                "if gateIp == localIp then\n" +
                "    redis.call(\"HSET\", key, \"userStatus\", \"false\")\n" +
                "    return 1\n" +
                "else\n" +
                "    return 0\n" +
                "end";
    }
 
@Test
public void testTrans() { 
  ......
  Jedis jedis = new Jedis("localhost"); 
  result = jedis.evalsha(getSCRIPT, keys, args);
  ......
}
相关文章
相关标签/搜索