Redis(三)jedis与锁

1 Jedis

引入依赖
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>4.2.3</version>
        </dependency>
测试连通
    public static void main(String[] args) {
        String host = "192.168.60.100";
        int port = 6379;

        Jedis jedis = new Jedis(host, port);
        String ping = jedis.ping();
        System.out.println(ping);
    }

输出“PONG”表示连接Redis成功

如果显示连接超时,则检查以下几点:

① 检查配置文件bind以及protectedmodel

② 检查防火墙是否关闭

API 基本是前面的命令 这里略过了
案例:模拟短信验证码
    public static void main(String[] args) {
        verifyCodeSend("1568887221");
        System.out.println(verifyCode("1568887221", "94987"));
    }

    public static void verifyCodeSend(String phoneNum) {
        String host = "192.168.60.100";
        int port = 6379;
        Jedis jedis = new Jedis(host, port);

        String countKey = "VERIFY_CODE_COUNT_" + phoneNum;
        String codeKey = "VERIFY_CODE_" + phoneNum;

        // 每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if(count == null) {
            jedis.setex(countKey, 24 * 60 * 60, "1");
        } else if(Integer.parseInt(count) <= 2) {
            jedis.incr(countKey);
        } else {
            System.out.println("发送三次大于三了");
            jedis.close();
            return ;
        }
        // 验证码发送
        String code = generateCode();

        jedis.setex(codeKey, 5 * 60, code);

        System.out.println("发送成功" + code);

        jedis.close();
    }


    public static boolean verifyCode(String phoneNum, String code) {
        String host = "192.168.60.100";
        int port = 6379;
        Jedis jedis = new Jedis(host, port);

        String codeKey = "VERIFY_CODE_" + phoneNum;

        String codeRedis = jedis.get(codeKey);

        if(codeRedis == null) {
            System.out.println("手机号错误");
            jedis.close();
            return false;
        } else {
            boolean result = codeRedis.equals(code);
            jedis.close();
            return result;
        }

    }

    public static String generateCode() {
        Random random = new Random();
        StringBuilder str = new StringBuilder();
        for(int i = 0; i < 6; i++) {
            str.append(random.nextInt(10));
        }
        return str.toString();
    }

感觉这里老师讲的逻辑好像不大对,上面是进行修改后的

2 SpringBoot整合Redis

引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
配置redis
spring:
  redis:
    host: 192.168.60.100
    port: 6379
#    默认数据库连接索引
    database: 0
    timeout: 1800000
#    连接池最大连接数(默认为8,负数表示无限制)
    lettuce:
      pool:
        max-active: 20
    jedis:
      pool:
#        最大阻塞等待时间(默认为-1)
        max-wait: -1
#        最大空闲连接(默认为8)
        max-idle: 5
#         最小空闲连接(默认为-1)
        min-idle: 0

3 Redis事务和锁操作

3.1 简介

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

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

3.2 基本命令
Multi 开启事务
Exec 执行事务
Discard 放弃组队

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

3.3 两个实例
组队期间的错误(即编译的语法错误)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value
QUEUED
127.0.0.1:6379(TX)> set key2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> 

可以看到结果是直接无法排队

执行期间的错误
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 v1
QUEUED
127.0.0.1:6379(TX)> incr key1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range

可以看到组队成功但执行失败,但是第一条语句还是能够执行,也就是Redis

3.4 悲观锁与乐观锁解决事务冲突问题

悲观锁:顾名思义就是很悲观,每次去获取数据的时候都认为别的事务操作会进行修改,因此需要加锁保证别的事务拿不到数据。传统的关系型数据库里面用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都用到了这种锁的机制。

乐观锁:乐观锁则是认为每次获取数据的时候都没有其他事务修改数据,因此不会上锁,只是对数据添加一个版本字段,只有当自己修改数据的时候才去检查当前数据的版本字段和之前自己的版本字段,如果不一致则取消更新。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,Redis就是利用这种check-and-set实现事务的

抢票就是乐观锁的一个典型应用场景,即很多人抢票只能有一个人成功,如果使用悲观锁的话虽然也能够实现但是 单位时间 微观上 只能有一个人在抢票,系统的吞吐量太小

乐观锁基本命令 watch 乐观锁监视数据
127.0.0.1:6379> get balance
"100"
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 110
127.0.0.1:6379> get balance
"100"
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
(nil)

可以看到第二个客户端的事务监测到数据版本和之前的不同终止了事务

乐观锁基本命令 unwatch 停止对所有数据的监视
3.5 Redis事务的三大特性
  • 单独的隔离操作

    • 事务中的所有命令都会序列化 按照顺序执行,不会被其他客户端发来的命令所打断
  • 没有隔离级别的概念

    • 队列中的命令没有提交之前都不会被实际执行
  • 不保证原子性

    • 事务中如果有一条命令执行失败,其他命令依然会被执行,没有回滚

4 事务和锁机制-秒杀案例

案例分析

在redis中使用字段sk:product:qt存储库存,使用set存储购买成功的用户id

原始代码
    //秒杀过程
    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.44.168",6379);
        //通过连接池得到jedis对象
        Jedis jedis = new Jedis("192.168.60.100", 6379);

        //3 拼接key
        // 3.1 库存key
        String kcKey = "sk:" + prodid + ":qt";

        // 3.2 秒杀成功用户key
        String userKey = "sk:" + uid + ":user";


        //4 获取库存,如果库存null,秒杀还没有开始
        String kc = jedis.get(kcKey);
        if(kc == null) {
            System.out.println(uid + "您好,秒杀还没有开始");
            jedis.close();
            return false;
        }

        // 5 判断用户是否重复秒杀操作
        if(jedis.sismember(userKey, uid)) {
            System.out.println(uid + "您好,不能重复购买");
            jedis.close();
            return false;
        }

        //6 判断如果商品数量,库存数量小于1,秒杀结束
        if(Integer.parseInt(jedis.get(kcKey)) <= 0) {
            System.out.println(uid + "您好,秒杀活动已经结束");
            jedis.close();
            return false;
        }


        //7 秒杀过程
        //7.1 库存-1
        jedis.decr(kcKey);
        System.out.println(uid+",购买成功");
        //7.2 把秒杀成功用户添加清单里面
        jedis.sadd(userKey, uid);
        jedis.close();

        return true;
    }

