Redis6总纲
https://www.jianshu.com/p/901dc5a0c683
目录
一、基本操作(Multi、Exec、discard)
二、乐观锁和悲观锁
三、秒杀案例
一、基本操作
1、Redis的事务定义
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
Redis事务的主要作用就是串联多个命令防止别的命令插队
2、Multi、Exec、discard
从输入Multi命令开始(开启事务),输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。
(1)组队案例
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
(2)放弃组队
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> discard
1) OK
(3)任何一个组队失败,都不会执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
3、事务的错误处理
(1)组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。(就是上面3的操作)
(2)如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
案例:字符串+1
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
二、乐观锁和悲观锁
1、事务冲突问题
一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000
2、悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
3、乐观锁
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
4、WATCH key [key ...]
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
5、unwatch
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
6、Redis事务三特性
1、单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
2、没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
3、不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
三、秒杀案例
1、案例:计数器和人员记录
初版秒杀代码(没有事务)
@RestController
@RequestMapping("/test")
public class TestController {
// 秒杀过程
@RequestMapping("/doSecKill")
public static boolean doSecKill(String uid, String prodid) throws Exception {
uid = String.valueOf ((int) (Math.random ( ) * 10000));
prodid = "0101";
// 1、入参的判空
if (uid == null || prodid == null) {
return false;
}
// 2.连接redis
Jedis jedis = new Jedis ("106.52.23.202", 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;
}
}
redis 添加测试数据
[root@VM-0-13-centos redis]# docker exec -it redis redis-cli
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set sk:0101:qt 10
OK
127.0.0.1:6379> keys *
1) "sk:0101:qt"
127.0.0.1:6379> keys *
1) "sk:0101:user"
2) "sk:0101:qt"
127.0.0.1:6379> get sk:0101:qt
"9"
127.0.0.1:6379> smembers sk:0101:user
1) "1025"
-------------------在客户端访问(进行秒杀)-------------------------
-----点了三次后-----
127.0.0.1:6379> smembers sk:0101:user
1) "1025"
2) "2281"
3) "2756"
4) "2927"
127.0.0.1:6379> get sk:0101:qt
"6"
127.0.0.1:6379>
2、用 ab工具 模拟秒杀并发
(1)安装,yum install httpd-tools
(2)编写并发文件 和格式
vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。
内容:
prodid=0101&
2000个请求,每次200个并发同时进行
ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://106.52.23.202:8080/test/doSecKill
问题一:超卖,最终出现了问题,-4
问题二:连接超时问题
3、解决以上问题
(1)连接池工具类(防止超时)
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
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;
}
// 释放资源, 3.0 后重新启用 close
public static void release(Jedis jedis) {
if (null != jedis) {
jedis.close ();
}
}
}
(2)解决超卖代码(修改上面的方法)
public static boolean doSecKill(String uid, String prodid) throws Exception {
uid = String.valueOf ((int) (Math.random ( ) * 10000));
prodid = "0101";
// 1、入参的判空
if (uid == null || prodid == null) {
return false;
}
// 2.连接redis
//Jedis jedis = new Jedis ("106.52.23.202", 6379);
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);
multi.sadd (userKey,uid);
//执行
List
(3)库存遗留问题
秒杀完之后,还有库存:
如果同时有2000人抢到500个商品,其中一个人抢到了并且把版本号从1.0改成2.0,就会导致其余1999个人版本号不同,无法再往加操作,导致修改失败,乐观锁带来的弊端。
(4)库存遗留问题解决方案:用lua脚本语言(仅支持redis2.6以上):
原理:实际是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
lua代码:
第一行,第二行:表示定义两个变量,传入两个参数
第三行、第四行:拼接库存key,用户key
第五行:判断当前用户是否在清单中存在(sismember和代码中5,调用)
returm 2,表示一个约定,秒杀过了
returm 0,表示一个约定,秒杀结束
returm 1,表示一个约定,秒杀成功
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr';
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",qtkey);
redis.call("sadd",usersKey,userid);
end
return 1;
lua脚本工具类
public class SecKill_redisByScript {
static String secKillScript = "local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":usr\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1";
static String secKillScript2 =
"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
" return 1";
public static boolean doSecKill(String uid, String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance ( );
Jedis jedis = jedispool.getResource ( );
//String sha1= .secKillScript;
String sha1 = jedis.scriptLoad (secKillScript);
Object result = jedis.evalsha (sha1, 2, uid, prodid);
String reString = String.valueOf (result);
if ("0".equals (reString)) {
System.err.println ("已抢空!!");
} else if ("1".equals (reString)) {
System.out.println ("抢购成功!!!!");
} else if ("2".equals (reString)) {
System.err.println ("该用户已抢过!!");
} else {
System.err.println ("抢购异常!!");
}
jedis.close ( );
return true;
}
}
lua调用
boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);
小坑记录
1、springboot 打 jar失败
把插件的版本号去掉