【3.7】Redis数据类型、CPU缓存一致性、哈希表

news2025/7/21 0:53:30

文章目录

    • 数据类型篇
      • String
      • List
      • Hash
      • Set
      • Zset
      • BitMap
      • HyperLogLog
      • GEO
      • Stream
    • CPU 缓存一致性
    • CPU是如何执行任务的?
    • 什么是软中断?
    • 为什么0.1 + 0.2不等于0.3?
  • 哈希表

数据类型篇

String

  • String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M

  • 内部实现

    String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。

  • SDS 相比于 C 的原生字符串:

    • SDS 是二进制安全的。不会像C字符串碰到结束字符就停止读取。

    • SDS 获取字符串长度的时间复杂度是 O(1)。SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)

    • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

  • 字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr

    • 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int
    • 如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr
    • 如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw
    • embstr和raw
      • embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次;
      • 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数;
      • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。
      • 缺点:embstr编码的字符串对象是只读的如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。
  • 应用场景

    缓存对象:使用SET user:1 '{"name":"xiaolin", "age":18}'

    常规计数:SET aritcle:readcount:1001 0之后INCR aritcle:readcount:1001

    分布式锁:SET lock_key unique_value NX PX 10000

    • lock_key 就是 key 键;
    • unique_value 是客户端生成的唯一的标识;
    • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
    • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
    • 解锁过程有两步操作,检查是否为加锁客户端,之后解锁(将lock_key删除)。要使用Lua脚本。

    共享session信息:使用session保存用户状态,使得同一个用户即使访问不同服务器都是同一个session,不需要重复登录。

List

  • List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。

  • 内部实现

    List 类型的底层数据结构是由双向链表或压缩列表实现的:

    • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
    • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

    但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表

  • 应用场景

    消息队列:消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性

    1. 消息保序:List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列。

      Redis提供了 BRPOP 命令,替换RPOP命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。和消费者程序自己不停地调用RPOP命令相比,这种方式能节省CPU开销。

    2. 重复消息处理: List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一ID,生成之后,我们在用 LPUSH 命令把消息插入 List 时,需要在消息中包含这个全局唯一 ID。

      消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。

    3. 保证消息可靠性:为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存

    总结:

    • 消息保序:使用 LPUSH + RPOP;
    • 阻塞读取:使用 BRPOP;
    • 重复消息处理:生产者自行实现全局唯一 ID;
    • 消息的可靠性:使用 BRPOPLPUSH

    List作为消息队列的缺点:

    List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。

    要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现。Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。

Hash

Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},...{fieldN,valueN}]。Hash 特别适合用于存储对象。

  • 内部实现

    Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

    • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
    • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

    在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

  • 应用场景

    缓存对象:一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。比如购物车。

Set

Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。

一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。

  • Set 类型和 List 类型的区别如下:

    • 存储元素:List 可以存储重复元素,Set 只能存储非重复元素;

    • 存储顺序:List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。

  • 内部实现

    Set 类型的底层数据结构是由哈希表或整数集合实现的:

    • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
    • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
  • 应用场景

    Set 类型比较适合用来数据去重和保障数据的唯一性(不可重复性),还可以用来统计多个集合的交集、错集和并集(支持并交差集性质)等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。

    Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞

    在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计

    • 点赞:保证用户只点一个赞SADD article:1 uid:1添加唯一元素。
    • 共同关注:Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。SINTER uid:1 uid:2获取交集。
    • 抽奖:存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。

Zset

Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。

  • 内部实现

    Zset 类型的底层数据结构是由压缩列表或跳表实现的:

    • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
    • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

    在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

  • 应用场景

    • 电话、姓名排序:

    • 排行榜:利用有序集合的特性ZADD user:xiaolin:ranking 200 arcticle:1

    • 电话、姓名排序:

      ZRANGEBYLEX 返回指定成员区间内的成员,按成员字典正序排序, 分数必须相同。

      ZREVRANGEBYLEX 返回指定成员区间内的成员,按成员字典倒序排序, 分数必须相同。(不要在分数不一样的SortedSet集合中使用)

    • ZSet实现消息队列

      • Redis ZADD命令将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

      • Redis ZRANGEBYSCORE 命令返回有序集合 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)顺序排列。

        语法:

        ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
        

