成为工程师 - 如何提升系统写性能(上)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 成为工程师 - 如何提升系统写性能(上)

今日内容



讲完了【读性能】,我们自然就开始讲【写性能】了。


上一篇文章的末尾我说,【写性能】比【读性能】要复杂一些,那我们就先来分析分析复杂性。知道了复杂性你才能更好地理解后面给出的方案。


01写性能为什么难提升


所谓【写性能】指的是:涉及数据持久化行为的性能。相比于读性能,有以下这些难点:


1) 丢“写”的代价往往比丢“读”大


比如用户浏览商品,丢了10个查询请求也许从转化量上来看,只潜在丢了1笔交易。而丢了10笔交易那就是实打实的订单损失。


从更本质上看,“写”的行为是在给你的系统创造数据,而数据本身就是价值。无论是可以直接收佣的订单,还是用户上传评论,修改个人信息等。


所以,我们针对“读”可以采用拦截流量的方式,因为损失不大(尤其是一些秒杀场景,本来库存就有限),而针对“写”,则往往要求全部都要“扛下来”。


2)“写”必须和磁盘打交道,而“读”是可以使用缓存的


从“持久化”的字面上看就很清楚了,就是数据不能丢!写入内存的数据是有风险,断电则丢失,写入磁盘则是稳妥的(当然磁盘也有损坏率,这个话题我们不做展开)。


3)“写”往往需要加锁,而“读”不需要


大家都知道并发写操作存在相互影响覆盖的情况。


无论是单机内的多线程写内存,还是多机(本质上还是多线程,只是在不同机器上)写数据库,都需要面对这样的问题。


典型的方案就是加锁。一个人抢到,其他人等待。但是,“锁”无疑是性能的第一大杀手。


4)“写”容易造成资损,而“读”一般不涉及资损


“读”的行为一般是查询,大量的并发查询如果处理不好,一般最差的结果就是显示出问题。显示出问题的结果,就是用户对功能不好用的投诉及抱怨。


“写”的行为往往是一种资产的凭证(当然也有不是的,比如用户信息,评论等)。而在系统没有控制好并发,或者没有严格约定好上下游rpc调用出入参的含义时,就非常容易出现资损。


典型的场景就是,调用方把异常当作失败。比如提现操作,提现服务调用出口网关超时,如果当作失败,则余额不扣除。但是超时并不代表银行网关没有收到请求,结果可能就是“余额没减,银行卡加钱”。



02写性能提升整体策略


我们先来看下提升【写性能】的整体策略:


如图所示。因为“写”行为主要是和DB打交道,那自然就推导出两个优化方向。一方面是对DB的优化,一方面则是对使用的优化。


【DB方面优化】分为三块:有钱任性、数据分片、合理选型。


【使用方面优化】分为五块:合理加锁、异步法、批量插入、文件法、缓存法。


今天,我们来讲其中的一部分。


03DB优化方向 - 有钱任性


没错,就是字面上的意思。


现在各大云服务商针对持久化存储都有很好的支持,封装了很多的功能,做了很多的改进。只要你舍得花钱,分库分表,上千qps的写入都可以到手。


但我相信,一般来说也只有企业初期,在规模还较小,并且战略上要快速抢占市场的时候才会这么去做。

就算是各个不差钱的大厂,也难免最后屈服于成本的压力,转向自研或者半自研。


前两天还看到一篇文章说到,国外很多的中大互联网公司,正在逐步降低使用云服务商提供的服务,转向【自采硬件+开源软件+自建运维团队】的方式,因为云产品真的太!贵!了!


04DB优化方向 - 数据分片


数据分片,顾名思义就是把数据拆拆拆。


在扩展性篇章中我们提到,Z轴的扩展就是对数据和服务进行分区。拆分数据可以很好地应对数据量持续上涨的扩展性问题。


此外,数据拆分开了,索引就会变小,数据查询也能做并发,各种资源瓶颈都能被打散,自然也能提升性能。

常规的拆分及访问方式如下:



这种拆分方式很典型,但问题也很明显。如果一个公司有N个服务,每个服务都需要自己去处理请求的路由,整体成本会很大。


不要小看这块的改造,例如DB分片的扩展要做重路由、DB多分片数据查询还涉及到数据merge、如果还想提供例如分片数据查询回来后的排序等操作,工作量是真不小。而且其中坑很多,不能让每个服务都去自己踩一遍吧?


所以,一般我们会用如下三种方案来代替:


方案1:引入中间层,数据服务。



我们行业最经典的话之一:没有什么事情,是不可以通过加一层来解决的。


这种方式好处很明显:功能复用


各种基础或者高级特性都可以被所有服务使用。同时,团队可以由数据库相关专业方向的同学组成,并且做好专业的运维和稳定性保障。


有好处就有坏处。增加了一个节点后,链路的复杂度变高,大量的数据需要多一层传递,各种超时、性能问题都会出现。同时这个服务的稳定性要求变得奇高,等同于一般对数据库的要求。


方案2:业务服务引入client



client相比数据服务的好处是:没有一个中间服务,也就没有引入一个高可用风险点,数据链路的传输也会变得顺畅高效。


