0%

10.动态内存

10.1 生命周期

目前的我们接触到的对象或者静态static都有着严格的生命周期:

  • 全局:程序启动时自动分配,程序结束时销毁
  • 局部对象:进入其所定义的程序时被创建,离开块时销毁
  • 静态:第一次使用前分配,程序结束时销毁

上述中的变量只使用了静态内存和栈内存。它们会自动创建和销毁。静态内存保存局部static、类static成员以及定义在任何函数之外的变量。栈内存保存定义在函数内的非static对象

除了上述的自动分配外,c++还支持动态分配对象。(其生命周期与它们在哪创建无关,只有显式的被释放时,这些对象才会被销毁)。它们被分配在内存池,称作自由空间或堆。程序用堆来存储动态分配

阅读全文 »

1 condition_variable:同步

上面的互斥锁只是在共享数据处执行保护操作,但是数据的同步,即线程对数据的操作的先后次序并不确定,当我们还想对线程同步时,必须采取一定的同步操作。条件变量是达到这个目的方法。

C++标准库对条件变量有两套实现:

  • std::condition_variablestd::condition_variable_any 。这两个实现都包含在<condition_variable>头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是 为了同步)
    • 前者仅限于与std::mutex一起工作,
    • 而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀,因此从体积、性能,以及系统资源的使用方面产生额外的开销.
    • 所以 std::condition_variable 一般作为首选的类型,当对灵活性有硬性要求时,我们才会去考虑 std::condition_variable_any
阅读全文 »

1 C++中的锁

1.1 C++中的锁机制

C++中的锁机制以下几种:

  • 互斥锁:包括std::mutex、std::recursive_mutex、std::timed_mutex、std::recursive_timed_mutex等。互斥锁用于保护共享资源,可以保证同一时刻只有一个线程访问共享资源。

  • 读写锁:包括std::shared_mutex、std::shared_timed_mutex等。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

  • 条件变量:包括std::condition_variable、std::condition_variable_any等。条件变量允许线程等待某个条件发生变化,只有当条件满足时才能继续执行。

  • 原子操作:包括std::atomic、std::atomic_flag等。原子操作用于保证某个操作的执行不会被其他线程中断,从而避免了数据竞争的发生。

  • 自旋锁:包括std::spin_lock、std::atomic_flag等。自旋锁在等待锁的过程中不断地循环检查锁是否可用,而不是放弃CPU,从而避免了线程上下文切换带来的开销。

  • 信号量:包括std::binary_semaphore、std::counting_semaphore等。信号量用于控制同时访问某个资源的线程数量,可以实现线程的互斥和同步。

1.2 悲观锁和乐观锁

在C++中,锁通常被分为两种类型:悲观锁和乐观锁

  • 其中悲观锁是指在访问共享资源时先获取锁,防止其他线程同时修改该资源,适用于写操作多的场景。C++中的互斥锁就是一种悲观锁。
  • 而乐观锁则是在不加锁的情况下,尝试去读取和修改共享资源,如果遇到冲突,再使用重试等机制解决冲突,适用于读操作多于写操作的场景。
    • 在C++中,可以使用atomic类型来实现乐观锁。atomic类型提供了对基本类型的原子操作,包括读、写、比较交换等。在进行原子操作时,它使用硬件原语实现同步,避免了使用锁所带来的额外开销和死锁的问题。
    • 除了atomic类型,C++11还引入了一些使用乐观锁的算法,如无锁队列和无锁哈希表等。这些算法使用原子操作来实现线程安全,同时充分利用了乐观锁的优势,避免了使用锁所带来的开销。

2 Mutex:互斥

所有线程间共享数据的问题,都是修改数据导致的(竞争条件)。如果所有的共享数据都是只读的,就没问题,因为一个线程所读取的数据不受另一个线程是否正在读取相同的数据而影响

2.1 恶性条件竞争

