Docker部署Redis集群----第十节(docker-redis哨兵集群“秒杀”篇实例二) - 知乎

本篇章是建立在上个篇章的基础之上来实现我们的实例二“秒杀”案例。

废话我们就不说了。

秒杀就是实实在在的高并发场景,一般都会经过“抢订单”这个环节,而这个环节是最能考验我们业务上提供的抗压能力。处理不好很容易照成超卖现象,这个肯定不符合我们的业务需求,因为任何商品都会有库存数量的上限。解决这个超卖其性能上注重的是优化:缓存、扩容和流量限制。当然这不是本篇章要讲的内容。

超卖的原因:

库存固定数额,当剩余的商品还有 1 个时,突然系统发来了N条并发请,导致这些请求都读取到了该商品仅有的 1 个余量,然后这时都通过了这个余量的判断,最终导致超卖。见下图:

大家可能会说悲观锁、乐观锁的处理机制。这里只讲下概念,不去给大家做实践了,我们要说的是 redis 高并发处理场景。悲观锁就不讲了,无法达到高并发的要求,不适合做秒杀,一点儿意义都没有。

那什么是乐观锁呢?

为什么要讲乐观锁,因为它跟我们今天要讲的篇章有一定的关系。乐观锁假设认为数据一般情况下不会照成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户自己决定如何去做。

而redis当中的 watch 指令在redis 事物中提供了检测的行为。为了检测被 watch 的keys在是否有多个clients 同时改变引起冲突,这些keys 将会被监控。如果至少有一个被监控的key在执行exec命令钱被修改,整个事物将被回滚,则不会执行任何动作,从而保证了原子性操作,并且执行了exec会得到null的回复。

乐观锁的实现方法:

简单一句话:就是使用数据版本记录机制来实现。

意思是为数据增加一个版本标识,一般都是通过为数据库表增加一个数字类型的“version”字段来实现。当在读取数据时,将version 字段的值一同读出,数据没进行一次更新,version 的值就加 1。当我们提交数据更新的时,判断数据库表对应记录值的当期版本信息与第一次取出来的 version 值进行对比,如果数据库表当前版本号与第一次取出来的 version 值相等时才给与更新,否则认为是过期数据。思路是这样的:用户在获取订单时,更新版本号 version+1,在下单时用当前的版本号与库中的版本号做对比检测,存在则下单,不存在说明已被其他用户更新了,那么本次的秒杀就失败结束了。

讲到了这里不知道大家有没有理解,我们先看看这样的实例:

在目录 /home/wwwroot/default 创建 msa 项目文件

实例一

准备两个php:index.php 主程序执行文件、mysql.php 数据库连接文件于msa 文件中:

下面是mysql.php数据库连接代码【应用单例模式构建】

class Db
{
    //存放实例
    private static $_instance = null;
    private $db;

    private function __construct()
    {
        $dsn = "mysql:host=localhost;port=3306;dbname=msa";
        $options = array(PDO::ATTR_PERSISTENT => false);
        try {
            $db = new PDO($dsn, "root", $pass, $options);
            $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            //设置如果sql语句执行错误则抛出异常,事务会自动回滚
            $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);             
            //禁用prepared statements的仿真效果(防SQL注入)
            $db->setAttribute(PDO::ATTR_TIMEOUT, 5);
            //设置连接超时时间                         
        } catch (PDOException $e) {
            die('Connection failed: ' . $e->getMessage());
        }
        $db->exec('SET NAMES utf8mb4');
        $this->db = $db;
    }

    public static function getInstance()
    {
        if (!(self::$_instance instanceof DB)) {
            self::$_instance = new self();
    }
    return self::$_instance;
    }
    
    private function __clone()
    {
    }

    public function getConn()
    {
        return $this->db;
    }

    public function fetch($sql)
    {
        $query = $this->db->prepare($sql);
        $query->execute();
        $data = $query->fetch(PDO::FETCH_ASSOC);
        return $data;
    }
   
    public function cz($sql)
    {
        return $this->db->exec($sql);
    }
}
//简单地数据库连接就写完了。

注意:本篇章的内容是建立在上一节课的基础之上,如果上个篇章没有构建完成的话,请先构建上个篇章的内容。

数据库 database msa 表 table seckill 字段创建:
CREATE TABLE `seckill` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `num` int(11) NOT NULL,
  `verson` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

index.php我们创建两个index1.php、index2.php

