Redis Key-Value数据库 【实战】

文章目录

    • 一、手机验证码【简单】
    • 二、Redis-事务-锁机制【原理】
      • 1、redis事务
      • 2、redis事务锁
      • 3、redis事务三特性
    • 三、Redis秒杀案例【实操】
      • 1、ab测试
      • 2、超卖问题—乐观锁
      • 3、高并发导致连接超时问题—连接池
      • 4、高并发且乐观锁导致库存遗留问题—LUA脚本
    • 四、Redis应用问题解决
      • 1、缓存穿透
        • 1.1、缓存穿透解决方案
      • 2、缓存击穿
        • 2.1、缓存击穿解决方案
      • 3、缓存雪崩
        • 3.1、缓存雪崩解决方案
      • 4、分布式锁【核心】
        • 4.1、代码实现1——基本实现分布式锁功能
        • 4.2、代码实现2——优化锁过期时间
        • 4.3、代码实现3——优化防UUID误删
        • 4.4、代码实现4——优化使用LUA脚本保证原子性

相关链接:
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。

Redis Key-Value数据库 【实战】_第1张图片

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

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

Redis Key-Value数据库 【实战】_第2张图片

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

Redis Key-Value数据库 【实战】_第3张图片

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

Redis Key-Value数据库 【实战】_第4张图片

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 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

Redis Key-Value数据库 【实战】_第5张图片

  • 代码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获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

Redis Key-Value数据库 【实战】_第6张图片

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

Redis Key-Value数据库 【实战】_第7张图片

解决方案:

(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压垮。
Redis Key-Value数据库 【实战】_第8张图片

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——基本实现分布式锁功能

Redis Key-Value数据库 【实战】_第9张图片

  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——优化锁过期时间

Redis Key-Value数据库 【实战】_第10张图片

//设置过期时间有两种方式:
//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误删

Redis Key-Value数据库 【实战】_第11张图片

//场景:如果业务逻辑的执行时间是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();
            }
        }
    }

你可能感兴趣的:(中间件,数据库,数据库,redis,lua,分布式锁)