【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务

本博客为尚硅谷课程笔记,课程来源:【尚硅谷】Redis 6 入门到精通 超详细 教程_哔哩哔哩_bilibili
部分参考博文:Redis6从入门到精通

文章目录

  • Redis配置文件
  • 发布和订阅
  • Jedis操作
    • Jedis常用API
    • Jedis实例
  • Redis事务
    • Multi、Exec、discard
    • 事务冲突
    • Redis秒杀案例
    • ab工具模拟并发
    • 超时和超卖问题
      • 连接超时
      • 超卖问题

Redis配置文件

参考:菜鸟教程-Redis配置; bilibili-尚硅谷Redis6

要将bind 127.0.0.1注释掉,而且还要将protected-mode改为no,不然后文无法通过Jedis工具连接到Redis数据库。

发布和订阅

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。Redis客户端可以订阅任意数量的频道。
【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第1张图片
两个终端运行redis-cli演示sub&pub

  • psubscribe pattern [pattern .....]订阅一个或多个符合给定模式的频道,每个模式以 * 作为匹配符,比如it*匹配所有以 it 开头的频道( it.news 、 it.blog 、 it.tweets 等等)。 news.* 匹配所有以 news. 开头的频道( news.it 、 news.global.today 等等),诸如此类
  • PUBSUB subcommand [argument [argument ...]]查看订阅与发布系统状态
  • PUBLISH channel message将信息发送到指定的频道
  • PUNSUBSCRIBE [pattern [pattern ...]]退订所有给定模式的频道
  • SUBSCRIBE channel [channel ...]订阅给定的一个或多个频道的信息
  • UNSUBSCRIBE [channel [channel ...]]指退订给定的频道

Jedis操作

首先需要检查配置文件是否按照上文的要求修改。
新建JAVA的Maven项目,依赖项如下:

<dependencies>
        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.13.2version>
        dependency>
        
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>4.2.3version>
        dependency>

        
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-simpleartifactId>
            <version>1.7.25version>
            <scope>compilescope>
        dependency>
    dependencies>

使用下面的程序测试连通性:

import org.junit.Test;
import redis.clients.jedis.Jedis;

public class JedisTest {
    @Test
    public void ConnectTest(){
        // 创建Jedis对象 构造方法选择有参(host,port)
        Jedis jedis = new Jedis("192.168.211.130", 6379);

        // 连通性测试
        jedis.auth("******"); // 设置的密码(可选)
        String value = jedis.ping();
        System.out.println(value);
    }
}

输出"PONG"即代表成功。
出现SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“.的解决方法

若是出现连接超时的问题

排除配置文件的原因很可能是虚拟机或者主机的防火墙未关闭

 sudo ufw status # ubuntu查看防火墙状态
 # 开启和关闭防火墙
 sudo ufwenable
 sudo ufwdisable
 ######
 systemctl status firewalld # centos
 systemctl enable firewalld.service
 systemctl disable firewalld.service

Jedis常用API

具体函数与上文的命令行操作类似,参考博文:Jedis常用API

Jedis实例

手机验证码功能

需求如下:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入3次
【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第2张图片

分析如下:
【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第3张图片
PhoneCode.java代码如下:

import redis.clients.jedis.Jedis;

import java.util.Random;

/**
 * @author moonjay
 */
public class PhoneCode {
    public static void main(String[] args) {
        //模拟验证码发送
//        verifyCode("13678765435");

        //模拟验证码校验
        getRedisCode("13678765435","120603");
    }

    /**1. 获取验证码*/
    public static String getCode(){
        Random random = new Random();
        StringBuffer sbf = new StringBuffer();
        for (int i = 0; i < 6; i++) {
            int rand = random.nextInt(10);
            sbf.append(rand);
        }
        return sbf.toString();
    }

    /**2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120s*/
    public static void verifyCode(String phone) {
        //连接redis
        Jedis jedis = new Jedis("192.168.211.130", 6379);
        jedis.auth("******");

        //拼接key
        //手机发送次数key
        String countKey = "VerifyCode"+phone+":count";
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";

        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if(count == null) {
            //没有发送次数,第一次发送
            //设置发送次数是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();
            return ;
        }

        //发送验证码放到redis里面
        String vcode = getCode();
        jedis.setex(codeKey,120,vcode);
        jedis.close();
    }