恶性条件竞争通常发生于多线程对多于一个的数据块的修改时,产生了非预想的执行效果,即竞态条件是多个线程同时访问共享资源,导致结果依赖于线程执行的顺序和时间。

  • 竞态条件(Race Condition)指的是多个线程访问共享变量时,最终的结果取决于多个线程的执行顺序。竞态条件不一定总是错误的,但它们可能导致非预期的结果。

  • 数据竞争(Data Race)指的是多个线程同时访问同一个共享变量,并且至少有一个线程对该变量进行了写操作。数据竞争是一种错误,因为它可能导致未定义的行为。

在多线程编程中,竞态条件和数据竞争是常见的问题。解决这些问题的关键是使用同步机制。

避免恶性条件竞争:

  • 要避免恶性的条件竞争,一种方法是就使用一定的手段,对线程共享的内存区域的数据结构采用某种保护机制,如使用锁
  • 另一种就是选择对该区域的数据结构和不变量的设计进行修改,如保证该区域为原子操作,,修改完的结构必须能完成一系列不可分割的变化,但是这种无锁的方法很难一定保证线程安全
  • 另一种处理条件竞争的方式是,使用事务(transacting)的方式去处理该数据共享区域
阅读全文 »

1 volatile

C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier。这是 BS 在 "The C++ Programming Language" 对 volatile 修饰词的说明:

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

  • volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。

  • 应对场景:遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化。这是因为volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,就极有可能暂时使用寄存器中的值,此时这个变量由别的线程更新了的话,将出现不一致的现象

  • 示例:int volatile vInt; 当要求使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

阅读全文 »

1 信息的存储

大多数的计算机使用8位的块,或者说是字节(byte),作为最小的可寻址内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,我们称为虚拟内存,内存的每个字节都由一个唯一的数字来标识,即称为地址;所有可能的地址空间的集合就是虚拟地址空间

顾名思义,虚拟地址空间只是一个展现给机器级程序的概念性映像。实际的实现(第九章:虚拟内存)是将DRAM、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来,位程序提供一个看上去统一的字节数组。

阅读全文 »

1 学习本书的目的

计算机系统由硬件和系统软件组成。本书是推荐给哪些希望深入了解这些组件如何工作,以及这些组件是如何影响程序正确性和性能,以此来提高自身技能的读者。学完本书,你将知道:

  • 如何避免由计算机表示数字的方式引起的奇怪的数字错误(第二章:信息的表示和处理)
  • 学会一些小窍门来优化自己的C代码,以充分利用现代处理器和存储器系统的设计
  • 你将了解编译器是如何实现过程调用的
  • 如何利用这些知识来避免缓冲区溢出错误带来的安全漏洞
  • 将学会如何识别和避免链接时那些令人讨厌的错误(第七章:链接)
  • 学会如何编写自己的Unix shell、自己的动态存储分配包、自己的web服务器
  • 并发带来的希望和陷阱
阅读全文 »

C++11 中引入 std::ref 用于取某个变量的引用,这个引入是为了解决一些传参问题。

我们知道 C++ 中本来就有引用的存在,为何 C++11 中还要引入一个 std::ref 了?主要是考虑函数式编程(如 std::bind)在使用时,是对参数直接拷贝,而不是引用。下面通过例子说明

阅读全文 »

1 概览

23种设计模式主要可以分为三种类型:

  • 创建型模式:用来创建对象

    • 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式。
  • 结构型模式:是从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题。(关注对象和类的组成关系)

    • 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
  • 行为型模式:关注系统中对象之间的相互交互,研究系统在运行时对象之间的相互通信和协作,进一步明确对象的职责

    • 模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
阅读全文 »

1.秒杀系统的设计

秒杀一般出现在商城的促销活动中,指定了一定数量(比如:10个)的商品(比如:手机),以极低的价格(比如:0.1元),让大量用户参与活动,但只有极少数用户能够购买成功。这类活动商家绝大部分是不赚钱的,说白了是找个噱头宣传自己。

像这种瞬时高并发的场景,传统的系统很难应对,我们需要设计一套全新的系统。可以从以下几个方面入手:

  • 页面静态化
  • CDN(content delivery Network)加速
  • 缓存
  • mq异步处理
  • 限流
  • 分布式锁

