带你详细了解Redis事务锁机制-加实列演示-上

165 阅读5分钟

Redis_事务_锁机制_秒杀

Redis 的事务是什么?

1、Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行

2、事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

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

Redis 事务三特性

一单独的隔离操作

1、事务中的所有命令都会序列化、按顺序地执行

2、事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

二没有隔离级别的概念

队列中的命令(指令), 在没有提交前都不会实际被执行

三不保证原子性

事务执行过程中, 如果有指令执行失败,其它的指令仍然会被执行, 没有回滚

事务相关指令Multi、Exec、discard

示意图

Redis 事务指令示意图

在这里插入图片描述

解读上图
  1. 从输入Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行(类似Mysql的start transaction 开启事务)
  2. 输入Exec 后,Redis 会将之前的命令队列中的命令依次执行(类似Mysql 的commit 提交事务)
  3. 组队的过程中可以通过discard 来放弃组队(类似Mysql 的rollback 回顾事务)
  4. 说明: Redis 事务和Mysql 事务本质是完全不同的, 用Mysql 的做类似说明, 是为了好理解

快速入门

1、需求: 请依次向Redis 中, 添加三组数据, k1-v1 k2-v2 k3-v3, 要求使用Redis 的事务完成

在这里插入图片描述

注意事项和细节

1、组队的过程中, 可以通过discard 来放弃组队

在这里插入图片描述

2、如果在组队阶段报错, 会导致exec 失败, 那么事务的所有指令都不会被执行 在这里插入图片描述

在这里插入图片描述

3、如果组队成功, 但是指令有不能正常执行的, 那么exec 提交, 会出现有成功有失败情况,也就是事务得到部分执行, 这种情况下, Redis 事务不具备原子性.

在这里插入图片描述

在这里插入图片描述

事务冲突及解决方案

先看一个问题

经典的抢票问题

  1. 一个请求想购买6

  2. 一个请求想购买5

  3. 一个请求想购买1 在这里插入图片描述

解读上图

  1. 如果没有控制, 会造成超卖现象
  2. 如果3 个指令, 都得到执行, 最后剩余的票数是-2

悲观锁

工作示意图

在这里插入图片描述

解读上图

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

乐观锁

工作示意图

在这里插入图片描述

解读上图

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

watch & unwatch

watch

1、基本语法: watch key [key ...]

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

3、这里可以结合乐观锁机制进行理解.

实验:

在这里插入图片描述

unwatch

1、基本语法unwatch

2、取消watch 命令对所有key 的监视。

3、如果在执行watch 命令后,exec 命令或discard 命令先被执行了的话,那么就不需要再执行unwatch 了

火车票-抢票

需求分析/图解

创建一个web项目

在这里插入图片描述

思路分析

1、一个user 只能购买一张票, 即不能复购

2、不能出现超购,也是就多卖了.

3、不能出现火车票遗留问题/库存遗留, 即火车票不能留下

在这里插入图片描述

版本1:完成基本购票流程, 暂不考虑事务和并发问题

1、创建Java Web 项目, 参照以前讲过搭建Java Web 项目流程即可

2、引入相关的jar 包和jquery

创建index

sec_kill_ticket\web\index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Insert title here</title>
  <base href="<%=request.getContextPath() + "/"%>">
</head>
<body>
<h1>北京-成都 火车票 ! 秒杀!
</h1>


<form id="secKillform" action="secKillServlet" enctype="application/x-www-form-urlencoded">
  <input type="hidden" id="ticketNo" name="ticketNo" value="bj_cd">
  <input type="button" id="seckillBtn" name="seckillBtn" value="秒杀火车票【北京-成都】"/>
</form>

</body>
<script type="text/javascript" src="script/jquery/jquery-3.1.0.js"></script>
<script type="text/javascript">
  $(function () {
    $("#seckillBtn").click(function () {
      var url = $("#secKillform").attr("action");
      console.log("url->" , url)// secKillServlet,完整的url http://localhost:8080/seckill/secKillServlet
      console.log("serialize->", $("#secKillform").serialize())
      //
      $.post(url, $("#secKillform").serialize(), function (data) {
        if (data == "false") {
          alert("火车票 抢光了:)");
          $("#seckillBtn").attr("disabled", true);
        }
      });
    })
  })
</script>
</html>

创建SecKillRedis

src\com\seckill\redis\SecKillRedis.java

