使用redis解决一些并发访问的问题

redis的一些锁机制以及事务机制,可以高效地解决并发访问以及抢购问题,这里举例说明一下

这里模拟并发抢购的实现思路:

1.竞拍的物品预先设定一个订单号

2.很多用户针对该订单的物品下单

3.先下单的能抢购成功、后下单的抢购失败

4.先下单的如果处理失败,则别人可以继续抢购


<?php
header('Content-Type: text/html;charset=utf-8');
	//操作redis的类
class Test_redis 
{
	private $redis = null;
	public function __construct() {
		$this->redis = new Redis();
		$this->redis->connect('192.168.1.102',6379);
	}
	
	public function test() {	
		$this->redis->set('name','areyouok');
		$output = $this->redis->get('name');
		var_dump($output);
	}
	/**
	 * 测试方法是否存在
	 * @param $methodName string 方法名
	 */
	public function isMethodExist($methodName)
	{
		$refObj = new ReflectionClass($this->redis);
		//var_dump($refObj->getMethods());
		$result = $refObj->hasMethod($methodName);
		var_dump($result);
	}
	
	/**
	 * 使用setnx模拟并发抢购加锁机制
	 * @param int $orderId 拍卖的单据号
	 */
	public function  buy_lock($orderId,$userId)
	{		
		$ttl  = 1800;//针对此订单号在30分钟后会关闭,关闭后不允许下单
		$orderId = (int) $orderId;
		$userId = (int) $userId;
		$key = 'lock_order_'.$orderId;	//针对该订单号加锁

		$ok = $this->redis->setnx($key, 1); //如果key写入值则返回false
		if ($ok) {
			$this->redis->expire($key,$ttl);		//设置key在30分钟后失效,假定针对此次抢购共30分钟
			echo "redis get({$key})".$this->redis->get($key)."<br/>";
			echo "setnx {$key} is successful<br/>";
		        $ret = $this->update();		 //模拟将订单写入数据库,执行方法后成功返回true,失败返回false
		        var_dump($ret);
		        if (!$ret) {		    	
		        	$this->redis->del($key); //执行失败后删除当前key,释放锁,其他用户可以继续下单
		        } else {
                             //todo:关闭该订单,关闭改单后不允许再下单
                        }	    
		} else {
			echo "setnx {$key} is failed<br/>";//抢购失败
		}
	}
	

	/**
	 * 测试redis消息订阅功能,先订阅一个频道才能收到该频道的消息
	 */
	public function test_subscribe()
	{
		$this->redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
		echo '222';
		$this->redis->subscribe(array('redischat'), array($this, 'process_msg'));
	}

	public function process_msg($redis, $chan, $msg)
	{
		echo $chan.'|'.$msg."\n";
	}

	/**
	 * 测试redis发布消息功能
	 */
	public function test_publish()
	{
		for($i=1; $i<10; ++$i) {
			$this->redis->publish('redischat', 'user'.$i.' is bidding, price is:'.(7000+$i*200));
		}
	}

	/**
	 * 模拟将竞拍出价写入数据库的过程
	 */
	private function update()
	{
		//usleep(1000);
		$random = mt_rand(1,100000);
		if($random % 2 === 0) {
			return true;
		}else {
			return false;
		}
	}
	
	/**
	 * 操作list类型
	 */
	public function operateList()
	{
		$task1Key = 'needToProcess';
		$task2Key = 'Processing';
		
		$taskArr = array(['exec_time'=>1,'orderId'=>'123'],['exec_time'=>1,'orderId'=>'124'],['exec_time'=>1,'orderId'=>'125'],['exec_time'=>1,'orderId'=>'126']);
		foreach($taskArr as $task) {
			$this->redis->rpush($task1Key,json_encode($task));
		}
		$taskList = $this->redis->lRange($task1Key,0,-1);
		foreach($taskList as $taskJson) {
			$this->redis->rPush($task2Key,$taskJson);		//将内容插入另一个队列
			$this->redis->lRem($task1Key,$taskJson,1);		//从队列中删除一项
		}
		echo 'after processing<br/>';
		$taskList = $this->redis->lRange($task1Key,0,-1);
		var_dump($taskList);
		echo '<br/>';
		$task2List = $this->redis->lRange($task2Key,0,-1);
		var_dump($task2List);				
	}

