Redis Key-Value数据库 【实战】_获取缓存中value-程序员宅基地

技术标签: 中间件  lua  分布式锁  数据库  redis  

相关链接:
Redis Key-Value数据库【初级】:https://blog.csdn.net/qq_41822345/article/details/125527045
Redis Key-Value数据库【高级】:https://blog.csdn.net/qq_41822345/article/details/125568007
Redis Key-Value数据库【实战】:https://blog.csdn.net/qq_41822345/article/details/125568012

一、手机验证码【简单】

需求:

1、输入手机号,点击发送后随机生成6位数字码,2分钟有效。
2、输入验证码,点击验证,返回成功或失败。
3、每个手机号每天只能输入3次。

import redis.clients.jedis.Jedis;
import java.util.Random;

public class PhoneCode {
    
    public static void main(String[] args) {
    
        //模拟验证码发送
        String code = sendCode("18896725688");
        System.out.println("获取验证码:" + code);
        //模拟验证码校验
        verifyCode("18896725688", code);
    }

    //step2: 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120
    public static String sendCode(String phone) {
    
        //连接redis
        Jedis jedis = new Jedis("192.168.168.101", 6379);
        //拼接key
        //手机发送次数key
        String countKey = "VerifyCode" + phone + ":count";
        //验证码key
        String codeKey = "VerifyCode" + phone + ":code";
        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if (count == null) {
    
            //没有发送次数,第一次发送
            //设置发送次数是1,过期时间是1天
            jedis.setex(countKey, 24 * 60 * 60, "1");
        } else if (Integer.parseInt(count) <= 2) {
    
            //发送次数+1
            jedis.incr(countKey);
        } else if (Integer.parseInt(count) > 2) {
    
            //发送三次,不能再发送
            System.out.println("今天发送次数已经超过三次");
            jedis.close();
        }

        //发送验证码放到redis里面
        String vCode = getCode();
        //过期时间是120s
        jedis.setex(codeKey, 120, vCode);
        jedis.close();
        return vCode;
    }

    //step3: 验证码校验
    public static void verifyCode(String phone, String code) {
    
        //从redis获取验证码
        Jedis jedis = new Jedis("192.168.168.101", 6379);
        //验证码key
        String codeKey = "VerifyCode" + phone + ":code";
        String redisCode = jedis.get(codeKey);
        //判断
        if (redisCode.equals(code)) {
    
            System.out.println("成功");
        } else {
    
            System.out.println("失败");
        }
        jedis.close();
    }

    //step1: 生成6位数字验证码
    public static String getCode() {
    
        Random random = new Random();
        String code = "";
        for (int i = 0; i < 6; i++) {
    
            int rand = random.nextInt(10);
            code += rand;
        }
        return code;
    }
}

二、Redis-事务-锁机制【原理】

1、redis事务

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

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

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

  • Redis事务命令:multi、exec、discard
## 组队成功,提交成功
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 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) OK
## 组队阶段报错,提交失败  #组队阶段某个命令出现了报告错误,执行时整个的所有队列都会被取消。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set m1 n1
QUEUED
127.0.0.1:6379(TX)> set m2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> set m3 n3
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
## 组队成功,提交有成功有失败情况 #执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set p1 q1
QUEUED
127.0.0.1:6379(TX)> incr p1
QUEUED
127.0.0.1:6379(TX)> set p2 q2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> 
  • Redis事务命令:watch、unwatch

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

unwatch用来取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH。

2、redis事务锁

如果多个事务同时操作一个资源,可能会出现并发不安全甚至错误的现象。比如:有金额10000,同一时间内容共有三个请求:一个请求想给金额减8000;一个请求想给金额减5000;一个请求想给金额减1000。

在这里插入图片描述

所以需要有锁来保证并发安全

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

在这里插入图片描述

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

在这里插入图片描述

3、redis事务三特性

  • 单独的隔离操作
    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念
    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
  • 不保证原子性
    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

三、Redis秒杀案例【实操】

本案例代码位于:https://download.csdn.net/download/qq_41822345/85885803

案例:某商品有库存n个,规定秒杀活动中,每个用户最多能购买1个。

秒杀成功结果:商品库存减少个数=秒杀成功的用户个数

1、ab测试

使用工具ab模拟高并发请求测试,安转命令如下:

