Redis单机是支持事务的,Redis的事务是下面4个命令来实现:
1.multi,开启Redis的事务,置客户端为事务态。
2.exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态。
3.discard,取消事务,置客户端为非事务态。
4.watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。
将是否有watch命令分为普通类型事务和CAS(Check And Set)类型事务,无watch命令的为普通类型事务,有watch命令的为CAS类型事务。
但是对于Redis集群来说,以上这些命令都不支持集群模式,当使用spring-data-redis的RedisTemplate在集群中设置了setEnableTransactionSupport(true)时,执行命令就会报MUTLI is currently not supported in cluster mode. 如果是使用Jedis的JedisCluster,可以看到JedisCluster里没有multi命令。
如何实现Redis的事务,保证业务的原子性?
Redis在2.6以后的版本中增加了Lua脚本的功能,可以通过eval命令,直接在RedisServer环境中执行Lua脚本,并且可以在Lua脚本中调用Redis命令。
使用脚本的好处:
1.减少网络开销:可以把一些要批量处理的功能,发在一个脚本里面执行,减少客户端和redis的交互次数。
2.原子操作:这主要就是我们在这边主要利用的功能,在分布式环境下保证数据的原子性。
3.复用:客户端发送的脚本会永久的存储在redis中,这就意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。
于是使用以下Java代码,执行一串lua脚本:
String script = "redis.call('LPUSH', KEYS[1], ARGV[1]);"
+ "local is_exists = redis.call('EXISTS', KEYS[1]);"
+ "if is_exists == 1 then\n"
+ "redis.call('HINCRBY', KEYS[2], ARGV[2], 1);\n"
+ "else\n"
+ "redis.call('HSET', KEYS[2], ARGV[2], 1);\n"
+ "end";
List keys = Arrays.asList("key1", "key2");
List args = Arrays.asList("a1", "a2");
jedisCluster.eval(script, keys, args);
但是程序仍旧报错:No way to dispatch this command to Redis Cluster because keys have different slot。
查看eval源码,发现在run方法中是这样实现的:
public T run(int keyCount, String... keys) {
if (keys == null || keys.length == 0) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
}
// For multiple keys, only execute if they all share the
// same connection slot.
if (keys.length > 1) {
int slot = JedisClusterCRC16.getSlot(keys[0]);
for (int i = 1; i < keyCount; i++) {
int nextSlot = JedisClusterCRC16.getSlot(keys[i]);//这里查找对应的slot
if (slot != nextSlot) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster "
+ "because keys have different slots.");
}
}
}
}
实际上,假设在6节点集群中,有3个master,3个slave,每个slave都有对应的masterid,每个master都有对应的slot范围。在ClusterNodeInformationParser中,去解析每一行并将对应的slot填充进去,因为只有master上有slot,因此不会填充slave的slot。因此,当我们正常地通过访问JedisCluster的get/set时,通过计算key的slot来获取对应的Jedis Connection,根本不会使用到slave,只会访问master节点。只有一种情况,在tryRandomMode开启时(此时,正常通过slot无法获取有效连接时,可能考虑重新排序)。可以看出redis cluster的slot范围:0-16383,可以采用二分查找的方式,以上面为例,可以分成3个部分的范围slot,以其开头为标识,通过Collections.binarySearch来进行二分查找搜索。当拿到各个redis key后,通过getSlotByKey方法,获得对应的node编号,最后,当批量查询的keys数组>2时,再进行批量出,否则,只进行单独查询。
尽管两个slot在同一个连接上能够get到值,但是在cluster模式下,是通过slot判断而非节点node判断是否可以进行mget操作,不能靠跳过jedis客户端的方案来完成类似分组操作。
因此,只能通过HASH_TAG来实现cluster模式下的mget/mset批量操作,我们可以在命令行中通过cluster keyslot ${key}来查看某个key对应的slot,可以从Jedis客户端的源码查看对应的keyslot算法。keyslot算法中,如果key包含{},就会使用第一个{}内部的字符串作为hash key,这样就可以保证拥有同样{}内部字符串的key就会拥有相同slot。但是这样的话,本来可以hash到不同的slot中的数据都放到了同一个slot中,所以使用的时候要注意数据不要太多导致一个slot数据量过大,数据分布不均匀!
因此,对于多个key的操作,在redis集群中,仍然无法使用事务,保证其原子性。