菜菜哥,你换形象啦?java
这么巧,你也换啦!据说是不会画画的菜嫂通过九牛二虎之力的功劳哦!鼓掌......git
前几天我出去面试了,面试官问我微服务的知识,我回答的可好了github
看来微服务你真的下功夫研究了呀golang
是呀是呀,可是碰到一个问题,有状态的服务是什么意思呢?web
看来你又挂在这个问题上了,且听此次分解面试
对于初学者,内心对“有状态服务”的理解可能比较模糊,可是从面向对象编程思想的角度去理解也许会明朗不少。面向对象编程思想提倡的是用编程语言去描述世间万物,因此面向对象编程的语言都会提供描述对象的容器以及对象行为的表达方式。举一个很简单的栗子,在c#或者java中,表达对象的容器就是class,对象的行为经过一系列的接口或者函数来表达。更进一步,对象抽象出来以后,大多数对象都有本身的内部状态,体现到代码上也就是常见的类的属性。redis
面向对象编程的基本思想本质上是对现实世界的一种抽象,万物皆可抽象。算法
根据业务把对象抽象出来以后,每个实例化的对象其实均可以有本身的状态,好比:在最多见的游戏场景中,每个玩家都是“玩家"这类对象的一个实例,每个玩家都有本身的名字,性别,等级,HP等属性,这些属性本质上就是玩家的状态,随着时间的推移,每一个玩家的HP,等级等属性会随之变化,这些变化其实就是这个玩家状态的变化。对应到有状态的服务也是如此,之因此称之为有状态,是由于服务内部的对象状态会随着业务有着对应的变更,而这些变更只发生在这个服务内部,在外界看来,这个服务好像是有状态的。数据库
有状态的服务本质上是一些有状态对象的集合,这些对象状态的变化只发生在当前服务进程中。编程
有状态服务之因此被称为有状态,一个很大的缘由是它能够追溯状态的变化过程,也就是说一个有状态的服务保存着状态变化的记录,并能够根据这些历史记录恢复到指定的状态,这在不少场景下很是有用。举一个很简单的栗子:咱们平时玩的斗地主游戏,三个玩家,当有一个玩家由于网络缘由掉线,通过一段时间,这个玩家又从新上线,须要根据某些记录来恢复玩家掉线期间系统自动出牌的记录,这些出牌记录在这个业务中其实就是这个玩家的状态变化记录。在有状态的服务中,很容易作到这一点。
其实实际开发中不少场景不须要记录每一个状态的变化,只保留最新状态便可,不仅仅是由于保存每一个状态的变化须要大量的存储和架构设计,更由于是不少业务根本不须要这些状态变化记录,业务须要的只是最新的状态,因此大部分有状态的服务只保存着最新的状态。
有状态的服务在设计难度上比无状态的服务要大不少,不只仅是由于开发设计人员须要更好的抽象能力,更多的是一致性的设计问题。现代的分布式系统,都是由多个服务器组成一个集群来对外提供服务,当一个对象在服务器A产生以后,若是请求被分配到了服务器B上,这种状况下有状态的服务毫无心义,为何呢?当一个相同的业务对象存在于不一样的服务器上的时候,本质上就违背了现实世界的规则,你能说一我的,即出生在中国,又出生在美国吗? 因此有状态的服务对于一致性问题有着自然的要求,这种思想和微服务设计理想不谋而合,举个栗子:一个用户信息的服务,对外提供查询修改能力,凡是用户信息的业务必须经过这个服务来实现。同理,一个对象状态的查询修改以及这个对象的行为,必须由这个对象的服务来完成。
有状态的服务要求相同业务对象的请求必须被路由到同一个服务进程。
所以,有状态的服务对于同一个对象的横向扩容是作不到的,就算是作的到,多个相同对象之间的状态同步工做也必然会花费更多的资源。在不少场景下,有状态的服务要注意热点问题,例如最多见的秒杀,这里并不是是说有状态服务不适合大并发的场景,反而在高并发的场景下,有状态的服务每每表现的比无状态服务更加出色。
在众多的并发模型中,最适合有状态服务设计的莫过于Actor模型了,若是你对actor模型还不熟悉,能够撸一遍菜菜以前的文章:https://mp.weixin.qq.com/s/eEiypRysw5jsC7iYUp_yAg actor模型天生就具有了一致性这种特色,让咱们在对业务进行抽象的时候,没必要考虑一致性的问题,并且每个请求都是异步模式,在对象内部修改对象的状态没必要加锁,这在传统的架构中是作不到的。
基于actor模型,系统设计的难点在于抽象业务模型,一旦业务模型稳定,咱们彻底能够用内存方式来保存对象状态(也能够定时去持久化),内存方式比用其余网络存储(例如redis)要快上几个量级,菜菜也有一篇文章你们能够去撸一下:https://mp.weixin.qq.com/s/6YL3SnSriKEnpCyB5qkk0g ,既知足了一致性,又能够利用进程内对象状态来应对高并发业务场景,何乐而不为呢?
有很多同窗问过我,actor模型要避免出现热点问题,就算有内存状态为其加速,那并发数仍是超过actor的处理能力怎么办呢? 其实和传统作法相似,全部的高并发系统设计无非就是“分”一个字,不管是简单的负载均衡,仍是复杂的分库分表策略,都是分治的一种体现。一台服务器不够,我就上十台,百台.....
全部的高并发系统设计都是基于分治思想,把每一台服务器的能力发挥到极致,难度最大的仍是其中的调度算法。
用actor模型来应对高并发,咱们能够采用读写分离的思想,主actor负责写请求,并利用某种通讯机制把状态的变化通知到多个从actor,从actor负责对外的读请求,这个DB的读写分离思想一致,其中最难的当属actor的状态同步问题了,解决问题的方式千百种,总有一种适合你,欢迎你留言写下你认为最好的解决方案。
因为菜菜是c#出身,对c#的Actor服务框架Orleans比较熟悉,这里就以Orleans为例,其余语言的coder不要见怪,Orleans是一个很是优秀的Actor模型框架,并且支持最新的netcore 3.0版本,地址为:https://github.com/dotnet/orleans 有兴趣的同窗能够去看一下,并且分布式事物已经出正式版,很是给力。其余语言的也很是出色java:https://github.com/akka/akka
golang:https://github.com/AsynkronIT/protoactor-go
1. 首先咱们定义玩家的状态信息
//玩家的信息,其实也就是玩家的状态信息
public class Player {
/// <summary>
/// 玩家id,同时也是玩家这个服务的主键
/// </summary>
public long Id { get; set; }
/// <summary>
/// 玩家姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 玩家等级
/// </summary>
public int Level { get; set; }
}
2. 接下来定义玩家的服务接口
/// <summary>
/// 玩家的服务接口
/// </summary>
interface IPlayerService: Orleans.IGrainWithIntegerKey
{
//获取玩家名称
Task<string> GetName();
//获取玩家等级
Task<int> GetLevel();
//设置玩家等级,这个操做会改变玩家的状态
Task<int> SetLevel(int newLevel);
}
3. 接下来实现玩家服务的接口
public class PlayerService : Grain, IPlayerService
{
//这里能够用玩家的信息来表明玩家的状态信息,并且这个状态信息又充当了进程内缓存的做用
Player playerInfo;
public async Task<int> GetLevel()
{
return (await LoadPlayer()).Level;
}
public async Task<string> GetName()
{
return (await LoadPlayer()).Name;
}
public async Task<int> SetLevel(int newLevel)
{
var playerInfo =await LoadPlayer();
if (playerInfo != null)
{
//先进行数据库的更新,而后在更新缓存的状态, 进程内缓存更新失败的概率几乎为0
playerInfo.Level = newLevel;
}
return 1;
}
private async Task< Player> LoadPlayer()
{
if (playerInfo == null)
{
var id = this.GetPrimaryKeyLong();
//这里模拟的信息,真实环境彻底能够从持久化设备进行读取
playerInfo= new Player() { Id = id, Name = "玩家姓名", Level = 1 };
}
return playerInfo;
}
}
以上只是一个简单案例,有状态的服务还有更多的设计方案,以上只供参考完