Redis中的事务(transaction)是一组命令的集合。事务同命令同样都是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。Redis事务的实现须要用到 MULTI 和 EXEC 两个命令,事务开始的时候先向Redis服务器发送 MULTI 命令,而后依次发送须要在本次事务中处理的命令,最后再发送 EXEC 命令表示事务命令结束。redis
举个例子,使用redis-cli链接redis,而后在命令行工具中输入以下命令: 数据库
从输出中能够看到,当输入MULTI命令后,服务器返回OK表示事务开始成功,而后依次输入须要在本次事务中执行的全部命令,每次输入一个命令服务器并不会立刻执行,而是返回”QUEUED”,这表示命令已经被服务器接受而且暂时保存起来,最后输入EXEC命令后,本次事务中的全部命令才会被依次执行,能够看到最后服务器一次性返回了三个OK,这里返回的结果与发送的命令是按顺序一一对应的,这说明此次事务中的命令全都执行成功了。服务器
再举个例子,在命令行工具中输入以下命令: dom
和前面的例子同样,先输入MULTI最后输入EXEC表示中间的命令属于一个事务,不一样的是中间输入的命令有一个错误(set写成了sett),这样由于有一个错误的命令致使事务中的其余命令都不执行了(经过后续的get命令能够验证),可见事务中的全部命令是同呼吸共命运的。分布式
若是客户端在发送EXEC命令以前断线了,则服务器会清空事务队列,事务中的全部命令都不会被执行。而一旦客户端发送了EXEC命令以后,事务中的全部命令都会被执行,即便此后客户端断线也不要紧,由于服务器已经保存了事务中的全部命令。ide
除了保证事务中的全部命令要么全执行要么全不执行外,Redis的事务还能保证一个事务中的命令依次执行而不会被其余命令插入。试想一个客户端A须要执行几条命令,同时客户端B发送了几条命令,若是不使用事务,则客户端B的命令有可能会插入到客户端A的几条命令中,若是想避免这种状况发生,也可使用事务。 工具
若是一个事务中的某个命令执行出错,Redis会怎样处理呢?要回答这个问题,首先要搞清楚是什么缘由致使命令执行出错:性能
1.语法错误:就像上面的例子同样,语法错误表示命令不存在或者参数错误,这种状况须要区分Redis的版本,Redis 2.6.5以前的版本会忽略错误的命令,执行其余正确的命令,2.6.5以后的版本会忽略这个事务中的全部命令,都不执行,就好比上面的例子(使用的Redis版本是2.8的)ui
2.运行错误 运行错误表示命令在执行过程当中出现错误,好比用GET命令获取一个散列表类型的键值。这种错误在命令执行以前Redis是没法发现的,因此在事务里这样的命令会被Redis接受并执行。若是食物里有一条命令执行错误,其余命令依旧会执行(包括出错以后的命令)。好比下例: spa
Redis中的事务并无关系型数据库中的事务回滚(rollback)功能,所以使用者必须本身收拾剩下的烂摊子。不过因为Redis不支持事务回滚功能,这也使得Redis的事务简洁快速。
回顾上面两种类型的错误,语法错误彻底能够在开发的时候发现并做出处理,另外若是能很好地规划Redis数据的键的使用,也是不会出现命令和键不匹配的问题的。
从上面的例子咱们能够看到,事务中的命令要所有执行完以后才能获取每一个命令的结果,可是若是一个事务中的命令B依赖于他上一个命令A的结果的话该怎么办呢?就好比说实现相似Java中的i++的功能,先要获取当前值,才能在当前值的基础上作加一操做。这种场合仅仅使用上面介绍的MULTI和EXEC是不能实现的,由于MULTI和EXEC中的命令是一块儿执行的,并不能将其中一条命令的执行结果做为另外一条命令的执行参数,因此这个时候就须要引进Redis事务家族中的另外一成员:WATCH命令
换个角度思考上面说到的实现i++的方法,能够这样实现:
- 监控i的值,保证i的值不被修改
- 获取i的原值
- 若是过程当中i的值没有被修改,则将当前的i值+1,不然不执行
这样就可以避免竞态条件,保证i++可以正确执行。
WATCH命令能够监控一个或多个键,一旦其中有一个键被修改(或删除),以后的事务就不会执行,监控一直持续到EXEC命令(事务中的命令是在EXEC以后才执行的,EXEC命令执行完以后被监控的键会自动被UNWATCH)。举个例子:
上面的例子中,首先设置mykey的键值为1,而后使用WATCH命令监控mykey,随后更改mykey的值为2,而后进入事务,事务中设置mykey的值为3,而后执行EXEC运行事务中的命令,最后使用get命令查看mykey的值,发现mykey的值仍是2,也就是说事务中的命令根本没有执行(由于WATCH监控mykey的过程当中,mykey被修改了,因此随后的事务便会被取消)。
UNWATCH命令能够在WATCH命令执行以后、MULTI命令执行以前取消对某个键的监控。举个例子:
上面的例子中,首先设置mykey的键值为1,而后使用WATCH命令监控mykey,随后更改mykey的值为2,而后取消对mykey的监控,再进入事务,事务中设置mykey的值为3,而后执行EXEC运行事务中的命令,最后使用get命令查看mykey的值,发现mykey的值仍是3,也就是说事务中的命令运行成功。
DISCARD命令则能够在MULTI命令执行以后,EXEC命令执行以前取消WATCH命令并清空事务队列,而后从事务状态中退出。举个例子:
上面的例子中,首先设置mykey的键值为1,而后使用WATCH命令监控mykey,随后更改mykey的值为2,而后进入事务,事务中设置mykey的值为3,而后执行DISCARD命令,再执行EXEC运行事务中的命令,发现报错“ERR EXEC without MULTI”,说明DISCARD命令成功执行——取消WATCH命令并清空事务队列,而后从事务状态中退出。
上面介绍的Redis的WATCH、MULTI和EXEC命令,只会在数据被其余客户端抢先修改的状况下,通知执行这些命令的客户端,让它撤销对数据的修改操做,并不能阻止其余客户端对数据进行修改,因此只能称之为乐观锁(optimistic locking)。
而这种乐观锁并不具有可扩展性——当客户端尝试完成一个事务的时候,可能会由于事务执行失败而进行反复的重试。保证数据准确性很是重要,可是当负载变大的时候,使用乐观锁的作法并不完美。这时就须要使用Redis实现分布式锁。
分布式锁:是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,经常须要协调他们的动做。若是不一样的系统或是同一个系统的不一样主机之间共享了一个或一组资源,那么访问这些资源的时候,每每须要互斥来防止彼此干扰来保证一致性,在这种状况下,便须要使用到分布式锁。
Redis命令介绍:
Redis实现分布式锁主要用到命令是SETNX命令(SET if Not eXists)。
语法:SETNX key value
功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不作任何动做,并返回0。
使用Redis构建锁:
思路:将“lock:”+参数名设置为锁的键,使用SETNX命令尝试将一个随机的uuid设置为锁的值,并为锁设置过时时间,使用SETNX设置锁的值能够防止锁被其余进程获取。若是尝试获取锁的时候失败,那么程序将不断重试,直到成功获取锁或者超过给定是时限为止。
代码:
public String acquireLockWithTimeout( Jedis conn, String lockName, long acquireTimeout, long lockTimeout) { String identifier = UUID.randomUUID().toString(); //锁的值 String lockKey = "lock:" + lockName; //锁的键 int lockExpire = (int)(lockTimeout / 1000); //锁的过时时间 long end = System.currentTimeMillis() + acquireTimeout; //尝试获取锁的时限 while (System.currentTimeMillis() < end) { //判断是否超过获取锁的时限 if (conn.setnx(lockKey, identifier) == 1){ //判断设置锁的值是否成功 conn.expire(lockKey, lockExpire); //设置锁的过时时间 return identifier; //返回锁的值 } if (conn.ttl(lockKey) == -1) { //判断锁是否超时 conn.expire(lockKey, lockExpire); } try { Thread.sleep(1000); //等待1秒后从新尝试设置锁的值 }catch(InterruptedException ie){ Thread.currentThread().interrupt(); } } // 获取锁失败时返回null return null; }
锁的释放:
思路:使用WATCH命令监视表明锁的键,而后检查键的值是否和加锁时设置的值相同,并在确认值没有变化后删除该键。
代码:
public boolean releaseLock(Jedis conn, String lockName, String identifier) { String lockKey = "lock:" + lockName; //锁的键 while (true){ conn.watch(lockKey); //监视锁的键 if (identifier.equals(conn.get(lockKey))){ //判断锁的值是否和加锁时设置的一致,即检查进程是否仍然持有锁 Transaction trans = conn.multi(); trans.del(lockKey); //在Redis事务中释放锁 List<Object> results = trans.exec(); if (results == null){ continue; //事务执行失败后重试(监视的键被修改致使事务失败,从新监视并释放锁) } return true; } conn.unwatch(); //解除监视 break; } return false; }
经过在客户端上面实现一个真正的锁(非乐观锁),将会为程序带来更好的性能,更简单易用的API,可是与此同时,请记住Redis并不会主动使用这个自制的分布式锁,咱们必须本身使用这个锁来代替WATCH命令,或者协同WATCH命令一块儿工做,从而保证数据的准确性与一致性。
参考:
http://qifuguang.me/2015/09/30/Redis%E4%BA%8B%E5%8A%A1%E4%BB%8B%E7%BB%8D/
http://blog.csdn.net/ugg/article/details/41894947