1. Redis的事务定义
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
2. Multi、Exec、discard
从输入Multi 命令开始(相当于开启事务),输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。
案例如下:
3. 事务的错误处理
组队过程中某个命令出现了报告错误,执行时整个队列的命令都会被取消,即都不执行:
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他正确的命令都会执行(不会回滚):
4. 事务冲突问题及解决方法
4.1 事务冲突案例
假如一个账户上只有 10000 块钱,现在有三个人来操作这个账户,他们查询账户的时候显示的都是 10000(从数据库查询),一个人想买一个 8000 的东西,另一个人想买一个 5000 的东西,一个想买一个 1000 的东西。如果只按照他们刚查询时的余额来判断,那么最终结果账户里剩余 -4000,这肯定是不对的:
对于上述问题,可以通过悲观锁和乐观锁来解决。
4.2 悲观锁
悲观锁 (Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block(阻塞) 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的缺点是效率低,不能多人同时操作。
上图中:A拿到数据就给数据上锁(橙色部分),B就拿不到数据了(绿色部分),直到A用完后释放锁,B才能拿到数据并且同时给数据上锁。
4.3 乐观锁
乐观锁 (Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候(增删改)会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set 机制实现事务的。
上图中:A和B同时拿到了数据,A是橙色,B是绿色,只不过拿到的是原始数据v1.0版本,A先操作了数据,操作后数据变成了2000,版本号变成了v1.1版本。此时B再想操作数据,操作前会检查B手里的数据v1.0是不是现在的数据v1.1,如果版本号不一致,B就不能再进行操作了。
4.4 watch key [key…]
在执行 multi 之前,先执行 watch key1 [key2]
,可以监视一个(或多个)key ,如果在事务执行之前这个(或这些)key 被其他命令所改动,那么事务将被打断。使用了 watch key,就表示给这个 key 加上了乐观锁,那么在一个事务的执行中,每次操作这个 key 都会检查 “版本号”,如果事务没有执行前该 key 对应的值被改动,那么次事务就失败。
4.5 unwatch
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
5. Redis 事务三特性
1、单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
2、没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
3、不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
6. Redis 事务秒杀案例
代码在老师提供的资料 Seckkill 项目里面。
6.1 解决计数器和人员记录的事务操作
假如有一批商品要拿出来做秒杀活动,在秒杀的过程中实际上就是两个操作,一个是商品数减一,另一个就是秒杀成功的用户加入到数据库:
6.2 秒杀案例——无并发
代码模板在老师提供的资料里面。首先找到入口程序 SecKillServlet:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//生成userID和商品ID
String userid = new Random().nextInt(50000) +"" ;
String prodid =request.getParameter("prodid");
//传入userID和商品ID,调用秒杀方法
boolean isSuccess=SecKill_redis.doSecKill1(userid,prodid);
response.getWriter().print(isSuccess);
}
初版秒杀方法 在 SecKill_redis 类里面如下:
//秒杀过程(初版)
public static boolean doSecKill1(String uid, String prodid) throws IOException {//传入一个userID和一个商品ID
//1 uid和prodid非空判断
if(uid == null || prodid == null) {
return false;
}
//2 连接redis
Jedis jedis = new Jedis("192.168.88.130",6379);
//3 拼接key
// 3.1 库存key
String kcKey = "sk:"+prodid+":qt";
// 3.2 秒杀成功用户key
String userKey = "sk:"+prodid+":user";
//4 获取库存,如果库存null,表示秒杀还没有开始
String kc = jedis.get(kcKey);
if(kc == null) {
System.out.println("秒杀还没有开始,请等待");
jedis.close();
return false;
}
// 5 判断用户是否重复秒杀操作
if(jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if(Integer.parseInt(kc)<=0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7 秒杀过程
//7.1 库存-1
jedis.decr(kcKey);
//7.2 把秒杀成功用户添加清单里面
jedis.sadd(userKey, uid);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
配置好Tomcat后启动:
页面如下:
点击“秒杀点我”:
首先手动在 redis 中添加商品库存:
然后再次点击“秒杀点我”按钮,控制台输出:
连续点击十次后:
注意,此时只是单机在操作秒杀,但是实际中秒杀会有很多人同时进行,因此会产生并发的问题,但是在并发场景中,上面的代码是有问题的。