事务和lua脚本都是redis内存数据库实现原子性操作的手段,两者虽然类似,但区别不小。而且,尽管Redis内置有事务,但是很多开发者还是更倾向于使用Lua脚本来实现相应的功能。这是为什么呢?
一、事务不回滚
在比较事务与lua脚本之前,小义先带大家复习一下,为什么redis中的事务失败时是不会进行回滚的?
在传统的ACID事务中,如果事务执行过程中出现错误,所有已经执行的操作都会回滚,保证数据的一致性。
而在Redis中,事务是通过MULTI、EXEC、DISCARD和WATCH四个命令来实现的,它们构成了一个队列,通过EXEC命令来一次性、顺序、无中断地执行队列中所有命令。在执行EXEC之前,如果有任何错误(比如命令的语法错误或命令用错),Redis将立即停止并返回错误。但是,如果EXEC命令执行后,在执行队列中的命令时出现错误,它并不会回滚之前已经执行成功的命令,而是继续执行队列中的其他命令。也就是说,Redis的事务没有所谓的“回滚”机制。
这是由Redis性能优先的设计理念决定的。如果引入传统数据库的回滚机制,会大大增加Redis的复杂性。此外,Redis主要用于处理非关系型数据,其数据模型并不像关系型数据库那样需要维护复杂的数据关系和一致性。而且开发者可以在应用层面通过妥善的错误处理和设计,保证数据的一致性。
二、lua脚本优势
那lua脚本相比起内置事务,就有以下几大优势:
1、更强的灵活性:redis事务无法依赖于前一个命令的结果来进行后续的处理,但Lua脚本提供了更强的灵活性。通过编写自己的脚本,开发者可以设计出更符合业务需求的事务处理方案。
2、提供原子性操作:虽然Redis的事务可以保证一组命令的原子性执行,但并不能保证在执行过程中数据不被其他客户端修改。而Lua脚本则可以避免这种情况,所有命令在同一Lua脚本中执行,从而保证数据在执行过程中的一致性。
3、减轻服务器压力:使用Redis内置的事务功能,每个命令都需要进行网络通信,这会增加服务器的压力。而将命令放入Lua脚本执行,只需要一次通信,既减轻了服务器压力又提高了执行效率。
三、代码实践示例
假设,我们有一个需求,从A账户转账100元到B账户,这是一个典型的事务应用场景。
首先,我们来看Redis事务的一个使用的例子:
Jedis jedis = new Jedis("localhost");
//redis watch命令给事务提供check-and-set(CAS)机制,
//被watch的key被持续监控,如果key在exec命令执行前有改变,那整个事务取消
jedis.watch("accountA");
int balanceA = Integer.parseInt(jedis.get("accountA"));
if(balanceA < 100) {
jedis.unwatch();
} else {
Transaction txn = jedis.multi();
txn.decrBy("accountA", 100);
txn.incrBy("accountB", 100);
txn.exec();
}在上面的代码中,我们必须注意其他客户端可能在事务过程中改变accountA的值。写起来比较复杂,而且理解起来也需要一点时间。
然后,我们来看看在这种场景下如何使用Lua脚本:
String luaScript = "if redis.call('get', KEYS[1]) >= ARGV[1] then " +
"redis.call('decrby', KEYS[1], ARGV[1]) " +
"redis.call('incrby', KEYS[2], ARGV[1]) " +
"end";
jedis.eval(luaScript, 2, "accountA", "accountB", "100");在这个Lua脚本的示例中,我们只需在服务器端就可以完成所有的任务,而不需要担心数据在执行过程中被其他客户端修改。这个版本的代码更简洁易懂,性能也更好。
因此,若要保证数据的一致性和原子性,绝大多数开发者会选择使用Lua脚本完成Redis的事务操作。