redis事务简介:
redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。
事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
redis事务的主要作用就是串联多个命令防止别的命令插队。
事务命令:
Multi、Exec、discard
输入 Multi 命令后,输入的命令会依次进入命令队列中(这个阶段是组队阶段);直到输入
Exec,所有输入的命令会依次执行(这个阶段是执行阶段。类似先进先出)。
在 Multi 中,如果中途不想再继续事务(或是出现了错误),可以输入 discard 命令来取消。
事务的错误处理:
①组队时有任意命令报告出现错误,执行时整个队列会被取消。会返回一个nil。
②组队时没有命令报告出现错误,执 行时发现有命令报错,则错误命令不执行,其他命令照旧执行,不会回滚。
redis的命令是原子性的,事务是非原子性的。
redis 事务的三特性:
①单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
②没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
③不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
事务冲突
问题:如一个账号同时发出三笔交易请求,当金额单项不超过余额,总额超过余额时。
解决方式:锁(悲观锁、乐观锁)
悲观锁:
当操作一个 key 时,给它上锁,使别人不能操作,只有当自己操作完解锁后,别人才能操作 key。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在操作之前先上锁。
缺点:效率低。
乐观锁:
每次拿数据都假设没有其他人修改,不去上锁,当到了更新操作,会去判断这个数据有没有在此期间被修改。
如同时操作一个数据,都会获得当前数据的版本号,当其中一个操作成功数据后,给数据加版本号,其他人修改时,发现数据版本号不一致,操作失败。抢票就是乐观锁的应用。
乐观锁适用于多读应用类型,这样可以提高吞吐量。redis就是利用这种 check-and-set 机制实现事务的。
执行 mutli 之前,先执行 watch key1 [key2],可以监视一个(或多个)key,如果在事务执行之前这个(或多个)key 被其他命令改动,事务执行将不成功。
unwatch:取消对key的所有监视。
如果在执行 watch 命令之后,exec 命令或 discard 命令先被执行了的话,那么就不需要再执行 unwatch 了。
代码实现秒杀思想
秒杀案例:在多请求多并发情况下,会出现超卖、请求超时问题、库存遗留问题。
请求超时解决方案:用连接池(通过连接池得到 jedis 对象)
超卖解决方案:使用 watch 监视操作 key ,使用事务 multi 进行组队,用 exec 执行事务。判断exec 返回的值,如果为 null 提示秒杀失败
库存遗留解决方案:用redis lua脚本实现基于缓存的分布式锁。(乐观锁造成库存遗留、连接超时问题。redis中默认不能直接使用悲观锁。)可以用LUA脚本语言解决。
Lua:
一个小巧的脚本语言,Lua脚本可以很容易的被 c/c++ 代码调用,也可以反过来调用 c/c++
的函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200k,所以 Lua 不适合作
为开发独立应用程序的语言,而是作为嵌入式脚本语言。
很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
比如魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。
lua脚本 在 redis中的优势:
可以将复杂多步骤的 redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接
redis 的次数,提升性能。
lua 脚本类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务
性的操作。
但是注意 redis 的 lua 脚本功能,只有在 redis 2.6 以上的版本才可以使用。
利用 lua 脚本淘汰用户,解决超卖问题。
redis 2.6 版本以后,通过 lua 脚本解决秒杀问题,实际上是利用 redis 单线程的特性,用
任务队列的方式解决多任务并发问题。
解决超卖、连接池超时、库存依赖问题脚本:
local userid=keys[1]; // 传入参数
local prodid=keys[2];
local qtkey="sk:"..prodid.":qt"; // 拼接库存key、用户key
local userskey="sk:"..prodid.":usr";
# 调用redis中sismenber
local userExists=redis.call("sismenber",userskey,userid);
if tonumber(userExists)==1 then # 看对应用户是否已经买过商品
return 2; # 已经秒杀到商品,不能再买第二次
end
# 调用redis中get 方法
local num=redis.call("get",qtkey);
if tonumber(num)<=0 then # 看 num 是否小于等于0
return 0; # 满足条件,返回0
else
redis.call("decr",qtkey); # 库存减一
redis.call("sadd",userskey,userid);
end
return 1; # 表示秒杀成功
java 类调用:
# 建连接池
JedisPoll jedisPool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPool.getResource();
# 调用lua脚本 LuaScript中存放lua脚本(LuaScript = "lua脚本内容")
String shal = jedis.scriptLoad(LuaScript);
Object result = jedis.evalsha(shal,2,userid,prodid);