    /**3 验证码校验*/
    public static void getRedisCode(String phone,String code) {
        //从redis获取验证码
        Jedis jedis = new Jedis("192.168.211.130", 6379);
        jedis.auth("******");

        //验证码key
        String codeKey = "VerifyCode"+phone+":code";
        String redisCode = jedis.get(codeKey);
        //判断
        if(redisCode.equals(code)) {
            System.out.println("成功");
        }else {
            System.out.println("失败");
        }
        jedis.close();
    }
}

SpringBoot整合

参考博文:Redis(二)jedis API

Redis事务

Redis事务是一个单独的隔离操作∶事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队。

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

  • 没有隔离级别的概念:批量操作在发送 EXEC 命令前被放入队列缓存。
  • 不保证原子性:收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
  • 单独的隔离操作:在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

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

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

Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis 会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。
【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第4张图片

  • multi 标记一个事务块的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行

    单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
    事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做
    官网对此的说明【redis docs on transactions】

  • discard 取消事务,放弃执行事务块内的所有命令

  • exec 执行所有事务块内的命令,当操作被打断时,返回空值 nil

  • WATCH key [key ...] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断【乐观锁的过程】

    # 监视 key ,且事务被打断
    redis 127.0.0.1:6379> WATCH lock lock_times
    OK
    redis 127.0.0.1:6379> MULTI
    OK
    redis 127.0.0.1:6379> SET lock "joe"        # 就在这时,另一个客户端修改了 lock_times 的值
    QUEUED
    redis 127.0.0.1:6379> INCR lock_times
    QUEUED
    redis 127.0.0.1:6379> EXEC                  # 因为 lock_times 被修改, joe 的事务执行失败
    (nil)
    
  • UNWACTH 取消 WATCH 命令对所有 key 的监视

事务冲突

为什么将Redis操作做成事务?
想象一个场景,很多人有你的账户,同时去参加双十一抢购。假设有三个请求:

  • 一个请求想给金额减8000
  • 一个请求想给金额减5000
  • 一个请求想给金额减1000
    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第5张图片

悲观锁
【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第6张图片
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。缺点是效率低。

乐观锁
【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第7张图片
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种 check-and-set机制实现事务的。

参考博文:【脏读、不可重复读、幻读和MVCC】

如果不考虑事务隔离性,产生哪些读问题?

  • 脏读(读取未提交数据)

    A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。

    就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。

    这种情况常发生于转账与取款操作中

    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第8张图片
  • 不可重复读(前后多次读取,数据内容不一致)

    事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。

    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第9张图片
  • 幻读(Phantom Read),是指当事务不是独立执行时发生的一种现象。

    事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第10张图片

不可重复读和幻读到底有什么区别呢?

(1) 不可重复读是读取了其他事务更改的数据,针对update操作

解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。

(2) 幻读是读取了其他事务新增的数据,针对insert和delete操作

解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。


MVCC(多版本并发控制)
英文全称为Multi-Version Concurrency Control,乐观锁为理论基础的MVCC(多版本并发控制),MVCC的实现没有固定的规范。每个数据库都会有不同的实现方式。

Redis秒杀案例

【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第11张图片

  • 项目结构(使用POM管理)
    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第12张图片
  • pom.xml引入依赖项
    <dependencies>
          
          <dependency>
              <groupId>redis.clientsgroupId>
              <artifactId>jedisartifactId>
              <version>4.2.3version>
          dependency>
          <dependency>
              <groupId>org.slf4jgroupId>
              <artifactId>slf4j-simpleartifactId>
              <version>1.7.25version>
              <scope>compilescope>
          dependency>
          <dependency>
              <groupId>javax.servlet.jspgroupId>
              <artifactId>jsp-apiartifactId>
              <version>2.2version>
              <scope>providedscope>
          dependency>
          <dependency>
              <groupId>javax.servletgroupId>
              <artifactId>jstlartifactId>
              <version>1.2version>
          dependency>
          <dependency>
              <groupId>javax.servletgroupId>
              <artifactId>javax.servlet-apiartifactId>
              <version>4.0.1version>
              <scope>compilescope>
          dependency>
      dependencies>
    
  • 右键该模块根目录spring-06-jedis,设置模块
    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第13张图片
  • 构造工件(war包没有这些库文件会报500错误)
    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第14张图片
  • 界面index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8"%>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>SecKilltitle>
    <link crossorigin="anonymous" href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css" rel="stylesheet">
  head>
  <body>
  <h1>iPhone 14 Pro 一元秒杀!!!h1>

  <form id="msform" method="post" action="${pageContext.request.contextPath}/doSecKill" enctype="application/x-www-form-urlencoded">
    <input type="hidden" id="prodid" name="prodid" value="0101">
    <input class="btn btn-info" type="button" id="secKill_btn" name="secKill_btn" value="点我抢"/>
  form>
  body>
  <script crossorigin="anonymous" src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js">script>
  <script type="text/javascript">
    $(function () {
      $("#secKill_btn").click(function (){
        var url=$("#msform").attr("action");
        $.post(url, $("#msform").serialize(), function (data) {
          if (data == "false"){
            alert("ops!! 抢完了");
            $("#secKill_btn").attr("disabled", true);
          }
        });
      })
    })
  script>
