1. redis事物

1.1 原理介绍

image.png
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
image.png
操作1、操作2和操作3一起,首先进行序列化,按从1-2-3的顺序执行。在执行的过程中,不允许有其他的事情插入。——-》事物操作相互隔离,别的操作不能进入当前事物中。

1.2 操作命令 Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。
image.png
案例:

image.png
组队成功,提交成功(queued)

image.png
组队阶段报错,提交失败



image.png
组队成功,提交有成功有失败情况


image.png
discard 放弃组队

1.3 事务的错误处理

分为组队中和执行中两种清空:

1.3.1 组队中

某个命令出现了报告错误,执行时整个的所有队列都会被取消。
image.png

1.3.2 执行阶段

某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
image.png

1.4 为什么要做成事务

1.4.1 问题场景

想想一个场景:有很多人有你的账户,同时去参加双十一抢购.
———》即三个人有同一个账号,账号内1W余额,但购买的商品总价值超过1W,

1.4.2 例子

一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000

image.png

1.4.3 过程分析

和1.3 事物的错误处理—执行阶段一致。
在multi阶段没问题,exec过程中有些执行失败。

1.5 事物冲突解决—锁

1.5.1 悲观锁

操作之前先上锁。
操作之后释放锁。
一个一个按顺序来,不能同时多人进行。
image.png
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁。

1.5.2 乐观锁

操作之前给数据加上版本号,eg: v1.0, v1.1——>每个人都可以得到该版本的数据;
当a对数据进行操作之后,就会相应更新版本号和数值。当B操作时,需要判断b拿到得版本号和现在的是否一致。如果不一样,就不能再进行操作。

image.png
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

乐观锁的场景—-》抢票

1.6 乐观锁的使用

1.6.1 WATCH key [key …]

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
image.png
解析: 打开2个窗口,同时watch balance. 当第一个窗口执行了incrby balance 10 之后,版本号会跟着变,此时再在窗口2执行 incrby balance 10就会报错。因为窗口一执行之后,版本好与窗口二的版本号不一致了。

1.6.2 unwatch

取消WATCH命令对所有key 的监视。
如果在执行WATCH命令之后,EXEC命令或DISCARD命令先被执行了的话,那么就不需要再执行UNWATCH了。
http://doc.redisfans.com/transaction/exec.html

1.7 redis事物特性

1.7.1 单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

1.7.2 没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

1.7.3 不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚.

2. Redis事务秒杀案例

2.1 解决计数器和人员记录的事务操作

image.png
需要2个数据记录:一个是库存清单计数,二是成功者的清单。

2.2 Redis事务—秒杀并发模拟

使用工具ab模拟测试
CentOS6 默认安装
CentOS7需要手动安装

2.2.1 联网:yum install httpd-tools

2.2.2 无网络

(1)进入cd /run/media/root/CentOS 7 x86_64/Packages(路径跟centos6不同)
(2)顺序安装
apr-1.4.8-3.el7.x86_64.rpm
apr-util-1.5.2-6.el7.x86_64.rpm
httpd-tools-2.4.6-67.el7.centos.x86_64.rpm

2.2.3 高并发产生的问题

  1. 超卖
  2. 连接超时—-排队等待处理

2.3 高并发解决超时连接问题

2.3.1 step1: 连接池配置

  1. public static JedisPool getJedisPoolInstance() {
  2. if (null == jedisPool) {
  3. synchronized (JedisPoolUtil.class) {
  4. if (null == jedisPool) {
  5. JedisPoolConfig poolConfig = new JedisPoolConfig();
  6. poolConfig.setMaxTotal(200);
  7. poolConfig.setMaxIdle(32);
  8. poolConfig.setMaxWaitMillis(100*1000);
  9. poolConfig.setBlockWhenExhausted(true);
  10. poolConfig.setTestOnBorrow(true); // ping PONG
  11. jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
  12. }
  13. }
  14. }
  15. return jedisPool;
  16. }

2.3.2 通过连接池得到jedis对象

  1. //2 连接redis
  2. //Jedis jedis = new Jedis("127.0.0.1",6379);
  3. //通过连接池得到jedis对象
  4. JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
  5. Jedis jedis = jedisPoolInstance.getResource();