控制台的输出:

34074,购买成功
39830,购买成功
19620,购买成功
9727,购买成功
31847您好,秒杀活动已经结束

此时的redis:

127.0.0.1:6379> keys *
 1) "sk:0101:qt"
 2) "sk:9727:user"
 3) "sk:2618:user"
 4) "sk:34074:user"
 5) "sk:39446:user"
 6) "sk:19620:user"
 7) "sk:19756:user"
 8) "sk:39830:user"
 9) "sk:14215:user"
10) "sk:39589:user"
11) "sk:33691:user"
127.0.0.1:6379> get sk:0101:qt
"0"
使用ab工具进行高并发检测
工具安装
yum install httpd-tools
基本命令

ab -n -c -p -T

-n 表示请求的数量

-c 表示请求中并发的数量

-p 表示请求为post请求的时候的内容

-T 表示请求的类型

             'application/x-www-form-urlencoded'
             Default is 'text/plain'
测试
ab -n 1000 -c 100 -p ./postfile -T application/x-www-form-urlencoded http://192.168.1.108//Seckill/doseckill
Percentage of the requests served within a certain time (ms)
  50%     98
  66%    103
  75%    107
  80%    109
  90%    115
  95%    118
  98%    121
  99%    123
 100%    128 (longest request)

此时java中的控制台

34074,购买成功
39830,购买成功
19620,购买成功
9727,购买成功
31847您好,秒杀活动已经结束
05-Nov-2022 18:15:06.190 信息 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory 把web 应用程序部署到目录 [D:\software\apache-tomcat-8.5.78\webapps\manager]
05-Nov-2022 18:15:06.222 信息 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Web应用程序目录[D:\software\apache-tomcat-8.5.78\webapps\manager]的部署已在[33]毫秒内完成
49635,购买成功
20104,购买成功
20281,购买成功
38295,购买成功
36060,购买成功
20169,购买成功
16599,购买成功
1757,购买成功
27781,购买成功
37401,购买成功
24778,购买成功
16002您好,秒杀活动已经结束
47051,购买成功
47757,购买成功
47240您好,秒杀活动已经结束
33393您好,秒杀活动已经结束
6163您好,秒杀活动已经结束
4882,购买成功
17448您好,秒杀活动已经结束
19355您好,秒杀活动已经结束
6336您好,秒杀活动已经结束
47492,购买成功
43854您好,秒杀活动已经结束
7106您好,秒杀活动已经结束
44598,购买成功
7747,购买成功
9785,购买成功
25385,购买成功
1499,购买成功
26152,购买成功
31252,购买成功
4589,购买成功
30801,购买成功
13182,购买成功
6426,购买成功
46654,购买成功
45300,购买成功
43455,购买成功
24394您好,秒杀活动已经结束
6417您好,秒杀活动已经结束
14826您好,秒杀活动已经结束
37355,购买成功
22473您好,秒杀活动已经结束
45251,购买成功
30914,购买成功
13849,购买成功
39553您好,秒杀活动已经结束
9444,购买成功
954您好,秒杀活动已经结束
133您好,秒杀活动已经结束

redis中:

127.0.0.1:6379> get sk:0101:qt
"-27"
超卖和超时问题解决

超时问题:使用数据库连接池

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;
    }

    public static void release(JedisPool jedisPool, Jedis jedis) {
        if (null != jedis) {
            jedisPool.returnResource(jedis);
        }
    }

}

超卖问题 : 使用乐观锁检测数据并使用事务操作

    //秒杀过程
    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.44.168",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);
        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;
    }
lua解决乐观锁造成的库存遗留

问题分析:当并发度特别高的时候,会出现没有结束的情况

ab -n 1000 -c 400 -p ./postfile -T application/x-www-form-urlencoded http://192.168.1.108//Seckill/doseckill

然后redis中的库存显示为:

127.0.0.1:6379> get sk:0101:qt
"13"
public class SecKill_redisByScript {

    private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

    public static void main(String[] args) {
        JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();

        Jedis jedis=jedispool.getResource();
        System.out.println(jedis.ping());

        Set<HostAndPort> set=new HashSet<HostAndPort>();

    //    doSecKill("201","sk:0101");
    }

    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;
    }
}

此时的redis:

[root@hadoop100 hikaru]# docker exec -it redis redis-cli
127.0.0.1:6379> keys *
1) "sk:0101:qt"
2) "sk:0101:usr"
127.0.0.1:6379> get sk:0101:qt
"0"

这里也没有讲太清楚。。lua的作用就是把两个操作(减少库存并添加用户)变成了原子性操作,实际上就是变成了使用了悲观锁?只不过因为redis没有悲观锁吗

查了一下网上说:减库存逻辑其实就是先是用lua脚本减redis库存,如果成功再去减数据库中的真实库存,如果减redis库存失败,库存不足,就不会再走后面减真实库存的逻辑了。

5 Redis持久化

5.1 RDB
简介

在指定的时间间隔内,将内存中的数据集快照写入磁盘

posted @ 2022-12-09 15:42  Tod4  阅读(203)  评论(0编辑  收藏  举报