public class SecKillRedis {
    /**
     * 测试一下是否连通了Redis
     *
     * @param args
     */
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.198.130", 6379);
        System.out.println(jedis.ping());
        jedis.close();
    }
    //秒杀过程
    /**
     * @param uid 用户id
     * @param ticketNo 票编号, 比如北京-成都的ticketNo "bj_cd"
     * @return
     */
    public static boolean doSecKill(String uid, String ticketNo) {
        //1 uid 和ticketNo 非空判断
        if (uid == null || ticketNo == null) {
            return false;
        }
        //2.连接到redis
        //解读
        //1) 每一个来秒杀的用户, 都会连接一把Reids
        Jedis jedis = new Jedis("192.168.198.130", 6379);
        //3 拼接key
        // 3.1 库存key
        String stockKey = "sk:" + ticketNo + ":ticket";
        // 3.2 秒杀成功用户key=> 对应的值是set , 可以存放有多个秒杀成功用户的id
        String userKey = "sk:" + ticketNo + ":user";
        //4 获取库存,如果库存null,秒杀还没有开始
        String stock = jedis.get(stockKey);
        if (stock == null) {
            System.out.println("秒杀还没有开始,请等待..");
            jedis.close();
            return false;
        }
        // 5 判断用户是否重复秒杀操作
        if (jedis.sismember(userKey, uid)) {
            System.out.println(uid + " 不能重复秒杀...");
            jedis.close();
            return false;
        }
        //6 判断如果火车票数量,剩余数量小于1,秒杀结束
        if (Integer.parseInt(stock) <= 0) {
            System.out.println("票已经卖光, 秒杀已经结束了");
            jedis.close();
            return false;
        }
        //7.1 火车票数量- 1
        jedis.decr(stockKey);
        //7.2 把秒杀成功用户添加清单里面
        jedis.sadd(userKey, uid);
        System.out.println("秒杀成功了..");
        jedis.close();
        return true;
    }
}

创建SecKillServlet

src\com\seckill\web\SecKillServlet.java

public class SecKillServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //请求时, 模拟生成一个userId
        String userId = new Random().nextInt(10000) + "";
        //获取用户要购买的票的编号
        String ticketNo = request.getParameter("ticketNo");
        //调用秒杀
        boolean isOK = SecKillRedis.doSecKill(userId, ticketNo);
        //返回结果
        response.getWriter().print(isOK);
    }
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws
            ServletException, IOException {
        doPost(request, response);
    }
}

向Redis 中, 增加测试数据

在这里插入图片描述

测试效果

在这里插入图片描述

版本2:抢票并发模拟, 出现超卖问题

安装工具ab 模拟测试

  1. 说明: 工具ab 可以模拟并发发出Http 请求, 老韩说明(模拟并发http 请求工具还有jemeter, postman,我们都使用一下, 开阔眼界, 这里老师使用ab 工具)
  2. 安装指令: yum install httpd-tools (提示: 保证当前linux 是可以联网的)
  3. 如果你不能联网, 可以使用rpm 安装, 这里使用yum 方式安装
  4. 另外, 使用rpm 方式安装我也给小伙伴说明一下, 如下:-先挂载centos 安装文件ios, 这个文件

在这里插入图片描述

在这里插入图片描述

--进入cd /run/media/root/CentOS 7 x86_64/Packages

--顺序安装

  1. apr-1.4.8-3.el7.x86_64.rpm
  2. apr-util-1.5.2-6.el7.x86_64.rpm
  3. httpd-tools-2.4.6-67.el7.centos.x86_64.rpm 在这里插入图片描述

--测试是否安装成功

在这里插入图片描述

在ab 指令执行的当前路径下创建文件postfile

vi postfile

在这里插入图片描述

在这里插入图片描述

执行指令

注意保证linux 可以访问到Tomcat 所在的服务器.

--先查看Tomcat 所在Windows 的网络配置情况 在这里插入图片描述

--确认Linux 可以ping 通Windows

在这里插入图片描述

如果Ping 不通, 确认一下Windows 防火墙是否关闭 在这里插入图片描述

--指令, 测试前把Redis 的数据先重置一下
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.198.1:8080/seckill/secKillServlet

解读指令

(1) ab 是并发工具程序

(2) -n 1000 表示一共发出1000 次http 请求

(3) -c 100 表示并发时100 次, 你可以理解1000 次请求, 会在10 次发送完毕

(4) -p ~/postfile 表示发送请求时, 携带的参数从当前目录的postfile 文件读取(这个你事先要准备好)

(5) -T application/x-www-form-urlencoded 就是发送数据的编码是基于表单的url 编码

(6) ~的含义: blog.csdn.net/m0_67401134…

在这里插入图片描述

(7)http://192.168.198.1:8080/seckill/secKillServlet 就是请求的url, 注意这里的IP:port/uri 必须写正确.

在这里插入图片描述 在这里插入图片描述

查看执行结果

在这里插入图片描述 在这里插入图片描述

注意我们这里先讲连接池然后在讲解决方法