yum install httpd-tools
  • 代码1——基础版本

    基础版本的代码有两个问题:1、高并发会有超卖问题;2、数据库连接超时问题。

# 使用工具ab模拟高并发
[root@k8s101 myredis]# pwd
/root/myredis
[root@k8s101 myredis]# cat postfile 
prodid=0101&
[root@k8s101 myredis]# ab -n 1000 -c 100 -k -p postfile -T application/x-www-form-urlencoded http://192.168.149.1:8080/Seckill/doseckill

在这里插入图片描述

import java.io.IOException;
import redis.clients.jedis.Jedis;
/**
 * 基础版本的代码有两个问题:1、高并发会有超卖问题;2、数据库连接超时问题。
 */
public class SecKill_base {
    
    public static void main(String[] args) {
    
        Jedis jedis = new Jedis("192.168.168.101", 6379);
        System.out.println(jedis.ping());
        jedis.close();
    }

    //秒杀过程
    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("192.168.168.101", 6379);
        //3 拼接key
        // 3.1 库存key  set sk:0101:qt 100
        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;
    }
}

2、超卖问题—乐观锁

  • 代码2——乐观锁+连接池

    乐观锁代码可以解决问题:1、高并发会有超卖问题;
    连接池可以解决问题:2、数据库连接超时问题。

    但是高并发可能会带来新的问题:库存遗留问题[在库存多的情况下容易复现]。【高并发下,当第一个客户端会修改版本号,这时如果剩余其它客户端会发现版本号已经修改,无法占用库存,导致库存遗留

import java.io.IOException;
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
/**
 * 乐观锁代码可以解决问题:1、高并发会有超卖问题;
 * 连接池可以解决问题:   2、数据库连接超时问题。
 *
 * 更高并发可能会带来新的问题:库存遗留问题
 */
public class SecKill_redis {
    
    public static void main(String[] args) {
    
        Jedis jedis = new Jedis("192.168.168.101", 6379);
        System.out.println(jedis.ping());
        jedis.close();
    }

    //秒杀过程
    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("192.168.168.101",6379);
        //通过连接池得到jedis对象    2、数据库连接超时问题。
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedisPoolInstance.getResource();
        
        //3 拼接key
        // 3.1 库存key  set sk:0101:qt 100
        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<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;
    }
}

3、高并发导致连接超时问题—连接池

通过创建数据库连接池,解决连接超时问题。连接池配置如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

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); // 控制一个pool可分配多少个jedis实例
					poolConfig.setMaxIdle(32);   //控制一个pool最多有多少个状态为idle(空闲)的jedis实例
					poolConfig.setMaxWaitMillis(100*1000); //表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
					poolConfig.setBlockWhenExhausted(true);
					poolConfig.setTestOnBorrow(true);  // 获得一个jedis实例的时候是否检查连接可用性(ping  PONG)
					jedisPool = new JedisPool(poolConfig, "192.168.168.101", 6379, 60000 );
				}
			}
		}
		return jedisPool;
	}
}

4、高并发且乐观锁导致库存遗留问题—LUA脚本

Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言

很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

redis使用lua的优势

将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

redis 在2.6版本以后,通过lua脚本解决争抢问题,实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

在这里插入图片描述

  • 代码3——LUA脚本:利用lua脚本淘汰用户,解决超卖问题。
import java.io.IOException;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class SecKill_redisByScript {
    
    public static void main(String[] args) {
    
        JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedispool.getResource();
        System.out.println(jedis.ping());
    }
    // 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..\":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";

    public static boolean doSecKill(String uid, String prodid) throws IOException {
    
        JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = jedispool.getResource();
        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;
    }
}

四、Redis应用问题解决

1、缓存穿透

访问原来就不存在的数据。 key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

在这里插入图片描述

1.1、缓存穿透解决方案

一个一定不存在缓存或查询不到的数据,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

解决方案:

(1) 对空值缓存: 如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟

(2) 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难

(4) **进行实时监控:**当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。

2、缓存击穿

访问某个原本存在缓存中的数据缓存过期。key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候高并发的请求可能会瞬间把后端DB压垮。

2.1、缓存击穿解决方案

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

在这里插入图片描述

解决方案:

(1)预先设置热门数据: 在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长。

(2)实时调整: 现场监控哪些数据热门,实时调整key的过期时长。

(3)使用锁:

a、就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。

b、先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key。

c、当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key。

d、当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