坏处是:如果要对功能做升级,就要和调用方协调client的升级。


干过这个事情的人都知道,这个不容易。除非有组织从上层给支持,不然举步维艰。数据服务的升级就全部自己做主啦。


方案3:单元化访问



单元化的含义我们在扩展性篇章中已经详细讲过了。


简单来说就是:把服务器和数据库成组的进行隔离,一组服务器对应一组数据库(以及一组缓存)


这个组里的rpc、数据库、缓存访问都在这个组内完成。这样,就不会出现复杂的网状访问结构。


这个方案往往应用于体量已经达到一定规模的产品。而且,在单个区域内也还是需要借助数据服务或者client来进行DB访问的自动路由。


所以,如果你没有那么大的数据体量(以及稳定性需求),可以不用过多考虑。


单元化更多细节不多赘述,大家可以回到扩展性篇章中去翻看。


05DB优化方向 - 选择合适的数据库


在讨论数据库的选型之前,我们先来讨论一个看似没啥意义的问题,那就是“什么是数据库”


广义上讲,数据库是:存储数据的软件。存储数据的软件就包括:cache(缓存)、NoSQL(非关系型数据库)、RDBMS(关系型数据库)、FS(文件系统)等


狭义上讲,数据库一般满足于两个要求:实时查询 + 持久化。那就包括:NoSQL(非关系型数据库)、RDBMS(关系型数据库)


但是,一般我认为,cache也在数据库狭义定义范畴内,因为现在的cache中间件,都有一定的持久化能力。


那回到“选择合适的数据库”这个问题。既然要做选择,就要知道优劣,我们从5个维度来对这三者作比较,见下图:


【可靠性】虽然cache现在都配有持久化能力,但在极端情况下,是会丢数据的。所以在数据丢失零容忍情况下,我们不会依赖cache的持久化能力。而NoSQL和RDBMS在这方面没问题。


【性能】显然基于内存的cache读写性能都是最好的。其次是NoSQL,这是因为NoSQL的设计不涉及复杂的索引构建,NoSQL的事务也不是典型的事务,并且结合上NoSQL的数据存储格式,其写性能比RDBMS好。


【易扩展】目前市面上比较流行的cache和NoSQL解决方案都有成熟的扩展能力。因为他们就是在互联网浪潮下诞生的产物,天然就考虑了这些功能。而传统关系型数据库则往往依赖额外的能力建设。当然,现在相关的能力也越来越丰富,但易用性还是不太够的。


【事务】cache一般提供批量更新的能力。因为考虑到cache挂掉的时候,一般我们不依赖其持久化数据进行重建,所以对极端情况下的事务保证也没有强要求。NoSQL的事务不是典型的数据库事务,其主要支持批量的数据操作同时提交,但是对ACID的要求并不高。


【复杂查询】多表关联的复杂查询自然是RDBMS的拿手绝活。但cache和NoSQL看到如今的互联网应用,低效的关联查询并不是痛点,于是抛弃对复杂查询的过度支持,转而追求高性能。


所以,不同存储各有好坏,要结合自己的场景选择。有时候,你在那里想半天优化,也许出发点就错了。选择合适的存储,你就解决了一半的问题。


06使用优化 - 合理加锁


上面我们有提到,写逻辑设计并发时,往往为了保证数据的正确性,会采用加锁的方式。


01乐观锁与悲观锁


聊锁,我们就不得不了【乐观锁】和【悲观锁】。这两者的概念大家应该都知道,就不赘述了。但我想说,很多同学在理解这两者性能差别上,是存在问题的。


很多同学认为:乐观锁使用了版本控制,不阻塞线程,性能高。悲观锁使用了分布式锁或者DB行锁,阻塞运行,性能低。


但你仔细想想就会发现有问题。同样是大量的并发请求过来,乐观锁如果反复失败,大量并发线程会重复尝试,性能会直线下降。不见得比悲观锁“傻傻地”等待要好。


如果你了解JVM里synchronized关键字背后的原理,你就应该知道,CAS的乐观锁策略往往在不生效的时候,会转向阻塞的思路。


乐观锁比悲观锁效率高主要是因为:乐观锁将判断和更新通过一次调用就完成了,而悲观锁则需要多步。


那是不是所有的时候都可以使用乐观锁呢?


很抱歉,很多场景下还是需要使用悲观锁。


(场景1)上述提到的乐观锁反复失败的情况下,悲观锁就要出来救场了。


(场景2)如果涉及到多条数据操作,并且要控制事务的时候,乐观锁就力不从心了。


而上面两种场景真的很常见,所以,我们可以说,悲观锁是有价值的!


所以综合上述的说法,我们得到一个切实的用锁策略:尽量使用乐观锁,悲观锁场景控制锁粒度。


02尽量使用乐观锁


在使用锁的地方,我们先判断这里是否可以使用乐观锁。比如下面这个场景:


这个场景就可以使用乐观锁。但,如果是一个热点账户,就不合适了,乐观锁的where判断会大量失败。


但是热点账户的话,悲观锁也同样不合适。那要怎么做呢?我们下一篇文章会提到的。


