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 PONG
jedisPool = 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 库存key
String kcKey = "sk:"+prodid+":qt";
// 3.2 秒杀成功用户key
String 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); // 库存-1
multi.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脚本