PHP利用Redis 事务实现商品秒杀

一、事务

1、开启事务  multi        

2、执行事务 exec

3、丢弃事务 discard

注:下面这两种情况,

        a. 如果再执行事务队列过程中某个命令有报错,整个事务命令队列都会被丢弃

        b. 执行加入队列时候没有错,在执行事务某个命令有报错,其他命令会执行成功。(无原子性

PHP利用Redis 事务实现商品秒杀_第1张图片

 二、悲观锁和乐观锁

1、悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

2、乐观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种check-and-set机制实现事务的。

场景:抢票。

下面举个例子:左面客户端设置sign=10,在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。·可以看到坐标设置为20成功了,右面设置30不成功。

注:unwatch,取消监视

PHP利用Redis 事务实现商品秒杀_第2张图片PHP利用Redis 事务实现商品秒杀_第3张图片

 三、利用redis实现秒杀功能

代码如下

class RedisPool
{
    private static $connections = array(); //定义一个对象池
    private static $servers = array(); //定义redis配置文件 包含所有redis 服务器
    public static function addServer($conf) //定义添加redis配置方法
    {
        foreach ($conf as $alias => $data){
            self::$servers[$alias]=$data;
        }
    }
 
    public static function getRedis($alias,$select = 0)//两个参数要连接的服务器KEY,要选择的库
    {
        if(!array_key_exists($alias,self::$connections)){  //判断连接池中是否存在
            $redis = new \Redis();
            $redis->connect(self::$servers[$alias][0],self::$servers[$alias][1]);
            self::$connections[$alias]=$redis;
            if(isset(self::$servers[$alias][2]) && self::$servers[$alias][2]!=""){
                self::$connections[$alias]->auth(self::$servers[$alias][2]);
            }
        }
        self::$connections[$alias]->select($select);
        return self::$connections[$alias];
    }
}
 
function connect_to_redis()
{
    //使用redis连接池
    $conf = array(
        'RA' => array('127.0.0.1','6379')   //定义Redis配置
    );
    RedisPool::addServer($conf); //添加Redis配置
    $redis = RedisPool::getRedis('RA',1); //连接RA,使用默认0库
    return $redis;
}
$user_id = rand(100,999);
$redis = connect_to_redis();
// 监控库存
$redis->watch(['goods_num']);
$goods_num = $redis->get('goods_num');
if ($goods_num <= 0){
    echo "没有库存";
    return 1;
}
// 商品数量
$redis->multi();
$redis->DECR('goods_num', 1);
// 购买者加入队列
$redis->lPush('users', $user_id);
$status = $redis->exec();
// //失败则取消事务
if (!$status) {
    $redis->discard();
}

当然都是在高并发下测试的

使用ab压测工具
ab -n 500 -c 30 http://re.com/re.php

这段代码我们解决了以下问题:

1、超卖-redis 事务+乐观锁+库存监听
2、redis连接超时-redis连接池

但是有个问题:当总共请求数 -n正好是50的时候出现了库存遗留问题,为了解决这个问题,我们用LUA脚本来实现

PHP利用Redis 事务实现商品秒杀_第4张图片

PHP利用Redis 事务实现商品秒杀_第5张图片
3、库存遗留问题-LUA脚

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。但是注意redis的 lua脚本功能,只有在Redis 2.6以上的版本才可以使用。利用lua 脚本淘汰用户,解决超卖问题。,
redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis利用其单线程的特性,用任务队列的方式解决多任务并发问题。"

class RedisPool
{
    private static $connections = array(); //定义一个对象池
    private static $servers = array(); //定义redis配置文件 包含所有redis 服务器
    public static function addServer($conf) //定义添加redis配置方法
    {
        foreach ($conf as $alias => $data){
            self::$servers[$alias]=$data;
        }
    }
 
    public static function getRedis($alias,$select = 0)//两个参数要连接的服务器KEY,要选择的库
    {
        if(!array_key_exists($alias,self::$connections)){  //判断连接池中是否存在
            $redis = new \Redis();
            $redis->connect(self::$servers[$alias][0],self::$servers[$alias][1]);
            self::$connections[$alias]=$redis;
            if(isset(self::$servers[$alias][2]) && self::$servers[$alias][2]!=""){
                self::$connections[$alias]->auth(self::$servers[$alias][2]);
            }
        }
        self::$connections[$alias]->select($select);
        return self::$connections[$alias];
    }
}
 
function connect_to_redis()
{
    //使用redis连接池
    $conf = array(
        'RA' => array('47.95.33.138','6379')   //定义Redis配置
    );
    RedisPool::addServer($conf); //添加Redis配置
    $redis = RedisPool::getRedis('RA',1); //连接RA,使用默认0库
    return $redis;
}
$user_id = rand(100,999);
$redis = connect_to_redis();

$lua = <<     local num = redis.call("get", 'goods_num')
    if tonumber(num)<=0 then
        return 0
    else
        redis.call("decr", 'goods_num')
        redis.call("lpush", 'users', KEYS[1])
    end
    return 1
LUA;

$l = <<     redis.call("lpush", 'users', KEYS[1])
    return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}
LUA;
$s = $redis->eval($lua,array($user_id),1);
if ($s == 0){
    echo "秒杀已结束";
}else{
    echo "恭喜秒杀成功";
}

ab -n 50 -c 30 http://re.com/re.php 

PHP利用Redis 事务实现商品秒杀_第6张图片

 到此秒杀完成,以上代码解决了秒杀中的几个关键问题:redis链接超时,少卖、超买问题,当然在实际生产环节中还会出现别的问题,如果遇到欢迎留言讨论。

你可能感兴趣的:(redis,redis,数据库,php)