• 高并发研究


    高并发研究

    1.系统拆分

    将系统拆分为多个小系统,通过统一调度,分布式部署多个系统,来提高并发量。分布式系统架构很适合大并发量,也很适合大型的系统进行代码精简,拆分。不然几十万的代码跑在一个单体应用中,多人开发跟维护时会有很多冲突和麻烦。

    将服务拆分成多个服务,每个服务两万行代码撑死。代码继续变多的话就继续拆分成更细的服务。服务之间可以使用阿里的dubbo来进行调用,帮助进行负载均衡、服务实例上下线感知、超时重试等。

    2.缓存

    研究合适的缓存数据,尽量对不怎么变更的数据缓存起来,定一个缓存策略,减少对数据库的查询压力

    2.1缓存雪崩,单缓存服务(例如redis)挂掉之后,大量的请求直接访问数据库,导致数据库宕机。即使重启数据库,也会因为请求过多导致再次宕机。最终导致服务不可用,引发缓存雪崩。针对缓存血本,可以采用redis的主从结构以及哨兵模式,防止缓存服务挂掉。

    2.2缓存穿透,如果有大量请求访问数据库并不存在的数据,那么因为不会有响应的缓存会导致大量的请求还是会直接访问数据库,此时缓存相当于失效。

    2.2.1这时候可以增加布隆过滤器来判断请求携带的查询参数在数据库中是否存在,若不存在,则返回默认值,不让他访问数据库。

    2.2.2也可以针对查不到的数据,写一个默认值在缓存中,并且设置一个超时时间,在这个超时时间内,如果大量请求某个数据,就返回这个默认值。

    2.3缓存击穿,在缓存失效(超时)的那一瞬间,大量的并发请求发过来,还是会直接读取数据库,导致数据库宕机。这时候可以在维护一个定时器,在缓存失效之前就先读取新的值放到缓存中或者直接延长缓存的超时时间。

    2.4缓存可以有三种预防方式。

    2.4.1方式一:事前预防,采用redis主从架构,哨兵模式

    2.4.2方式二:事中处理,redis挂掉之后,查不到redis中的缓存,则从本地缓存中查数据,本地缓存再查不到则查数据库

    2.4.3方式三:事后处理,redis起来之后,顺序根据日志将缓存数据重新缓存起来

    2.5缓存一致性问题。缓存一般会出现缓存跟数据库内容不一致的问题,这种时候一般采用cache-aside模式,也就是先从缓存中读取,缓存中没有就从数据库中读取,然后把数据放到缓存中。更新的时候,先更新数据库,接着要更新缓存。

    2.5.1不过有可能存在更新数据库失败,却更新了缓存,导致数据不一致。这时候可以采取更新数据时,先把缓存删了,然后再更新数据库。

    3.增加事务队列

    把某些消耗性能的操作和请求放到队列里面,等待系统慢慢消费这些请求。

    队列MQ有好处也有坏处,好处是可以解耦、异步、削峰。坏处是增加了系统复杂性和不可维护性。

    2.1解耦,可以把系统解耦出来,提供发布订阅模式,让别人调用即可,不需要主动推送。

    2.2异步,对于时间开销比较长的操作,可以先返回结果给调用方,再放到队列中,慢慢消费消息。但是容易导致数据不一致,如果先返回结果,但是在消费过程当中失败了,就导致结果不一致。

    2.3削峰,可以在请求量比较大的时候将消息放到队列中,然后慢慢处理,不至于大量请求把服务搞宕机

    2.4复杂性,引入了一个额外的组件依赖,如果MQ挂了,那么整个系统就宕机了,所以还需要解决MQ的高可用性,需要花费比较高的成本

    2

    4.分库分表

    为了缓解数据库的查询压力,可以对数据量特别大的表进行分表,设计好分表策略,方便连表查询。如果查询压力依旧很大,那么就需要进行分库,根据查询条件和定好的分库策略,从不同的库查询数据。

    4.1一般来说,并发压力大时(大于2000/qps),就要分库了,否则并发更不上。如果单表数据量超过200万,就要考虑分表了,否则sql查询效率会下降。

    4.2分库分表一般都采用雪花算法弄唯一主键

    4.3如果是中小型互联网公司,指定分库分表架构时,可以考虑一次性就设置为32个库,每个库32张表,总共1024张表。如果是后续再加表,那么后续要考虑在生产环境中无缝衔接进行分库分表(需要写一个数据迁移的程序,先准备好分库分表环境,然后将生产环境的写入操作同时写入到老数据库和分库分表环境中,接着迁移老数据到分库分表中,迁移时只有分库分表环境中没有的数据以及老数据的时间比分库分表环境的时间新才迁移,最后还要校验老数据和分库分表的数据是否一致,不一致就要重新迁移一遍老数据,直到完全一致)

    4.4分库分表一般都会根据表的主键来进行取模,来路由到相应的库和表中

    分库分表和读写分离可以考虑JAVA中间件SharedingSphere

    5.读写分离

    查询比较多时,可以设置数据库主从架构,主库专门写入,从库从主库读取数据,查询时则是在从库查询,缓解查询压力

    如何实现 MySQL 的读写分离?
    其实很简单,就是基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。
    
    MySQL 主从复制原理的是啥?
    主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL,这样就可以保证自己跟主库的数据是一样的。
    
    mysql-master-slave
    
    这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。
    
    而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。
    
    所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。
    
    这个所谓半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。
    
    所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。
    
    MySQL 主从同步延时问题(精华)
    以前线上确实处理过因为主从同步延时问题而导致的线上的 bug,属于小型的生产事故。
    
    是这个么场景。有个同学是这样写代码逻辑的。先插入一条数据,再把它查出来,然后更新这条数据。在生产环境高峰期,写并发达到了 2000/s,这个时候,主从复制延时大概是在小几十毫秒。线上会发现,每天总有那么一些数据,我们期望更新一些重要的数据状态,但在高峰期时候却没更新。用户跟客服反馈,而客服就会反馈给我们。
    
    我们通过 MySQL 命令:
    
    show slave status
    查看 Seconds_Behind_Master ,可以看到从库复制主库的数据落后了几 ms。
    
    一般来说,如果主从延迟较为严重,有以下解决方案:
    
    分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。
    打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s,并行复制还是没意义。
    重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
    如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库。不推荐这种方法,你要是这么搞,读写分离的意义就丧失了。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    6.优化SQL查询

    1. 尽量减少对数据库的访问次数, 可以用缓存保存查询结果, 减少查询次数);
    2. 通过搜索参数, 尽量减少对表的访问行数,最小化结果集, 从而减轻网络负担;
    3. 能够分开的操作尽量分开处理, 提高每次的响应速度;
    4. 算法的结构尽量简单;
    5. Explain 你的 sql 语句 看看执行效率低的是什么
    具体要注意的:
    
    1.应尽量避免在 where 子句中对字段进行 null 值判断, 否则将导致引擎放弃使用索引而进行
    
    全表扫描, 如:
    
    select id from t where num is null
    
    可以在 num 上设置默认值 0, 确保表中 num 列没有 null 值, 然后这样查询:
    
    select id from t where num=0
    
    2.应尽量避免在 where 子句中使用!=或<>操作符, 否则将引擎放弃使用索引而进行全表扫描。
    
    优化器将无法通过索引来确定将要命中的行数,因此需要搜索该表的所有行。
    
    3.应尽量避免在 where 子句中使用 or 来连接条件, 否则将导致引擎放弃使用索引而进行全表
    
    扫描, 如:
    
    select id from t where num=10 or num=20
    
    可以这样查询:
    
    select id from t where num=10
    
    union all
    
    select id from t where num=20
    
    4.in 和 not in 也要慎用, 因为 IN 会使系统无法使用索引,而只能直接搜索表中的数据。 如:
    
    select id from t where num in(1,2,3)
    
    对于连续的数值, 能用 between 就不要用 in 了:
    
    select id from t where num between 1 and 3
    
    5.尽量避免在索引过的字符数据中, 使用非打头字母搜索。 这也使得引擎无法利用索引。
    
    见如下例子:
    
    SELECT * FROM T1 WHERE NAME LIKE „%L%‟
    
    SELECT * FROM T1 WHERE SUBSTING(NAME,2,1)=‟L‟
    
    SELECT * FROM T1 WHERE NAME LIKE „L%‟
    
    即使 NAME 字段建有索引, 前两个查询依然无法利用索引完成加快操作, 引擎不得不对全表所有数据逐条操作来完成任务。 而第三个查询能够使用索引来加快操作。
    
    6.必要时强制查询优化器使用某个索引, 如在 where 子句中使用参数, 也会导致全表扫描。 因为 SQL 只有在运行时才会解析局部变量, 但优化程序不能将访问计划的选择推迟到运行时; 它
    
    必须在编译时进行选择。 然而, 如果在编译时建立访问计划, 变量的值还是未知的, 因而无法作为索引选择的输入项。 如下面语句将进行全表扫描:
    
    select id from t where num=@num
    
    可以改为强制查询使用索引:
    
    select id from t with(index(索引名)) where num=@num
    
    7.应尽量避免在 where 子句中对字段进行表达式操作, 这将导致引擎放弃使用索引而进行全表扫描。 如:
    
    SELECT * FROM T1 WHERE F1/2=100
    
    应改为:
    
    SELECT * FROM T1 WHERE F1=100*2
    
    SELECT * FROM RECORD WHERE SUBSTRING(CARD_NO,1,4)=‟5378‟
    
    应改为:
    
    SELECT * FROM RECORD WHERE CARD_NO LIKE „5378%‟
    
    SELECT member_number, first_name, last_name FROM members  WHERE DATEDIFF(yy,datofbirth,GETDATE()) > 21
    
    应改为:
    
    SELECT member_number, first_name, last_name FROM members WHERE dateofbirth < DATEADD(yy,-21,GETDATE())
    
    即: 任何对列的操作都将导致表扫描, 它包括数据库函数、 计算表达式等等, 查询时要尽可能将操作移至等号右边。
    
    8.应尽量避免在 where 子句中对字段进行函数操作, 这将导致引擎放弃使用索引而进行全表扫
    
    描。 如:
    
    select id from t where substring(name,1,3)='abc'--name 以 abc 开头的 id
    
    select id from t where datediff(day,createdate,'2005-11-30')=0--„2005-11-30‟生成的 id
    
    应改为:
    
    select id from t where name like 'abc%'
    
    select id from t where createdate>='2005-11-30' and createdate
    
    9.不要在 where 子句中的“=”左边进行函数、 算术运算或其他表达式运算, 否则系统将可能无
    
    法正确使用索引。
    
    10.在使用索引字段作为条件时, 如果该索引是复合索引, 那么必须使用到该索引中的第一个字
    
    段作为条件时才能保证系统使用该索引, 否则该索引将不会被使用, 并且应尽可能的让字段顺序
    
    与索引顺序相一致。
    
    11.很多时候用 exists 是一个好的选择:
    
    elect num from a where num in(select num from b)
    
    用下面的语句替换:
    
    select num from a where exists(select 1 from b where num=a.num)
    
    SELECT SUM(T1.C1)FROM T1 WHERE(
    
    (SELECT COUNT(*)FROM T2 WHERE T2.C2=T1.C2>0)
    
    SELECT SUM(T1.C1) FROM T1WHERE EXISTS(
    
    SELECT * FROM T2 WHERE T2.C2=T1.C2)
    
    两者产生相同的结果, 但是后者的效率显然要高于前者。 因为后者不会产生大量锁定的表扫描或
    
    是索引扫描。
    
    如果你想校验表里是否存在某条纪录, 不要用 count(*)那样效率很低, 而且浪费服务器资源。 可
    
    以用 EXISTS 代替。 如:
    
    IF (SELECT COUNT(*) FROM table_name WHERE column_name = 'xxx')
    
    可以写成:
    
    IF EXISTS (SELECT * FROM table_name WHERE column_name = 'xxx')
    
    经常需要写一个 T_SQL 语句比较一个父结果集和子结果集, 从而找到是否存在在父结果集中有
    
    而在子结果集中没有的记录, 如:
    
    SELECT a.hdr_key FROM hdr_tbl a---- tbl a 表示 tbl 用别名 a 代替
    
    WHERE NOT EXISTS (SELECT * FROM dtl_tbl b WHERE a.hdr_key = b.hdr_key)
    
    SELECT a.hdr_key FROM hdr_tbl a
    
    LEFT JOIN dtl_tbl b ON a.hdr_key = b.hdr_key WHERE b.hdr_key IS NULL
    
    SELECT hdr_key FROM hdr_tbl
    
    WHERE hdr_key NOT IN (SELECT hdr_key FROM dtl_tbl)
    
    三种写法都可以得到同样正确的结果, 但是效率依次降低。
    
    12.尽量使用表变量来代替临时表。 如果表变量包含大量数据, 请注意索引非常有限(只有主键
    
    索引) 。
    
    13.避免频繁创建和删除临时表, 以减少系统表资源的消耗。
    
    14.临时表并不是不可使用, 适当地使用它们可以使某些例程更有效, 例如, 当需要重复引用大
    
    型表或常用表中的某个数据集时。 但是, 对于一次性事件, 最好使用导出表。
    
    15.在新建临时表时, 如果一次性插入数据量很大, 那么可以使用 select into 代替 create table,
    
    避免造成大量 log , 以提高速度; 如果数据量不大, 为了缓和系统表的资源, 应先 create table,
    
    然后 insert。
    
    16.如果使用到了临时表, 在存储过程的最后务必将所有的临时表显式删除, 先 truncate table ,
    
    然后 drop table , 这样可以避免系统表的较长时间锁定。
    
    17.在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON , 在结束时设置 SET
    
    NOCOUNT OFF 。 无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC
    
    消息。
    
    18.尽量避免大事务操作, 提高系统并发能力。
    
    19.尽量避免向客户端返回大数据量, 若数据量过大, 应该考虑相应需求是否合理。
    
    20. 避免使用不兼容的数据类型。 例如 float 和 int、 char 和 varchar、 binary 和 varbinary 是不兼
    
    容的(条件判断时) 。 数据类型的不兼容可能使优化器无法执行一些本来可以进行的优化操作。
    
    例如:
    
    SELECT name FROM employee WHERE salary > 60000
    
    在这条语句中,如 salary字段是money型的,则优化器很难对其进行优化,因为60000是个整型数。
    
    我们应当在编程时将整型转化成为钱币型,而不要等到运行时转化。
    
    21.充分利用连接条件(条件越多越快) , 在某种情况下, 两个表之间可能不只一个的连接条件,
    
    这时在 WHERE 子句中将连接条件完整的写上, 有可能大大提高查询速度。
    
    例:
    
    SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO
    
    SELECT SUM(A.AMOUNT) FROM ACCOUNT A,CARD B WHERE A.CARD_NO = B.CARD_NO AND
    
    A.ACCOUNT_NO=B.ACCOUNT_NO
    
    第二句将比第一句执行快得多。
    
    22、 使用视图加速查询
    
    把表的一个子集进行排序并创建视图, 有时能加速查询。 它有助于避免多重排序 操作, 而且在
    
    其他方面还能简化优化器的工作。 例如:
    
    SELECT cust.name, rcvbles.balance, ……other columns
    
    FROM cust, rcvbles
    
    WHERE cust.customer_id = rcvlbes.customer_id
    
    AND rcvblls.balance>0
    
    AND cust.postcode>“98000”
    
    ORDER BY cust.name
    
    如果这个查询要被执行多次而不止一次, 可以把所有未付款的客户找出来放在一个视图中, 并按
    
    客户的名字进行排序:
    
    CREATE VIEW DBO.V_CUST_RCVLBES
    
    AS
    
    SELECT cust.name, rcvbles.balance, ……other columns
    
    FROM cust, rcvbles
    
    WHERE cust.customer_id = rcvlbes.customer_id
    
    AND rcvblls.balance>0
    
    ORDER BY cust.name
    
    然后以下面的方式在视图中查询:
    
    SELECT * FROM V_CUST_RCVLBES
    
    WHERE postcode>“98000”
    
    视图中的行要比主表中的行少, 而且物理顺序就是所要求的顺序, 减少了磁盘 I/O, 所以查询工
    
    作量可以得到大幅减少。
    
    23、 能用 DISTINCT 的就不用 GROUP BY (group by 操作特别慢)
    
    SELECT OrderID FROM Details WHERE UnitPrice > 10 GROUP BY OrderID
    
    可改为:
    
    SELECT DISTINCT OrderID FROM Details WHERE UnitPrice > 10
    
    24.能用 UNION ALL 就不要用 UNION
    
    UNION ALL 不执行 SELECT DISTINCT 函数, 这样就会减少很多不必要的资源
    
    25.尽量不要用 SELECT INTO 语句。
    
    SELECT INOT 语句会导致表锁定, 阻止其他用户访问该表。
    
    上面我们提到的是一些基本的提高查询速度的注意事项,但是在更多的情况下,往往需要反复试
    
    验比较不同的语句以得到最佳方案。 最好的方法当然是测试, 看实现相同功能的 SQL 语句哪个
    
    执行时间最少, 但是数据库中如果数据量很少, 是比较不出来的, 这时可以用查看执行计划, 即:
    
    把实现相同功能的多条 SQL 语句考到查询分析器, 按 CTRL+L 看查所利用的索引, 表扫描次数
    
    (这两个对性能影响最大) , 总体上看询成本百分比即可
    
    26.在查询时, 不要过多地使用通配符如 SELECT * FROM T1 语句, 要用到几列就选择几列如:
    
    SELECTCOL1,COL2 FROM T1;
    
    27.在可能的情况下尽量限制尽量结果集行数如: SELECT TOP 300 COL1,COL2,COL3 FROM
    
    T1,因为某些情况下用户是不需要那么多的数据的。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
  • 相关阅读:
    【权威出版/投稿优惠】2024年机器视觉与自动化技术国际会议(MVAT 2024)
    usb gadget configfs分析
    Mysql数据库管理-blackhole存储引擎
    java:包与修饰符
    在macOS上安装Homebrew教程
    技术团队:研发中的短跑冲刺和马拉松游戏
    基于Java毕业设计休闲网络宾馆管理源码+系统+mysql+lw文档+部署软件
    关于Ubuntu ssh远程连接报错和无法root登录的解决方法
    Java Stream 的操作这么多,其实只有两大类,看完这篇就清晰了
    RLHF的替代算法之DPO原理解析:从Zephyr的DPO到Claude的RAILF
  • 原文地址:https://blog.csdn.net/yunfeather/article/details/126530495