redis事务初探
1.问题情景表述
背景:工作中需要维护一个tcc管理后台,主要用来人工干预tcc事务。tcc事务数据已哈希结构存储在redis服务器上,后台的管理任务也就落实于将当下的tcc事务数据从redis中捞出来,并予以展示,并且在管理界面上赋予“重置”“删除”“确认”等操作入口。操作入口对应的实现就是将指定tcc事务的存储于redis中的数据结构的某一个表示状态的字段设置为相应的状态表征,而问题就出在这个节骨眼上。
问题:当人工操作某条tcc事务数据时,该事务可能已经被业务系统正常消化调了,所以此时再操作“重置”,“删除”,“确认”,那么就相当于人工往redis中添加了一条tcc事务数据。要命的是,这条数据并不完整,因为只有状态字段。而这条不完整的数据,被业务系统处理的时候必然处理异常而报警(哎,相关业务系统为什么要主动处理这条数据呢?是tcc框架推过去的吗?不是tcc框架推过去的,是运行在相关业务中的tcc逻辑主动从redis中拉取得,拉取时对自己感兴趣的key做处理)。
问题说明:redis中是没有数据结构的概念的。我们平常的做法是将一个数据结构使用redis的hash结构来保存,然后redis是不会保证这个数据结构的整体性的:redis的数据操作没有getter,setter限制(其实数据库性质的工具都没有这种程序级别的逻辑限制的),而可以直接操作某个键的值,比如随便的设置字段Field的值,随意增加字段和删除字段。这些操作在数据存储级别当然是没有任何问题的,但是上升到代码级别,就会导致很多问题,比如字段为null(对应删除Filed),整体结构破坏(对应操作增加了Field,当然,高级点的序列化应该能兼容这个问题)。
2.redis事务
解决上面的问题,就是在操作的时候,做一个key值的存在检查:发现当前key还在(即该条tcc事务数据还没有被业务系统消化掉),再来设置hash结构中状态字段Field的值。而且“检查-设置”必须是原子性的,不能被打断。
“多条命令的原子执行”是中常见需求,redis提供了multi操作和pipeline就是用来这么做的。具体可以参见redis.io文档关于"MULTI",“WATCH”,“UNWATCH”,“EXEC”,“DISCARD”,等命令
关于redis事务,可以参见官方文档 redis-transaction
2.1 为什么不能使用redis-transaction
reason:然而java版本中比较流行的一个redis客户端jedis,其官网明确指出,jedis的transaction内部不能使用中间结果,原文如下:
//Redis does not allow to use intermediate results of a transaction within that same transaction. This does not work:
// this does not work! Intra-transaction dependencies are not supported by Redis!
jedis.watch(...);
Transaction t = jedis.multi();
if(t.get("key1").equals("something"))
t.set("key2", "value2");
else
t.set("key", "value");
相关的解决策略:
- watch实现key的监控,不适合key不存在的情况,而且也会遇到“使用中间状态”的问题。
- redis原生命令,比如SETNX,HSETNX;其中HSETNX的语义是如果key不存在才设置哈希的某个字段。如果判断条件相反,这个命令再合适不过了。
2.2 最终的解决方案——lua脚本
其实redis事务这方面的发展:将逐渐弃用Multi五大工具,而使用lua。这一点在官网redis-transaction说的很清楚了。
redis在外部可以调用lua脚本,而lua脚本内部又可以调用redis命令,两者的数据类型一一对应。
第一个lua脚本实例:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
执行lua脚本:
redis-cli --eval singlelock.lua sdfssd , abcd
解释:用–eval可以执行lua脚本文件。注意,使用,来区分KEYS和ARGV这两个参数列表,其中,两边的空格不能省略。官网也有解释
当然,jedis中执行lua脚本就跟简单了,参见jedis源码。
总结
- redis存储数据结构的松散型而导致的结构破坏问题是很普遍的现象。
- redis的transaction的五大工具:WATCH,UNWATCH,MULTI,EXEC,DISCARD,没有pepiline。pepiline是jedis提供的工具。
- 关于redis事务的特点:
- 多个命令打包发送,然后在服务端顺序执行而且不会被其他client发来的命令打断,因为redis是单线程的。
- 执行失败的命令不影响其他命令的执行,参见
- 有执行命令执行失败,整个multi不会回滚
- 不能使用中间状态
- 有网上说,学好redis,先学lua脚本,比较赞同。
继续学习
- redis官网文档:https://redis.io/documentation
- jedis wiki:https://github.com/xetorthio/jedis