BitMap

Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景

  • 内部实现

    Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

    String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

  • 应用场景

    • 签到统计:用0和1表示签到和未签到,就是一个典型的二值状态。签到统计时,每个用户一天的签到用1个bit就可以表示。SET uid:sign:100:202206 3 1:记录该用户2022 6 月 3 号已签到。
    • 判断用户登录状态:SETBIT login_status 10086 1通过一个偏移值 offset 对 bit 数组的 offset 位置的 bit 位进行读写操作。表示ID = 10086的用户 已登录。
    • 连续签到用户总数:假设要统计 3 天连续打卡的用户数,则是将三个 bitmap 进行 AND 操作,并将结果保存到 destmap 中,接着对 destmap 执行 BITCOUNT 统计,三天的key进行“AND”操作。BITOP AND destmap bitmap:01 bitmap:02 bitmap:03 统计bit位 = 1的个数:BITCOUNT destmap

HyperLogLog

HyperLogLog 提供不精确的去重计数。HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。

  • 应用场景
    • 百万级网页UV计数:PFADD page1:uv user1 user2 user3 user4 user5:把访问页面的每个用户添加到HyperLogLog中。PFCOUNT page1:uv,获取page1的uv值,返回统计结果。

GEO

Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。

  • 内部实现

    GEO直接使用了 Sorted Set 集合类型。GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

    这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。

  • 应用场景

    • 滴滴叫车:把 ID 号为 33 的车辆的当前经纬度位置存入 GEO 集合中:GEOADD cars:locations 116.034579 39.030452 33。当用户寻找自己附近的网约车时,GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10,根据输入的用户的经纬度信息(116.054579,39.030452 ),查找以这个经纬度为中心的 5 公里内的车辆信息,并返回给 LBS 应用。

Stream

Redis 专门为消息队列设计的数据类型。Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

  • 应用场景:消息队列

    1. 生产者通过 XADD 命令插入一条消息:
    XADD mymq * name xiaolin
    "1654254953808-0"
    

    插入成功后会返回全局唯一的 ID:“1654254953808-0”。消息的全局唯一 ID 由两部分组成:

    第一部分“1654254953808”是数据插入时,以毫秒为单位计算的当前服务器时间;

    第二部分表示插入消息在当前毫秒内的消息序号,这是从 0 开始编号的。例如,“1654254953808-0”就表示在“1654254953808”毫秒内的第 1 条消息。

    1. 消费者通过 XREAD 命令从消息队列中读取消息时,可以指定一个消息 ID,并从这个消息 ID 的下一条消息开始进行读取(注意是输入消息 ID 的下一条信息开始读取,不是查询输入ID的消息)。
# 从 ID 号为 1654254953807-0 的消息开始,读取后续的所有消息(示例中一共 1 条)。
> XREAD STREAMS mymq 1654254953807-0
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"
  • 消费组

    • Stream 可以使用 XGROUP 创建消费组,创建消费组之后,Stream 可以使用 XREADGROUP 命令让消费组内的消费者读取消息。

    • 同一个消费组里的消费者不能消费同一条消息。而不同消费组的消费者可以消费同一条消息(但是有前提条件,创建消息组的时候,不同消费组指定了相同位置开始读取消息)

    • 使用消费组的目的是让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的。

  • 内部队列

    • Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。

    • 消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成

  • Redis 基于 Stream 消息队列与专业的消息队列有哪些差距?

    • Redis消息中间件会丢失消息:

      • AOF 持久化配置为每秒写盘,但这个写盘过程是异步的,Redis 宕机时会存在数据丢失的可能

      • 主从复制也是异步的,主从切换时,也存在丢失数据的可能 。

    • Redis Stream 消息积压风险

      Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险。

  • 能不能将 Redis 作为消息队列来使用,关键看你的业务场景:

    • 如果你的业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把 Redis 当作队列是完全可以的。

    • 如果你的业务有海量消息,消息积压的概率比较大,并且不能接受数据丢失,那么还是用专业的消息队列中间件吧。

  • Redis发布/订阅机制为什么不可以做消息队列?

    • 发布/订阅机制没有基于任何数据类型实现,所以不具备「数据持久化」的能力。
    • 发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。发布/订阅机制只适合即时通讯的场景,比如构建哨兵集群的场景采用了发布/订阅机制。

CPU 缓存一致性

