Redis 通过 MULTI、EXEC、WATCH 和 DISCARD 命令来实现事务功能。事务可以一次执行多个命令, 并且带有以下两个重要的保证:
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 事务中的命令要么全部被执行,要么全部都不执行。
以下是一个事务的执行过程:
WATCH "name"
REDIS MULTI
GET "name"
SET "name" "Bob"
EXEC
MULTI 命令的执行标志着事务的开始,当事务开始之后,客户端执行的所有非事务命令都会被存放到一个事务队列里面。EXEC 命令是向服务器提交事务,当客户端执行 EXEC 命令把事务提交给服务器之后,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后再将执行命令所得到的结果全部返回给客户端。
WATCH 命令是一个乐观锁,它可以在 EXEC 命令执行前,监视任意数量的数据库键,并在 EXEC 命令执行的时候,检查被监视的键是否被修改过,如果被修改过的话,服务器将拒绝执行事务。当 EXEC 被调用时,不管事务是否成功执行,对所有键的监视都会被取消。例如上面的程序,首先开启了对 “name" 的监视,然后启动事务,把 GET 和 SET命令加入到事务队列里,如果在这时有另外一个客户端对 ”name" 键的值进行修改的话,那么当这个事务提交给服务器执行的时候,服务器将拒绝执行,并返回空回复。
Redis 的事务和传统的关系型数据库事务的最大区别在于,Redis 不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直至事务队列中所有命令都执行完毕为止。但是,如果一个事务的事务队列中包含错误的命令或者命令的格式不正确等情况,那么这个事务将会被服务器拒绝执行。Redis 的作者认为,Redis 事务的执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚的功能。
补充:
因为 Redis 的事务是在执行之后才统一返回命令的执行结果的,所以我们是无法获取和使用事务中的中间结果的。举个例子,假设我们现在通过 java 程序来判断 Redis 数据库中是否存在一个名为 "prop.host" 的键,如果不存在,就给它赋一个 "127.0.0.1" 的值,那么我们可能会这么写:
Jedis client = JedisFactory.getJedisInstance();
Transaction tx = client.multi();
String host = tx.get("prop.host");
if(host == null) {
tx.set("host", "127.0.0.1");
}
tx.exec();
但是,其实这样写是不对的,因为在事务中,所有命令的执行结果是在最后 client.exec() 执行后统一返回的,所以在代码中的 host 变量是无法获取到数据库中 "prop.host" 的值的。要实现这种获取事务执行过程中的值的话,只能通过编写脚本来实现。Redis 客户端可以通过使用 Lua 脚本,直接在服务器端原子地执行多个 Redis 命令。对 Lua 脚本感兴趣的,可以自行查阅资料,在这里就不对 Lua 脚本多做介绍了。