程序员修神之路--有状态的服务其实可以做更多的事情

简介: 程序员修神之路--有状态的服务其实可以做更多的事情

菜菜哥,你换形象啦?

这么巧,你也换啦!听说是不会画画的菜嫂经过九牛二虎之力的功劳哦!鼓掌......

前几天我出去面试了,面试官问我微服务的知识,我回答的可好了

看来微服务你真的下功夫研究了呀

是呀是呀,但是碰到一个问题,有状态的服务是什么意思呢?

看来你又挂在这个问题上了,且听这次分解

简介

对于初学者,心里对“有状态服务”的理解可能比较模糊,但是从面向对象编程思想的角度去理解也许会明朗很多。面向对象编程思想提倡的是用编程语言去描述世间万物,所以面向对象编程的语言都会提供描述对象的容器以及对象行为的表达方式。举一个很简单的栗子,在c#或者java中,表达对象的容器就是class,对象的行为通过一系列的接口或者函数来表达。更进一步,对象抽象出来之后,大多数对象都有自己的内部状态,体现到代码上也就是常见的类的属性。

面向对象编程的基本思想本质上是对现实世界的一种抽象,万物皆可抽象。

根据业务把对象抽象出来之后,每一个实例化的对象其实都可以有自己的状态,比如:在最常见的游戏场景中,每一个玩家都是“玩家"这类对象的一个实例,每一个玩家都有自己的名字,性别,等级,HP等属性,这些属性本质上就是玩家的状态,随着时间的推移,每个玩家的HP,等级等属性会随之变化,这些变化其实就是这个玩家状态的变化。对应到有状态的服务也是如此,之所以称之为有状态,是因为服务内部的对象状态会随着业务有着对应的变动,而这些变动只发生在这个服务内部,在外界看来,这个服务好像是有状态的。

有状态的服务本质上是一些有状态对象的集合,这些对象状态的变化只发生在当前服务进程中

优势和劣势

有状态服务之所以被称为有状态,一个很大的原因是它可以追溯状态的变化过程,也就是说一个有状态的服务保存着状态变化的记录,并可以根据这些历史记录恢复到指定的状态,这在很多场景下非常有用。举一个很简单的栗子:我们平时玩的斗地主游戏,三个玩家,当有一个玩家因为网络原因掉线,经过一段时间,这个玩家又重新上线,需要根据某些记录来恢复玩家掉线期间系统自动出牌的记录,这些出牌记录在这个业务中其实就是这个玩家的状态变化记录。在有状态的服务中,很容易做到这一点。

其实实际开发中很多场景不需要记录每个状态的变化,只保留最新状态即可,不单单是因为保存每个状态的变化需要大量的存储和架构设计,更因为是很多业务根本不需要这些状态变化记录,业务需要的只是最新的状态,所以大部分有状态的服务只保存着最新的状态。

有状态的服务在设计难度上比无状态的服务要大很多,不仅仅是因为开发设计人员需要更好的抽象能力,更多的是一致性的设计问题。现代的分布式系统,都是由多个服务器组成一个集群来对外提供服务,当一个对象在服务器A产生之后,如果请求被分配到了服务器B上,这种情况下有状态的服务毫无意义,为什么呢?当一个相同的业务对象存在于不同的服务器上的时候,本质上就违背了现实世界的规则,你能说一个人,即出生在中国,又出生在美国吗? 所以有状态的服务对于一致性问题有着天然的要求,这种思想和微服务设计理想不谋而合,举个栗子:一个用户信息的服务,对外提供查询修改能力,凡是用户信息的业务必须通过这个服务来实现。同理,一个对象状态的查询修改以及这个对象的行为,必须由这个对象的服务来完成。

有状态的服务要求相同业务对象的请求必须被路由到同一个服务进程。