随着时间推移,CPU和内存的访问速度越差越大,于是引入了CPU Cache ,CPU Cache离CPU核心相当近,因此访问速度很快,充当CPU与内存之间的缓存角色。CPU Cache通常分为三级缓存 : L1 Cache 、L2 Cache、L3 Cache。其中L1 与L2级缓存在单独的每个CPU核心中,而L3缓存由多个核心共享。L1 缓存也分为L1数据缓存和L1指令缓存。

  • 事实上,数据不光是只读操作,还有写回操作。当数据写入Cache, 内存与Cache中的数据将会不同,这种情况下Cache和内存数据都不一致,所以要把Cache 中的数据同步到内存里。下面以a=5修改为a=3为例:

  • Cache中的数据写回内存有两种方法:

    • 写直达(Write Through)

      将数据同时写入Cache和内存中。写入之前,会先判断数据是否存在Cache中

      如果数据没有在Cache中,则直接写到内存,否则先更新Cache中的数据,之后再写入到内存中。

    • 写回(Write Back)

      写直达中,CPU每次执行写操作都会将数据写回到内存,影响性能,于是出现了写回操作。

      写回机制中,当发生写操作时,新的数据仅仅被写入Cache Block 中,并且Cache Block被标记为脏,只有脏的Cache Block被替换时,才需要将新的数据写入到内存中。具体执行过程如下:

      • 当发生写操作时,如果数据已经在CPU Cache中,则把数据更新到CPU Cache中,并且标记CPU Cache中的Cache Block为脏。(代表Cache Block 的数据和内存不一致,但是不把数据写入到内存)
      • 如果数据对应的Cache Block 中存放的是别的内存地址的数据,要先检查Cache Block 中的数据有没有被标记为脏的:
        • 如果是脏数据,则先把Cache Block 中的数据写回到内存,然后从内存读取数据到Cache Block中(先读取到a=5,再修改),再把当前数据写入到Cache Block,标记为脏。
        • 如果不是脏数据,则从内存读取数据到Cache Block中,接着将数据写入到Cache Block中,并标记为脏。
    • 写回的好处是:如果我们大量的操作都可以命中缓存,那么就不需要多次去从内存中读写数据,从而浪费时间。

    • 当数据对应的Cache Block 中存放的是别的内存地址的数据,为什么要检查Cache Block中是否有脏数据?

      • 因为即使缓存没有命中,但是此时存在脏数据,我们要帮助其他数据写回到内存。
      img
  • 缓存一致性问题

    由于L1/L2 Cache是多个核心独有的,所以多核心会产生缓存一致性问题。

  • 举例:假设 A 号核心和 B 号核心同时运行两个线程,都操作共同的变量 i(初始值为 0 )。

    • 这时如果 A 号核心执行了 i++ 语句的时候,为了考虑性能,使用了我们前面所说的写回策略,先把值为 1 的执行结果写入到 L1/L2 Cache 中,然后把 L1/L2 Cache 中对应的 Block 标记为脏的,这个时候数据其实没有被同步到内存中的,因为写回策略,只有在 A 号核心中的这个 Cache Block 要被替换的时候,数据才会写入到内存里。

    • 如果这时旁边的 B 号核心尝试从内存读取 i 变量的值,则读到的将会是错误的值,因为刚才 A 号核心更新 i 值还没写入到内存中,内存中的值还依然是 0。这个就是所谓的缓存一致性问题,A 号核心和 B 号核心的缓存,在这个时候是不一致,从而会导致执行结果的错误。

  • 解决缓存不一致的问题

    • 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(*Write Propagation*)
      • 写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常见实现的方式是总线嗅探(*Bus Snooping*)。也就是CPU时刻监听总线上的一切活动,不管别的核心的Cache是否缓存了相同的数据,但是会加重总线的负载。
    • 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串行化(*Transaction Serialization*)
      • 总线嗅探不能保证事务的串行化,使用MESI协议,基于总线嗅探机制实现了事务串行化,CPU缓存一致性。
  • MESI协议

    • Modified,已修改
      • 已修改状态就是脏标记,代表该 Cache Block 上的数据已经被更新过,但是还没有写到内存里。
    • Exclusive,独占
    • Shared,共享
    • Invalidated,已失效
      • 已失效状态表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据。

    这四个状态来标记 Cache Line 四个不同的状态。

    • 「独占」和「共享」状态都代表 Cache Block 里的数据是干净的,也就是说,这个时候 Cache Block 里的数据和内存里面的数据是一致性的。

    • 「独占」和「共享」的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要通知其他 CPU 核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。

    • 另外,在「独占」状态下的数据,如果有其他核心从内存读取了相同的数据到各自的 Cache ,那么这个时候,独占状态下的数据就会变成共享状态。

    • 那么,「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有,所以当我们要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据。

  • MESI 协议,是已修改、独占、共享、已失效这四个状态的英文缩写的组合。整个 MESI 状态的变更,**则是根据来自本地 CPU 核心的请求,或者来自其他 CPU 核心通过总线传输过来的请求,从而构成一个流动的状态机。**另外,对于在「已修改」或者「独占」状态的 Cache Line,修改更新其数据不需要发送广播给其他 CPU 核心,这在一定程度上减少了总线带宽压力。

  • 举例理解四个状态的流转:

    1. 当 A 号 CPU 核心从内存读取变量 i 的值,数据被缓存在 A 号 CPU 核心自己的 Cache 里面,此时其他 CPU 核心的 Cache 没有缓存该数据,于是标记 Cache Line 状态为「独占」,此时其 Cache 中的数据与内存是一致的;
    2. 然后 B 号 CPU 核心也从内存读取了变量 i 的值,此时会发送消息给其他 CPU 核心,由于 A 号 CPU 核心已经缓存了该数据,所以会把数据返回给 B 号 CPU 核心。在这个时候, A 和 B 核心缓存了相同的数据,Cache Line 的状态就会变成「共享」,并且其 Cache 中的数据与内存也是一致的;
    3. 当 A 号 CPU 核心要修改 Cache 中 i 变量的值,发现数据对应的 Cache Line 的状态是共享状态,则要向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据,同时标记 Cache Line 为「已修改」状态,此时 Cache 中的数据就与内存不一致了。
    4. 如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值,由于此时的 Cache Line 是「已修改」状态,因此不需要给其他 CPU 核心发送消息,直接更新数据即可。
    5. 如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」,发现 Cache Line 状态是「已修改」状态,就会在替换前先把数据同步到内存。

