- 1 redis 实现分布式锁。 悲观锁。
原理,进去则抢锁,抢失败了 等一秒再抢,再等一秒再抢,如此反复循环。
解锁就是删掉对应的键。
/**
* 实现redis 悲观锁
* User: babytuo
*/
Class RedisLock {
public $expire = 2;
public function test(){
$this->lock("test1");
echo "111";
}
public function lock($key){
$redis = self::createRedisObj();
$now = time();
/** 理解setnx
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
*/
//抢锁.
$isLock = $redis->setnx($key , time() + $this->expire); //设为过期时间.
//不成功
while ( ! $isLock) {
$now++;
$time = $now + $this->expire;
//再重新创建
$lock = $redis->setnx($key, $time);
if ($lock == 1 || ($now > $redis->get($key) && $now > $redis->getset($key, $time))) {//争锁成功后,设置新的过期时间.
break;
} else {
sleep(1);//0.5s
}
}
return true;
}
/**
* 解锁
* @param type $flag
* @return boolean
*/
public function unlock($key) {
$redis = self::createRedisObj();
$redis->del($key);
return true;
}
/**
* 检查锁是否存在
* @param $key
* @return bool
*/
public function checklock($key){
$redis = self::createRedisObj();
return $redis->exists($key);
}
public static $_redis;
/**
* 创建一个redis 对象.
* @return Redis
*/
public static function createRedisObj(){
if( ! self::$_redis){
$redis = new Redis();
$info = Yii::app()->cache->servers[0]; //读取配置
$host = $info["host"];
$port = $info["port"];
$redis->connect($host,$port);
$redis_db = Yii::app()->settings->get('system' , 'redis_db'); //设置默认库
$redis->select($redis_db);
self::$_redis = $redis;
}
return self::$_redis;
}
}
代码详解:
setnx()命令:
get(key) 获取key的值,如果存在,则返回;如果不存在,则返回nil;
getset()命令:
这个命令主要有两个参数 getset(key, newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。
假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:
1. getset(key, "value1") 返回nil 此时key的值会被设置为value1
2. getset(key, "value2") 返回value1 此时key的值会被设置为value2
3. 依次类推!
二.具体的使用步骤如下:
1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。
该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。
get()命令:get(key) 获取key的值,如果存在,则返回;如果不存在,则返回nil;
getset()命令:
这个命令主要有两个参数 getset(key, newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。
假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:
1. getset(key, "value1") 返回nil 此时key的值会被设置为value1
2. getset(key, "value2") 返回value1 此时key的值会被设置为value2
3. 依次类推!
二.具体的使用步骤如下:
1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
2 redis 共享session
//进入php.ini 配置文件,进行如下设置。
[Session]
; Handler used to store/retrieve data.
; http://php.net/session.save-handler
;session.save_handler = files
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?database=5"
- 3 redis 实现秒杀功能。
//3.1一点点准备工作,两张表格。商品表+订单表。 以及数据初始化。
CREATE TABLE `sec_goods` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`stock` int(11) unsigned DEFAULT '1' COMMENT '放入库存',
`title` varchar(32) DEFAULT NULL COMMENT '商品名称',
`stock_avail` int(11) DEFAULT '1' COMMENT '剩余可售',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `sec_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`sn` varchar(64) NOT NULL COMMENT '订单号',
`goods_id` int(11) DEFAULT NULL COMMENT '商品编号',
`user_id` int(11) DEFAULT NULL COMMENT '用户编号',
`create_at` int(11) DEFAULT NULL COMMENT '创建时间',
`num` int(11) DEFAULT '0' COMMENT '成交数量',
PRIMARY KEY (`id`),
UNIQUE KEY `sn` (`sn`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
//初始化数据
$sql = "TRUNCATE sec_goods"; Yii::app()->db->createCommand($sql)->execute(); $sql = "TRUNCATE sec_order"; Yii::app()->db->createCommand($sql)->execute();
//准备五件商品,每件100 份
for($i = 1;$i<6;$i++){
$sql = "INSERT INTO sec_goods SET title = 'test_goods_$i' ,stock = 100,stock_avail = 100";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("执行失败".$sql); }
}
echo "data init succ!";
//3.2 redis 数据准备。
//初始化redis 对象。
$redis = self::createRedisObj();
//有多少件商品,往队列里写多少条记录,使用 pop 的原子性,一次仅能取出一条。
$sql = "select * from sec_goods";
$rows = Yii::app()->db->createCommand($sql)->queryAll();
foreach( $rows as $key => $row ):
$goods_id = $row["id"];
$stock_avail = $row["stock_avail"];
$redis_key = "goods_avail_".$goods_id;
for($i =0 ; $i< $stock_avail; $i++){
$redis->lpush($redis_key , 1);
}
echo $goods_id."llen is ".$redis->lLen($redis_key)."<br/>";
endforeach;
//3.3 执行购买。
//模拟用户购买参数
$uid = rand(1,10);
$amount = rand(1,5);
$goods_id = rand(1,6);
$time = time();
//用redis 来验证是否卖光。
$redis = BusinessHelper::createRedisObj();
$redis_key = "goods_avail_".$goods_id;
$len = $redis->lLen($redis_key);
if( $len == 0 ){
exit("抢光了!");
}else if( $len < $amount){
exit("库存不足!");
}
//验证通过,开始pop 出队列。 pop 一个,相当于买一个。
for( $i =0 ; $i< $amount;$i++){
$bool = $redis->rPop( $redis_key );
}
//执行购买操作。
$sql = "select stock_avail from sec_goods where id = $goods_id";
$stock_avail = Yii::app()->db->createCommand($sql)->queryScalar();
if( $stock_avail > $amount ){ //份额足够。
$sn = date("YmdHis")."-".$uid."-".$goods_id.rand(1000,9999);
$sql = "insert into sec_order set sn = '$sn',user_id = $uid, goods_id = $goods_id, create_at = $time,num = $amount";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("执行失败".$sql); }
$sql = "update sec_goods set stock_avail = stock_avail - $amount where id= $goods_id";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("执行失败".$sql); }
}
最后,使用 apache 的 ab 压一下。
//- n 请求多少次。 -c 单次多少个并发。
ab -c 100 -n 10000 www.demo.com/test
备注,这里再描述下redis 秒杀。
1 初始化商品数据。
2 将商品数据,按库存量写到队列中去。 例如编号1 商品有10件。 就往 goods_1 队列里写10个1 进去。
3 下单时,例用redis pop 的原子性。
为了篇幅简洁,有些地方略掉。(例如,用户购买失败时,需要重新压回队列 。 购买代码,需要写在一个事务里等。。。)