English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french
查看: 6|回复: 0

十. Redis 事务和 “锁机制”——> 并发秒杀处理的详细说明

[复制链接]
查看: 6|回复: 0

十. Redis 事务和 “锁机制”——> 并发秒杀处理的详细说明

[复制链接]
查看: 6|回复: 0

397

主题

0

回帖

1201

积分

金牌会员

积分
1201
34345

397

主题

0

回帖

1201

积分

金牌会员

积分
1201
2025-2-6 22:54:28 | 显示全部楼层 |阅读模式
十. Redis 事务和 “锁机制”——> 并发秒杀处理的详细说明

@
目录


1.  Redis 的事务是什么?


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


  • 单独的隔离操作:

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

  • 没有隔离级别的概念:
队列中的命令(指令),在没有提交前都不会实际被执行。

  • 不保证原子性:
事务执行过程中,如果有指令执行失败,其他的指令仍然会被执行,没有回滚
MySQL中的事务是支持回滚的,而 Redis 中的事务是不支持回滚的。
3. Redis 关于事务相关指令 Multi、Exec、discard和 “watch &    unwatch”







Redis 事务指令示意图:

上图解读:

  • 从输入 multi 命令开始,输入的命令都会依次进入命令队列 中,但不会执行类似(MySQL的 start transaction 开始事务)。
  • 输入 Exec 命令后,Redis 会将之前的命令队列中的命令依次执行(类似于 MySQL的 commit 提交事务)。
  • 组队的过程中可以通过 discard 来放弃组队(类似 MySQL的 rollback 回滚事务)
  • 说明:Redis 事务和 MySQL 事务本质是完全不同的。 ——> MySQL中的事务是支持回滚的,而 Redis 中的事务是不支持回滚的。
3.1 快速入门(演示 Redis 事务控制)


127.0.0.1:6379> multi
127.0.0.1:6379(TX)> set k1 v1QUEUED127.0.0.1:6379(TX)> set k2 v2QUEUED127.0.0.1:6379(TX)> set k3 v3QUEUED127.0.0.1:6379(TX)>
127.0.0.1:6379(TX)> exec
3.2 注意事项和细节


  • 组队的过程中,  可以通过 discard  来放弃组队。注意是在 [TX] 队列当中,还没有执行 exce 命令之前,才有效。

127.0.0.1:6379(TX)> discard

  • 如果在组队阶段报错(这里的报错信息指的是,命令输入错误,语法错误,编译上的错误)   会导致 exec 失败   那么事务的所有指令都不会被执行




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


127.0.0.1:6379> multiOK127.0.0.1:6379(TX)> set k1 "v1"QUEUED127.0.0.1:6379(TX)> incr k1QUEUED127.0.0.1:6379(TX)> set k2 "v2"QUEUED127.0.0.1:6379(TX)> exec1) OK2) (error) ERR value is not an integer or out of range3) OK127.0.0.1:6379>
4. Redis 事务冲突及解决方案(悲观锁,乐观锁) watch &    unwatch

我们先来看一个经典的抢票 问题。

  • 一个请求(用户)想购买 6 张票
  • 一个请求(用户)想购买 5 张票
  • 一个请求(用户)想购买 1 张票

上述解图:
一共只有10张票,但是并发开始,三个用户(三个请求),买 6 张,买 5 张,买 1 张票的。同时进入购票系统,并发同时刻进入判断,都显示还剩10张票(还没有减),最后执行减票,超卖了 2张票。
4.1 “悲观锁” 解决


上图解图:

  • 悲观锁(Pessimistic Lock),  顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
  • 这样别人/其他请求想要拿到这个数据都会被(block 锁上,因为被锁了就无法修改/拿到数据了),只有直到在他前面的人拿到数据/修改数据后,将锁释放了,它才能将数据拿到。
  • 悲观锁是锁设计理念 ,传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等等,都是在做操作之前先上锁(防止被其他的人/请求操作,修改了数据,导致数据不一致。)
4.2 “乐观锁” 解决


上图解读:

  • 乐观锁(Optimistic Lock),  顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁。
  • 但是在更新的时候会判断一下,在此期间别人/请求是否有去更新了这个数据,可以使用版本号等机制。版本号机制:就是当这个数据被修改了,那么就会产生一个版本信息,如果这个版本信息,与你一开始对应,并应该获取的版本信息不一致,那么就修改失败/无法修改数据(或者说获取的版本信息不一致,拿不到该数据信息)
  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量。 Redis 就是利用这种 check-and-set 机制实现事务的。
  • 乐观锁是锁设计理念。