3、缓存雪崩

访问批量存在缓存中的数据缓存过期【同时过期】。key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
在这里插入图片描述

3.1、缓存雪崩解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕!

解决方案:

(1) 构建多级缓存架构: nginx缓存 + redis缓存 +其他缓存(ehcache等)

(2) 使用锁或队列

用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。

(3) 设置过期标志更新缓存:

记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。

(4) 将缓存失效时间分散开:

比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

4、分布式锁【核心】

本案例代码位于:https://download.csdn.net/download/qq_41822345/85885803

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案

  • 基于数据库实现分布式锁

  • 基于缓存(Redis等)

  • 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点

  • 性能:redis最高

  • 可靠性:zookeeper最高

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须具有原子性。

redis命令

set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
# eg:
set sku:1:info "OK" NX PX 10000
#EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value
#PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value
#NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 
#XX :只在键已经存在时,才对键进行设置操作
4.1、代码实现1——基本实现分布式锁功能

在这里插入图片描述

  1. 多个客户端同时获取锁(setnx)
  2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
  3. 其他客户端等待重试
//基本实现可能会出现问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放
//解决方案:给锁设置过期时间
@GetMapping("testLock")
public void testLock(){
    
    //1获取锁,setne
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    //2获取锁成功、查询num的值
    if(lock){
    
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判断num为空return
        if(StringUtils.isEmpty(value)){
    
            return;
        }
        //2.2有值就转成成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
        //2.4释放锁,del
        redisTemplate.delete("lock");

    }else{
    
        //3获取锁失败、每隔0.1秒再获取
        try {
    
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
    }
}

ab模拟高并发:

127.0.0.1:6379> set num 0
OK
127.0.0.1:6379> get num
"0"
[root@k8s101 ~]# ab -n 1000 -c 100 http://192.168.149.1:8080/redisTest/testLock
127.0.0.1:6379> get num
"1000"
4.2、代码实现2——优化锁过期时间

在这里插入图片描述

//设置过期时间有两种方式:
//1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
//2. 在set时指定过期时间(推荐)
//设置过期时间可能会引起的问题:可能会释放其他服务器的锁。
@GetMapping("testLock")
    public void testLock(){
    
        //1获取锁,setnx
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","1111",3, TimeUnit.SECONDS);
        //2获取锁成功、查询num的值
        if(lock){
    
            Object value = redisTemplate.opsForValue().get("num");
            //2.1判断num为空return
            if(StringUtils.isEmpty(value)){
    
                return;
            }
            //2.2有值就转成成int
            int num = Integer.parseInt(value+"");
            //2.3把redis的num加1
            redisTemplate.opsForValue().set("num", ++num);
            //2.4释放锁,del
            //判断比较uuid值是否一样
            String lockUuid = (String)redisTemplate.opsForValue().get("lock");
            redisTemplate.delete("lock");
        }else{
    
            //3获取锁失败、每隔0.1秒再获取
            try {
    
                Thread.sleep(100);
                testLock();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        }
    }
4.3、代码实现3——优化防UUID误删

在这里插入图片描述

//场景:如果业务逻辑的执行时间是7s。执行流程如下
//1.index1业务逻辑没执行完,3秒后锁被自动释放。
//2.index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
//3.index3获取到锁,执行业务逻辑
//4.index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。
//最终等于没锁的情况。
//解决方案:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
@GetMapping("testLock")
public void testLock() {
    
        //setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
        String uuid = UUID.randomUUID().toString();
        //1获取锁,setnx
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
        //2获取锁成功、查询num的值
        if (lock) {
    
            Object value = redisTemplate.opsForValue().get("num");
            //2.1判断num为空return
            if (StringUtils.isEmpty(value)) {
    
                return;
            }
            //2.2有值就转成int
            int num = Integer.parseInt(value + "");
            //2.3把redis的num加1
            redisTemplate.opsForValue().set("num", ++num);
            //2.4释放锁,del
            //判断比较uuid值是否一样
            String lockUuid = (String) redisTemplate.opsForValue().get("lock");
            if (lockUuid.equals(uuid)) {
    
                redisTemplate.delete("lock");
            }
        } else {
    
            //3获取锁失败、每隔0.1秒再获取
            try {
    
                Thread.sleep(100);
                testLock();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        }
    }
4.4、代码实现4——优化使用LUA脚本保证原子性
//问题:删除操作缺乏原子性。
//场景:
//1.index1执行删除时,查询到的lock值确实和uuid相等
//2.index1执行删除前,lock刚好过期时间已到,被redis自动释放
//3.index2获取了lock
//index2线程获取到了cpu的资源,开始执行方法
//4.index1执行删除,此时会把index2的lock删除
//index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行,除了的index2的锁!
//解决方案:使用A脚本保证删除的原子性
@GetMapping("testLockLua")
public void testLockLua() {
    
        //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
        String uuid = UUID.randomUUID().toString();
        //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
        String skuId = "25"; // 访问skuId 为25号的商品 100008348542
        String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
        // 3 获取锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
        // 第一种: lock 与过期时间中间不写任何的代码。
        // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
        // 如果true
        if (lock) {
    
            // 执行的业务逻辑开始
            // 获取缓存中的num 数据
            Object value = redisTemplate.opsForValue().get("num");
            // 如果是空直接返回
            if (StringUtils.isEmpty(value)) {
    
                return;
            }
            // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
            int num = Integer.parseInt(value + "");
            // 使num 每次+1 放入缓存
            redisTemplate.opsForValue().set("num", String.valueOf(++num));
            /*使用lua脚本来锁*/
            // 定义lua 脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            // 使用redis执行lua执行
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            // 设置一下返回值类型 为Long
            // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
            // 那么返回字符串与0 会有发生错误。
            redisScript.setResultType(Long.class);
            // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
            redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
        } else {
    
            // 其他线程等待
            try {
    
                // 睡眠
                Thread.sleep(1000);
                // 睡醒了之后,调用方法。
                testLockLua();
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
        }
    }
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_41822345/article/details/125568012

智能推荐

以MapBox为核心构建Vue地图组件库教程_vue 省份 地图组件库-程序员宅基地

文章浏览阅读951次。不多废话直接讲干货,首先我们要清楚如何写一套组件库,类似于使用vue编写的elementui,使用react编写的antdesign等,我们现在要以GIS为核心写组件库,其实原理类似。一个是组件的主体vue文件,另一个是将组件局部暴露出去的index.js文件,当然你可以再此基础上增加你想要的其他的js文件和vue文件,上面讲的两个文件是必须的。这行命令可以将你写的组件库打包成压缩文件,一般是一个dist静态目录,在进行npm发布的时候也是将这个静态的dist发布在官网上。_vue 省份 地图组件库

【控制control】四足机器人弹簧加载倒立摆(SLIP)动力学模型_【控制control】四足机器人动力学模型-slip-程序员宅基地

文章浏览阅读4.9k次,点赞5次,收藏32次。系列文章目录提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加TODO:写完再整理文章目录系列文章目录前言1.动力学建模构型方法2.四足机器人动力学模型(1)多体动力学模型【针对躯干+脚建模】方法一:VMC( Virtual Model Controller)模型方法二:SLIP模型(2)浮基单体动力学模型【针对躯干建模】【用于MPC】前言认知有限,望大家多多包涵,有什么问题也希望能够与大家多交流,共同成长!本文先对四足机器人动力学模型-VMC、SLIP和浮动机体模型做个简_【控制control】四足机器人动力学模型-slip

html5手指点击速度,CPS手速测试 - 鼠标点击速度测试插件-程序员宅基地

文章浏览阅读1.2w次。CPS手速测试插件背景简介为了刺激客户的消费很多购物平台都推出来秒杀抢购的活动,在这个活动中如果你的手速慢就抢不到商品,所以有时我们会需要锻炼一下自己的手速,那如何知道自己的手速是快还是慢呢,在世界平均范围中又处于何种地步,今天小编为大家推荐一款可以检测自己手速的插件CPS手速测试。CPS手速测试插件简介CPS手速测试插件是一款可以在线测试鼠标点击速度的检测工具,它可以是1/3/5/10/15/3..._测速度插件

VLAN以及三层交换机_核心交换机如何查询vlan-程序员宅基地

文章浏览阅读2k次,点赞2次,收藏7次。VLAN以及三层交换机1、VLAN概述与优势1.1vlan概述1.2vlan优势1.3VLAN的分类Trunk概述三层交换技术1、VLAN概述与优势1.1vlan概述VLAN(Virtual Local Area Network),中文称为虚拟局城网。是一组逻辑上隔离的设备和用户。这些设备和用户不受物理位置限制,可根据部门成组等进行灵活划分,保障信息安全。同时隔绝广播信息,提升网络效能,防止广播风暴的产生。1.2vlan优势1. 限制广播域。广播域被限制在一个VLAN内,提高了网络处理能力。 2_核心交换机如何查询vlan

clearTimeout无效_cleartimeout不生效-程序员宅基地

文章浏览阅读7.3k次。如图所示clearTimeout接受id作为参数,所以检查一下是否传入的不是id因为默认情况下setTimeout方法是会返回id但有时候会返回一个setTimeout对象比如使用vsCode 开发的同学在使用setTimeout时会自动引入timer对象,此时setTimeout就会返回Timeout对象,此时只需要将对应的id传入即可或者直接将引用注掉..._cleartimeout不生效

安卓发送post请求_android post-程序员宅基地

文章浏览阅读1.6k次。在HTTP通信中使用最多的就是GET和POST了,GET请求可以获取静态页面,也可以把参数放在URL字符串的后面,传递给服务器。本文将使用标准Java接口HttpURLConnection,以一个实例演示如何使用POST方式向服务器提交数据,并将服务器的响应结果显示在Android客户端。在Android中,提供了标准Java接口HttpURLConnection和Apache接口HttpClient,为客户端HTTP编程提供了丰富的支持。将提交的数据写入Log\Log.php文件中。_android post

随便推点

探索iOS转场动画_ios 转场动画-程序员宅基地

文章浏览阅读1.5k次,点赞2次,收藏3次。iOS提供图像转场动画,可实现酷炫的转场特效。动画包括:溶解、折叠、复印机、闪烁、翻页、波纹、滑动等等。_ios 转场动画

Java 本地内存 & 直接内存 & 元空间_java 本地内存和直接内存-程序员宅基地

文章浏览阅读4.1k次,点赞7次,收藏26次。Java虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机内存;同时对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。元空间不在虚拟机中,而是使用本地内存,JVM不会再出现方法区的内存溢出问题。..._java 本地内存和直接内存

C++静态库与动态库_c++的静态库 类一定是静态的吗?-程序员宅基地

文章浏览阅读158次。C++静态库与动态库这次分享的宗旨是——让大家学会创建与使用静态库、动态库,知道静态库与动态库的区别,知道使用的时候如何选择。这里不深入介绍静态库、动态库的底层格式,内存布局等,有兴趣的同学,推荐一本书《程序员的自我修养——链接、装载与库》。什么是库库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质..._c++的静态库 类一定是静态的吗?

快速上手JDBC_jdbc url用于标识一个被注册的驱动程序,驱动程序管理器通过与这个url选择正确-程序员宅基地

文章浏览阅读793次。目录一、步骤二、ORM思想三、如何获取连接1.Driver接口实现类2.URL3.用户名和密码4.代码示例四、如何实现CRUD操作1.PreparedStatement 与 Statement ​2.ResultSet与ResultSetMetaData3.增删改代码示例(通用)4.查询代码示例(通用)一、步骤1.导入所需数据库的jar包,(这里以mysql为例)2.获取连接3.连接后进行CRUD操作二、ORM思想..._jdbc url用于标识一个被注册的驱动程序,驱动程序管理器通过与这个url选择正确

【安卓学习之第三方库】 Rxpay学习:支付宝微信支付-程序员宅基地

文章浏览阅读990次。█ 【安卓学习之第三方库】 Rxpay学习:支付宝微信支付█ 相关文章:【安卓学习之第三方库】库的使用2-jar类库的使用(以dom4j为例)和升级(以极光推送为例)【安卓学习之第三方库】 消息推送之极光推送【安卓学习之第三方库】 消息推送之阿里云推送 █ 读前说明:本文通过学习别人写demo,学习一些课件,参考一些博客,’学习相关知识,如果涉及侵权请告知本文只简单罗列相关的代码实现过程涉及到的逻辑以及说明也只是简单介绍,主要当做笔记,了解过程而已下载demo█ 在An_rxpay

#671. 优美!最长上升子序列、#736. 上帝的集合_优美!最长上升子序列-程序员宅基地

这篇文章是关于一个优美的问题:如何找出给定数组中最长的递增子序列。文章提出了解决这个问题的方法,并给出了相应的代码实现。

推荐文章

热门文章

相关标签