html>
  • Servlet和后台逻辑实现

SecKillServlet.java

package com.mao;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Random;

public class SecKillServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public SecKillServlet(){super();}

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String userid = new Random().nextInt(5000) + "";
        String prodid = request.getParameter("prodid");

        boolean isSuccess = SecKillRedis.doSecKill(userid, prodid);
        response.getWriter().print(isSuccess);
    }
}

SecKillRedis.java

package com.mao;

import redis.clients.jedis.Jedis;

public class SecKillRedis {
    public static boolean doSecKill(String userid, String prodid){
        //1. uid和prodid非空判断
        if(userid == null || prodid == null){
            return false;
        }
        //2. 连接redis
        Jedis jedis = new Jedis("192.168.211.130", 6379);
        jedis.auth("******");
        //3. 拼接key
        // 3.1 库存key
        String kc_key = "sk:" + prodid + ":qt";
        // 3.2 秒杀成功用户id
        String user_key = "sk:" + prodid + ":user";
        //4.获取库存,如果为null说明秒杀还未开始
        String kc = jedis.get(kc_key);
        if (kc == null){
            System.out.println("秒杀还未开始");
            jedis.close();
            return false;
        }
        //5.用户是否重复秒杀
        if (jedis.sismember(user_key, userid)){
            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(kc_key);
        // 7.2 将秒杀成功的用户添加进清单里
        jedis.sadd(user_key, userid);
        System.out.println("已抢到一台!");
        jedis.close();
        return true;
    }
}
  • 配置文件

web.xml若未配置,点击按钮时会报404错误


<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <display-name>jedis_secKilldisplay-name>
    
    <servlet>
        <servlet-name>seckillservlet-name>
        <servlet-class>com.mao.SecKillServletservlet-class>
        
        <load-on-startup>1load-on-startup>
    servlet>
    <servlet-mapping>
        <servlet-name>seckillservlet-name>
        <url-pattern>/doSecKillurl-pattern>
    servlet-mapping>
web-app>

ab工具模拟并发

ab(apache bench)是apache下的一个工具,主要用于做web站点的压力测试

扩展:Jmeter工具

  • Ubuntu下安装
    sudo apt-get install apache2-utils
    