思路:

  1. 首先,对于秒杀系统中的活动页面一般是固定的,比如:商品名称、商品描述、图片等。为了减少不必要的服务端请求,因此要对活动页面做静态化处理。用户浏览商品等常规操作,并不会请求到服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问服务端。
  2. 但只做页面静态化还不够,因为用户分布在全国各地,有些人在北京,有些人在成都,有些人在深圳,地域相差很远,网速各不相同。为了让用户最快访问到活动页面,这就需要使用CDN,它的全称是Content Delivery Network,即内容分发网络。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
  3. 秒杀是一个读多写少的场景(因为在秒杀过程中,每一个请求一般都会检查库存是否充足,足够了才允许修改库存下单,由于大量用户抢少量商品,只有极少部分用户能够抢成功,所以绝大部分用户在秒杀时,库存其实是不足的,系统会直接返回该商品已经抢完。这是非常典型的:读多写少 的场景)。
  4. 如果直接使用MYSQL,这么高的并发数据库极有可能挂点,因此应该对库存信息使用redis缓存,,在秒杀活动开始前,将库存信息预缓存倒redis中,同时采用分布式锁防止缓存击穿,并且为了提高系统可用性可以使用集群的方式部署多个节点。
  5. 为防止超买超卖(query查询操作非原子操作),扣减库存操作中,使用redis扣减库存。redis的incr方法是原子性的,可以用该方法扣减库存,:

    • 1)先判断该用户有没有秒杀过该商品,如果已经秒杀过,则直接返回-1。2)扣减库存,判断返回值是否小于0,如果小于0,则直接返回0,表示库存不足。3)如果扣减库存后,返回值大于或等于0,则将本次秒杀记录保存起来。然后返回1,表示成功。(下述代码不足点:由于预先执行incrby,导致库存变负数,后面有回退库存时,会导致库存不准)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      bool exist = redisClient.query(productId,userId);
      if(exist) {
        return -1;
      }
      if(redisClient.incrby(productId, -1)<0) {
        return 0;
      }
      redisClient.add(productId,userId);
      return 1;
    • 因此,最好的策略:我们都知道lua脚本,是能够保证原子性的,而redis支持lua脚本,它跟redis一起配合使用,能够完美解决上面的问题
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
        StringBuilder lua = new StringBuilder();
        lua.append("if (redis.call('exists', KEYS[1]) == 1) then");
        lua.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        lua.append("    if (stock == -1) then");
        lua.append("        return 1;");
        lua.append("    end;");
        lua.append("    if (stock > 0) then");
        lua.append("        redis.call('incrby', KEYS[1], -1);");
        lua.append("        return stock;");
        lua.append("    end;");
        lua.append("    return 0;");
        lua.append("end;");
        lua.append("return -1;");
  6. 同时,为了防止redis因为宕机导致的最后数据的不一致性,需要开启redis的持久化机制。
  7. 真实的秒杀场景中,有三个核心流程:秒杀-->生成订单-->支付。真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。
    • 往mq发送下单消息的时候,有可能会失败。原因有很多,比如:网络问题、broker挂了、mq服务端磁盘问题等。这些情况,都可能会造成消息丢失。那么,如何防止消息丢失呢?
    • 方法1:在生产者发送mq消息之前,先把该条消息写入消息发送表,初始状态是待处理,然后再发送mq消息。消费者消费消息时,处理完业务逻辑之后,再回调生产者的一个接口,修改消息状态为已处理。
    • 防范2:借助mq的消息的持久化
  8. 回退库存: 如果用户秒杀成功了,下单之后,在15分钟之内还未完成支付的话,该订单会该被自动取消,库存+1,这种场景可以使用延时队列下单时消息生产者会先生成订单,此时状态为待支付,然后会向延迟队列中发一条消息。达到了延迟时间,消息消费者读取消息之后,会查询该订单的状态是否为待支付。如果是待支付状态,则会更新订单状态为取消状态。如果不是待支付状态,说明该订单已经支付过了,则直接返回。

  • 限流:
    • 基于nginx的限流:针对整体而言
      • 基于请求速率的限流:通过ngx_http_limit_req_module模块来实现,该模块支持基于固定窗口算法和令牌桶算法的限流。
      • 固定窗口算法:
        • 允许定义一个请求速率(如每秒允许的请求数量)和一个时间窗口。
        • 当请求到达时,Nginx会检查在当前的时间窗口内是否已经达到了设置的速率限制。
        • 如果请求超出了速率限制,Nginx可以配置为返回错误状态码(如503 Service Temporarily Unavailable)或者延迟处理请求。
      • 令牌桶算法:
        • 通过固定速率向桶中添加令牌。
        • 每个请求都需要消耗一个令牌才能被处理。
        • 如果桶中有足够的令牌,请求将立即被处理;如果没有令牌,则请求可以被延迟处理或拒绝。
        • 这种算法允许一定程度的突发流量,因为桶中可以积累令牌以应对短时间内的请求峰值。