因此,有状态的服务对于同一个对象的横向扩容是做不到的,就算是做的到,多个相同对象之间的状态同步工作也必然会花费更多的资源。在很多场景下,有状态的服务要注意热点问题,例如最常见的秒杀,这里并非是说有状态服务不适合大并发的场景,反而在高并发的场景下,有状态的服务往往表现的比无状态服务更加出色。

Actor模型

在众多的并发模型中,最适合有状态服务设计的莫过于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;
        }
    }

以上只是一个简单案例,有状态的服务还有更多的设计方案,以上只供参考

相关文章
|
30天前
|
开发者
开发项目小问题总结,带有详解解释,让自己的代码走向完美之路,持续更新
这篇文章总结了开发项目中遇到的小问题及解决方案,包括字符串比较、资源管理、代码优化、异常处理等方面的内容,旨在帮助开发者写出更规范、高质量的代码。
34 2
开发项目小问题总结,带有详解解释,让自己的代码走向完美之路,持续更新
|
3月前
|
存储 前端开发 JavaScript
解锁前端高手之路:Vuex 状态管理实战,从零到精通的旅程!
【8月更文挑战第27天】状态管理在大型单页应用开发中至关重要。Vue.js 通过其官方工具 Vuex 提供了一套强大且直观的 API。本文从零开始引导你逐步掌握 Vuex。首先介绍如何安装和配置,然后通过具体示例深入探讨状态(State)、变更(Mutations)、动作(Actions)以及模块化 Store 的使用方法。最后,通过购物车管理实战案例展示如何运用 Vuex 解决复杂状态管理问题。掌握这些技巧后,你将能在项目中高效地利用 Vuex。
30 1
|
3月前
|
算法 测试技术 持续交付
技术感悟:代码之外的智慧
【8月更文挑战第14天】在技术的海洋中,我们常常沉浸于代码的编写和调试,追求着更高效的算法和更优雅的解决方案。然而,技术的世界远不止于此。它还包括了对问题的理解、对工具的运用、以及与他人的协作等多个方面。这些看似与代码无关的技能,实际上对我们的技术成长有着深远的影响。本文将分享一些在代码之外的技术感悟,希望能够为大家提供一些新的视角和思考。
|
5月前
|
Java 开发者
【技术成长日记】Java 线程的自我修养:从新手到大师的生命周期修炼手册!
【6月更文挑战第19天】Java线程之旅,从新手到大师的进阶之路:始于创建线程的懵懂,理解就绪与运行状态的成长,克服同步难题的进阶,至洞悉生命周期的精通。通过实例,展示线程的创建、运行与同步,展现技能的不断提升与升华。
37 2
|
6月前
|
NoSQL Java 程序员
阿里开发人员献礼“Java架构成长笔记”,深入内核,拒绝蒙圈
提起阿里,行外人联想到的关键词无非是“交易”、“淘宝”、“支付宝”,但对于程序员来说,阿里庞大的技术体系才是最吸引人的。实际上阿里作为国内一线互联网公司的头把交椅,内部的技术体系和发展都是备受关注的,对于程序员来说,能够进到阿里工作,就是对自己的技术水平进行一个提升和学习。
阿里开发人员献礼“Java架构成长笔记”,深入内核,拒绝蒙圈
|
JavaScript
作者经历过的各种报错处理--干货持续分享
作者经历过的各种报错处理--干货持续分享
56 0
|
图形学
R编程之路_常用参数
R编程之路_常用参数
|
运维 监控 前端开发
带团队后的日常思考(十一)
带团队后的日常思考(十一)
|
SQL 数据管理
好好编程-物流项目16【基础数据-修改和删除】
好好编程-物流项目16【基础数据-修改和删除】
好好编程-物流项目16【基础数据-修改和删除】
|
程序员
【5.11更新】图管够!灌篮高手、女儿国…阿里日,这帮程序员太会玩了!
除了很美的照片外,还有一组格外美的,你懂的。
8349 0
下一篇
无影云桌面