CPU是如何执行任务的?

  1. CPU 内部的多个 Cache + 外部的内存和磁盘都就构成了金字塔的存储器结构,在这个金字塔中,越往下,存储器的容量就越大,但访问速度就会小。

  2. CPU 读写数据的时候,并不是按一个一个字节为单位来进行读写,而是以 CPU Cache Line 大小为单位,CPU Cache Line 大小一般是 64 个字节,也就意味着 CPU 读写数据的时候,每一次都是以 64 字节大小为一块进行操作。

  3. 因此,如果我们操作的数据是数组,那么访问数组元素的时候,按内存分布的地址顺序进行访问,这样能充分利用到 Cache,程序的性能得到提升。但如果操作的数据不是数组,而是普通的变量,并在多核 CPU 的情况下,我们还需要避免 Cache Line 伪共享的问题。

  4. 所谓的 Cache Line 伪共享问题就是,多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象。那么对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个 Cache Line 中,避免的方式一般有 Cache Line 大小字节对齐,以及字节填充等方法。

  • Cache 伪共享

    多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing

    • 两个线程共享同一Cache Line的数据,由于MESI协议,当A线程在Cache Line修改变量A时,B线程中的Cache Line就是失效状态。此时线程B修改变量B,A线程变为了失效状态。
  • 避免伪共享

      1. 在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题。可以将变量的内存地址设置为Cache Line对齐地址,这样两个变量就不在同一个Cache Line中了。所以,避免 Cache 伪共享实际上是用空间换时间的思想,浪费一部分 Cache 空间,从而换来性能的提升。

        SMP:Symmetric Multi-Processing,对称多处理结构imgimg

      1. 应用层面也存在规避方案:有一个 Java 并发框架 Disruptor 使用「字节填充 + 继承」的方式,来避免伪共享的问题。Disruptor 中有一个 RingBuffer 类会经常被多个线程使用。
      • CPU Cache 从内存读取数据的单位是 CPU Cache Line,一般 64 位 CPU 的Cache Line 的大小是 64 个字节,一个 long 类型的数据是 8 个字节,所以 CPU 一下会加载 8 个 long 类型的数据。

      • RingBufferPad 中的 7 个 long 类型数据作为 Cache Line 前置填充,而 RingBuffer 中的 7 个 long 类型数据则作为 Cache Line 后置填充,这 14 个 long 变量没有任何实际用途,更不会对它们进行读写操作。

      • 另外,RingBufferFelds 里面定义的这些变量都是 final 修饰的,意味着第一次加载之后不会再修改, 又由于「前后」各填充了 7 个不会被读写的 long 类型变量,所以无论怎么加载 Cache Line,这整个 Cache Line 里都没有会发生更新操作的数据,于是只要数据被频繁地读取访问,就自然没有数据被换出 Cache 的可能,也因此不会产生伪共享的问题

  • CPU如何选择线程?

    • 在 Linux 内核中,进程和线程都是用 task_struct 结构体表示的,区别在于线程的 task_struct 结构体里部分资源是共享了进程已创建的资源,比如内存地址空间、代码段、文件描述符等,所以 Linux 中的线程也被称为轻量级进程,因为线程的 task_struct 相比进程的 task_struct 承载的 资源比较少,因此以「轻」得名。

    • 一般来说,没有创建线程的进程,是只有单个执行流,它被称为是主线程。如果想让进程处理更多的事情,可以创建多个线程分别去处理,但不管怎么样,它们对应到内核里都是 task_struct。所以,Linux 内核里的调度器,调度的对象就是 task_struct,接下来我们就把这个数据结构统称为任务

    • 在 Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:

      • 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在 0~99 范围内的就算实时任务;

      • 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内都是普通任务级别

        • 对于普通任务来说,公平性最重要,在 Linux 里面,实现了一个基于 CFS 的调度算法,也就是完全公平调度(*Completely Fair Scheduling*)
  • 完全公平调度

    • 这个算法的理念是想让分配给每个任务的 CPU 时间是一样,于是它为每个任务安排一个虚拟运行时间 vruntime,如果一个任务在运行,其运行的越久,该任务的 vruntime 自然就会越大,而没有被运行的任务,vruntime 是不会变化的。那么,在 CFS 算法调度的时候,会优先选择 vruntime 少的任务,以保证每个任务的公平性。
  • 系统中需要运行的多线程数一般都会大于 CPU 核心,这样就会导致线程排队等待 CPU,这可能会产生一定的延时,如果我们的任务对延时容忍度很低,则可以通过一些人为手段干预 Linux 的默认调度策略和优先级。

