11、Redis_事务_秒杀案例


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;
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值