> **Redis - 事务及锁应用**
## 说明 ##
**Redis 中也存在简单的事务处理。并且利用watch指令可以实现乐观锁的相关操作,常用于秒杀场景中。**
----------
##事务的概念##
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
##相关命令##
watch key1 key2 ... : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
multi : 标记一个事务块的开始( queued )
exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
discard : 取消事务,放弃事务块中的所有命令
unwatch : 取消watch对所有key的监控
##与mysql事务的区别##
redis 支持简单的事务,以下是跟mysql的简单区别。
![请输入图片描述][1]
Discard 是放弃本次事务的意思。跟MySQL回滚是有些不一样的
当出现错误语法时,exec执行也会有问题。整个事务对列不会执行。
当操作语法正确,但操作类型错误时,整个事务队列还是会被执行,只有操作类型错误的那个语句不会执行。所以跟mysql的回滚机制不一样,日常开发中要注意
##事务简单实现##
<h5>正常使用</h5>
1用户有1000块钱,2用户有500块钱,1向2转账操作,实现1减100,2加100。
127.0.0.1:6379> set 1 1000
OK
127.0.0.1:6379> set 2 500
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby 1 100
QUEUED
127.0.0.1:6379> incrby 2 100
QUEUED
127.0.0.1:6379> exec
1) (integer) 900
2) (integer) 600
<h5>错误语法</h5>
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby 1 100
QUEUED
127.0.0.1:6379> incrby1 2 100
(error) ERR unknown command 'incrby1'
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
当出现错误语法时,exec执行也会有问题。
<h5>语法正确,对象错误</h5>
127.0.0.1:6379> decrby 1 100
QUEUED
127.0.0.1:6379> sadd 2 100
QUEUED
127.0.0.1:6379> set 3 300
QUEUED
127.0.0.1:6379> exec
1) (integer) 700
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
当操作语法正确,但操作类型错误时,整个事务队列还是会被执行,只有操作类型错误的那个语句不会执行。所以跟mysql的回滚机制不一样,日常开发中要注意。
##锁机制##
Redis 中提供了WATCH命令用于监控数据的变化,当数据产生变化时,在此之后的事务都将会被取消,这种特性特别适合用来用作乐观锁的实现。
WATCH:监控一个或者多个key,如果这些key在提交事务(EXEC)之前被其他用户修改过,那么事务将执行失败,需要重新获取最新数据重头操作(类似于乐观锁)。
UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。
##锁机制演示##
<h5>正常操作</h5>
模拟将余额100提现20到提现账户。
监听的数据没有变化时,事务可以正常提交。
127.0.0.1:6379> set balance 100
OK
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby balance 20
QUEUED
127.0.0.1:6379> incrby new_balance 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
<h5>并发演示</h5>
此时另外一个进程模拟新的余额到账
监听的数据发生了变化,事务不可以提交。
1.进行转账操作
127.0.0.1:6379> get balance
"80"
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby balance 20
QUEUED
127.0.0.1:6379> incrby new_balance 20
2.此时又有余额到账
在另外一个窗口中模拟到账
127.0.0.1:6379> get balance
"80"
127.0.0.1:6379> incrby balance 100
(integer) 180
127.0.0.1:6379>
3.事务不可以提交,发现数据发生了变化
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get balance
"180"
<h5>整体的操作步骤</h5>
![请输入图片描述][2]
##基于乐观锁的秒杀场景简单实现##
<h5>设置秒杀商品</h5>
设置秒杀商品,模拟代理商创建秒杀商品,添加商品1为秒杀商品,秒杀数量为10,秒杀价格为100
sk:h:1 gid 1 num 10 price 100 //秒杀商品Hash数据
Redis::hset("sk:h:1","gid","1"); //秒杀商品id
Redis::hset("sk:h:1","gum","10");//秒杀数量
Redis::hset("sk:h:1","price","100");//秒杀金额
----------
对应在redis里的数据
127.0.0.1:6379> keys *
1) "sk:h:1"
127.0.0.1:6379> hgetall sk:h:1
1) "gid"
2) "1"
3) "gum"
4) "10"
5) "price"
6) "100"
<h5>对列+乐观锁实现秒杀</h5>
//监听已抢购的数量
Redis::watch("sk:1:num"); //已经秒杀完的商品数量
$skNum = Redis::hget("sk:h:1",'gum'); //秒杀商品Hash信息
$isSkNum = (int)Redis::get("sk:1:num");
if($isSkNum < $skNum ){
$uid = $this->rand(5);//随机生成用户id
// 暂时用setnx 跟 expire 处理限购 问题是并发情况下会不止10个人进来,但是不影响限购啊
// 没有考虑到更好的解决方案,先这么处理吧
// Redis::set("sk:su:1", 1 , 'NX', 'EX', "1000"); 不清楚predis下set 同时设置 NX和EX为什么老是不生效,暂时用下边的方法处理
if(Redis::setnx("sk:su:".$uid,$uid)){
Redis::expire("sk:su:".$uid,10); //设置过期时间,保证10秒内一个用户只能秒杀成功一次
}else{
Rddis::incr('fail');
echo "10秒内允许抢购一次。";
}
//上述代码不能放在multi之内。 否则if(Redis::setnx("sk:su:".$uid,$uid)) 会报错
//放在multi当中,相当于未执行,结果不会返回,所以会一直报错
Redis::multi();
Redis::incr('sk:1:num');
Redis::lpush("sk:l:1",$uid);
Redis::exec();
}else{
Redis::incr('fail');
echo "不好意思,秒杀已经结束了。";
}
}
----------
//php生成不重复随机字符串
//https://www.cnblogs.com/gorey/p/5647664.html
function rand($len)
{
$chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
$string=time();
for(;$len>=1;$len--)
{
$position=rand()%strlen($chars);
$position2=rand()%strlen($string);
$string=substr_replace($string,substr($chars,$position,1),$position2,0);
}
return $string;
}
<h5>ab 压测</h5>
项目在自己本地的电脑上,就简单的模拟了以下。
1000 个请求100的并发量。
C:\Users\Depp\Desktop\Apache24\bin>ab -n 1000 -c 100 http://blog.com/admin/redis
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking blog.com (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: nginx/1.15.11
Server Hostname: blog.com
Server Port: 80
Document Path: /admin/redis
Document Length: 0 bytes
Concurrency Level: 100
Time taken for tests: 20.847 seconds
Complete requests: 1000
Failed requests: 172
(Connect: 0, Receive: 0, Length: 172, Exceptions: 0)
Non-2xx responses: 297
Total transferred: 973697 bytes
HTML transferred: 27176 bytes
Requests per second: 47.97 [#/sec] (mean)
Time per request: 2084.699 [ms] (mean)
Time per request: 20.847 [ms] (mean, across all concurrent requests)
Transfer rate: 45.61 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 1
Processing: 155 1967 1724.6 991 5855
Waiting: 145 1967 1724.6 990 5855
Total: 155 1967 1724.6 991 5855
Percentage of the requests served within a certain time (ms)
50% 991
66% 2467
75% 3971
80% 4222
90% 4702
95% 4836
98% 5154
99% 5339
100% 5855 (longest request)
<h5>抢购结果</h5>
127.0.0.1:6379> keys *
1) "sk:h:1" //以Hash类型存储的秒杀商品信息
2) "fail" //没有抢购到的人 实际场景中可以不用记录
3) "sk:l:1" //抢购到的list用户队列
4) "sk:1:num" //秒杀商品的总库存 之后清楚或者过期都可以
----------
127.0.0.1:6379> get sk:1:num
"10"
127.0.0.1:6379> lrange sk:l:1 0 -1
1) "t106A0502910239"
2) "160A50HR21k23R9"
3) "160i7O502f12Y39"
4) "160j5021W23UzZ9"
5) "Wd160c5i0212539"
6) "1Pw60I502Ae1239"
7) "146050RgG213239"
8) "1605p0aL212p3u9"
9) "16050wyAa2123Y9"
10) "16050fi2012f3j9"
## 结尾 ##
<p style="background-image: -webkit-linear-gradient(left, #3498db, #f47920 10%, #d71345 20%, #f7acbc 30%,#ffd400 40%, #3498db 50%, #f47920 60%, #d71345 70%, #f7acbc 80%, #ffd400 90%, #3498db);color: transparent;-webkit-text-fill-color: transparent;-webkit-background-clip: text;text-align:center;">
心如花木,向阳而生。
</p>
[1]: https://blog.zxliu.cn/usr/uploads/2020/11/1618088135.png
[2]: https://blog.zxliu.cn/usr/uploads/2020/11/3034037858.png