什么是软中断?

在计算机中,中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。

操作系统收到了中断请求,会打断其他进程的运行,所以中断请求的响应程序,也就是中断处理程序,要尽可能快的执行完,这样可以减少对正常进程运行调度地影响。

而且,中断处理程序在响应中断时,可能还会「临时关闭中断」,这意味着,如果当前中断处理程序没有执行完之前,系统中其他的中断请求都无法被响应,也就说中断有可能会丢失,所以中断处理程序要短且快。

那 Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是「上半部和下半部分」

  • 上半部用来快速处理中断,也就是硬中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。

    • 比如先禁止网卡中断,避免频繁硬中断,而降低内核的工作效率
  • 下半部用来延迟处理上半部未完成的工作,也就是软中断,一般以「内核线程」的方式运行。

    • 比如需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。

Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,可以通过查看 /proc/softirqs 来观察软中断的累计中断次数情况,如果要实时查看中断次数的变化率,可以使用 watch -d cat /proc/softirqs 命令。

每一个 CPU 都有各自的软中断内核线程,我们还可以用 ps 命令来查看内核线程,一般名字在中括号里面的,都认为是内核线程。

如果在 top 命令发现,CPU 在软中断上的使用率比较高,而且 CPU 使用率最高的进程也是软中断 ksoftirqd 的时候,这种一般可以认为系统的开销被软中断占据了。

这时我们就可以分析是哪种软中断类型导致的,一般来说都是因为网络接收软中断导致的,如果是的话,可以用 sar 命令查看是哪个网卡的有大量的网络包接收,再用 tcpdump 抓网络包,做进一步分析该网络包的源头是不是非法地址,如果是就需要考虑防火墙增加规则,如果不是,则考虑硬件升级等。