  • CentOS下安装
    yum install httpd-tools
    

ab工具参数如下:

moonjay@vm-ubuntu:~/桌面$ ab -help
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
    -n requests     Number of requests to perform
    -c concurrency  Number of multiple requests to make at a time
    -t timelimit    Seconds to max. to spend on benchmarking
                    This implies -n 50000
    -s timeout      Seconds to max. wait for each response
                    Default is 30 seconds
    -b windowsize   Size of TCP send/receive buffer, in bytes
    -B address      Address to bind to when making outgoing connections
    -p postfile     File containing data to POST. Remember also to set -T
    -u putfile      File containing data to PUT. Remember also to set -T
    -T content-type Content-type header to use for POST/PUT data, eg.
                    'application/x-www-form-urlencoded'
                    Default is 'text/plain'
    -v verbosity    How much troubleshooting info to print
    -w              Print out results in HTML tables
    -i              Use HEAD instead of GET
    -x attributes   String to insert as table attributes
    -y attributes   String to insert as tr attributes
    -z attributes   String to insert as td or th attributes
    -C attribute    Add cookie, eg. 'Apache=1234'. (repeatable)
    -H attribute    Add Arbitrary header line, eg. 'Accept-Encoding: gzip'
                    Inserted after all normal header lines. (repeatable)
    -A attribute    Add Basic WWW Authentication, the attributes
                    are a colon separated username and password.
    -P attribute    Add Basic Proxy Authentication, the attributes
                    are a colon separated username and password.
    -X proxy:port   Proxyserver and port number to use
    -V              Print version number and exit
    -k              Use HTTP KeepAlive feature
    -d              Do not show percentiles served table.
    -S              Do not show confidence estimators and warnings.
    -q              Do not show progress when doing more than 150 requests
    -l              Accept variable document length (use this for dynamic pages)
    -g filename     Output collected data to gnuplot format file.
    -e filename     Output CSV file with percentages served
    -r              Don't exit on socket receive errors.
    -m method       Method name
    -h              Display usage information (this message)
    -I              Disable TLS Server Name Indication (SNI) extension
    -Z ciphersuite  Specify SSL/TLS cipher suite (See openssl ciphers)
    -f protocol     Specify SSL/TLS protocol
                    (SSL2, TLS1, TLS1.1, TLS1.2 or ALL)
    -E certfile     Specify optional client certificate chain and private key

运行ab -n 100 -c 10 URL,对URL进行100次请求,10个并发请求压力测试结果。

对上节的秒杀案例进行压力测试

  • Redis数据库对0101号商品进行库存初始化

  • 使用vim新建postfile
    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第15张图片

  • ab测试命令

    ab -n 100 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://[Tomcat sever ip]:8080/doSecKill
    
  • IDEA启动Tomcat服务器后,控制台输出如下
    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第16张图片

  • Redis数据库查询到的结果
    【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第17张图片

超时和超卖问题

连接超时


节省每次连接redis服务带来的消耗,把连接好的实例反复利用,通过参数管理连接的行为。
代码如下(单例模式实现):

package com.mao;

public class JedisPoolUtil {
    private static volatile JedisPool jedisPool = null;

    private JedisPoolUtil(){}

    public static  JedisPool getJedisPoolInstance(){
        if(null == jedisPool){
            synchronized (JedisPoolUtil.class){
                if (jedisPool == null){
                    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
                    jedisPoolConfig.setMaxTotal(200);  // 控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;
                    // 如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
                    jedisPoolConfig.setMaxIdle(32);  // 控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
                    jedisPoolConfig.setBlockWhenExhausted(true);
                    jedisPoolConfig.setTestOnBorrow(true);  // 获得一个jedis实例的时候是否检查连接可用性( ping() );如果为true,则得到的jedis实例均是可用的;

                    jedisPool = new JedisPool(jedisPoolConfig, "192.168.211.130", 6379, 60000, "******")
                }
            }
        }
        return jedisPool;
    }

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

然后在SecKillRedis.java中修改连接Redis使用连接池即可

 // 使用连接池
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPool.getResource();

超卖问题

【学习笔记】尚硅谷-Redis6 2.Redis配置文件和事务_第18张图片
使用乐观锁解决:

// 监视库存(乐观锁),在声明kc_key之后
jedis.watch(kc_key);
......
//7.秒杀过程
// 增加事务(multi)
Transaction transaction = jedis.multi();
// 7.1 库存-1
transaction.decr(kc_key);  //jedis.decr(kc_key);
// 7.2 将秒杀成功的用户添加进清单里
transaction.sadd(user_key, userid);  //jedis.sadd(user_key, userid);
// 执行事务
List<Object> results = transaction.exec();
if (results == null || results.size() == 0){
    System.out.println("抢购失败了!");
    jedis.close();
    return false;
}
System.out.println("已抢到一台!");
jedis.close();
return true;

再次使用ab测试命令

ab -n 100 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://[Tomcat sever ip]:8080/doSecKill

可以在Redis客户端使用get sk:0101:qt查看库存不为负数,而是0


上一篇文章:【学习笔记】尚硅谷-Redis6 1.Redis入门及数据类型介绍
下一篇文章:…

你可能感兴趣的:(开发手记,redis,学习,java)