• redis事务、乐观锁和悲观锁以及秒杀测试案例



    redis其他笔记链接:
    redis简介及八种数据类型
    redis事务、乐观锁和悲观锁以及秒杀测试案例
    redis持久化储存(RDB、AOF)和主从复制
    redis缓存穿透、击穿、雪崩及解决方案&&分布式锁
    参考学习视频链接

    一、事务

    Redis事务定义

    Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

    Redis事务的主要作用就是串联多个命令防止别的命令插队

    Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

    • 批量操作在发送 EXEC 命令前被放入队列缓存。
    • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
    • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

    一个事务从开始到执行会经历以下三个阶段:

    • 开始事务。
    • 命令入队。
    • 执行事务。

    Redis事务三大特性

    • 单独的隔离操作

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

    • 没有隔离级别的概念

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

    • 不保证原子性

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

    Multi、Exec、discard

    命令描述
    discard取消事务,放弃执行事务块内的所有命令。
    exec执行所有事务块内的命令。
    multi标记一个事务块的开始。
    UNWATCH取消 WATCH 命令对所有 key 的监视。
    watch [key1]…监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

    事务的错误处理

    组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
    在这里插入图片描述

    如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

    在这里插入图片描述

    乐观锁:

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

    悲观锁:

    悲观锁(Pessimistic Lock),每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁

    十、秒杀测试案例

    测试工具

    Apache Bench

    安装

    • yum -y install httpd-tools

    测试命令

    • ab -n 500 -c 100 -p postfile -T application/x-www-form-urlencoded http://192.168.1.105:8080/Seckill/doseckill
    参数描述
    -n测试次数
    -c并发线程个数
    -pPOST数据文件(参数文件)
    -TContent-type头信息
    并发控制
    • 请求次数的量巨大,避免重复创建对象导致时间问题,使用连接池来解决超时问题
    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.135.100", 6379, 60000 );
    				}
    			}
    		}
    		return jedisPool;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    以常规的形式去测试,会发现在请求数大于秒杀库存的情况下,会出现库存变为负值的情况

    • 加入事务控制(乐观锁)来优化案例
    //秒杀过程
    	public static boolean doSecKill(String uid,String prodid) throws IOException {
    		//1 uid和prodid非空判断
    		if (uid == null || prodid == null) {
    			return false;
    		}
    
    		//2 连接redis
    		//可以解决超时问题
    		JedisPool pool = JedisPoolUtil.getJedisPoolInstance();
    		Jedis jedis = pool.getResource();
    		//通过连接池得到jedis对象
    
    
    		//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(uid, userKey)) {
    			System.out.println("已经秒杀过不能再秒杀了");
    			jedis.close();
    			return false;
    		}
    
    		//6 判断如果商品数量,库存数量小于1,秒杀结束
    
    		if (Integer.parseInt(kc) <= 0) {
    			System.out.println("秒杀已结束");
    			jedis.close();
    			return false;
    		}
    
    
    		//7 秒杀过程
    		//使用事务
    		Transaction transaction = jedis.multi();
    
    
    		//组队操作
    		//7.1 库存-1
    		//7.2 把秒杀成功用户添加清单里面
    		transaction.decr(kcKey);
    		transaction.sadd(userKey, uid);
    		//执行
    		List<Object> list = transaction.exec();
    		if (list != null && list.size() != 0) {
    			System.out.println("秒杀成功");
    		} else {
    			System.out.println("秒杀失败");
    		}
    
    		//7.1 库存-1
    		//7.2 把秒杀成功用户添加清单里面
    		/*jedis.decr(kcKey);
    
    		jedis.sadd(userKey, uid);*/
    
    		jedis.close();
    		return true;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    这种方式在库存与请求数差别不大的时候可能会导致冗余库存, 用悲观锁可以有效解决这个问题

    • 使用lua脚本实现悲观锁
    	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..\":user\";\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;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
  • 相关阅读:
    保洁实业如何使用虚拟机器人提高安全性
    设计模式
    华为云国际版实名账号:亚太已发展超2500个本地生态伙伴 超50%收入由伙伴创造
    React Hooks ——性能优化Hooks
    用vuex对token/refresh_token 进行管理以及处理token过期问题
    【李宏毅 | 深度学习】自注意力机制(Self-attention)
    第四十四周:文献阅读 + SG滤波+基于LSTM的编码器-解码器
    模拟大数相加
    uniapp vue2 首页生命周期函数等待app.vue加载完毕后执行
    Vue2和Vue3的区别——实例创建、响应式数据代理、v-for和v-if优先级、生命周期
  • 原文地址:https://blog.csdn.net/weixin_51405802/article/details/127125267