为什么0.1 + 0.2不等于0.3?

  • 为什么负数使用补码表示?

    • 十进制转二进制使用除商逆序取余法。整数在计算机中的存储就是十进制转为二进制。比如:int 类型是 32 位的,其中最高位是作为「符号标志位」,正数的符号位是 0,负数的符号位是 1剩余的 31 位则表示二进制数据

    • 负数在计算机中使用补码表示,所谓补码,就是把对应的正数的二进制位全部取反再 + 1。为什么负数要这么表示?如果不这么表示,计算机在进行二进制加法时就不能使用常规的加法,需要特殊地处理,还要多一步操作,来判断是否为负数。

    • 而用了补码的表示方式,对于负数的加减法操作,实际上是和正数加减法操作一样的

  • 对于二进制小数转换为十进制,小数点后面的幂指数是负数。

  • 十进制小数与二进制的转换

    使用乘二取整法,整数部分使用除商逆序取余计算,将十进制中的小数部分乘以2作为二进制的一位。但是这样就出现了一些问题,比如0.1 的二进制表示是无限循环的。

    img

由于计算机的资源是有限的,所以在表示0.1时,只能使用近似值来表示,于是就会出现精度缺失的情况。

  • 计算机是怎么存小数的?

    计算机是以浮点数的形式存储小数的,大多数计算机都是 IEEE 754 标准定义的浮点数格式,包含三个部分:比如 1000.101 这个二进制数,可以表示成 1.000101 x 2^3,类似于数学上的科学记数法。

    • 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
    • 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大;
    • 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011,而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度;

    用 32 位来表示的浮点数,则称为单精度浮点数,也就是我们编程语言中的 float 变量,而用 64 位来表示的浮点数,称为双精度浮点数,也就是 double 变量。

  • 0.1 + 0.2 == 0.3 ?

    对计算机而言,0.1无法精确表达,这是浮点数计算造成损失的根源。因此,IEEE 754标准定义的浮点数只能根据精度舍入,然后用近似值来表示二进制。所以两个近似值相加,得到的只能是近似值。

哈希表