	/**
	 * 模拟用户1竞拍出价并发场景
	 * 在watch与multi操作之间如果有其他人竞拍出价,则本次出价不能成功
	 * sleep(3) 这个操作模拟用户1出价处理比较慢的场景
	 * @param $orderId 订单Id
	 * @param $price  用户1的竞拍报价
	 */
	public function user1_buy($orderId, $price)
	{
		$key = 'order:'.$orderId;
		$value = $this->redis->get($key);
		echo 'in '.__METHOD__.',before providing price:'.$price.',the price is:'.$value.'<br/>';

		$this->redis->watch($key);//添加对该订单当前报价的监控
		sleep(3);						
		$result = $this->redis->multi()->set($key, $price)->exec(); //从multi()方法到exec()方法的一系列步骤可以看做是一个原子操作
		var_dump($result);
		if ($result) {
			echo '竞拍出价成功<br/>';
		} else {
			echo '竞拍出价失败,出价过程中已经有其他人出价<br/>';	//在watch()与multi()中间有其他人成功出价的话,则本次操作会失败回滚
		}
		//$this->redis->unwatch();									//unwatch()方法此时不再需要,因为执行EXEC命令后redis会取消对所有键的监控
		$value = $this->redis->get($key);
		echo 'in '.__METHOD__.',after providing price,the price is:'.$value.'<br/>';
	}

	/**
	 * 模拟用户2拍卖出价并发场景
	 * @param $orderId 订单Id
	 * @param $price  用户1的竞拍报价
	 */
	public function user2_buy($orderId, $price)
	{
		$key = 'order:'.$orderId;
		$value = $this->redis->get($key);
		echo 'in '.__METHOD__.',before providing price:'.$price.',the price is:'.$value.'<br/>';

		$this->redis->watch($key);
		$result = $this->redis->multi()->set($key, $price)->exec();
		var_dump($result);
		if ($result) {
			echo '竞拍出价成功<br/>';
		} else {
			echo '竞拍出价失败,出价过程中已经有其他人出价<br/>';
		}
		$value = $this->redis->get($key);
		echo 'in '.__METHOD__.',after providing price,the price is:'.$value.'<br/>';
	}
	
}


$testRedis  = new Test_redis();

if(isset($argv) && count($argv)>1) {
	$action = $argv[1];
}else {
	$action = htmlentities($_GET['action']);
}

switch($action) {
	case 'test':
		$testRedis->test();
		break;
	case 'buy_lock':
		$testRedis->buy_lock();
		break;
	case 'session1':
		$testRedis->write_lock_session1();
		break;
	case 'session2':
		$testRedis->write_lock_session2();
		break;
	case 'ismethodexist':	//测试方法是否存在
		$methodName = $_GET['method'];
		echo $methodName.':';
		$testRedis->isMethodExist($methodName);
		break;
	case 'subscribe':	//测试订阅消息
		$testRedis->test_subscribe();
		break;
	case 'publish':		//测试发布消息
		$testRedis->test_publish();
		break;
	case 'operateList':	//测试操作redisList
		$testRedis->operateList();
		break;
	case 'user1_buy':	//模拟用户1竞拍出价
		$testRedis->user1_buy(2,650);	
		break;
	case 'user2_buy':	//模拟用户2竞拍出价
		$testRedis->user2_buy(2,660);
		break;
}

1.模拟抢购场景:如果有一批商品正在抢购,可以先对这些商品生成一个抢购的订单,后续如果用户抢到该订单,可以只更新订单表中的一些字段

可以开启两个终端页面,同时访问:http://127.0.0.1/test_redis.php?action=buy_lock

会发现结果如下:

页面1:

redis get(lock_order_1)1
setnx lock_order_1 is successful
return:

页面2:

setnx lock_order_1 is failed

就是说加入一个用户抢到了该订单,那么其他用户必然抢单失败。


2.模拟两个用户同时竞拍出价的过程

如果用户1的出价过程中已经有其他用户出价成功,则A用户出价会失败

具体例子也需要开启两个终端页面,分别同时访问 http://127.0.0.1/test_redis.php?action=user1_buy 和 http://127.0.0.1/test_redis.php?action=user2_buy

访问结束后页面分别如下:

页面1:

    in Test_redis::user1_buy,before providing price:650,the price is:660
    bool(false)竞拍出价失败,出价过程中已经有其他人出价
    in Test_redis::user1_buy,after providing price,the price is:660

页面2:

    in Test_redis::user2_buy,before providing price:660,the price is:660
    array(1) { [0]=> bool(true)}竞拍出价成功
    in Test_redis::user2_buy,after providing price,the price is:660


具体应用的方法主要有: watch() multi()  exec() 这几个方法,其中watch() 会监控某一个key值  multi() exec()  会针对一系列操作作为原子操作


3.模拟消息订阅和消息发布的过程

可以后台开启一个任务,php命令执行某一个页面,具体可以在项目根目录下:

使用以下命令开启任务:web101-php-dev-bj1:~/www/site >$ php other/test_redis.php subscribe

然后在浏览器地址栏中输入URL :http://127.0.0.1/other/test_redis.php?action=publish

然后再切换到php命令行界面,会发现结果如下图所示:


从图上可以看到,发布的消息在这里都列举出来了

实际场景中,需要先订阅某一个消息频道,然后当其他人发布消息的时候,后台任务都能够收到。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值