Redis_事务_锁机制_秒杀
Redis 的事务是什么?
1、Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行
2、事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
3、Redis 事务的主要作用就是串联多个命令防止别的命令插队
Redis 事务三特性
一单独的隔离操作
1、事务中的所有命令都会序列化、按顺序地执行
2、事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
二没有隔离级别的概念
队列中的命令(指令), 在没有提交前都不会实际被执行
三不保证原子性
事务执行过程中, 如果有指令执行失败,其它的指令仍然会被执行, 没有回滚
事务相关指令Multi、Exec、discard
示意图
Redis 事务指令示意图
解读上图
- 从输入Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行(类似Mysql的start transaction 开启事务)
- 输入Exec 后,Redis 会将之前的命令队列中的命令依次执行(类似Mysql 的commit 提交事务)
- 组队的过程中可以通过discard 来放弃组队(类似Mysql 的rollback 回顾事务)
- 说明: Redis 事务和Mysql 事务本质是完全不同的, 用Mysql 的做类似说明, 是为了好理解
快速入门
1、需求: 请依次向Redis 中, 添加三组数据, k1-v1 k2-v2 k3-v3, 要求使用Redis 的事务完成
注意事项和细节
1、组队的过程中, 可以通过discard 来放弃组队
2、如果在组队阶段报错, 会导致exec 失败, 那么事务的所有指令都不会被执行
3、如果组队成功, 但是指令有不能正常执行的, 那么exec 提交, 会出现有成功有失败情况,也就是事务得到部分执行, 这种情况下, Redis 事务不具备原子性.
事务冲突及解决方案
先看一个问题
经典的抢票问题
-
一个请求想购买6
-
一个请求想购买5
-
一个请求想购买1
解读上图
- 如果没有控制, 会造成超卖现象
- 如果3 个指令, 都得到执行, 最后剩余的票数是-2
悲观锁
工作示意图
解读上图
- 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁
- 这样别人/其它请求想拿这个数据就会block 直到它拿到锁。
- 悲观锁是锁设计理念, 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁.
乐观锁
工作示意图
解读上图
- 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁
- 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种check-and-set机制实现事务的
- 乐观锁是锁设计理念
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 模拟测试
- 说明: 工具ab 可以模拟并发发出Http 请求, 老韩说明(模拟并发http 请求工具还有jemeter, postman,我们都使用一下, 开阔眼界, 这里老师使用ab 工具)
- 安装指令: yum install httpd-tools (提示: 保证当前linux 是可以联网的)
- 如果你不能联网, 可以使用rpm 安装, 这里使用yum 方式安装
- 另外, 使用rpm 方式安装我也给小伙伴说明一下, 如下:-先挂载centos 安装文件ios, 这个文件
–进入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
–测试是否安装成功
在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) ~的含义: https://blog.csdn.net/m0_67401134/article/details/123973115
(7)http://192.168.198.1:8080/seckill/secKillServlet 就是请求的url, 注意这里的IP:port/uri 必须写正确.
查看执行结果
注意我们这里先讲连接池然后在讲解决方法