文章目录
- 11、Redis_事务_秒杀案例
- 11.1 解决计数器和人员记录的事务操作
- 11.2 Redis事务--秒杀并发模拟
- 11.2.1 联网
- 11.2.2 无网络
- 11.2.3 测试及结果
- 11.2.3.1 通过ab测试
- 11.2.3.2 超卖
- 11.3 超卖问题
- 11.4 利用乐观锁淘汰用户,解决超卖问题。
- 11.5 继续增加并发测试
- 11.5.1 连接有限制
- 11.5.2 已经秒光,可是还有库存
- 11.5.3 连接超时,通过连接池解决
- 11.5.4 连接池
- 11.6 解决库存遗留问题
- 11.6.1 LUA脚本
- 11.6.2 LUA脚本在Redis中的优势
- 11.7 Redis_事务_秒杀案例_代码
- 11.7.1 项目结构
- 11.7.2 第一版:简单版
- 11.7.3 第二版:加事务-乐观锁(解决超卖),但出现遗留库存和连接超时
- 11.7.4 第三版:连接池解决超时问题
- 11.7.5 第四版:解决库存依赖问题,LUA脚本
Redis 6 入门到精通-讲师:王泽
世态炎凉,世界并不善良
11、Redis_事务_秒杀案例
11.1 解决计数器和人员记录的事务操作
视频使用的是javaWeb,我为了省事,使用StringBoot
Service层
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class Seckill_redis {
@Autowired
private RedisTemplate redisTemplate;
// 秒杀过程
public boolean doSecKill(String uid, String prodid) {
// 1.uid和prodid非空判断
if (uid == null || prodid == null) {
return false;
}
// 2.连接redis,就是用spring的RedisTemplate
// 3.拼接key
// 3.1库存key
String kcKey = "sk:" + prodid + ":qt";
// 3.2秒杀成功用户的key
String userKey = "sk:" + prodid + ":user";
// 4.获取库存,如果库存null,秒杀还没有开始,这里redis存的就是数字,不是字符串
Integer kc = (Integer) redisTemplate.opsForValue().get(kcKey);
if (kc == null) {
System.out.println("秒杀还没有开始,请稍后");
return false;
}
// 5.判断用户是否做重复秒杀操作
if (redisTemplate.opsForSet().isMember(userKey, uid)) {
System.out.println("已经秒杀成功,不能在重复秒杀");
return false;
}
// 6.判断商品的数量,库存数量小于1,秒杀结束
if (kc <= 0) {
System.out.println("秒杀已经结束了");
return false;
}
// 7.秒杀过程
// 7.1 库存-1
redisTemplate.opsForValue().decrement(kcKey);
// 7.2 把秒杀成功用户添加清单里面
redisTemplate.opsForSet().add(userKey,uid);
System.out.println("秒杀陈功");
return true;
}
}
Controller层
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import pers.tianyu.redis_springboot.service.Seckill_redis;
import java.util.Random;
import java.util.UUID;
@RestController
@RequestMapping("/rediTest")
public class RedisTest {
@Autowired
private Seckill_redis seckill_redis;
// 秒杀
@RequestMapping("/doSecKill")
public boolean doSecKill(@RequestParam("prodid") String prodid) {
String userid = new Random().nextInt(5000) + "";
return seckill_redis.doSecKill(userid,prodid);
}
}
没有做前台页面,使用浏览器直接访问,url传值。
http://localhost:8080/rediTest/doSecKill?prodid=0101
连续秒杀,查看redis
127.0.0.1:6379> keys *
1) "sk:0101:user"
2) "sk:0101:qt"
127.0.0.1:6379> get sk:0101:qt
"0"
127.0.0.1:6379> smembers sk:0101:user
1) "\"2579\""
2) "\"2438\""
3) "\"3398\""
4) "\"2307\""
5) "\"4449\""
6) "\"3773\""
7) "\"4191\""
8) "\"2542\""
9) "\"3628\""
10) "\"389\""
11.2 Redis事务–秒杀并发模拟
使用工具ab模拟测试
CentOS6 默认安装
CentOS7需要手动安装
11.2.1 联网
yum install httpd-tools
[root@centos7-101 ~]# yum install httpd-tools
已加载插件:fastestmirror, langpacks
Determining fastest mirrors
* base: mirrors.bfsu.edu.cn
* extras: mirrors.bfsu.edu.cn
* updates: mirrors.bfsu.edu.cn
base | 3.6 kB 00:00:00
extras | 2.9 kB 00:00:00
updates | 2.9 kB 00:00:00
正在解决依赖关系
--> 正在检查事务
---> 软件包 httpd-tools.x86_64.0.2.4.6-97.el7.centos.5 将被 安装
--> 正在处理依赖关系 libaprutil-1.so.0()(64bit),它被软件包 httpd-tools-2.4.6-97.el7.centos.5.x86_64 需要
--> 正在处理依赖关系 libapr-1.so.0()(64bit),它被软件包 httpd-tools-2.4.6-97.el7.centos.5.x86_64 需要
--> 正在检查事务
---> 软件包 apr.x86_64.0.1.4.8-7.el7 将被 安装
---> 软件包 apr-util.x86_64.0.1.5.2-6.el7 将被 安装
--> 解决依赖关系完成
依赖关系解决
=================================================================================
Package 架构 版本 源 大小
=================================================================================
正在安装:
httpd-tools x86_64 2.4.6-97.el7.centos.5 updates 94 k
为依赖而安装:
apr x86_64 1.4.8-7.el7 base 104 k
apr-util x86_64 1.5.2-6.el7 base 92 k
事务概要
=================================================================================
安装 1 软件包 (+2 依赖软件包)
总下载量:290 k
安装大小:584 k
Is this ok [y/d/N]: y
Downloading packages:
(1/3): apr-1.4.8-7.el7.x86_64.rpm | 104 kB 00:00:00
(2/3): apr-util-1.5.2-6.el7.x86_64.rpm | 92 kB 00:00:00
(3/3): httpd-tools-2.4.6-97.el7.centos.5.x86_64.rpm | 94 kB 00:00:00
---------------------------------------------------------------------------------
总计 260 kB/s | 290 kB 00:01
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
正在安装 : apr-1.4.8-7.el7.x86_64 1/3
正在安装 : apr-util-1.5.2-6.el7.x86_64 2/3
正在安装 : httpd-tools-2.4.6-97.el7.centos.5.x86_64 3/3
验证中 : apr-1.4.8-7.el7.x86_64 1/3
验证中 : httpd-tools-2.4.6-97.el7.centos.5.x86_64 2/3
验证中 : apr-util-1.5.2-6.el7.x86_64 3/3
已安装:
httpd-tools.x86_64 0:2.4.6-97.el7.centos.5
作为依赖被安装:
apr.x86_64 0:1.4.8-7.el7 apr-util.x86_64 0:1.5.2-6.el7
完毕!
查看工具
[root@centos7-101 ~]# ab --help
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
# 当前的请求次数
-n requests Number of requests to perform
# 当前的并发次数
-c concurrency Number of multiple requests to make at a time
-t timelimit Seconds to max. to spend on benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default is 30 seconds
-b windowsize Size of TCP send/receive buffer, in bytes
-B address Address to bind to when making outgoing connections
# 使用post提交,里面有参数,将参数放在一个postfile文件中,进行提交
-p postfile File containing data to POST. Remember also to set -T
-u putfile File containing data to PUT. Remember also to set -T
# 如果提交使用post或put,需要设置数据类型为'application/x-www-form-urlencoded'
-T content-type Content-type header to use for POST/PUT data, eg.
'application/x-www-form-urlencoded'
Default is 'text/plain'
-v verbosity How much troubleshooting info to print
-w Print out results in HTML tables
-i Use HEAD instead of GET
-x attributes String to insert as table attributes
-y attributes String to insert as tr attributes
-z attributes String to insert as td or th attributes
-C attribute Add cookie, eg. 'Apache=1234'. (repeatable)
-H attribute Add Arbitrary header line, eg. 'Accept-Encoding: gzip'
Inserted after all normal header lines. (repeatable)
-A attribute Add Basic WWW Authentication, the attributes
are a colon separated username and password.
-P attribute Add Basic Proxy Authentication, the attributes
are a colon separated username and password.
-X proxy:port Proxyserver and port number to use
-V Print version number and exit
-k Use HTTP KeepAlive feature
-d Do not show percentiles served table.
-S Do not show confidence estimators and warnings.
-q Do not show progress when doing more than 150 requests
-g filename Output collected data to gnuplot format file.
-e filename Output CSV file with percentages served
-r Don't exit on socket receive errors.
-h Display usage information (this message)
-Z ciphersuite Specify SSL/TLS cipher suite (See openssl ciphers)
-f protocol Specify SSL/TLS protocol
(SSL3, TLS1, TLS1.1, TLS1.2 or ALL)
11.2.2 无网络
(1) 进入cd /run/media/root/CentOS 7 x86_64/Packages(路径跟centos6不同)
(2) 顺序安装
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
11.2.3 测试及结果
11.2.3.1 通过ab测试
-n:请求次数。
-c:当前的并发次数。
-T:如果使用pust或put提交,要设置参数类型。
-p:设置一个文件,文件内放的参数就是post提交的参数。
vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。
内容:prodid=0101&
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.0.110:8080/rediTest/doSecKill
注意:如果访问ip地址或者端口号写错了,会出现apr_socket_recv: Connection refused (111)
将项目运行,redis数据还原。
11.2.3.2 超卖
127.0.0.1:6379> smembers sk:0101:user
1) "\"3502\""
2) "\"916\""
3) "\"3528\""
4) "\"1546\""
5) "\"4475\""
6) "\"3519\""
7) "\"3078\""
8) "\"833\""
9) "\"941\""
10) "\"3422\""
11) "\"3473\""
12) "\"971\""
13) "\"103\""
14) "\"1108\""
15) "\"4012\""
16) "\"1190\""
17) "\"4368\""
18) "\"4997\""
19) "\"1101\""
20) "\"4205\""
21) "\"508\""
22) "\"648\""
23) "\"1978\""
24) "\"282\""
25) "\"4356\""
26) "\"1916\""
27) "\"3859\""
28) "\"4140\""
29) "\"2764\""
30) "\"933\""
31) "\"963\""
32) "\"2966\""
33) "\"4778\""
34) "\"3599\""
35) "\"3988\""
36) "\"3197\""
37) "\"2303\""
38) "\"2616\""
39) "\"3480\""
40) "\"4748\""
41) "\"2374\""
42) "\"2503\""
43) "\"4448\""
44) "\"360\""
45) "\"4077\""
46) "\"2537\""
47) "\"94\""
48) "\"4038\""
49) "\"3575\""
50) "\"741\""
51) "\"3309\""
52) "\"44\""
53) "\"376\""
54) "\"915\""
55) "\"1993\""
56) "\"636\""
57) "\"3319\""
58) "\"4324\""
59) "\"3856\""
60) "\"3462\""
61) "\"1238\""
62) "\"834\""
63) "\"3129\""
64) "\"4625\""
65) "\"1668\""
66) "\"1864\""
67) "\"4707\""
68) "\"3866\""
69) "\"4514\""
70) "\"3914\""
71) "\"2900\""
72) "\"867\""
73) "\"1267\""
74) "\"2535\""
75) "\"3024\""
76) "\"919\""
77) "\"1086\""
78) "\"389\""
79) "\"3898\""
80) "\"594\""
81) "\"2430\""
82) "\"484\""
83) "\"2192\""
84) "\"4715\""
85) "\"4313\""
86) "\"3600\""
87) "\"3275\""
88) "\"1184\""
89) "\"1742\""
90) "\"1195\""
91) "\"4737\""
92) "\"3685\""
93) "\"140\""
94) "\"4487\""
95) "\"1707\""
96) "\"3775\""
97) "\"3831\""
98) "\"158\""
99) "\"4429\""
127.0.0.1:6379> get sk:0101:qt
"-91"
11.3 超卖问题
11.4 利用乐观锁淘汰用户,解决超卖问题。
//增加乐观锁
jedis.watch(qtkey);
//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null || "".equals(qtkeystr.trim())) {
System.out.println("未初始化库存");
jedis.close();
return false ;
}
int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {
System.err.println("已经秒光");
jedis.close();
return false;
}
//增加事务
Transaction multi = jedis.multi();
//4.减少库存
//jedis.decr(qtkey);
multi.decr(qtkey);
//5.加人
//jedis.sadd(usrkey, uid);
multi.sadd(usrkey, uid);
//执行事务
List<Object> list = multi.exec();
//判断事务提交是否失败
if(list==null || list.size()==0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.err.println("秒杀成功");
jedis.close();
11.5 继续增加并发测试
11.5.1 连接有限制
ab -n 2000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.0.101:8080/rediTest/doSecKill
增加-r参数,-r Don’t exit on socket receive errors.
ab -n 2000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.0.101:8080/rediTest/doSecKill
11.5.2 已经秒光,可是还有库存
ab -n 2000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.0.101:8080/rediTest/doSecKill
已经秒光,可是还有库存。
原因,就是乐观锁导致很多请求都失败。
先点的没秒到,后点的可能秒到了。
11.5.3 连接超时,通过连接池解决
11.5.4 连接池
节省每次连接redis服务带来的消耗,把连接好的实例反复利用。
通过参数管理连接的行为
代码见项目中
- 链接池参数
- MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
- maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
- MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
- testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
连接池工具类
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil(){
}
public static JedisPool getJedisPoolInstance(){
if (null == jedisPool){
synchronized (JedisPoolUtil.class){
if (null == jedisPool){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.ssetMaxIdle(32);
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig,"192.168.0.101",6379,60000);
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis){
if (null != jedis){
jedisPool.returnResource(jedis);
}
}
}
11.6 解决库存遗留问题
11.6.1 LUA脚本
Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。
w3cschool教程
11.6.2 LUA脚本在Redis中的优势
将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
利用lua脚本淘汰用户,解决超卖问题。
redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
11.7 Redis_事务_秒杀案例_代码
11.7.1 项目结构
11.7.2 第一版:简单版
老师点10次,正常秒杀。
同学一起点试一试,秒杀也是正常的。
这是因为还达不到并发的效果。
使用工具ab模拟并发测试,会出现超卖情况。
查看库存会出现负数。
11.7.3 第二版:加事务-乐观锁(解决超卖),但出现遗留库存和连接超时
11.7.4 第三版:连接池解决超时问题
// 通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
11.7.5 第四版:解决库存依赖问题,LUA脚本
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr';
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",qtkey);
redis.call("sadd",usersKey,userid);
end
return 1;