最近想给学生做个带页面的redis秒杀场景,网上找了很多都是java的,由于最近刚好学生也在学python,想着用python的相关web框架写个页面然后实现redis缓存数据库支持秒杀的场景。
页面效果如下:
先看下项目代码结构:
前端seckill.html页面代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redis高并发测试</title>
<script src="../static/jquery-3.5.1.min.js"></script>
<style>
#user,#submit{
margin-bottom: 10px;
padding: 0;
border: 0.5px solid gray;
border-radius: 3px;
width: 200px;
height: 30px;
position: relative;
left: 25px;
}
#submit{
color: #fff;
background-color: #4b95ff;
color: yellow;
font-weight: bolder;
width: 80px;
}
</style>
</head>
<body>
<h1>使用Flask框架搭建的简单网页模拟Redis在“秒杀”场景中的使用</h1>
<input id="user" type="text" name="user" placeholder="用户ID">
<button id="submit" type="submit">秒杀</button>
<hr/>
<h3 id="rep"></h3>
<h3 id="res"></h3>
</body>
<script>
request_method = 'POST'
if(window.XMLHttpRequest){
var xhr = new XMLHttpRequest();
}else{
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
// AJAX原生方式请求-post
$("#submit").on("click",function(){
console.log($("#user").val())
xhr.open('post','http://127.0.0.1:5000/seckill',true)
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded")
xhr.send('msg='+$("#user").val())
request_method = 'POST'
})
xhr.onreadystatechange = function(){
if (xhr.readyState==4&&xhr.status==200){
// 将JSON格式的数据转为字典
console.log($.parseJSON(xhr.response))
console.log(JSON.parse(xhr.response))
// 修改原网页内容
console.log(xhr.response)
var rep_num = JSON.parse(xhr.response)["response_num"]
$('#rep').text(`${request_method}方式提交数据成功,提交的用户ID前缀为${$("#user").val()},服务器的响应状态码为${rep_num}`)
switch(rep_num){
case 1:var user_text = '秒杀成功!'
break
case -1:var user_text = '秒杀已经结束。'
break
case -2:var user_text = '不能重复秒杀。'
break
}
$('#res').text(`用户${JSON.parse(xhr.response)["user_id"]}:${user_text}`)
}
}
</script>
</html>
后端代码:
# -*- coding:utf8 -*-
from flask import Flask, request, render_template, jsonify
from flask.json import loads
import random
import redis
app = Flask(__name__)
# 使用连接池和乐观锁处理连接超时和超卖问题,可能因锁的影响导致库存遗留
# 初始化Redis数据库 decode_responses=True:这样写存的数据是字符串格式
pyredis = redis.Redis(host='192.168.42.29', port=6379, db=0, decode_responses=True)
# pyredis.set(name='commodity:num',value=10)
pyredis.set(name='commodity:num', value=500)
pyredis.delete('user:id')
pyredis.sadd('user:id', 'null')
pyredis.close()
# 使用Redis连接池:处理连接超时问题
# Redis虽然是单线程但使用多路IO复用,性能瓶颈更有可能是建立连接的网络通信延迟
# 使用线程池,在连接建立后不会频繁释放、重建连接,而是返回连接池等待下次连接
pool = redis.ConnectionPool(host='192.168.42.29', port=6379, db=0)
# 原生AJAX请求
# 考虑并发请求(使用乐观锁和事务:处理超卖问题)
@app.route('/seckill', methods=['POST', 'GET'])
def fun_send():
if request.method == 'GET':
return render_template('seckill.html')
else:
pyredis = redis.Redis(connection_pool=pool)
rpipe = pyredis.pipeline(transaction=True)
baseid = request.form.get('msg', type=str).strip()
# print(baseid)
# 使用随机函数生成用户id,baseid使用postfile值
userid = baseid + str(random.randint(0, 100000))
# 增加乐观锁:解决超卖问题
rpipe.watch('commodity:num')
if int(rpipe.get('commodity:num')) < 1:
print('秒杀已结束')
return jsonify(user_id=userid, response_num=-1)
elif rpipe.sismember('user:id', userid):
print(f'用户%{userid}已秒杀过商品,不可重复秒杀!')
return jsonify(user_id=userid, response_num=-2)
else:
# 使用事务
rpipe.multi()
# 组队
rpipe.decr('commodity:num', amount=1)
rpipe.sadd('user:id', userid)
# 执行
reslist = rpipe.execute()
rpipe.close()
print(reslist)
print(f'用户{userid}成功秒杀商品!')
print('当前剩余商品数:%d' % (loads(pyredis.get('commodity:num'))))
return jsonify(user_id=userid, response_num=1)
# 以debug模式启动网页服务,这里的host填写自己windows服务器ip,因为一会要以linux的ab工具压力测试
app.run(debug=True, host='192.168.1.42', port=5000)
# 活动结束关闭网页打印出秒杀成功的用户
pyredis = redis.Redis(connection_pool=pool)
print('活动成功秒杀商品的用户:')
print(pyredis.smembers('user:id'))
print('当前剩余商品数:%d' % (loads(pyredis.get('commodity:num'))))
# ab(Apache bench)压力测试工具
# 对post请求进行压力测试
'''
在你的linux系统中使用yum install http-tools就有ab的压测工具了
ab -n 1000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.1.42:5000/seckill
'''
# 创建postfile
'''
touch postfile
echo "msg=#ABCD" > postfile
'''
# 终端执行python脚本生成输出文件
'''
Linux:
chmod u+x flask_redis_web3.py
./flask_redis_web3.py > output 2>&1
Windos:
使用pycharm执行 页面访问http://你python项目服务器的ip:5000/seckill.html
'''
后端服务打开后在网页点击提交的效果:
前端页面提交一次请求
pycharm有日志打印
redis数据库有记录秒杀成功用户的id
前端页面多次点击秒杀会有多次库存消减的结果
后台数据库同时也会记录到
这里我们只是手动单次点击模拟不出来真实并发场景秒杀抢购的情况,因为实际上比如双11抢购秒杀iphone13是有几万几百万甚至上千万的人同时在线秒杀,所以我们这里就不能用单个用户模拟了(不过如果可以实现多个在线用户模拟点击秒杀按钮事件也是可以的,比如selenium),这里我们使用apache bench工具,也就是业内常说的ab工具,ab是apache自带的一个很好用的压力测试工具 ,可以模拟大并发量的用户请求。
ab安装:
通常使用yum安装就可以了
yum install -y http-tools
安装完成后可以使用ab --help看下帮助菜单,同时也能验证是否安装成功。
这里避免看不懂英文的,贴出中文
-n 要执行请求数,默认会执行一个请求
-c 一次执行多个请求的数量,默认是一次一个请求。
-t 用于基准测试的最大秒数,使用它在固定的总时间内对服务器进行基准测试。默认情况下,没有时间限制。
-s 超时之前等待的最大秒数。 默认值是30秒。
-b TCP发送/接收缓冲区的大小,以字节为单位。
-B 进行传出连接时要绑定的地址。
-p 包含数据到POST的文件。 还请记住设置-T。
-u 包含PUT数据的文件。 还请记住设置-T 。
-T Content-type用于POST / PUT数据的内容类型内容类型标题,例如:'application/x-www-form-urlencoded' 默认是'text/plain'
-v verbosity 要打印多少个疑难解答信息,设置详细级别 - 4和以上打印标题信息,3和以上打印响应代码(404,200等),2和以上打印警告和信息。
-w 在HTML表格中打印结果。
-i 使用HEAD代替GET。
-x 用作<table>的属性的字符串。 属性被插入<table here>。
-y 用作<tr>的属性的字符串。
-z 用作<td>的属性的字符串。
-C 将cookie添加到请求。 参数通常采用名称=值对的形式。 这个字段是可重复的。
-H attribute 例如 ‘Accept-Encoding: gzip’ 插入所有普通标题行之后。(重复)
-A 添加基本的WWW认证,该属性是一个冒号分隔的用户名和密码,auth-username:password
-P 添加基本代理验证,属性是一个冒号分隔的用户名和密码,proxy-auth-username:password
-X 使用代理服务器和端口号。
-V 打印版本号并退出。
-k 使用HTTP KeepAlive功能。
-d 不要显示百分点服务表。
-S 不要显示信心估计和警告。
-q 做超过150个请求时不要显示进度。
-g 将收集的数据输出到gnuplot格式文件。
-e 输出提供百分比的CSV文件。
-r 不要退出套接字接收错误。
-h 显示使用情况信息(此消息)。
-Z 密码套件指定SSL / TLS密码套件(请参阅openssl密码)
-f 指定SSL / TLS协议 (SSL3, TLS1, TLS1.1, TLS1.2 or ALL)
压力测试(这里模拟1000个请求量,并发数是100个):
ab -n 1000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.1.42:5000/seckill
压力测试结果解释:
##首先是apache的版本信息
This is ApacheBench, Version 2.3 <Revision:655654>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.xxx.xxx/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking xxx.xxx.com (be patient)
Server Software: Apache/2.2.19 ##apache版本
Server Hostname: vm1.xxx.com ##请求的机子
Server Port: 80 ##请求端口
Document Path: /xxx.html
Document Length: 25 bytes ##页面长度
Concurrency Level: 100 ##并发数
Time taken for tests: 0.273 seconds ##共使用了多少时间
Complete requests: 1000 ##请求数
Failed requests: 0 ##失败请求
Write errors: 0
Total transferred: 275000 bytes ##总共传输字节数,包含http的头信息等
HTML transferred: 25000 bytes ##html字节数,实际的页面传递字节数
Requests per second: 3661.60 [#/sec] (mean) ##每秒多少请求,这个是非常重要的参数数值,服务器的吞吐量
Time per request: 27.310 [ms] (mean) ##用户平均请求等待时间
Time per request: 0.273 [ms] (mean, across all concurrent requests) ##服务器平均处理时间,也就是服务器吞吐量的倒数
Transfer rate: 983.34 [Kbytes/sec] received ##每秒获取的数据长度
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 2.3 0 16
Processing: 6 25 3.2 25 32
Waiting: 5 24 3.2 25 32
Total: 6 25 4.0 25 48
Percentage of the requests served within a certain time (ms)
50% 25 ## 50%的请求在25ms内返回
66% 26 ## 60%的请求在26ms内返回
75% 26
80% 26
90% 27
95% 31
98% 38
99% 43
100% 48 (longest request)
这时候发现后台redis数据库 记录了成功用户的随机id有458个,加上我前台页面点了两次总共有500个用户抢购成功。Redis数据库默认使用乐观锁,使用watch命令监控键值对,在事务对监控的键值对进行修改前,会比较此时的键值对和开启监视时的版本是否一致,若出现了修改,则不允许操作。
代码中设置的500个键值
记得要先打开redis数据库服务,再启动python网页服务,就可以用ab测试工具模拟高并发访问出现的异常。
注意事项:
使用乐观锁和事务方式较好的解决了“超卖”问题,但在库存较多时,可能出现遗留库存。大量的请求在某个时间点同时发起,很多请求因为键值对发生了修改,版本不一致导致后续操作失败,因此库存可能没有一次清空,出现遗留。
建立连接时网络的延迟也是并发场景下重要影响因素,可能在查询时花费的时间比频繁建立、关闭连接所花费的时间还少,这时最好将连接方式改为使用连接池,连接建立后就不会频繁释放、重建连接,而是返回连接池等待下次连接。
代码中已经是连接池
连接池的作用就是为了提高性能。
连接池的作用:连接池是将已经创建好的连接保存在池中,当有请求来时,直接使用已经创建好的连接对数据库进行访问。这样省略了创建连接和销毁连接的过程。这样性能上得到了提高。
关于原理可以看下这篇文章:连接池的作用及讲解_cbmljs的博客-CSDN博客_连接池
python页面秒杀参考牛人链接:使用Flask框架搭建的简单网页模拟Redis在“秒杀”场景中的使用(上) - 简书