1. redis事物
1.1 原理介绍

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
操作1、操作2和操作3一起,首先进行序列化,按从1-2-3的顺序执行。在执行的过程中,不允许有其他的事情插入。——-》事物操作相互隔离,别的操作不能进入当前事物中。
1.2 操作命令 Multi、Exec、discard
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。
案例:
![]() 组队成功,提交成功(queued) ![]() 组队阶段报错,提交失败 ![]() 组队成功,提交有成功有失败情况 ![]() discard 放弃组队 |
|---|
1.3 事务的错误处理
1.3.1 组队中
1.3.2 执行阶段
某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
1.4 为什么要做成事务
1.4.1 问题场景
想想一个场景:有很多人有你的账户,同时去参加双十一抢购.
———》即三个人有同一个账号,账号内1W余额,但购买的商品总价值超过1W,
1.4.2 例子
| 一个请求想给金额减8000 一个请求想给金额减5000 一个请求想给金额减1000 |
|---|
1.4.3 过程分析
和1.3 事物的错误处理—执行阶段一致。
在multi阶段没问题,exec过程中有些执行失败。
1.5 事物冲突解决—锁
1.5.1 悲观锁
操作之前先上锁。
操作之后释放锁。
一个一个按顺序来,不能同时多人进行。
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
1.5.2 乐观锁
操作之前给数据加上版本号,eg: v1.0, v1.1——>每个人都可以得到该版本的数据;
当a对数据进行操作之后,就会相应更新版本号和数值。当B操作时,需要判断b拿到得版本号和现在的是否一致。如果不一样,就不能再进行操作。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
乐观锁的场景—-》抢票
1.6 乐观锁的使用
1.6.1 WATCH key [key …]
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
解析: 打开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 解决计数器和人员记录的事务操作

需要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 高并发产生的问题
- 超卖
- 连接超时—-排队等待处理
2.3 高并发解决超时连接问题
2.3.1 step1: 连接池配置
public static JedisPool getJedisPoolInstance() {if (null == jedisPool) {synchronized (JedisPoolUtil.class) {if (null == jedisPool) {JedisPoolConfig poolConfig = new JedisPoolConfig();poolConfig.setMaxTotal(200);poolConfig.setMaxIdle(32);poolConfig.setMaxWaitMillis(100*1000);poolConfig.setBlockWhenExhausted(true);poolConfig.setTestOnBorrow(true); // ping PONGjedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );}}}return jedisPool;}
2.3.2 通过连接池得到jedis对象
//2 连接redis//Jedis jedis = new Jedis("127.0.0.1",6379);//通过连接池得到jedis对象JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();Jedis jedis = jedisPoolInstance.getResource();
2.4 高并发中的超卖
2.4.1 场景
![]() |
![]() |
|---|---|
2.4.2 超卖问题原理
2.4.3 利用乐观锁淘汰用户解决超卖问题
2.4.4 代码实现
step1: 监视库存
//监视库存jedis.watch(kcKey);
step2: 通过事物—组队—执行的顺序进行减库存/加入成功清单的操作
//监视库存jedis.watch(kcKey);//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;}
step3: 完整代码
/*** 实现秒杀:* step1: 参数合法性验证(非空判断)* step2:连接redis--jedis* step3:拼接2个key(库存key+成功的用户key)* step4:获取库存,如果库存为null,则秒杀还没开始* step5:开始秒杀,如果用户秒杀到了商品,则不能秒杀第二次* step6:当商品剩余数小于1,秒杀结束* step7:秒杀过程:库存-1,把成功用户加入用户列表中去* @param uid 用户id* @param prodid product id* @return* @throws IOException*/public static boolean doSecKill(String uid,String prodid) throws IOException {//1 uid和prodid非空判断if(uid == null || prodid == null) {return false;}//2 连接redis//Jedis jedis = new Jedis("127.0.0.1",6379);//通过连接池得到jedis对象JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();Jedis jedis = jedisPoolInstance.getResource();//3 拼接key// 3.1 库存keyString kcKey = "sk:"+prodid+":qt";// 3.2 秒杀成功用户keyString userKey = "sk:"+prodid+":user";//监视库存jedis.watch(kcKey);//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 秒杀过程: 事物--组队--执行//使用事务Transaction multi = jedis.multi();//组队操作:multi.decr(kcKey); // 库存-1multi.sadd(userKey,uid); //成功清单中加上新的成功用户//执行List<Object> results = multi.exec();if(results == null || results.size()==0) {System.out.println("秒杀失败了....");jedis.close();return false;}//7.1 库存-1//jedis.decr(kcKey);//7.2 把秒杀成功用户添加清单里面//jedis.sadd(userKey,uid);System.out.println("秒杀成功了..");jedis.close();return true;}
2.5 高并发中的库存遗留问题
——》Lua脚本