针对科技限流: - 对同一用户限流:了防止某个用户,请求接口次数过于频繁,可以只针对该用户做限制。比如某个id,每分钟只能请求5次接口。 - 对同一ip限流:有时候只对某个用户限流是不够的,有些高手可以模拟多个用户请求,这种nginx就没法识别了。这时需要加同一ip限流功能。 - 对接口限流:别以为限制了用户和ip就万事大吉,有些高手甚至可以使用代理,每次都请求都换一个ip。这时可以限制请求的接口总次数。这种限制对于系统的稳定性是非常有必要的。但可能由于有些非法请求次数太多,达到了该接口的请求上限,而影响其他的正常用户访问该接口。看起来有点得不偿失。 - 加验证码:相对于上面三种方式,加验证码的方式可能更精准一些,同样能限制用户的访问频次,但好处是不会存在误杀的情况。

2. SQL的优化

在平常中我是通过这样去优化慢查询的:

  • 首先通过慢查询日志去定位慢SQL语句,使用mysqldumplow工具分析慢查询日志,找到慢SQL
    1
    mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
  • 使用EXPLAIN分析SQL语句,主要看这条查询语句当中的TYPEkeyrowsextra字段,其中type观察该SQL语句的访问类型,是否使用索引,好坏层级为system > const > eq_ref > ref > range > index >ALL,如果是allindex的话,说明走了全表扫描,没有走索引或者索引失效,导致查询慢,尝试使用走索引优化,比如建立复合索引,最少优化到range范围,更好的则尽量ref\const
  • 接着通过key得到实际实际的索引,观察返回的记录数rows是否过多,如果过多则限制返回记录数,比如去除不必要的记录或者将查询分割成小范围查询
  • 另外一点的话,如果是index,看是否能直接使用索引覆盖,即using index

    1
    2
    3
    4
    5
    +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
    | id | select_type | table | type | key | rows | Extra |
    +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
    | 1 | SIMPLE | orders | ALL | NULL | 10000| Using where |
    +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+

  • SQL的重新可以从减少查询数据量、减少返回的记录数、拆分复杂查询出发

1
2
3
4
5
6
7
8
9
10
11
// 减少数据量
优化前:
SELECT * FROM logs WHERE create_time>'2023-01-01';
优化后
SELECT id,message FROM logs WHERE create_time>'2023-01-01';

//拆分复杂查询
优化前(有嵌套子查询)
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount>1000);
优化后(使用联合查询)
SELECT u.* FROM users u JOIN orders o ON u.id=o.user_id WHERE o.amount>1000;

为什么大多数场景下连接查询比嵌套子查询好

  • 嵌套子查询可能导致内部查询重复执行,并且零时表的生成和管理增加了额外的开销
  • 联合查询通过JOIN一次性将两个表的数据关联合并,生成中间结果集,避免了重复执行。

3. 索引的优化

Mysql索引的优化

  • 索引类型选择:MySQL支持多种索引类型,有B+tree索引、哈希索引、全文索引(FULLTEXT,Innodb和MyISAm均支持)。可以根据实际需求选择合适的索引类型,比如B+tree索引适用于要求排序、范围查询等场景;如果是经常查询单条记录,用=情况,在这样的精确匹配查询下,使用哈希索引合适(MEMORY存储引擎)
  • 选择适当的列建立聚簇索引(适当的列是指频繁查询且唯一的字段,这样对于大多数查询来说都避免了回表查询)
  • 建立联合索引:选择多个经常结合查询的字段建立联合索引,并且按照最左前缀匹配,选择性高的字段放在前面。后面在查询过程中能狗进行覆盖索引,这样对于大多数查询来说都避免了回表查询。
  • 对于字符类型索引,如果经常查询,可以尝试建立前缀索引,按照区分度选择合适的长度建立