03悲观锁控制锁粒度


如果你有个商品库存表如下:

这里假设商品库存的扣减还联动其他表一起变更,并且要保证事务。


那么,这里一定会涉及悲观锁(自己在某行上加行锁,或者事务写更新触发行锁)。


那我们要怎么提升性能呢?


我们可以“一锁变十锁”,如下面所示这样:



当用户请求扣库存的时候,需要选择其中一个分片进行扣减库存。


这样的好处是:扣减库存本来只有一个锁,现在变成了十个锁,冲突的概率和等待的时间都会降低。这和jdk8前的hashmap分段锁是相同的设计思路。


这里其实还有很多细节。例如在扣减库存前还需要一个用户维度的锁,不然如果有重复的请求扣减库存,请求落到不同的库存分片上,就会扣两笔。


有同学可能会问,如果某个分片的库存先被扣完了怎么办?


好问题!


一种方式是,不管他。如果你的库存总量本来就不大,但抢购的流量很大,并且均匀分布到各个分片上,那分片库存耗尽的时间不会差太多。


但如果流量不大,就需要做一些设计,不然有些用户会出现“看着有库存,但是抢不到”的情况,客诉就来了。


可以使用下图这种方案:



可以用redis记录每个分片的库存情况。商品上架前初始化redis中库存,并在db扣减库存后将最近库存设置到redis中。


这里有个小细节,为什么是更新库存到redis,而不是将redis库存-1?这个就留给你思考了


今日小结



今天我们就到这里了。对于【写性能】,由于内容较多,我分成两次内容来说,今天讲了上半部分,下一篇我们继续。


今天我们开始讲【写性能】。我们先是讲了提升【写性能】比提升【读性能】要难的各方面原因。

接着我们讲了,在使用DB方面,我们可以做数据分片,并提供了一些典型的实践。


然后,你需要了解各种数据存储软件的特性,并根据你的场景选择。


最后我们讲,【写性能】的一大挑战点是解决锁的问题,我们给出了“尽量使用乐观锁,悲观锁控制粒度”的设计思路。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
5月前
|
人工智能 JSON 前端开发
有关D2C工具的思考和分享, 提升前端研发效率
有关D2C工具的思考和分享, 提升前端研发效率
249 1
|
6天前
|
监控 安全
Fantom 语言在公司监控电脑编程中的性能考量
在数字化办公环境中,公司对电脑监控的需求日益增长。Fantom 语言作为一种新兴的编程语言,具备简洁高效的语法结构,适用于构建公司电脑监控系统。其面向对象和函数式编程特性,使得代码易于管理和扩展。Fantom 在内存管理方面的独特优势有助于降低资源占用,提高响应速度,并且易于添加新功能。然而,实际应用中需根据具体需求和硬件环境不断优化,确保监控系统的稳定性和安全性。
11 3
|
2月前
|
存储 缓存 分布式计算
成为工程师 - 如何提升系统写性能(下)
成为工程师 - 如何提升系统写性能(下)
|
2月前
|
缓存 负载均衡 网络协议
成为工程师 - 如何提高系统的扩展性?
成为工程师 - 如何提高系统的扩展性?
|
2月前
|
缓存 NoSQL 算法
成为工程师 - 如何提升系统读性能(上)
成为工程师 - 如何提升系统读性能(上)
|
2月前
|
缓存 运维 监控
成为工程师 - 如何提升系统稳定性(1)
成为工程师 - 如何提升系统稳定性(1)
|
2月前
|
缓存 前端开发 数据库
成为工程师 - 如何提升系统读性能(下)
成为工程师 - 如何提升系统读性能(下)
|
2月前
|
缓存 监控 网络协议
Linux性能大飞跃:揭秘系统优化秘籍,让你的服务器秒变性能怪兽,轻松应对万千挑战!
【8月更文挑战第5天】作为开源界的常青树,Linux以稳定高效著称,但也可能面临性能瓶颈。本文从五个维度探讨Linux优化:1) 更新系统与软件确保最新状态;2) 调整文件系统以适应应用需求;3) 根据具体需求调整内核参数;4) 利用工具管理进程与资源;5) 优化磁盘I/O性能。通过这些策略,可显著提升Linux系统的运行效率与响应速度,使其保持最佳状态。
81 7
|
2月前
|
消息中间件 缓存 Java
如何优化大型Java后端系统的性能:从代码到架构
当面对大型Java后端系统时,性能优化不仅仅是简单地提高代码效率或硬件资源的投入,而是涉及到多层次的技术策略。本篇文章将从代码层面的优化到系统架构的调整,详细探讨如何通过多种方式来提升Java后端系统的性能。通过对常见问题的深入分析和实际案例的分享,我们将探索有效的性能优化策略,帮助开发者构建更高效、更可靠的后端系统。
|
3月前
|
Java 大数据 测试技术
开发与运维测试问题之UseCompactObjectHeaders在SPECjbb2015基准测试中的表现如何解决
开发与运维测试问题之UseCompactObjectHeaders在SPECjbb2015基准测试中的表现如何解决
195 1
下一篇
无影云桌面