题目关键点
242. 有效的字母异位词 - 力扣(LeetCode)数组代替哈希表
349. 两个数组的交集 - 力扣(LeetCode)利用HashSet特点
202. 快乐数 - 力扣(LeetCode)解读题目、HashSet
1. 两数之和 - 力扣(LeetCode)HashMap减少一层for循环
  • 什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。

  • 242. 有效的字母异位词 - 力扣(LeetCode)

    哈希表做法:

    class Solution {
        public boolean isAnagram(String s, String t) {
            Map <Character , Integer> map = new HashMap<>();
            if(s.length() != t.length()) return false;
            for(int i = 0 ; i < s.length() ; i ++){
                map.put(s.charAt(i) ,map.getOrDefault(s.charAt(i) , 0) + 1);
            }
            for(int i = 0 ; i < t.length(); i ++){
                if(map.containsKey(t.charAt(i))){
                    map.put(t.charAt(i) , map.get(t.charAt(i)) - 1);
                }
            }
            for(int i = 0 ; i < s.length() ; i++){
                if(map.get(s.charAt(i)) != 0){
                    return false;
                }
            }
            return true;
        }
    }
    

    数组就是一个简单的hash表,下标是key,数组内的值是value。用数组代替:

    class Solution {
        public boolean isAnagram(String s, String t) {
            int [] map = new int [26];
            for(int i = 0 ; i < s.length() ; i ++){
                map[s.charAt(i) - 'a'] ++;
            }
            for(int i = 0 ; i < t.length(); i ++){
                map[t.charAt(i) - 'a'] --;
            }
            for(int i = 0 ; i < map.length ; i++){
                if(map[i] != 0){
                    return false;
                }
            }
            return true;
        }
    }
    
  • 349. 两个数组的交集 - 力扣(LeetCode)

    使用HashSet自动去重。

    class Solution {
        public int[] intersection(int[] nums1, int[] nums2) {
            Set <Integer> set = new HashSet<>();
            Set <Integer> result = new HashSet<>();
            for(int i : nums1){
                set.add(i);
            }
            for(int i : nums2){
                if(set.contains(i)){
                    result.add(i);
                }
            }
    
            int[] arr = new int[result.size()];
            int j = 0;
            for(int i : result){
                arr[j++] = i;
            }
            
            return arr;
        }
    }
    
  • 202. 快乐数 - 力扣(LeetCode)

    如果是快乐数,就不会出现无限循环的数字,所以只需要比较set中是否有重复出现的数字即可。

    class Solution {
        public boolean isHappy(int n) {
            
            Set <Integer> set = new HashSet<>();
            while(n != 1 && !set.contains(n)){
                set.add(n);
                n = nextSum(n);
            }
            return n == 1;
        }
        int nextSum(int n){
            int ans = 0;
            while(n > 0){
                int ret = n % 10;
                ans += ret * ret;
                n /= 10;
            }
            return ans;
        }
    }
    
  • 1. 两数之和 - 力扣(LeetCode)

    使用数组,会造成内存空间浪费,使用Set,无法获取数字下标。

    所以使用哈希表,哈希表可以减少一层for循环,将遍历过的元素放入哈希表,避免 j 重复扫描元素。

    过程一:过程一

    过程二:过程二

    class Solution {
        public int[] twoSum(int[] nums, int target) {
            int [] result = new int [2];
            Map <Integer,Integer> map =new HashMap <>();
            for(int i = 0 ; i < nums.length ; i++){
                if(map.containsKey(target - nums[i])){
                    result[1] = i;
                    result[0] = map.get(target - nums[i]);
                    break;
                }
                map.put(nums[i] , i);
            }
            return result;
        }
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/395294.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

03 | 授权服务:授权码和访问令牌的颁发流程是怎样的? 笔记

03 | 授权服务&#xff1a;授权码和访问令牌的颁发流程是怎样的&#xff1f; 授权服务的工作过程 小兔软件需要去到京东的平台那里”备案“注册&#xff0c;京东商家开放平台就会给小兔软件 app_id 和 app_secret 等信息&#xff0c;以方便后面授权时的各种身份校验&#xff0…

scratch绘制雷达 电子学会图形化编程scratch等级考试三级真题和答案解析2022年9月

目录 scratch绘制雷达 一、题目要求 1、准备工作 2、功能实现 二、案例分析

阶段二12_面向对象高级_继承1

一.继承的入门介绍 (1)继承的概念理解 让类与类之间产生关系&#xff08;子父类关系&#xff09;&#xff0c;子类可以直接使用父类中非私有的成员 (2)通过extends关键字实现继承 格式&#xff1a;public class 子类名 extends 父类名 { } 范例&#xff1a;public class Zi e…

Grafana 如何使用本地CSV文件作为数据源

Grafana提供了一个插件&#xff0c;可以把CSV文件作为数据源&#xff0c;关于CSV插件的说明&#xff0c;可以参考&#xff1a;https://grafana.com/grafana/plugins/marcusolsson-csv-datasource/?tabinstallation。我是在本地使用命令行grafana-cli plugins install marcusol…

通过45人!1-2月,誉天红帽RHCE学员再创佳绩!

学习的喜悦在于结果&#xff0c;也在于过程&#xff1b;在于取得成功时的豁然开朗&#xff0c;也在于持之以恒后的层层递进。结果固然重要&#xff0c;但在求知过程中获得的满足感&#xff0c;也同样让人乐在其中。 RHCE的学习过程就充满了这样的喜悦。对每一行命令的理解、对每…

【Linux学习】日积月累——调试器gdb的使用教程

一、背景 gdb是一款强大的命令行调试工具&#xff0c;可以形成执行程序、脚本。只需要几个简单的命令&#xff0c;就能够实现Windows环境下VC等IDE的图形化调式工具的功能。 调试的相关常识&#xff1a; 程序的发布方式有两种&#xff0c;debug模式和release模式&#xff1b;L…

197.Spark(四):Spark 案例实操,MVC方式代码编程

一、Spark 案例实操 1.数据准备 电商网站的用户行为数据,主要包含用户的 4 种行为:搜索,点击,下单,支付 样例类: 2. Top10 热门品类 先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。 我们有多种写法,越往后性能越…

【Linux开发笔记】《Linux嵌入式开发从0到1》(一):初探Linux——与Linux的初次相遇

1.什么是Linux Linux就是一个操作系统&#xff0c;就是一个开源、自由的操作系统&#xff0c;就是一个免费使用和自由传播的类UNIX操作系统&#xff0c;就是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。 简单来讲&#xff0c;Linux就是一个操作系统而已… …

React的Hooks

React Hooks useState useMemo 和usecallback Hooks显示的指明因变量有什么好处 当使用时&#xff0c;y与changeX会被缓存下来&#xff0c;只要x不变&#xff0c;始终读取的是缓存的值&#xff0c; 如果不使用时&#xff0c;每次函数组件执行时&#xff0c;实际会基于x&#xf…

计算机写论文时,怎么引用文献? - 易智编译EaseEditing

首先需要清楚哪些引用必须注明[1]&#xff1a; 任何直接引用都要用引号并注明来源&#xff1b; 任何不是自己的口头或书面的观点、解释和结论都应注明来源&#xff1b; 即使不用原话&#xff0c;但是他人的思路、概念或观点也应注明&#xff1b; 不要为了适合你的观点修改原…

机器学习——无监督学习

机器学习的分类一般分为下面几种类别&#xff1a;监督学习( supervised Learning )无监督学习( Unsupervised Learning )强化学习( Reinforcement Learning&#xff0c;增强学习)半监督学习( Semi-supervised Learning )深度学习(Deep Learning)Python Scikit-learn. http: // …

day40|198.打家劫舍、213.打家劫舍II、337.打家劫舍III

198.打家劫舍 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定一个代表每个…

软件测试8

一 缺陷 软件缺陷&#xff1a;是指软件或程序中存在的各种问题及错误&#xff0c;会导致软件产品在某种程度上不能满足用户的需求 二 软件缺陷的判定标准 1.软件未达到需求规格说明书中表明的功能 2.软件出现了需求规格说明书不会出现错误的地方 3.软件的功能超出了需求规格…

14 nuxt3学习(布局 渲染模式 插件plugin 生命周期)

布局 布局是围绕包含多个页面的公共用户界面的页面的包装器&#xff0c;例如页眉和页脚显示。 布局是使用slot 组件显示页面内容的Vue文件。 默认情况下使用layouts/default.vue文件。 自定义布局可以设置为页面元数据的一部分。 方式一&#xff1a;默认布局 在layouts目录下…

Xmind快捷键大全

Xmind快捷键大全 1、常用 CtrlShiftL 快捷键助手CtrlHome 返回中心主题Enter 插入主题Tab 插入子主题F2 编辑主题F3 添加/编辑标签F4 添加/编辑备注F6 下钻ShiftF6 上钻Delete 删除Ctrl] 插入摘要CtrlI 插入图片CtrlShiftH 插入超链接Ctrl1,2,3,4,5,6快速添加优先等级图标Ctr…