4.3 watch &    unwatch



  • 基本语法: watch key [key ...]
  • 在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些) key 被其他命令所改动过,那么事务将被打断,停止执行
  • 这里就可以结合乐观锁机制进行理解。
演示实操:



# A 连接127.0.0.1:6379> watch k1OK127.0.0.1:6379> multiOK127.0.0.1:6379(TX)> incrby k1 1QUEUED127.0.0.1:6379(TX)> exec1) (integer) 100127.0.0.1:6379> get k1"100"# B 连接127.0.0.1:6379> watch k1OK127.0.0.1:6379> multiOK127.0.0.1:6379(TX)> incrby k1 100QUEUED127.0.0.1:6379(TX)> exec(nil)127.0.0.1:6379> get k1"100"127.0.0.1:6379>
<hr>unwatch:


  • unwatch : 取消 watch 命令对所有 key 的监视。
  • 如果在执行 watch 命令后, exec 命令和 discard 命令先被执行了的话,那么久不需要再执行 unwatch 了。
5. 案例演示:火车票-抢票(解决超卖,库存遗留)问题

5.1 案例思路分析:

这里我们使用 WEB 项目来演示


思路分析:

  • 一个 user 只能购买一张票,即不能复购。
  • 不能出现超购,也就是多卖的情况。
  • 不能出现火车票遗留问题/库存遗留,即火车票不能留下

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


  • 创建 Java Web 项目,  参照以前讲过搭建 Java Web 项目流程即可
  • 引入相关的 jar 包  和  jquery
  • 创建 D:sec_kill_ticket\web\index.jsp













