一、Redis的事务定义
Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送过来的命令请求打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
Redis中的事务和MySQL中的事务不同。
Redis的事务没有MySQL中的完善,只保证了一致性和隔离性,不满足原子性和持久性。
原子性,redis会将事务中的所有命令执行一遍,哪怕是中间有执行失败也不会回滚。kill信号、宿主机宕机等导致事务执行失败,redis也不会进行重试或者回滚。
提问:redis是单线程为什么还要事务?
虽然redis是单线程,但是可以同时有多个客户端访问,每个客户端相当于一个线程。客户端访问之间存在竞争。当多个客户端并发操作同一Key值时,就会产生类似于多线程操作的现象。
二、Multi、Exec、discard
从输入 Multi 命令开始,输入的命令会依次进入命令队列中,但不会执行。
直到输入 Exec 后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过 discard 来放弃组队。
三、事务的错误处理
组队中某个命令出现了报告错误,执行时整个队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
四、为什么要做成事务
想象一个场景:有很多人有你的账户,同时去参加双十一抢购。
4.1 悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。
4.2 乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
Redis 就是利用这种 check-and-set机制实现事务的。
4.3 WATCH key [key ...]
在执行 multi 之前,先执行watch key1 [key2 ...],可以监视一个或多个key,如果在事务执行之前这个key被其他命令所改动,那么事务将被打断。(乐观锁的过程)
4.4 Redis事务三特性
- 单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的请求所打断。 - 没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被执行。 - 不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
五、Redis事务秒杀案例
5.1 解决计数器和人员记录的事务操作
//秒杀过程(未添加事务)
public static boolean deSecKill(String uid, String prodid) throws IOException {
//1.uid和prodid非空判断
if (uid == null || prodid == null) {
return false;
}
//2.连接redis
Jedis jedis = new Jedis("192.168.121.138", 6379);
//3.拼接key
//3.1 库存key
String kcKey = "sk:" + prodid + ":qt";
//3.2 秒杀成功用户key
String userKey = "sk:" + prodid + ":user";
//4.获取库存,如果库存为null,秒杀还没开始
String exists = jedis.get(kcKey);
if (exists == null) {
System.out.println("秒杀还没开始,请等待");
jedis.close();
return false;
}
//5.判断用户是否重复秒杀
boolean sismember = jedis.sismember(userKey, uid);
if (sismember) {
System.out.println("你已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6.判断商品数量,库存量小于1,秒杀结束
if (Integer.parseInt(exists) <= 0) {
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
//7.秒杀过程
//7.1 库存-1
jedis.decr(kcKey);
//7.2 成功用户添加
jedis.sadd(userKey, uid);
}
5.2 Redis事务-并发模拟
上面的代码能够完成单线程的秒杀,但是实际业务中肯定是高并发的,类似多线程。
模拟高并发
使用工具 ab 模拟测试(Linux中的一个工具)
联网:yum install httpd-tools
# -n:请求数量 -c:并发数量 [-p:参数 -T:Content-type]
ab -n 1000 -c 100 -p ./postfile -T application/x-www-form-urlencoded http://192.168.0.105:8093/redisTest/deSecKill/
5.2 加上watch
@GetMapping("/deSecKill/{prodid}")
public String deSecKill(@PathVariable String prodid) {
StringBuilder userId = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 4; i++) {
userId.append(random.nextInt(10));
}
String uid = userId.toString();
//2.连接redis
Jedis jedis = new Jedis("192.168.121.138", 6379);
//3.拼接key
//3.1 库存key
String kcKey = "sk:" + prodid + ":qt";
//3.2 秒杀成功用户key
String userKey = "sk:" + prodid + ":user";
//在获取库存之前先监视库存
jedis.watch(kcKey);
//4.获取库存,如果库存为null,秒杀还没开始
String exists = jedis.get(kcKey);
if (exists == null) {
System.out.println("秒杀还没开始,请等待");
jedis.close();
return "秒杀还没开始,请等待";
}
//5.判断用户是否重复秒杀
boolean sismember = jedis.sismember(userKey, uid);
if (sismember) {
System.out.println("你已经秒杀成功了,不能重复秒杀");
jedis.close();
return "你已经秒杀成功了,不能重复秒杀";
}
//6.判断商品数量,库存量小于1,秒杀结束
if (Integer.parseInt(exists) <= 0) {
System.out.println("秒杀已经结束");
jedis.close();
return "秒杀已经结束";
}
//7.秒杀过程
//添加事务
Transaction multi = jedis.multi();
//组队
multi.decr(kcKey);
multi.sadd(userKey, uid);
//执行
List<Object> results = multi.exec();
if (results == null || results.size() == 0){
System.out.println("秒杀失败了");
jedis.close();
return "秒杀失败了";
}
System.out.println("秒杀成功...");
return "秒杀成功";
}
5.3 连接池
节省每次连接redis服务带来的消耗,把连接好的实例反复利用(单例模式)。
通过参数管理连接的行为
具体实现,上百度。
5.4 库存遗留问题
这次将库存改为 500
个秒杀库存,2000
个请求,300
个并发。
为什么会出现这个问题?
2000个请求同时发起,有1个先秒杀成功了,就会修改版本号,那么这时另外的1999个获得的版本号就不对了,导致失败。
5.5 解决库存遗留问题
5.5.1 LUA脚本
Lua 是一个小巧的脚本语言,Lua 脚本可以很容易的被 C/C++ 代码调用,也可以反过来调用 C/C++的函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200k,所以 Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
很多应用程序、游戏使用 LUA 作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。
5.5.2 LUA脚本在Redis中的优势
将复杂的或者多步的Redis操作,写成一个脚本,一次提交给Redis执行,减少反复连接Redis的次数。提升性能。
LUA脚本是类似于Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务的操作。
但是注意Redis的LUA脚本功能,只有在Redis2.6以上版本可以使用。
利用LUA脚本淘汰用户,解决超卖问题。
用LUA封装一个事务,并给Redis提供原子性,类似于悲观锁串行执行。