为什么Redis是单线程的还需要事务功能?为什么pipeline不能代替事务?

1.Redis有竞态问题吗

Redis服务器是一个Reactor模型,即NIO+IO复用,通过IO复用获取有请求的对象,然后执行对应操作并将结果返回给对应客户端。且除redis虽然是多线程程序,但是其处理网络IO和执行客户端请求的只有一个线程,对于客户端而言是个单线程服务器。

while(!quit)
{
	clients=epoll_wait();
	for(client c:clients)
	{
		read(client);
		handle(client);
		write(client);
	}
	//处理其他事件
}

大致的处理逻辑应该如上所示,所以对于redis服务器本身而言是没有竞态的,将活跃的客户端一个一个取出,将客户端中的请求一条一条执行,所有的处理都是one by one的。

但是,对于客户端而言是存在静态的,一个redis服务器会有多个客户端进行连接,他们之间可能会出现竞态的。

举个例子:现在需要完成一个游戏的商城系统,商城中有一件物品A,现在有两名玩家B和C想要购买这个物品,购买商品的大致流程是:(1)客户端向服务器发起询问,商品还在商城中吗?(2)服务器对询问进行回答,商品还在。(3)客户端对服务器发起操作,减少用户的钱包金额,然后将物品这个键从商城中移除并在用户背包添加这个物品。(4)服务器执行完指令返回OK,购买完成。

这个过程中就会出现竞态问题:

玩家B玩家Credis服务器
发起询问发起询问
等待答复等待答复顺序答复B和C,商品还在
得到答复,进行下一步操作,发起物品转移和扣款得到答复,进行下一步操作,发起物品转移和扣款等到请求
顺序执行B和C的请求
完成完成完成

很容易发现,这个购物的过程有问题,一个商品A卖了两次,这显然是我们不愿意看到的。

2.用事务和WATCH指令解决这个问题

WATCH指令其实就是CAS锁,也叫乐观锁。使用这个指令的客户端会关注指定键值对,如果在关注期间有其他客户端修改了这个键值对,那么使用WATCH的客户端下一次的事务执行会失败(无论事务是否和关注的键值对相关)。
使用WATCH和事务功能就很容易解决上面那个问题,先使用WATCH监视商城这个键值对,然后将物品转移和扣款操作用MULTI和EXEC包裹成为一个事务,那么先买的B就会买到这个商品,然后因为商城键值对发生变换,C的事务执行失败,无法购买到商品A。

3.为什么pipeline(管线化)不能替代事务?

学习事务的时候就在想为什么pipeline不能替代事务,都可以将命令打包执行,而且还不需要MULTI和EXEC两个额外的指令。下面来看一看pipeline到底和事务有什么区别:
我们直接从实现上进行分析:

pipeline

pipeline是将要发送的命令都存储在客户端,等到需要发送时将所有命令打包一起发送给服务器,比如C语言的hiredis库中就实现了pipeline功能:

	redisAppendCommand(conn, "SADD set s1 s2 s3 s4");
	redisAppendCommand(conn, "SADD set s5 s6 s7");
	redisAppendCommand(conn, "SMEMBERS set");
	redisGetReply(conn, (void**)&reply);//reply for SADD set s1 s2 s3 s4
	redisGetReply(conn, (void**)&reply);//reply for SADD set s5 s6 s7

redisAppendCommand接口将想要执行的操作都放进conn这个结构体的输出缓冲区中,redisGetReply会从conn的输入缓冲区中取得redis服务器的回复并解析,如果conn的输入缓冲区为空,就会将输出缓冲区中的所有指令写入套接字,然后等待从输出缓冲区中获取结果。

事务

事务的执行过程是什么样的,客户端向redis服务器发送MULTI指令后,服务器不会再执行该服务器的请求(除特定指令外),而是将客户端的所有请求都装入指令队列中,直到接收到客户端的EXEC指令,这时会将对应该客户端队列中的所有指令都执行并返回结果给客户端。

redis服务器对所有有请求的服务器应该是逐个处理的,所以事务是具有原子性的,不会被其他客户端打断(当然redis事务没法回滚)。对于我们上述的例子,可以有效的完成扣款并转移物品。

pipeline为什么不行

虽然redis服务器是one by one对客户端进行服务的,会将一个服务器发送的所有打包指令执行完后再执行下一个,但是,pipeline依然没办法保证原子性,有网络编程经验的人应该很容易发现pipeline的问题所在。

pipeline将所有要执行的消息存在客户端的缓冲区,然后将所有的消息一并发送给服务器,即写入连接服务器的套接字
在这里插入图片描述
即客户端写入的消息只是写入了socket管理的内核缓冲区,内核会自动的根据当前网络状况(滑动窗口,拥塞窗口)将这些数据发送给TCP对端,每次到底能发多少数据只有内核知道。

然后到了通信对端,redis服务器读的消息也是从内核缓冲区中读取的,然而read操作不能每次将缓冲区中的数据都读出来(如果用的是Epoll的边沿触发的话可以),能读多少只有内核知道。

所以,虽然看似客户端将所有的数据打包交给了socket,但是这个数据能不能依然以这个数据包的形式交给服务器,这就不清楚了,所以pipeline是没办法保证原子性的。比如客户端对服务器发起了扣款和物品转移两个操作,然而,客户端的内核有可能将这两个指令分成两个包发送给了服务器(只是示例,这么短的数据一半不会分成两个TCP包),自然就没有办法保证原子性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值