1. Redis 的事务是什么?
-
Redis 事务时一个单独的隔离操作 :事务中的所有命令都会序列化,按顺序地执行。
-
事务在执行的过程中,不会被其他客户端发送来的命令请求所打断,中停。
-
Redis 事务的主要作用就是串联 多个命令防止别的命令插队 。
2. Redis 事务三特性
1、单独的隔离操作:
Redis 事务时一个单独的隔离操作 :事务中的所有命令都会序列化,按顺序地执行。
事务在执行的过程中,不会被其他客户端发送来的命令请求所打断,中停。
2、没有隔离级别的概念:
队列中的命令(指令),在没有提交前都不会实际被执行。
3、不保证原子性:
事务执行过程中,如果有指令执行失败,其他的指令仍然会被执行,没有回滚 。
MySQL中的事务是支持回滚的,而 Redis 中的事务是不支持回滚的。
3. Redis 关于事务相关指令 Multi、Exec、discard和 "watch & unwatch"
![](https://i-blog.csdnimg.cn/img_convert/5d85c3bbec0e104e1202e697061651b0.png)
![](https://i-blog.csdnimg.cn/img_convert/6d37cdead4936781b70ccce47ca24c47.png)
![](https://i-blog.csdnimg.cn/img_convert/f3771674406c3925636c2858b3196278.png)
![](https://i-blog.csdnimg.cn/img_convert/7a955d35275a2f64c9581d9b3fbdccc8.png)
![](https://i-blog.csdnimg.cn/img_convert/a3bdb173034c8382d056b1f234f2f77b.png)
![](https://i-blog.csdnimg.cn/img_convert/84e6f3c9e5cc4412b1546644f3a25c87.png)
Redis 事务指令示意图:
![](https://i-blog.csdnimg.cn/img_convert/f80f002cec1984f2ceebfffa2d07e245.png)
上图解读:
从输入
multi
命令开始,输入的命令都会依次 进入命令队列 中,但不会执行类似(MySQL的 start transaction 开始事务)。输入
Exec
命令后,Redis 会将之前的命令队列中的命令依次执行(类似于 MySQL的 commit 提交事务)。组队的过程中可以通过
discard
来放弃组队(类似 MySQL的 rollback 回滚事务)说明:Redis 事务和 MySQL 事务本质是完全不同的。 ------> MySQL中的事务是支持回滚的,而 Redis 中的事务是不支持回滚的。
3.1 快速入门(演示 Redis 事务控制)
![](https://i-blog.csdnimg.cn/img_convert/4ab5a8663c6412066a78c2a3b07450ed.png)
127.0.0.1:6379> multi
![](https://i-blog.csdnimg.cn/img_convert/ee55935ec4abc28adc2c8a3c6a1c3371.png)
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)>
![](https://i-blog.csdnimg.cn/img_convert/ea3216c3e15f2af644e5c74128c4c8f3.png)
127.0.0.1:6379(TX)> exec
![](https://i-blog.csdnimg.cn/img_convert/b09e069e1035a09adc60a232845f2dec.png)
3.2 注意事项和细节
1、组队的过程中, 可以通过 discard
来放弃组队。注意是在 [TX]
队列当中,还没有执行 exce
命令之前,才有效。
![](https://i-blog.csdnimg.cn/img_convert/37ed9b6f20205687fb84ef57605e5c17.png)
127.0.0.1:6379(TX)> discard
![](https://i-blog.csdnimg.cn/img_convert/98c8adffaf6beb9471f04302a3447243.png)
2、如果在组队阶段报错(这里的报错信息指的是,命令输入错误,语法错误,编译上的错误) 会导致 exec 失败 那么事务的所有指令都不会被执行。
![](https://i-blog.csdnimg.cn/img_convert/9d04ffc696bbd074edbedd82922e7a75.png)
![](https://i-blog.csdnimg.cn/img_convert/d2af2b34fbffc190e9738b8975953177.png)
![](https://i-blog.csdnimg.cn/img_convert/18144f8c316de6e9a3cdf47d6143135e.png)
3、如果组队成功(multii
), 但是指令有不能正常执行的,那么 exec
提交,会出现有成功有失败情况,也就是事务得到部分执行, 这种情况下, Redis 事务不具备原子性。
![](https://i-blog.csdnimg.cn/img_convert/19c7734c5b8ce740e9b6665d0111c47f.png)
![](https://i-blog.csdnimg.cn/img_convert/c05d7612cb39b564ca90126f6ac533ad.png)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 "v1"
QUEUED
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k2 "v2"
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>
![](https://i-blog.csdnimg.cn/img_convert/fc9a0ce5915946f7f155d90d9417363d.png)
4. Redis 事务冲突及解决方案(悲观锁,乐观锁) watch & unwatch
我们先来看一个经典的抢票 问题。
一个请求(用户)想购买 6 张票
一个请求(用户)想购买 5 张票
一个请求(用户)想购买 1 张票
![](https://i-blog.csdnimg.cn/img_convert/30ff40ad90070c499e65add3525be81e.png)
上述解图:
一共只有10张票,但是并发开始,三个用户(三个请求),买 6 张,买 5 张,买 1 张票的。同时进入购票系统,并发同时刻进入判断,都显示还剩10张票(还没有减),最后执行减票,超卖了 2张票。
4.1 "悲观锁" 解决
![](https://i-blog.csdnimg.cn/img_convert/097f81687c25be7674fc8cc13317c8bb.png)
上图解图:
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
这样别人/其他请求想要拿到这个数据都会被(block 锁上,因为被锁了就无法修改/拿到数据了),只有直到在他前面的人拿到数据/修改数据后,将锁释放了,它才能将数据拿到。
悲观锁是锁设计理念 ,传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等等,都是在做操作之前先上锁(防止被其他的人/请求操作,修改了数据,导致数据不一致。)
4.2 "乐观锁" 解决
![](https://i-blog.csdnimg.cn/img_convert/182c7169ce12dcb267475f7b9e799b53.png)
上图解读:
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁。
但是在更新的时候会判断一下,在此期间别人/请求是否有去更新了这个数据,可以使用版本号等机制。版本号机制:就是当这个数据被修改了,那么就会产生一个版本信息,如果这个版本信息,与你一开始对应,并应该获取的版本信息不一致,那么就修改失败/无法修改数据(或者说获取的版本信息不一致,拿不到该数据信息)
乐观锁适用于多读的应用类型,这样可以提高吞吐量。 Redis 就是利用这种
check-and-set
机制实现事务的。乐观锁是锁设计理念。
4.3 watch & unwatch
![](https://i-blog.csdnimg.cn/img_convert/7e21073cbf32fdc21ae152e640e23530.png)
-
基本语法:
watch key [key ...]
-
在执行
multi
之前,先执行 watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些) key 被其他命令所改动过,那么事务将被打断,停止执行 。 -
这里就可以结合乐观锁机制进行理解。
演示实操:
![](https://i-blog.csdnimg.cn/img_convert/708163cd9faa7acd5a08033c18e08712.png)
![](https://i-blog.csdnimg.cn/img_convert/9f1699ffae1783ad1579933f3ef4220c.png)
![](https://i-blog.csdnimg.cn/img_convert/42af617b90bac4eb648fa07456ebda95.png)
# A 连接
127.0.0.1:6379> watch k1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby k1 1
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 100
127.0.0.1:6379> get k1
"100"
unwatch:
![](https://i-blog.csdnimg.cn/img_convert/2079f9e9525156580ae954c095a8dbc6.png)
-
unwatch
: 取消 watch 命令对所有 key 的监视。 -
如果在执行 watch 命令后,
exec
命令和discard
命令先被执行了的话,那么久不需要再执行unwatch
了。
5. 案例演示:火车票-抢票(解决超卖,库存遗留)问题
5.1 案例思路分析:
这里我们使用 WEB 项目来演示
![](https://i-blog.csdnimg.cn/img_convert/0672460698101fc228c08d904aaeb739.png)
![](https://i-blog.csdnimg.cn/img_convert/2165303c7a6ea7f58cbdf4ade8d8ffe2.png)
思路分析:
-
一个 user 只能购买一张票,即不能复购。
-
不能出现超购,也就是多卖的情况。
-
不能出现火车票遗留问题/库存遗留,即火车票不能留下
![](https://i-blog.csdnimg.cn/img_convert/676859fda788a96df113d478af293d23.png)
5.2 完成基本购票流程,暂不考虑事务和并发问题
-
创建 Java Web 项目, 参照以前讲过搭建 Java Web 项目流程即可
-
引入相关的 jar 包 和 jquery
-
创建 D:sec_kill_ticket\web\index.jsp
![](https://i-blog.csdnimg.cn/img_convert/b33f8bdce0eff870c0d6862ee8785fed.png)
![](https://i-blog.csdnimg.cn/img_convert/a84009af174a94a22f26ac141992045b.png)
![](https://i-blog.csdnimg.cn/img_convert/bf7fa31134b6a3b4632a532202fcba80.png)
![](https://i-blog.csdnimg.cn/img_convert/0120840f11403585d5684fdeb641c08f.png)
![](https://i-blog.csdnimg.cn/img_convert/aa9fc5d6594b7d9fae80547acbf9462d.png)
![](https://i-blog.csdnimg.cn/img_convert/9823a31228be46f3add4566c310e4a8e.png)
![](https://i-blog.csdnimg.cn/img_convert/851a0e1dbd0d3de3afd6cb4cade86b1b.png)
![](https://i-blog.csdnimg.cn/img_convert/0058793845a8ad5d739b90953a4baf9f.png)
![](https://i-blog.csdnimg.cn/img_convert/306ad3d12d1671e38a835f9cae369ea9.png)
![](https://i-blog.csdnimg.cn/img_convert/cbe36437cee443bc42dfa6213c41faaa.png)
![](https://i-blog.csdnimg.cn/img_convert/d38fd978e1b27921d70cd03fd5755754.png)
![](https://i-blog.csdnimg.cn/img_convert/44d6ef274dc41a09758b6ed81d4b1318.png)
![](https://i-blog.csdnimg.cn/img_convert/adb2c56058d486daca96e111a8484cfc.png)
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="<%=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>
![](https://i-blog.csdnimg.cn/img_convert/3c3882433b6e809e3d3d8bad6f46f312.png)
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(); // 关闭连接
}
}
编写:秒杀过程
![](https://i-blog.csdnimg.cn/img_convert/e7aa9934bfc550644729a54cf3abd55a.png)
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 对外的服务
![](https://i-blog.csdnimg.cn/img_convert/3699fba22397a8e756df437dccc5bb26.png)
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);
}
}
![](https://i-blog.csdnimg.cn/img_convert/35cb6116ca0cadd8099daca846f80161.png)
<?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
![](https://i-blog.csdnimg.cn/img_convert/a7cce2e27839710f47ccc7136e6d787b.png)
![](https://i-blog.csdnimg.cn/img_convert/00ec8498d69fef7f2879b0e79a695c3c.png)
![](https://i-blog.csdnimg.cn/img_convert/0df6e3ee54fbe55f6c1e298d956857c5.png)
5.3 抢票并发模拟,出现超卖问题
抢票并发模拟, 出现超卖问题:
1、安装工具 ab 模拟测试
说明: 工具 ab 可以模拟并发发出 Http 请求, 说明(模拟并发 http 请求工具还有 jemeter, postman,我们都使用一下, 开阔眼界, 这里使用 ab 工具)
安装指令: yum install httpd-tools (提示: 保证当前 linux 是可以联网的)
如果你不能联网, 可以使用 rpm 安装, 这里我使用 yum 方式安装
另外, 使用 rpm 方式安装我也给小伙伴说明一下, 如下: -先挂载 centos 安装文件 ios, 这个文件,
![](https://i-blog.csdnimg.cn/img_convert/9476ad42169ce34449a9893d47915498.png)
![](https://i-blog.csdnimg.cn/img_convert/cad66df4c9d255e3ec0e1ef3b5e0277c.png)
进入
cd /run/media/root/CentOS 7 x86_64/Packages
顺序安装
apr-1.4.8-3.el7.x86_64.rpm apr-util-1.5.2-6.el7.x86_64.rpm httpd-tools-2.4.6-67.el7.centos.x86_64.rpm
![](https://i-blog.csdnimg.cn/img_convert/4929d811313ff6f35dc939e33e6f76c7.png)
![](https://i-blog.csdnimg.cn/img_convert/d5476436530808d56ac4507213ad2e8e.png)
测试是否安装成功
![](https://i-blog.csdnimg.cn/img_convert/735b88ec369b3abde26f8df1fb4b246b.png)
这里我们使用 yum
安装,执行如下命令即可:前提是我们要是联网的状态才行。
yum install httpd-tools
[root@localhost ~]# yum install httpd-tools
![](https://i-blog.csdnimg.cn/img_convert/99680bec9871ff3d97d83b6ca0f7e527.png)
![](https://i-blog.csdnimg.cn/img_convert/ba3cc96151f164caf6f499d276ef55d1.png)
![](https://i-blog.csdnimg.cn/img_convert/075822e1a80b02d82f006be3b981fb24.png)
安装完后,输入 ab
命令,测试是否安装成功
ab
![](https://i-blog.csdnimg.cn/img_convert/7e1a4a6be528b0086f03ceaa31d8bdff.png)
2、在 ab 指令执行的当前路径下 创建文件 postfile
vi postfile
vm postfile
![](https://i-blog.csdnimg.cn/img_convert/becaea22dc96a28720302bf61b2f4aa5.png)
![](https://i-blog.csdnimg.cn/img_convert/920a16b61166ea4e37571a23e2f413de.png)
ticketNo=bj_cd&
3、执行指令 , 注意保证 linux 可以访问到 Tomcat 所在的服务器.
先查看 Tomcat 所在 Windows 的网络配置情况。
在 Window 系统中输入 cmd ,进入命令行窗口,输入 ipconfig
命令,查看 VM虚拟机的IP地址情况。
![](https://i-blog.csdnimg.cn/img_convert/fdf754316532fc86778b737c32817f8f.png)
确认 Linux 可以 ping 通 Windows
![](https://i-blog.csdnimg.cn/img_convert/6aa75bdda86531416611f1747d220d98.png)
如果 Ping 不通, 确认一下 Windows 防火墙是否关闭.
![](https://i-blog.csdnimg.cn/img_convert/7b45849cb6033de20050b7830d4e9097.png)
防火墙关闭是一件比较危险的事情,记得学习操作完之后,回来将防火墙重新打开 。
![](https://i-blog.csdnimg.cn/img_convert/d00f7ffa0ab01157c4cefec5e3e513e6.png)
指令 , 测试前把 Redis 的数据先重置一下。因为前面我们操作过来, 票已经被卖光了,我们需要重新设置票数,这里还是设置为 6 张票。
![](https://i-blog.csdnimg.cn/img_convert/26f1ebe52dff288dc1b38db5891c253d.png)
如下是我们使用 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++
![](https://i-blog.csdnimg.cn/img_convert/7a6d1315df95e401365fe1f784ef59c6.png)
++http://192.168.198.1:8080/seckill/secKillServlet++ 就 是 请 求 的 url, 注 意 这 里 的 IP:port/uri 必须写正确.
![](https://i-blog.csdnimg.cn/img_convert/66b53ca3e327508bf93d71b48b339866.png)
![](https://i-blog.csdnimg.cn/img_convert/c9954845c43058faaf7f74fc28f189b5.png)
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
![](https://i-blog.csdnimg.cn/img_convert/c567beed6348d9869fa3d401c92c7be3.png)
执行之后。查看执行结果
![](https://i-blog.csdnimg.cn/img_convert/70825d4615d2ec41071e4bb4122e9e69.png)
![](https://i-blog.csdnimg.cn/img_convert/2573578b9fccf6d6923d2f55f32efbd8.png)
![](https://i-blog.csdnimg.cn/img_convert/f87c2ebe4a4f896da5aae33fc1f26269.png)
![](https://i-blog.csdnimg.cn/img_convert/aa92ee796dffb3dc96ff8bfea8706d6e.png)
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
![](https://i-blog.csdnimg.cn/img_convert/6d5a36d70c47008f663487422a6ba715.png)
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对象/连接,释放到连接池中。
}
}
}
线程可见性
简单说一下指令重排
![](https://i-blog.csdnimg.cn/img_convert/d8d5ca38b6fe95dc752057ecd047978f.png)
![](https://i-blog.csdnimg.cn/img_convert/18aaf018a09543925482355b3fb62502.png)
// 通过连接池获取 jedis对象/连接
JedisPool jedisPoolInstacne = JedisPoolUtil.getJedisPoolInstacne();
Jedis jedis = jedisPoolInstacne.getResource();
System.out.println("--使用的连接池");
![](https://i-blog.csdnimg.cn/img_convert/c90287fab74db664e2fa09d592e0510e.png)
JedisPoolUtil.release(jedis);
使用连接池:测试:
![](https://i-blog.csdnimg.cn/img_convert/3786d0b6a886175be2f0a73509dd6be4.png)
5.5 利用 Redis 的事务机制,解决超卖问题(使用 watch,multi )
控制超卖-Redis 事务底层(乐观锁机制分析)
![](https://i-blog.csdnimg.cn/img_convert/575ad60dc778c6de9ec9dc1ac3ded4da.png)
我们要处理的控制的就是,如下这部分代码,因为是这部分代码对 Redis 库当的数据进行操作和修改的,需要进行事务上的控制。
![](https://i-blog.csdnimg.cn/img_convert/d14d3ff0eaa2031d2b2109bf9727e606.png)
![](https://i-blog.csdnimg.cn/img_convert/219dbde2c59a5da1e84cd584af414265.png)
// 监控库存,开启 watch 的监控
jedis.watch(stockKey);
![](https://i-blog.csdnimg.cn/img_convert/c71fde0901701d15e368a8e271859653.png)
// 使用事务,完成秒杀
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 相关数据:
![](https://i-blog.csdnimg.cn/img_convert/ec243079251d24c4dfbcb33a17fc91d5.png)
执行指令:
[root@localhost ~]# ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.76.1:9090/seckill/secKillServle
![](https://i-blog.csdnimg.cn/img_convert/6a368cc4b6467a53667c21dd9a389d8f.png)
![](https://i-blog.csdnimg.cn/img_convert/c0bd908c5e932d96ced20c4ee13d2388.png)
![](https://i-blog.csdnimg.cn/img_convert/8b65839e2b3c74bbfc9bbbf9ffe655b6.png)
5.6 抢票并发模拟,分析出现库存遗留问题
抢票并发模拟,出现库存遗留问题
这里我们演示一下:出现库存遗留问题
先重置一下 redis 的数据
这里我们把库存量设的较大为 600
127.0.0.1:6379> set sk:bj_cd:ticket 600
OK
127.0.0.1:6379> get sk:bj_cd:ticket
"600"
127.0.0.1:6379> del sk:bj_cd:user
(integer) 1
127.0.0.1:6379> smembers sk:bj_cd:user
(empty array)
127.0.0.1:6379>
![](https://i-blog.csdnimg.cn/img_convert/50d75b8adc539ec73d5fd988ab94e917.png)
执行指令
[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
![](https://i-blog.csdnimg.cn/img_convert/183e61c98b95f9903c8c11c11395e57a.png)
运行结果:
![](https://i-blog.csdnimg.cn/img_convert/63fff35d9ea462aec8b8095887a8de8a.png)
从运行结果上看:我们可以看到600张票,1000个人抢,仅仅只卖出了 6 张票。这库存问题十分严重
出现库存遗留问题的分析
![](https://i-blog.csdnimg.cn/img_convert/94383fdb70414fcb19d593b550640f8f.png)
简单的理解就是:因为乐观锁的机制,这里 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 脚本, 解决库存遗留-思路分析图
![](https://i-blog.csdnimg.cn/img_convert/7e062b416a013283e7f0621d71f78d61.png)
上图解图:
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; -- 拼接 usersKey
local userExists=redis.call(sismember,usersKey,userid); -- 查 看 在 redis 的 usersKey set 中是否有该用户
if tonumber(userExists)==1 then
return 2; -- 如果该用户已经购买, 返回 2
end
local num= redis.call("get" ,stockKey); -- 获取剩余票数
if tonumber(num)<=0 then
return 0; -- 如果已经没有票, 返回 0
else
redis.call("decr",stockKey); -- 将剩余票数-1
redis.call("sadd",usersKey,userid); -- 将抢到票的用户加入 set
end
return 1 -- 返回 1 表示抢票成功
这里:我们根据 LUA脚本对 Java程序进行修改。如下图所示:
![](https://i-blog.csdnimg.cn/img_convert/858f3da9d85b6d3bd4afcaf51b83944b.png)
/**
* 说明
* 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脚本完成秒杀
![](https://i-blog.csdnimg.cn/img_convert/8993c9409d80aff56600320843c739d4.png)
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 600
OK
127.0.0.1:6379> get sk:bj_cd:ticket
"600"
127.0.0.1:6379> del sk:bj_cd:user
(integer) 1
127.0.0.1:6379> smembers sk:bj_cd:user
(empty array)
127.0.0.1:6379>
![](https://i-blog.csdnimg.cn/img_convert/5f789ccaa2bb44e21c2ee02a34e63027.png)
执行指令
[root@localhost ~]# ab -n 2000 -c 200 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.76.1:9090/seckill/secKillServlet
![](https://i-blog.csdnimg.cn/img_convert/f43a92d32849beed462ed6ec17ddbb27.png)
![](https://i-blog.csdnimg.cn/img_convert/21de38debedf9a0ce11836899e5d2ee6.png)
![](https://i-blog.csdnimg.cn/img_convert/75923858e215ee812138c3b91a372d00.png)
文章转载自: ++Rainbow-Sea++
原文链接: 十. Redis 事务和 "锁机制"------> 并发秒杀处理的详细说明 - Rainbow-Sea - 博客园