一、事务
1、开启事务 multi
2、执行事务 exec
3、丢弃事务 discard
注:下面这两种情况,
a. 如果再执行事务队列过程中某个命令有报错,整个事务命令队列都会被丢弃
b. 执行加入队列时候没有错,在执行事务某个命令有报错,其他命令会执行成功。(无原子性)
二、悲观锁和乐观锁
1、悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
2、乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种check-and-set机制实现事务的。
场景:抢票。
下面举个例子:左面客户端设置sign=10,在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。·可以看到坐标设置为20成功了,右面设置30不成功。
注:unwatch,取消监视
三、利用redis实现秒杀功能
代码如下
<?php
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脚本来实现
3、库存遗留问题-LUA脚
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。但是注意redis的 lua脚本功能,只有在Redis 2.6以上的版本才可以使用。利用lua 脚本淘汰用户,解决超卖问题。,
redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis利用其单线程的特性,用任务队列的方式解决多任务并发问题。"
<?php
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 = <<<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 = <<<LUA
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
到此秒杀完成,以上代码解决了秒杀中的几个关键问题:redis链接超时,少卖、超买问题,当然在实际生产环节中还会出现别的问题,如果遇到欢迎留言讨论。