在mysql终端执行插入数据命令:
mysql> insert into seckill (num) values (10);
index1.php代码:
//查找数据表信息
include "./mysql.php";
$sql  = "select * from `seckill` where id = 1";
$data = Db::getInstance()->fetch($sql);

if($data && $data['num'] > 0)
{
    sleep(1);
    //模拟真实环境、让程序休眠
    $sql = "update seckill set num=num-1 where id = 1";
    Db::getInstance()->cz($sql);
} else {
    echo "暂无数据,请新增";
}

上述的代码,在没有看到前面跟大家说的“超卖现象”一眼望去好像一点毛病都没有,可真实的情况呢?本篇章不是单纯告诉大家如何去解决秒杀,有时候很普遍,我们在做程序时,看到这种写法好像是没有问题,可是在高并发的情况下,往往你很难保证数据的无误性。所以这段代码一点儿都不够健壮,当然如果您的网站一天的pv量 都没几个鸟毛,不存在高并发的情况,那也就所谓了。好了现在我们执行下查找数据表信息:

mysql> select * from seckill;

好了我先执行下index1.php后再执行下结果看看

[root@instance-rttngj1u www]# php index1.php 

这个时候我们的num 就变成了9:

那么现在我们开始测试在高并发的场景下看看会发生什么情况:

我们选择apache 的ab 压力测试工具:

执行指令:
查看ab 是否安装:ab -V
如果没有安装的话执行指令:yum -y install httpd-tools

ab压力测试的参数:

-c 500 并发数
-n 1000 请求数
还有更多的参数指令 请执行ab --help 查看,我们这里只用 c 和 n 
现在我们执行下高并发请求:
[root@instance-rttngj1u default]# ab -c500 -n1000 http://106.12.212.131/msa/index1.php
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 106.12.212.131 (be patient)
Completed 100 requests
......
然后我们在查看数据信息会有什么变化

看到没num 变成了num -13了。下面是给大家准备的录制视频是整个实现的全过程:

秒杀超卖 过程演示

实例二

我们继续前面说的乐观锁看下面的结构流程图:

乐观锁顺序执行

开启watch监听提交冲突

下面我们接着上个篇章的内容改造下适用本篇章的内容:

include "./mysql.php";
include 'RoundSlave.php';
$sentinel = [
    ['ip' => '106.12.212.131','port' => 22536],
    ['ip' => '106.12.212.131','port' => 22537],
    ['ip' => '106.12.212.131','port' => 22538]
];
$getSentinel = $sentinel[array_rand($sentinel)];
//读redis数据
$redisRead = new Redis();
$redisRead->connect($getSentinel['ip'],$getSentinel['port']);
$slaveInfo = $redisRead->rawCommand('SENTINEL','slaves','mymaster');

//写redis数据
$redisWrite= new Redis();
$redisWrite->connect('106.12.212.131','6382');

$slaves=[];
foreach ($slaveInfo as $val){
    $slaves[]=['ip'=>$val[3],'port'=>$val[5]];
}

$slave = (new RoundSlave())->select($slaves);
try{
    //读
    $redisRead->connect($slave['ip'],$slave['port']);
    $redisWrite->watch('sales');       //监听销售量
    $sales = $redisRead->get('sales'); //获取销售量
    $num = $redisRead->get('num');     //获取库存数量【需要我们手动预定设置下】
    if($sales >= $num)
    {
        exit('您来晚啦!');
    } else {
        //写
        $redisWrite->multi();//将命令放入到队列中,且不执行。
        $redisWrite->incr('sales');
        sleep(1);
        $result = $redisWrite->exec(); //按照命令的先后顺序执行,如果监视的数据被修改了,命令执行就会失败,返回空值

        if($result)
        {
            $sql = "update seckill set num = num-1 where id = 1";
            Db::getInstance()->cz($sql);
        }
    }
} catch (\RedisException $e) {
    file_put_contents('log.log',$e->getMessage());
}                                                                                                               1,5          

代码部署完以后,我们再次执行压力测试会发现,没有超卖现象的出现,

下面是完整的操作视频过程:

百万秒杀解决超卖

好了,到此我们的哨兵集群就全部结束了,下个篇章就是更牛逼的redis-cluster集群了。

大家觉得不错记得给个赞,请关注我的主页和我的专栏,谢谢。


Original url: Access
Created at: 2019-04-30 18:20:26
Category: default
Tags: none

请先后发表评论
  • 最新评论
  • 总共0条评论