2.4 高并发中的超卖

2.4.1 场景

image.png image.png

2.4.2 超卖问题原理

image.png

2.4.3 利用乐观锁淘汰用户解决超卖问题

—-watch
image.png

2.4.4 代码实现

step1: 监视库存

  1. //监视库存
  2. jedis.watch(kcKey);

step2: 通过事物—组队—执行的顺序进行减库存/加入成功清单的操作

  1. //监视库存
  2. jedis.watch(kcKey);
  3. //4 获取库存,如果库存null,秒杀还没有开始
  4. String kc = jedis.get(kcKey);
  5. if(kc == null) {
  6. System.out.println("秒杀还没有开始,请等待");
  7. jedis.close();
  8. return false;
  9. }
  10. // 5 判断用户是否重复秒杀操作
  11. if(jedis.sismember(userKey, uid)) {
  12. System.out.println("已经秒杀成功了,不能重复秒杀");
  13. jedis.close();
  14. return false;
  15. }
  16. //6 判断如果商品数量,库存数量小于1,秒杀结束
  17. if(Integer.parseInt(kc)<=0) {
  18. System.out.println("秒杀已经结束了");
  19. jedis.close();
  20. return false;
  21. }

step3: 完整代码

  1. /**
  2. * 实现秒杀:
  3. * step1: 参数合法性验证(非空判断)
  4. * step2:连接redis--jedis
  5. * step3:拼接2个key(库存key+成功的用户key)
  6. * step4:获取库存,如果库存为null,则秒杀还没开始
  7. * step5:开始秒杀,如果用户秒杀到了商品,则不能秒杀第二次
  8. * step6:当商品剩余数小于1,秒杀结束
  9. * step7:秒杀过程:库存-1,把成功用户加入用户列表中去
  10. * @param uid 用户id
  11. * @param prodid product id
  12. * @return
  13. * @throws IOException
  14. */
  15. public static boolean doSecKill(String uid,String prodid) throws IOException {
  16. //1 uid和prodid非空判断
  17. if(uid == null || prodid == null) {
  18. return false;
  19. }
  20. //2 连接redis
  21. //Jedis jedis = new Jedis("127.0.0.1",6379);
  22. //通过连接池得到jedis对象
  23. JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
  24. Jedis jedis = jedisPoolInstance.getResource();
  25. //3 拼接key
  26. // 3.1 库存key
  27. String kcKey = "sk:"+prodid+":qt";
  28. // 3.2 秒杀成功用户key
  29. String userKey = "sk:"+prodid+":user";
  30. //监视库存
  31. jedis.watch(kcKey);
  32. //4 获取库存,如果库存null,秒杀还没有开始
  33. String kc = jedis.get(kcKey);
  34. if(kc == null) {
  35. System.out.println("秒杀还没有开始,请等待");
  36. jedis.close();
  37. return false;
  38. }
  39. // 5 判断用户是否重复秒杀操作
  40. if(jedis.sismember(userKey, uid)) {
  41. System.out.println("已经秒杀成功了,不能重复秒杀");
  42. jedis.close();
  43. return false;
  44. }
  45. //6 判断如果商品数量,库存数量小于1,秒杀结束
  46. if(Integer.parseInt(kc)<=0) {
  47. System.out.println("秒杀已经结束了");
  48. jedis.close();
  49. return false;
  50. }
  51. //7 秒杀过程: 事物--组队--执行
  52. //使用事务
  53. Transaction multi = jedis.multi();
  54. //组队操作:
  55. multi.decr(kcKey); // 库存-1
  56. multi.sadd(userKey,uid); //成功清单中加上新的成功用户
  57. //执行
  58. List<Object> results = multi.exec();
  59. if(results == null || results.size()==0) {
  60. System.out.println("秒杀失败了....");
  61. jedis.close();
  62. return false;
  63. }
  64. //7.1 库存-1
  65. //jedis.decr(kcKey);
  66. //7.2 把秒杀成功用户添加清单里面
  67. //jedis.sadd(userKey,uid);
  68. System.out.println("秒杀成功了..");
  69. jedis.close();
  70. return true;
  71. }

2.5 高并发中的库存遗留问题

——》Lua脚本