另外,当我们看到慢SQL后,不是马上去建立索引,而是看能不能优化SQL。大多数情况下,业务SQL比较复杂,很难优化,因此建立索引要参照下面的规则:

  • (1)索引并非越多越好,大量的索引不仅占用磁盘空间,而且还会影响insert,delete,update等语句的性能
  • (2)索引需要维护,因此避免对经常更新的表做更多的索引,并且索引中的列尽可能少;对经常用于查询的字段创建索引,避免添加不必要的索引
  • (3)数据量少的表尽量不要使用索引,由于数据较少,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果
  • (4)在条件表达式中经常用到不同值较多的列上创建索引,在不同值很少的列上不要建立索引
  • (5)在频繁进行排序或者分组的列上建立索引,如果排序的列有多个,可以在这些列上建立联合索引,联合索引中列顺序按照选择性排列。

4. mysql死锁的排查

  • 开启mysql的死锁日志
    1
    SET GLOBAL innodb_print_all_deadlock=ON;
  • 分析死锁日志
    • 检查SQL的执行顺序,观察事务是否因不同顺序访问相同资源导致
      1
      2
      3
      4
      5
      6
      7
      事务1
      BEGIN;
      UPDATE accounts SET banlance=banlance-100 WHERE Id=1; 锁住了id=1
      UPDATE accounts SET banlance=banlance+100 WHERE Id=2; 尝试锁住id=2
      事务2
      UPDATE accounts SET banlance=banlance-100 WHERE Id=2; 锁住了id=2
      UPDATE accounts SET banlance=banlance+100 WHERE Id=2; 尝试锁住id=1
    • 检查是是否走索引,无索引或索引不当会导致锁升级为表锁或间隙锁,增大死锁概率

在MySQL种有这样的机制,线程发现死锁后,会主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。可将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。但是它有额外负担的。每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,消耗大。

5. 数据库,如果突然掉电数据写入失败,redo log也来不及落盘,undo log 也没办法,这种情况怎么办?

针对该问题,我想到有事前的预防措施、事中的高可用架构以及事后的业务层补偿三个方面来解决:

  • 预防措施:避免极端情况的发生:
    • 比如使用UPS不间断电源,防止突然断电,提供零时电力完成未提交事物
    • BBU电池后备缓存,确保存储设备的缓存数据在断电后仍能写入磁盘
  • 高可用架构设计:考虑容灾和冗余,比如使用MySQL group replication.通过同步或半同步复制,确保事务在多个节点提交后才返回成功,这样即使一个节点宕机了也不影响整体的服务,从节点可以快速接管并恢复服务,

  • 业务层补偿机制:为所有事务生成一个全局的UUID,并在业务层维护一个日志,定时扫描未完成的事务,通过比对数据库的状态与操作日志,触发补偿,达到最终一致性。

MySQL Group Replication基于组复制的概念,并参考了MariaDB Galera Cluster和Percona XtraDB Cluster的设计。它允许数据库服务器组织成一个组,在组内的所有成员之间进行数据复制和同步,以确保数据的一致性和可用性。

  • 多主复制:
    • MGR支持多主复制模式,即允许多个节点同时处理读写请求,从而提高系统的吞吐量和可靠性。
    • 当某个成员执行写操作时,该操作会被记录并复制到组内的其他所有成员。
  • 事务一致性:
    • 事务的提交必须经过半数以上节点同意方可提交,以确保数据的一致性。
    • MGR使用Paxos算法(通过XCom基础设施实现)来确保数据库状态机在节点间的事务一致性。
  • 故障检测与自动故障转移:
    • MGR具有故障检测机制,当某个成员出现故障时,系统会自动调整组的成员关系。
    • 当某个节点发生故障时,Group Replication会自动重新配置集群,确保服务的连续性

6. 布隆过滤器说一下