index.jsp 代码编写
<%--  Created by IntelliJ IDEA.  User: 韩顺平  Version: 1.0--%><%@ 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="https://www.cnblogs.com/TheMagicalRainbowSea/p/<%=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="https://www.cnblogs.com/TheMagicalRainbowSea/p/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>
package com.rainbowsea.seckill.redis;import org.junit.Test;import redis.clients.jedis.Jedis;/** * 秒杀类 */public class SecKillRedis {    /**     * 编写一个测试方法-看看是否能够连通指定的 Redis     */    @Test    public void testRedis() {        Jedis jedis = new Jedis("192.168.76.146", 6379);        jedis.auth("rainbowsea");  // 设置了密码,需要进行一个验证        System.out.println(jedis.ping());        jedis.close(); // 关闭连接    }}
关于更多对应:Java程序连接 Redis 的内容,大家可以移步至:🌟🌟🌟 https://blog.csdn.net/weixin_61635597/article/details/145433348?spm=1001.2014.3001.5501
编写:秒杀过程

package com.rainbowsea.seckill.redis;import org.junit.Test;import redis.clients.jedis.Jedis;/** * 秒杀类 */public class SecKillRedis {    /**     * 秒杀过程     *     * @param uid      用户ID     * @param ticketNo 票的编号,比如 北京-成都的 ticketNo为 bj_cd     * @return     */    public static boolean doSecKill(String uid, String ticketNo) {        // -uid 和 ticketNo 进行一个非空校验        if (uid == null || ticketNo == null) {            return false;        }        // - 连接 Redis ,得到一个 jedis 对象        Jedis jedis = new Jedis("192.168.76.146", 6379);        jedis.auth("rainbowsea");  // 设置了密码,需要进行一个验证        // 判断 获取的jedis 是否为空        if (jedis == null) {            return false;        }        // 拼接票的库存 key        String stockKey = "sk:" + ticketNo + ":ticket";        // 拼接秒杀用户要存放到 set 集合对应的key,这个 set 集合可以存放多个 userId(同时set集合有着不可重复的特性,符合我们用户不够复购的特点)        String userKey = "sk:" + ticketNo + "user";        // 获取到对应的票的库存        String stock = jedis.get(stockKey);        // 获取到对应的票的库存,判断是否为 null        if (stock == null) {            System.out.println("秒杀还没有开始,请等待...");            jedis.close(); // 关闭连接            return false;        }        // - 判断用户是否重复秒杀/复购        if (jedis.sismember(userKey, uid)) {            System.out.println(uid + "不能重复秒杀...");            jedis.close();            return false;        }        // 判断火车票,是否还有剩余        if (Integer.parseInt(stock) <= 0) {            System.out.println("票已经卖完了,秒杀结束...");            jedis.close();            return false;        }        // 可以购买        // 1.将票的库存量 -1        jedis.decr(stockKey);        // 2. 将该用户加入到抢购成功对应 set 集合中        jedis.sadd(userKey, uid);        System.out.println(uid + "秒杀成功");        jedis.close();        return true;    }    /**     * 编写一个测试方法-看看是否能够连通指定的 Redis     */    @Test    public void testRedis() {        Jedis jedis = new Jedis("192.168.76.146", 6379);        jedis.auth("rainbowsea");  // 设置了密码,需要进行一个验证        System.out.println(jedis.ping());        jedis.close(); // 关闭连接    }}编写 ,Server 对外的服务

package com.rainbowsea.seckill.web;import com.rainbowsea.seckill.redis.SecKillRedis;import javax.servlet.ServletException;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 {    @Override    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        // 1. 请求时,模拟生成一个 userId        String userId = new Random().nextInt(10000) + "";        // 2. 获取用户购买的票的编号        String ticketNo = request.getParameter("ticketNo");        // 3. 调用秒杀的方法        boolean isOk = SecKillRedis.doSecKill(userId, ticketNo);        // 4. 将结果返回给前端,这个地方可以根据业务需要调整        response.getWriter().println(isOk);    }    @Override    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        doPost(request, response);    }}
<?xml version="1.0" encoding="UTF-8"?><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">    <servlet>        <servlet-name>SecKillServlet</servlet-name>        <servlet-class>com.rainbowsea.seckill.web.SecKillServlet</servlet-class>    </servlet>    <servlet-mapping>        <servlet-name>SecKillServlet</servlet-name>        <url-pattern>/secKillServlet</url-pattern>    </servlet-mapping></web-app>测试:
向 Redis 中, 增加测试数据。添加上 6 票
127.0.0.1:6379> set sk:bj_cd:ticket 6


5.3 抢票并发模拟,出现超卖问题

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


进入 cd    /run/media/root/CentOS 7 x86_64/Packages
顺序安装
apr-1.4.8-3.el7.x86_64.rpmapr-util-1.5.2-6.el7.x86_64.rpmhttpd-tools-2.4.6-67.el7.centos.x86_64.rpm

测试是否安装成功

这里我们使用 yum 安装,执行如下命令即可:前提是我们要是联网的状态才行。
yum install httpd-tools[root@localhost ~]# yum install httpd-tools


安装完后,输入 ab 命令,测试是否安装成功
ab

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

ticketNo=bj_cd&

  • 执行指令  ,  注意保证  linux 可以访问到 Tomcat 所在的服务器.
先查看 Tomcat 所在 Windows 的网络配置情况。
在 Window 系统中输入 cmd ,进入命令行窗口,输入 ipconfig 命令,查看 VM虚拟机的IP地址情况。

确认 Linux 可以 ping 通 Windows

如果 Ping 不通, 确认一下 Windows 防火墙是否关闭.

防火墙关闭是一件比较危险的事情,记得学习操作完之后,回来将防火墙重新打开

指令  ,  测试前把 Redis 的数据先重置一下。因为前面我们操作过来, 票已经被卖光了,我们需要重新设置票数,这里还是设置为 6 张票。

如下是我们使用 ab 工具要执行的命令:
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.76.1:9090/seckill/secKillServlet命令解读:

  • ab : 是并发工具程序
  • -n 1000 : 表示一共发出 1000 次 http 请求
  • -c 100: 表示并发时 100次,你可以理解为 1000次请求,分 10 次发送完毕,每次发出 100次。
  • -p ~/postfile: 表示发送请求时,携带的参数从当前目录的 postfile 文件读取(就是上述我们刚刚创建的文件)。~ 表示当前路径位置。
  • -T application/x-www-form-urlencoded: 就是发送数据的编码时基于表单的 url 编码的
  • ~ 的更多含义,可以移步至:https://blog.csdn.net/m0_67401134/article/details/123973115

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


127.0.0.1:6379> ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.76.1:9090/seckill/secKillServlet
执行之后。查看执行结果




5.4 Redis 连接池技术

连接池介绍

  • 节省每次连接 Redis 服务带来的消耗,把连接好的实例反复利用。
连接池参数:

  • MaxTotal:控制一个 pool 可分配多少个 jedis 实例,通过 pool.getResource() 来获取,如果赋值为 -1 ,则表示不限制。
  • maxldle:控制一个pool最多有多少个状态为 idle(空闲) 的 jedis 实例。
  • MaxWaitMillis:表示当获取一个 jedis 实例时,最大的等待毫米数,如果超过等待时间,则直接抛 JedisConnectionException
  • testOnBorrow:获得一个 jedis 实例的时候是否检查连接可用性(ping()) ;如果为 true ,则得到的 jedis 实例均是可用的。
使用连接池, 优化连接超时:
创建:sec_kill_ticket\src\com\rainbowsea\seckill\utils\JedisPoolUtil. java

package com.rainbowsea.seckill.uitl;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig;/** * 使用连接池的方式来获取 Redis 连接 */public class JedisPoolUtil {    // 解读 volatile 作用    /*    1. 线程可见性:当一个线程去修改一个共享变量时,另外一个线程可以读取这个修改的值    2. 顺序的一致性:禁止指令重排     */    private static volatile JedisPool jedisPool = null;    // 使用单例模式,将构造方法私有化    private JedisPoolUtil() {    }    // 单例:保证每次调用返回的 jedisPool是单例的    public static JedisPool getJedisPoolInstacne() {        if (null == jedisPool) {            synchronized (JedisPoolUtil.class) {                if (null == jedisPool) {                    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();                    // 对连接池进行配置                    jedisPoolConfig.setMaxTotal(200);                    jedisPoolConfig.setMaxIdle(32);                    jedisPoolConfig.setMaxWaitMillis(60 * 1000); // 单位是毫秒                    jedisPoolConfig.setBlockWhenExhausted(true);                    jedisPoolConfig.setTestOnBorrow(true);                    jedisPool = new JedisPool(jedisPoolConfig, "192.168.76.146", 6379, 60000, "rainbowsea");                }            }        }        return jedisPool;    }    /**     * 释放连接资源     *     * @param jedis     */    public static void release(Jedis jedis) {        if (null != jedis) {            jedis.close(); // 如果这个jedis 是从连接池获取的,这里 jedis.close()            // 就是将 jedis对象/连接,释放到连接池中。        }    }}线程可见性
简单说一下指令重排


   // 通过连接池获取 jedis对象/连接        JedisPool jedisPoolInstacne = JedisPoolUtil.getJedisPoolInstacne();        Jedis jedis = jedisPoolInstacne.getResource();        System.out.println("--使用的连接池");
      JedisPoolUtil.release(jedis);使用连接池:测试:
[attach]https://img2024.cnblogs.com/blog/3084824/202502/3084824-20250205215543829-1018512817.png[/attach]

5.5 利用 Redis 的事务机制,解决超卖问题(使用 watch,multi )

控制超卖-Redis 事务底层(乐观锁机制分析)

我们要处理的控制的就是,如下这部分代码,因为是这部分代码对 Redis 库当的数据进行操作和修改的,需要进行事务上的控制。


// 监控库存,开启 watch 的监控        jedis.watch(stockKey);
        // 使用事务,完成秒杀        Transaction multi = jedis.multi();        // 组成操作        multi.decr(stockKey); // 减去票的库存        multi.sadd(userKey, uid); // 将该抢到票的用户加入到抢购成功的 set 集合中        // 执行        List<Object> results = multi.exec();        if (results == null || results.size() == 0) {            System.out.println("抢票失败...");            JedisPoolUtil.release(jedis);            return false;        }        System.out.println(uid + "秒杀成功");        //jedis.close();        JedisPoolUtil.release(jedis);        return true;完成测试:
重启  Tomcat:
重置 Redis 相关数据:

执行指令:
[root@localhost ~]# ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.76.1:9090/seckill/secKillServlet


5.6 抢票并发模拟,分析出现库存遗留问题

抢票并发模拟,出现库存遗留问题
这里我们演示一下:出现库存遗留问题
先重置一下 redis 的数据
这里我们把库存量设的较大为 600
127.0.0.1:6379> set sk:bj_cd:ticket 600OK127.0.0.1:6379> get sk:bj_cd:ticket"600"127.0.0.1:6379> del sk:bj_cd:user(integer) 1127.0.0.1:6379> smembers sk:bj_cd:user(empty array)127.0.0.1:6379>
执行指令
[root@localhost ~]# ab -n 1000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.76.1:9090/seckill/secKillServlet这里我们将并发数变大  -c 300

运行结果:

从运行结果上看:我们可以看到600张票,1000个人抢,仅仅只卖出了 6 张票。这库存问题十分严重
出现库存遗留问题的分析

简单的理解就是:因为乐观锁的机制,这里 1000 次请求,分 4次,每次 300个请求,请求完毕的时候,当 300个请求同时涌入到 Redis ,向 Redis 要数据的时候,但是只有最前面的一次可以被请求到对应版本的信息(假设这里是 1.0 版本的数据),当最前面的一次请求获取到 Redis 当中的数据后,并修改了Redis 当中的数据,因为修改了数据,这时候(它们想要获取的数值的版本信息就变为了 1.1了)。因为它们一开始请求获取的是 1.0版本的数据,这时候的版本的数据变为了1.1 了,那它们这些请求就被打断了。所以这些请求都被打断无法获取到数据了,必须重新发出新的请求才行(获取到修改后的 1.1 版本的数据)。就这样导致大量的请求被打断了,无法获取到Redis 的数据值,也就自然无法修改Redis 当中的数值了。导致存有大量的票没有卖出。
5.7 运用 LUA 脚本(解决超卖,和库存遗留问题)

LUA 介绍

  • Lua 时一个小巧的脚本语言,Lua 脚本可以很容易的被 C/C++代码调用,也可以反过来调用C/C++的函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200K,所以 Lua 不适合作为开发独立应用程序的语言,而是作为 嵌入式脚本语言。
  • 很多应用程序,游戏使用 LUA 作为自己的嵌入式的脚本语言,以此来实现可配置性,可扩展性。
  • 将复杂的或者多步的 Redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数。提升性能。
  • LUA脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队 ,可以完成一些 Redis 事务性的操作。
  • Redis 的 Lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用,这里我们使用的是 Redis 6 可以使用。
  • 通过 lua 脚本解决争抢问题,实际上是 Redis 利用其单线程的特性 ,用任务队列 的方式解决多任务并发问题。
LUA 脚本, 解决库存遗留-思路分析图

上图解图:

  • LUA 脚本是类似于 Redis 事务,有一定的原子性,不会被其他命令插队 ,能完成 Redis 事务性的操作。
  • 通过 lua 脚本解决争抢问题,Redis  利用其单线程的特性,将请求形成任务队列,  从 而解决多任务并发问题
  • 简单的理解: 就是将 Redis 多个命令组合在一起,成为一个命令,然后,再将这个组合成的命令,按照顺序依次的存入到某个队列当中,存后之后,就算1000个请求来了,也得按照 Lua 编写的顺序,依次执行一个一个用户的请求。不会造成大量的请求同时涌入,让大量的请求失效,中断。
LUA 脚本, 解决库存遗留-代码实现:
local userid=KEYS[1]; --  获取传入的第一个参数local ticketno=KEYS[2]; --  获取传入的第二个参数 local stockKey='sk:'..ticketno..:ticket; --  拼接 stockKey local usersKey='sk:'..ticketno..:user; --  拼接  usersKeylocal userExists=redis.call(sismember,usersKey,userid); --  查 看 在 redis  的 usersKey set 中是否有该用户if tonumber(userExists)==1 thenreturn 2; --  如果该用户已经购买,  返回 2endlocal num= redis.call("get" ,stockKey); --  获取剩余票数if tonumber(num)<=0 thenreturn 0; --  如果已经没有票, 返回 0elseredis.call("decr",stockKey); --  将剩余票数-1redis.call("sadd",usersKey,userid); --  将抢到票的用户加入 setendreturn 1 --  返回 1 表示抢票成功想要了解更多关于 LUA脚本的内容,大家可以移步至🌟🌟🌟 https://blog.csdn.net/qq_41286942/article/details/124161359
这里:我们根据 LUA脚本对 Java程序进行修改。如下图所示:

/**     * 说明     * 1. 这个脚本字符串是在lua脚本上修改的, 但是要注意不完全是字符串处理     * 2. 比如 : 这里我就使用了 \" , 还有换行使用了 \r\n     * 3. 这些都是细节,如果你直接把lua脚本粘贴过来,不好使,一定要注意细节     * 4. 如果写的不成功,就在这个代码上修改即可     */    static String secKillScript = "local userid=KEYS[1];\r\n" +            "local ticketno=KEYS[2];\r\n" +            "local stockKey='sk:'..ticketno..\":ticket\";\r\n" +            "local usersKey='sk:'..ticketno..\":user\";\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\" ,stockKey);\r\n" +            "if tonumber(num)<=0 then \r\n" +            "   return 0;\r\n" +            "else \r\n" +            "   redis.call(\"decr\",stockKey);\r\n" +            "   redis.call(\"sadd\",usersKey,userid);\r\n" +            "end\r\n" +            "return 1";package com.rainbowsea.seckill.redis;import com.rainbowsea.seckill.uitl.JedisPoolUtil;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;/** * 使用 LUA脚本进行编写完成秒杀 */public class SecKillRedisByLua {    /**     * 说明     * 1. 这个脚本字符串是在lua脚本上修改的, 但是要注意不完全是字符串处理     * 2. 比如 : 这里我就使用了 \" , 还有换行使用了 \r\n     * 3. 这些都是细节,如果你直接把lua脚本粘贴过来,不好使,一定要注意细节     * 4. 如果写的不成功,就在这个代码上修改即可     */    static String secKillScript = "local userid=KEYS[1];\r\n" +            "local ticketno=KEYS[2];\r\n" +            "local stockKey='sk:'..ticketno..\":ticket\";\r\n" +            "local usersKey='sk:'..ticketno..\":user\";\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\" ,stockKey);\r\n" +            "if tonumber(num)<=0 then \r\n" +            "   return 0;\r\n" +            "else \r\n" +            "   redis.call(\"decr\",stockKey);\r\n" +            "   redis.call(\"sadd\",usersKey,userid);\r\n" +            "end\r\n" +            "return 1";    //使用lua脚本完成秒杀的核心方法    public static boolean doSecKill(String uid, String ticketNo) {        //先从redis连接池,获取连接        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstacne();        Jedis jedis = jedisPoolInstance.getResource();        //就是将lua脚本进行加载        String sha1 = jedis.scriptLoad(secKillScript);        //evalsha是根据指定的 sha1校验码, 执行缓存在服务器的脚本        Object result = jedis.evalsha(sha1, 2, uid, ticketNo);        String resString = String.valueOf(result);        //根据lua脚本执行返回的结果,做相应的处理        if ("0".equals(resString)) {            System.out.println("票已经卖光了..");            JedisPoolUtil.release(jedis);  // 归还连接给,redis 连接池            return false;        }        if ("2".equals(resString)) {            System.out.println("不能重复购买..");            JedisPoolUtil.release(jedis);  // 归还连接给,redis 连接池            return false;        }        if ("1".equals(resString)) {            System.out.println("抢购成功");            JedisPoolUtil.release(jedis);  // 归还连接给,redis 连接池            return true;        } else {            System.out.println("购票失败..");            JedisPoolUtil.release(jedis);  // 归还连接给,redis 连接池            return false;        }    }}修改:SecKillServlet 当中的 doPost 方法,使用 LUA脚本完成秒杀

package com.rainbowsea.seckill.web;import com.rainbowsea.seckill.redis.SecKillRedis;import com.rainbowsea.seckill.redis.SecKillRedisByLua;import javax.servlet.ServletException;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 {    @Override    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        // 1. 请求时,模拟生成一个 userId        String userId = new Random().nextInt(10000) + "";        // 2. 获取用户购买的票的编号        String ticketNo = request.getParameter("ticketNo");        // 3. 调用秒杀的方法        //boolean isOk = SecKillRedis.doSecKill(userId, ticketNo);        // 3.调用 Lua 脚本的完成秒杀方法        boolean isOk = SecKillRedisByLua.doSecKill(userId, ticketNo);        // 4. 将结果返回给前端,这个地方可以根据业务需要调整        response.getWriter().println(isOk);    }    @Override    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {        doPost(request, response);    }}测试:
重启抢票程序-Tomcat
重置 Redis 数据
127.0.0.1:6379> set sk:bj_cd:ticket 600OK127.0.0.1:6379> get sk:bj_cd:ticket"600"127.0.0.1:6379> del sk:bj_cd:user(integer) 1127.0.0.1:6379> smembers sk:bj_cd:user(empty array)127.0.0.1:6379>
执行指令
[root@localhost ~]# ab -n 2000 -c 200 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.76.1:9090/seckill/secKillServlet


6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

397

主题

0

回帖

1201

积分

金牌会员

积分
1201

QQ|智能设备 | 粤ICP备2024353841号-1

GMT+8, 2025-3-10 15:13 , Processed in 1.621196 second(s), 30 queries .

Powered by 智能设备

©2025

|网站地图