applicationContext相关加载

spring refresh 概述 refresh是一个方法&#xff0c;spring中所有的ApplicationContext容器都需要通过refresh方法初始化&#xff1b; 处理步骤 其中refresh方法包含12个主要的处理步骤&#xff1a; 1、第1个步骤做前置准备 2、第2~6步骤创建BeanFactory&#xff08;Appl…

Java中垃圾回收(GC)算法详解

咱们要进行垃圾回收&#xff0c;是不是要知道哪些对象是垃圾&#xff0c;然后针对这些垃圾要怎么回收呢&#xff1f;那本篇文章我们就将垃圾回收分为标记垃圾、清除垃圾两个阶段讲解&#xff0c;详细说明每个阶段都有那些算法。1、标记阶段算法在堆里存放着几乎所有的Java对象实…

2023年交通与智慧城市国际会议(ICoTSC 2023)

2023年交通与智慧城市国际会议(ICoTSC 2023) 重要信息 会议网址&#xff1a;www.icotsc.org 会议时间&#xff1a;2023年7月28-30日 召开地点&#xff1a;长沙 截稿时间&#xff1a;2023年6月15日 录用通知&#xff1a;投稿后2周内 收录检索&#xff1a;EI,Scopus 会议简介…

轻松玩转开源大语言模型bloom(一)

前言 chatgpt已经成为了当下热门&#xff0c;github首页的trending排行榜上天天都有它的相关项目&#xff0c;但背后隐藏的却是openai公司提供的api收费服务。作为一名开源爱好者&#xff0c;我非常不喜欢知识付费或者服务收费的理念&#xff0c;所以便有决心写下此系列&#…

【Flutter】入门Dart语言:操作符的基本用法

文章目录 一、前言二、常用的操作符1.算术操作符2.关系操作符3.逻辑操作符4.赋值操作符5.三元运算符三、总结一、前言 当我们在编写Flutter应用程序时,操作符是我们不可或缺的工具。它们是用于执行各种操作的关键字和符号,可以帮助我们简化代码并提高效率。熟练掌握各种类型…