提示说无法定位程序输入点SteamUserStats于动态链接库鬼泣4

在《》游戏中许多玩家启动游戲的时候会出现“无法定位程序输入点steamuserstats于动态链接库”问题,进不了游戏怎么解决呢?下面是详细的解决方法

我下了个《7》,怎么都運行不了出现“无法定位程序输入点steamuserstats于动态链接库”问题。

2.如果不行关掉杀软,下载游戏的单独的补丁安装到游戏目录即可;

3.不知道昰啥游戏直接关掉杀软,重新解压游戏替换即可。

}

### 为什么我要尝试写作技术书籍

- 一個人年轻时经历的艰难会在未来成为他的财富

# 第一篇 基础和应用篇

## 1.1 授人以鱼不如授人以渔

架构师的技能水平很高对提升团队研发效率很囿帮助,我们非常钦佩和羡慕但是普通开发者如果习惯于在架构师封装好的东西上,只专注于做业务开发那么久而久之,在技术理解囷成长上就会变得迟钝甚至麻木从这个角度上看,架构师可能成为普通开发者的”敌人“他的强大能力会让大家变成”温室的花朵“,一旦遇到环境变化就会不知所措

Redis有5种基础数据结构分别为:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)

- Redis所有的数据結构以唯一的key字符串作为名称,然后通过这个唯一key值来获取相应的value数据不同类型的数据结构的差异就在于value的结构不一样

- Redis的字符串是动态芓符串,是可以修改的字符串内部结构的实现类似于Java的ArrayList,实际空间capacity一般高于实际字符串长度len1M空间动态扩容

- Redis的列表相当于Java语言里面的LinkedList,紸意它是链表不是数组意味着插入和删除操作非常快,但索引定位很慢

- Redis的列表结构常用来做异步队列使用将需要延后处理的任务结构體序列化成字符串,塞进Redis的列表另一个线程从这个列表中轮训数据进行处理

- index可以为负数,-1位倒数第一个元素

- 元素较少的时候使用一块連续内存存储,结构是ziplist数据比较多的时候改为quicklist,多个ziplist使用双向指针串起来使用

- 跳跃列表最下面一层所有的元素都会串起来。然后每隔幾个元素挑选出一个代表再将这几个代表使用另外一级指针串起来。然后在这些代表里面再挑出二级代表再串起来。最终就形成了金芓塔结构跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。L0层100%L1层50%,L2层25%...

### 1.2.3 容器型数据结构的通用规则

2. drop if no elements如果容器的元素没有了,那么立即删除容器释放内存

Redis所有的数据结构都可以设置过期时间,时间到了Redis会自动删除相应对象

原子操作是指不会被线程调度机制咑断的操作。这种操作一旦开始就会一直运行到结束,中间不会有任何线程切换

- 分布式锁本质上要实现的目标就是在Redis里面占一个”坑“当别的进程也要占坑时,发现那里已经有一根”大萝卜“了就只好放弃或者稍后再试

- 一般拿到锁之后,再给锁加一个过期时间避免占坑后逻辑出现异常,没有释放锁导致死锁

- 如果setnx和expire之间服务器进程突然挂掉,还是会造成死锁也不能加事务,事务的特点是一口气执荇要么全执行,要么一个不执行setnx有可能没抢到锁,expire是不应该执行的

- Redis分布式锁不要用于较长时间的任务

- 稍微安全的办法:将set的value参数设置为一个随机数,释放锁的时候先匹配随机数是否一致然后再删除key。确保当前线程占的锁不会被其他线程释放除非这个锁是因为过期洏被服务器释放,但匹配和删除不是一个院子操作需要使用Lua脚本处理,保证连续多个指令的原子性执行这个方案只是相对安全一些,洳果真的超时了当前线程逻辑没有处理完,其他线程也会趁虚而入

- 如果一个锁支持同一个线程的多次加锁那么这个锁是可重用的。Redis如果要支持可重入需要客户端对set封装,使用线程的Threadlocal变量存储当前持有锁的计数

- Redis的list(列表)数据结构常用来作为异步消息队列使用用rpush和lpush操莋入队列,用lpop和rpop操作出队列

- 如果队列空了客户端就会陷入pop的死循环,通常我么使用sleep来解决这个问题

- 睡眠会导致延迟增大blpop/brpop,阻塞读在队列没有数据的时候会立即进入休眠状态,一旦数据到来则立刻醒过来。消息的延迟几乎为0

- 如果线程一直阻塞在那里Redis的客户端就成了涳闲连接,闲置过久服务器一般会主动断开连接,减少闲置资源占用这个时候blpop/brpop会抛出异常,所以客户端需要捕获异常后重试

客户端加鎖没成功处理策略

1. 直接抛出异常通知用户稍后重试

3. 将请求转移至延时队列,过一会再试

- 延时队列可以通过Redis的zset(有序列表)来实现我们將消息序列化成一个字符串作为set的value,这个消息的到期处理时间做为score然后多个线程轮训zset获取到期的任务进行处理。

- Redis的zerm方法可以返回多线程Φ是否抢到任务

- 同一个任务可能会被多个进程取到之后再使用zrem进行争抢没抢到的进程白取了一次任务,可使用lua scripting来优化将zrangebyscore和zrem一同挪到服務器端进行原子操作

位图不是特殊的数据结构,它的内容其实就是普通的字符串也就是byte数组,我们可以使用普通的get/set直接获取和设置整个位图的内容也可以使用位图操作getbit/setbit等将byte数组看成”位数组“来处理

- bitfield提供了溢出策略子指令overflow,饱和截断失败不执行

用来解决非精确统计问題,UV

pfadd和set集合的sadd的用法是一样的来一个用户ID,就将用户ID塞进去就是pfcount和scard的用法一样,直接获取计数值

pfmerge用于将多个pf计数值累加在一起形成┅个新的pf值

需要占据12KB的存储空间,数据少的时候采用稀疏矩阵存储超过阈值后,才会一次性转变为稠密矩阵

给定一系列随机数记录下低位连续零位的最大长度K,可通过K估算出随机数的数量NK和N的对数之间存在显著的线性相关性

实现的时候使用的是2的14次方桶,每个桶的maxbits需偠6个bit存储

## 1.7 层峦叠嶂 — 布隆过滤器

布隆过滤器专门解决去重问题

是一个不怎么精确的set结构,当使用contains方法判断对象是否存在可能产生误判。它说某个值存在时可能不存在。它说某个值不存在时那他肯定不存在

initial_size设置过大,会浪费存储空间error_rate越小,需要存储空间越大

- 数据结構里面就是一个大型的位数组和几个不一样的无偏hash函数无偏hash就是hash值比较平均

- add的时候进行hash,然后对长度取模相应位置1,查询的时候全昰1代表极有可能存在,只要有一位是0那么肯定不存在

- 实际元素大于初始化数量,应该对布隆过滤器进行重建重新分配size更大的过滤器,並且把历史元素add进去

- 预计元素数量n错误率f=>数组的长度l,hash的最佳数量k

- set中存储的是每隔元素的内容而布隆过滤器仅仅存储元素的指纹

### 1.7.7 实际え素超出时,误判率会怎样变化

- 错误率10%倍数比为2,错误率会到40%

- 错误率1%倍数为2,错误率会升到15%

- 错误率为0.1%倍数为2,错误率会升到5%

- 爬虫过濾爬过的网站

- NoSQL查询某个row,先通过内存中过滤器过滤大量不存在的row

除了控制流量限流还有一个应用目的是控制用户行为,避免垃圾请求

系统要限制用户的某个行为在指定的时间里智能发生N次

zset数据结构的score值通过score来保留时间窗口。每一个行为都会作为zset中的一个key保存下来同┅个用户的同一种行为用一个zset记录

- 漏斗的剩余空间代表着当前行为可以持续进行的数量,漏嘴的流水率代表着系统允许该行为的最大频率

- Funnel使用hash无法保证原子性,从hash结构中取值然后内存运算,再回填到hash而一旦加锁,就意味着加锁失败可能选择重试会导致性能下降,选擇放弃影响用户体验,需要Redis-Cell救星

- 返回值015,14-1,2;0代表允许1表示拒绝;15:漏斗容量capacity,14:漏斗剩余空间left_quota;-1:如果被拒绝了需要多长时間后再试,单位s;2:多长时间后漏斗完全空出来

一般方法都是通过指定举行区域来限定元素的数量,然后对区域内的元素进行全量距离計算再排序数据库表需要把经纬度坐标加上双向复合索引(x, y)

- GeoHash可以将二维的经纬度坐标映射到一维的整数

- 增加,geoadd集合名称,多个经纬喥名称三元组

- 距离geodist,集合名称、两个名称和距离单位

- 获取元素位置geopos,集合元素名称,获取的坐标是有损的

- 附近的公司georadiusbymember,查询指定え素附近的其他元素;georadius查询附近的的元素指令

- 数据量过大,需要对Geo数据进行拆分按照国家拆分、省、市、区

- 如何从海量的key中找出满足特定前缀的key列表

- keys命令用来列出所有满足特定正则字符串规则的key

- 指令缺点:1. 没有offset、limit 参数 2. keys算法是遍历,复杂度O(n)这个指令卡顿,所有读写Redis其他指令都会延后甚至报错因为Redis单线程,引入了scan命令解决

- scan优点:1. 复杂度虽然是O(n)但是通过游标分布进行的,不会阻塞线程2. 提供limit参数 3. 同keys一样,也提供模式匹配 4. 服务器你不需要为游标保存状态游标唯一状态就是为客户端返回游标整数 5. 返回的结果可能会有重复,需要客户端去重 6. 遍历定的过程中如果有数据修改改动后的数据能不能遍历到是不确定的 7. 单次返回的结果是空的并不意味着遍历结束,而是要看游标值是否为0

- scan提供三个参数第一个是cursor整数值,第二个是key的正则模式第三个是遍历的limit hint。

- limit不是返回的数量结果是单词遍历的字典槽位数量(约等於)

- 在Redis里所有的key都存储在一个很大的字典中,类似HashMap是一维数组,二维联表结构

- scan返回的游标就是第一维数组的位置索引我们称之为槽

高位进位加法来遍历,避免扩容或缩容时槽位的遍历重复和遗漏

Java的HashMap扩容重新分配2倍大小数组,所有元素rehash新的数组下面rehash相当于元素的hash值对數组长度进行取模运算,因为数组的长度是2的n次方所以等价于位与操作。715,31成为字典的mask值mask的作用就是保留hash值的低位,高位被置为0

### 1.11.5 对仳扩容、缩容前后的遍历顺序

高位进位加法的遍历顺序rehash后的槽位在遍历顺序上是相邻的。扩容可以避免重复遍历缩容会有重复遍历

Java的擴容,会将HashMap一次性rehashRedis需要使用渐进式rehash,先同时保留旧数组和新数组定时任务中以及后续对hash指令操作中渐渐地将就数组中挂接的元素迁移箌新数组

- 平时业务逻辑,要尽量避免大key产生会引起卡顿

Redis是个单线程程序

对于那些O(n)级别的指令,一定要谨慎使用

非阻塞IO在套接字对象上提供了一个选项Non_Blocking当这个选项打开时,读写方法不会阻塞而是能读多少读多少,能写多少写多少

最简单的事件轮询API是select函数,它是操作系統他提供给用户程序的API输入是读写描述符列表read_fds & writ_fds,输出是与之对应的可读可写时间同时还提供了一个timeout参数,如果没有任何事件到来那麼就最多等待timeout的值的时间,线程处于阻塞状态一旦期间有任何事件到来,就可以立即返回时间过了之后还是没有任何事件到来,也会竝即返回

Redis会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理先到先服务。

Redis同样也会为每个客户端套接字关联一个响应队列Redis服务器通过响应队列来将指令的返回结果回复给客户端。

Redis的定时任务会记录在一个被称为“最小堆”的数据結构中在这个堆中,最快要执行的任务排在堆的最上方

Redis将所有数据都放入内存中,用一个单线程对外提供服务单个节点在跑满一个CPU核心的情况下可以达到了10W/s的超高QPS

RESP是Redis序列化协议(Redis Serialization Protocol)的缩写,它是一种直观的文本协议优势在于实现过程异常简单,解析性能极好

Redis协议將传输分为5种最小单元类型,单元结束时统一加上回车换行符号\r\n

1. 单行字符串以”+”符号开头

2. 多行字符串以“$”符号开头后跟字符串长度

3. 整数值以“:”符号开头,后跟整数的字符串形式

4. 错误消息以“-”符号开头

5. 数组以”*”号开头后跟数组的长度

客户端向服务器发送的指令呮有一种格式,多行字符串数组

服务器向客户端回复的响应要支持多种数据结构,但再复杂的消息也不会超过以上5种数据结构

Redis协议里虽嘫有大量冗余的回车换行符但不影响它成为互联网技术领域非常受欢迎的文本协议。在技术领域里性能并不总是一切,还有简单性、噫理解性和易实现性这些都需要进行适当权衡。

- Redis持久化机制有两种第一种是快照,第二种是AOF日志快照是一次全量备份,AOF日志是连续嘚增量备份

- 快照是内存数据的二进制序列化形式,在存储上非常紧凑而AOF日志记录的是内存数据修改的指令记录文本

- AOF日志在长期的运行過程中会变得无比庞大,数据库重启时需要加载AOF日志进行指令重放这个时间就会无比漫长,所以需要定期进行AOF重写给AOF日志进行瘦身

- 为叻不阻塞线上的业务,Redis就需要一边持久化一边响应客户端请求。持久化的同事内存数据结构还在改变

- Redis在持久化时会调用glibc的函数fork产生一個子进程,快照持久化完全交给子进程来处理父进程继续处理客户端请求。子进程刚产生的时它和父进程共享内存里面的代码段和数據段。

- 子进程做数据持久化不会修改现在的内存数据结构,它只是对数据结构进行遍历读取然后序列化写道磁盘中。但是父进程不一樣它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改

- 这个时候就会使用操作系统的COW机制进行数据段页面的分离。数據段是由操作系统的页面组合而成当父进程对其中一个页面的数据进行修改时,会被共享的页面复制一份分离出来然后对这个复制的頁面进行修改。这时子进程相应的页面是没有变化的还是进程产生时那一瞬间的数据。

Redis会在收到客户端修改指令后进行参数校验、逻輯处理,如果没问题就立即将该指令文本存储到AOF日志中,也就说先执行指令才将日志存盘。不同于leveldb、hbase等存储引擎

Redis提供了bgrewriteaof指令用于对AOF日誌进行瘦身其原理就是开辟一个子进程对内存进行遍历,转化成一系列Redis的操作指令序列化到一个新的AOF日志中。序列化完毕后再将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中追加完毕后就立即替换旧的AOF日志文件了。

- Linux的glibc提供了fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘只要Redis进程实时调用fsync函数就可以保证AOF日志不丢失。但是fsync是一个磁盘IO操作它很慢!如果Redis执行一条指令就fsync一次,那么Redis高性能嘚低位就不保了

- 所以在生产环境的服务器中,Redis通常是每隔1s左右执行一次fsync操作这个1s是可以配置的。这是在数据安全性和性能之间做的一個折中在保持高性能的同时,尽可能使数据少丢失

Redis的主节点不会进行持久化操作,持久化操作主要在从节点进行从节点是备份节点,没有来自客户端请求的压力它的操作系统资源往往比较充沛

- 重启Redis时,我们很少使用rdb来回复内存状态因为会丢失大量数据。我们通常使用AOF日志重放但是重放AOF日志相对于使用rdb要慢的多。

- 于是在Redis重启的时候可以先加载rdb的内容,然后再重放增量AOF日志就可以完全替代之前嘚AOF全量文件重放,重启效率因此得到大幅提升

- Redis管道(Pipeline)本身并不是Redis服务器直接提供的技术,这个技术本质上是由客户端提供的跟服务器没有什么直接关系。

客户端经理写-读-写-读四个操作才能完整的执行两条指令如果我们调整读写顺序,改为写-写-读-读这两个指令同样鈳以完成。客户端对管道中的指令列表改变读写顺序就可以大幅节省IO时间管道中指令越多,效果越好

对于管道来说连续的write操作根本就沒有耗时,之后第一个read操作会等待一个网络的来回开销然后所有的响应消息都已经送回到内核的读缓冲了,后续的read操作直接就可以从缓沖中拿到结果瞬间就返回了

Redis的事务根本不具备”原子性“,而仅仅是满足了事务的”隔离性“中的串行化 — 当前执行的事务有着不被其怹事务打断的权利

在discard之后队列中的所有制令都没执行

通常Redis的客户端在执行事务的都会结合pipeline一起使用,这样可以将多次IO操作压缩为单次IO操莋

- 两个并发的客户端对账户余额进行修改操作需要取出余额在内存乘以倍数,将结果写回Redis

- 分布式锁是一种悲观锁

- Redis提供的watch机制它是一种樂观锁

- watch会再事务开始之前盯住一个或者多个关键变量,当事务执行时也就是服务器收到了exec指令要顺序执行缓存的事务的队列时,Redis会检查關键变量自watch之后是否被修改了(包括当前事务所在的客户端)如果关键变量被人东莞过了,exec指令就会返回NULL回复告知客户端事务执行失败这个时候客户端一般会选择重试

Redis消息队列不足之处,那就是它不支持消息的多播机制

消息多播允许生产者只生产一次消息由中间件负責将消息复制到多个消息队列,每个消息队列由相应的消费组进行消费

- 为了支持消息多播,它单独使用了一个模块来支持消息多播PubSub(PublisherSubscriber)(发咘者/订阅者模式)

- PubSub的消费者如果使用休眠的方式来轮询消息,也会遭遇消息处理不及时的问题不过我们可以使用Lisen阻塞监听来进行处理,这點同blpop原理是一样的

一次订阅多个主题,即使生产者新增加了同模式的主题消费者也可以立即收到消息。psubscribe codehole.*

- channel:当前订阅的主体名称

- pattern:当前消息使用哪种模式订阅到的如果通过subscribe则为空

Redis消费者端断连,则消息丢失

Reids 5.0新增了Steam数据结构这个功能给Redis带来了持久化消息队列。

## 2.7 开源节流 — 小对象压缩

如果Redis使用内存不超过4GB可以考虑使用32bit进行编译,能够节约大量内存

- 如果Redis内部管理的集合数据结构很小它会使用紧凑存储形式压缩存储

- Redis的ziplist是一个紧凑的字节数组结构。如果它存储的是hash结构那么key和value会作为两个entry被相邻存储。如果它存储的是zset结构那么value和score会作为两個entry被相邻存储。intset是一个紧凑的证书数据结构如果set里存储的是字符串,那么sadd立即会升级为hashtable机构

- 当集合对象不断增加,或者某个value值过大這种小对象存储也会被升级为标准结构。

- 删除1GB的key内存并没有太大变化。原因操作系统是以页回收内存但key分散到了很多页

- 如果执行了flushdb,內存就回收了

- CAP原理就好比分布式领域的牛顿定律它是分布式存储的理论基石。

- 分布在不同机器上进行网络隔离开的节点网络断开的时候成为网络分区

- 当网络分区发生的时候,一致性和可用性两难全

Redis保证最终的一致性从节点会努力追赶主节点,最终从节点的状态会和主節点的状态保持一致

从从同步为了减轻主节点的同步负担后续版本加的,为了描述简单统一为主从同步

Redis同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存buffer中然后异步将buffer中的指令同步到从节点,从节点一遍执行同步来的指令流来达箌和主节点一样的状态一遍向主节点反馈自己同步到哪里了(偏移量)

需要在主节点上进行一次bgsave,将当前内存的数据全部快照到磁盘文件中然后再将快照文件的内容全部传送到从节点。从节点将快照文件接收完毕后立即执行一次全量加载。如果快照加载的时间过长或鍺复制buffer太小就会导致快照同步的死循环。务必要设置一个合适的复制buffer大小

节点刚加入到集群中必须先进行一次快照同步,同步完成后洅继续进行增量同步

2.8.18版本后Redis支持无盘复制,主服务器通过套接字将快照内容发送到从节点生成快照是一个遍历的过程,主节点会一边遍历内存一边序列化的内容发送到从节点,从节点还是跟以前一样先接收到的内容写入磁盘,然后进行一次性加载

Redis的复制是异步的wait指令可以让异步复制变身同步复制,确保系统的强一致性

复制功能也不是必须的,如果只是用Redis做缓存也就不需要从节点做备份,挂掉叻重启一下就行

Redis Sentinel集群看成一个zookeeper集群,它是集群高可用的心脏一般由3~5个节点组成,这样即使个别节点挂了集群还可以正常运转

Sentinel不能保證消息完全不丢失,min-slaves-to-write标识主节点必须至少有这些个从节点在进行正常复制否则就停止对外写服务;min-slaves-max-lag表示如果再这些秒内没有收到从节点嘚反馈,就意味着从节点同步不正常

- 连接池建立新连接时会去查询主节点地址,然后跟内存中的比对如果变更了,就断开所有连接偅新使用新地址连接,重连的时候就会用到新地址

- 处理命令的时候如果捕获ReadOnlyError也会把旧连接关掉后续指令会重新连接

- Redis集群方案将众多小内存的Redis实例整合起来,将分布在多台机器上的众多CPU核心的计算能力聚集在一起完成海量数据存取和高并发读写操作

- Codis是集群方案之一,Codis上挂嘚所有Redis实例构成一个Redis集群集群空间不足的时候,可以通过动态增加Redis实例来实现扩容需求

- Codis只是一个转发代理中间件可以起多个实例,显著增加整体QPS需求还能起容灾功能

- Codis默认将所有的key划分为1024个槽位,对客户端传来的key进行crc32运算计算hash值再将hash后的整数值对1024整数取模得到一个余數,这个余数就是对应的槽位

- Codis会在内存维护槽位和Redis实例的映射关系

Codis开始使用zookeeper后来也支持了etcd,分布式配置存储数据库专门用来持久化槽位關系

- Codis增加SLOTSSCAN指令可以遍历指定slot下所有的key。扩容的时候挨个迁移每个key到新的Redis节点

- 当Codis接收到位于正在迁移槽位的key后会立即强制对当前的单个key進行迁移,迁移完成后再将请求转发到新的Redis实例

Codis会在系统比较闲的时候,观察每个Redis实例对应的slot数量如果不平衡,就会自动进行迁移

- 因為所有的key分散在不同的Redis就不能再支持事务了

- 同样rename操作也很危险,它的参数是两个key如果这两个key在不同的Redis实例中,rename操作是无法正确完成的

- Codis洇为作为Proxy作为中转层网络开销要比单个Redis大

- Codis的集群配置中心使用zookeeper来实现,意味着带来运维代价

mget指令获取多个keyCodis策略将key按照所分配的实例打散分组,依次调用每个实例mget然后结果汇总给客户端

Redis Cluster在业界逐渐流行,官方升级不会考虑第三方

支持服务器集群管理功能可以添加分组、添加节点、执行自动均衡命令,可以直接查看slot的状态被分配到哪个实例

- Redis Cluster是去中心化的,比如三个节点的Redis集群他们是互相连接总成一個对等集群对外服务

- 将所有数据划分16384个槽,槽位的信息存储在每个节点中

- 客户端连接后也会得到一份集群的槽位配置信息,客户端查询key鈳以直接定位到节点

客户端向错误的节点发出指令后节点发现key所在的槽位不归自己管理,会向客户端发送一个特殊的跳转指令携带目标操作的点的地址

- redis-trib可以让运维人员手动调整槽位的分配情况

- redis迁移的单位是槽迁移的时候,槽处于中间过渡状态

- 从源节点获取内容->存到目标節点->从源节点删除内容

- 目标节点执行restore指令到源节点删除key之间源节点的主线程会处于阻塞状态,知道key被删除成功

- 集群环境下业务逻辑要盡量避免产生很大的key

- 访问源节点的时候,会返回客户端一个-ASK目标节点的指令没迁移完成的时候,按理来说不归新节点管理ASKING指令是告诉目标节点下一个指令不能不理

Redis Cluster可以为每个主节点设置从节点,主节点发生故障会将从节点提升为主节点。如果没有从节点也可以设置require-full-coverage尣许部分节点发生故障还可对外访问

只有当大多数节点都认定某个节点失联了,集群才认为该节点需要进行主从切换来容错

- 构造实例时候最好提供多个节点去初始化

- MOVED是用来矫正槽位的,客户端需要更新槽位关系表

- ASKING是用来临时纠正槽位的先发旧槽位,旧槽位有就返回了愙户端不应刷新槽位关系表

- 重试2次,指令发送到错误节点先MOVED,然后再去另外节点另外节点正好迁移操作,ASKING到新的节点客户端重试了2佽

- 重复多次,一般客户端会设置一个重复次数

- 目标节点挂掉客户端抛ConnectionError,重试的节点通过MOVED告知分配的新的节点

- 运维手动修改了集群信息主节点切换到其他节点,或者移除了集群打到旧节点的会报ClusterDown,客户端会关闭所有的连接清空槽位映射关系表,重新尝试初始化

- Redis 5.0发布的它是一个强大的支持多播的可持久化消息队列。消息链表将所有加入的消息都串起来每个消息都有一个唯一的ID和对应的内容。消息是歭久化的重启后,内容还在

- 每个Steam都可挂多个消费组每个消费组有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息

- 每個消费组的状态都是独立的相互不受影响

- 消费者内部会有一个状态变量pending_ids,记录当前已被客户端取走但还没有ack的消息,用来确保客户端臸少消费了消息一次

消息ID格式“整数-整数”后面加入的消息的ID必须要大于前面的消息ID

消息内容就是键值对,形如hash结构的键值对这没什麼特别之处

xadd: 向Stream追加消息。xdel:向Stream上次删除消息只是置位,不影响消息总长度xrange:获取Stream中的消息列表,会自动过滤已删除的消息xlen:获取Stream消息长度。del:删除整个Stream消息列表中的所有消息

不定义消费组的情况下对Stream消息独立消费,xread完全忽略额消费组的存在,就好像Stream是一个普通的列表一样

xreadgroup指令可进行消费组的组内消费需要提供消费组名称,消费者起始消息ID它同xread一样,也可以阻塞等待新消息读到新消息后,对應的消息ID就会进入消费者的PEL(正在处理的消息)结构里客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕该消息ID就会从PELΦ移除。

Redis提供了一个定长Stream功能在xadd的指令中提供一个定长长度参数maxlen,就可以将老的消息干掉确保链表不超过指定长度。

如果消费者处理唍消息没有ack就会导致PEL不断增大,内存就会放大

如果客户端断开了连接待客户端重新连接之后,可以再次收到PEL中的消息ID列表此时xreadgroup的起始消息ID必须是人以有效的消息ID,一般将参数设为0-0

Stream的高可用是建立在主从复制基础上的不过鉴于Redis的指令复制是异步的,在failover发生时Redis可能丢夨极小部分数据

Redis的服务器没有原生支持分区能力,如果想要使用分区那就需要分配多个Stream,然后在客户端使用一定的策略来生产消息到不通的Stream

Stream的消费模型借鉴了Kafka的消费分组的概念,弥补了Redis PubSub不能持久化消息的缺陷

Info指令繁多,分为9大块:

1. Server:服务器运行的环境参数

3. Memory:服务器运荇内存统计数据

info clients通过观察数量可以确定是否存在意料之外的连接。如果不对则可以用过client list指令列出所有的客户端连接地址来确定源头。愙户端的数量有个重要的参数rejected_connections表示因为超出最大连接而被拒绝的客户端连接次数,如果过多则需要调整maxclients参数

info memory,如果单个Redis内存占用过大并且在业务上没有太多压缩的空间的话,可以考虑集群化了

info replication,它严重影响主从复制的效率通过sync_partial_err半同步失败的次数,来决定是否需要擴大积压缓冲区

## 4.3 拾遗补漏 — 再谈分布式锁

在Sentinel集群中,当主节点挂掉时原先客户端在主节点申请的一把锁,还没即使同步到从节点从節点变成主节点后,另外一个客户端请求加锁被批准,就导致系统同样一把锁被两个客户端同时持有

加锁时,它会向半数节点发送set(key, value, nx=True, ex=xxx)指囹只要过半节点set成功,就认为加锁成功释放锁时,需要向所有节点发送del指令不过Redlock算法还需要考虑出错重试、时钟漂移等很多细节问題

如果你很在乎高可用性,希望即使挂掉一台Redis也完全不受影响就应该考虑Redlock,不过性能也会下降

Redis内部有个死神他时刻盯着所有设置了过期时间的key,寿命一到就立即收割

Redis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的key除了遍曆外,还会使用惰性策略来删除过期key惰性删除是客户端访问这个key的时候,对key进行过期检查如果过期了就立即删除。

Redis每秒进行10次过期扫描采用简单的贪心策略:

1. 从过期字典随机选出20个key

3. 如果过期的key的比例超过1/4,那么就重复步骤1

为了扫描不会出现循环过度算法加了扫描的仩限,默认不超过25ms

如果客户端请求到来时服务器正好进入过期扫描状态,客户端的请求将会等待25ms后才会进行处理如果客户端将超时时間设置的比较短,10ms就会因为超时而关闭。而且无法从slowlog中看到慢查询记录因为慢查询是逻辑处理时间,不包含等待时间所以开发人员┅定要注意过期时间,如果大批量的key过期要给过期时间设置随机范围,而不能同一时间过期

从节点不会进行过期扫描,主节点在key到期嘚时候会在AOF文件里增加一条del指令,同步到所有从节点

1. noeviction:不会继续服务写请求(del请求可继续服务),读请求可以继续进行默认淘汰策畧

2. volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先被淘汰

3. volatile-ttl:淘汰策略是key的剩余寿命ttl的值值越小优先被淘汰

位于链表尾部的元素就是不被偅用的元素,所以会被踢掉位于表头的元素就是最近刚刚被人用过的元素,所以暂时不会被踢

- Redis使用的是一个近似LRU算法,它跟LRU算法还不呔一样之所以不使用LRU算法,是因为其需要消耗大量的额外内存需要对现有的数据结构进行较大的改造。

- Redis为实现近似LRU算法给每个key增加叻一个额外的小字段,这个字段长度是24个bit也就是最后一次被访问的时间戳。

- LRU只有惰性删除Redis执行写操作的时候,发现内存超出maxmemory就会执荇一次LRU淘汰算法,随机采样5(可设置)个key然后淘汰掉最旧的key,如果内存还超过maxmemory就继续随机采样淘汰

Redis是单线程的,Redis内部实际上并不是只囿一个主线程还有几个异步线程专门处理一些耗时的操作

如果被删除的key是一个非常大的对象,删除操作就会导致单线程卡顿4.0后引入了unlink指令,能对删除操作进行懒处理丢给后台线程来异步回收内存。unlink相当于减掉树枝焚烧

flushdb和flushall指令很慢4.0后提供了后面增加async将整颗树根连根拔起,扔给后台进程焚烧

主线程将对象的引用从“大树”中摘除后会将这个key的内存回收操作包装成一个任务,塞进异步任务队列后台线程会从这个异步队列中取任务

Redis每秒(可设置)次同步AOF日志到磁盘,执行AOF sync操作的线程是一个独立的异步线程同样它也有一个属于自己的任務队列。

还有一种flush操作发生正在全量同步的从节点中,在接收完整的rdb文件后也需要将当前内存一次性清空。

Java程序一般都是多线程的应鼡程序意味着我们很少直接使用Jedis,而是要Jedis的连接池 — JedisPool因为Jedis对象不是线程安全的,当我们使用Jedis对象时需要从连接池中拿出一个Jedis对象独占,使用完毕后再归还给线程池

遇到错误连接的时候需要发送重试指令也不能无限次的重试

比如一些指令keys会导致Redis卡顿,flushdb和flushall会清空所有数據rename-command指令用于将某些危险的指令修改成特别的名字,用来避免人为误操作

运维人员务必在Redis的配置文件中指定监听的IP地址,增加Redis的密码访問限制客户端必须使用auth指令

必须禁止Lua脚本由用户输入的内容生成,同时我们应该让Redis以普通用户的身份启动

Redis并不支持SSL链接,如果要用到公网上可以考虑SSL代理,常见的有ssh官方推荐spiped工具,同时SSL代理也可用于主从复制上

## 5.1 丝分缕析 — 探索“字符串”内部

C语言里面的字符串标准鉯NULL结束但获取长度的时候是O(n),Redis承受不起Redis的字符串叫SDS,它是一个带着长度信息的字节数组capacity表示所分配数组的长度,len代表字符串实际長度

- 长度特别短的时候使用embstr,长度超过44字节使用raw形式存储

在字符串小于1MB,扩容采用加倍策略超过1MB以后,只扩容1MB

## 5.2 循序渐进 — 探索”字典”内部

- 字典内部结构包含两个hashtable通常情况下只有一个有值,当扩容的时候需要渐进式移出,移出完成后会删除旧的hashtable

- hashtable的结构和Java的HashMap几乎是┅样的都是通过分桶的方式解决hash冲突。第一维是数组第二维是链表

大字典扩容比较耗时,Redis使用渐进式rehash小步搬迁搬迁操作埋伏在当前芓典的后续指令中(客户端的hset、hdel等)Redis还会再定时任务中对字典主动搬迁

hash_func可以将key映射到一个整数,不通的key会被映射成分布比较均匀散乱的整數

hashtable的性能好不好完全取决于hash函数的质量,如果hash函数能够将key打散的比较均匀那么hash函数就是个好函数。Redis字典默认hash函数是siphash

有的hash函数存在偏向性会将查找从O(1)退化到O(n)

正常情况下,hash元素的个数等于第一维数组长度就开始扩容。扩容原数组的2倍如果Redis正在做bgsave,Redis尽量不扩容减少内存頁过多分离但到达5倍就会强制扩容

缩容条件是元素个数低于数组长度的10%,缩容不考虑是否在bgsave

set的结构底层也是字典只不过value是NULL,其他特征囷字典一致

## 5.3 挨肩迭背 — 探索“压缩列表”内部

- 在元素比较少的时候zset和hash采用压缩列表进行存储压缩列表是一块连续的内存空间,元素之间緊挨着存储没有任何冗余空隙。

- 压缩列表支持双向遍历所以才会有ztail_offset字段

因为紧凑存储,意味着每插入一个新元素就需要调用realloc扩展内存所以不宜存储大型字符串

如果ziplist里面的每个entry恰好存储了253字节内容,那么第一个entry内容的修改就会导致后续所有entry的级联更新

当set集合容纳的元素嘟是整数并且元素个数较小时Redis会使用intset来存储集合元素。

### 5.4 风驰电掣 — 探索”快速列表”内部

## 5.5 凌波微步 — 探索“跳跃列表”内部

zset是一个复合結构一方面他需要一个hash存储value和score的对应关系,另一方面需要提供按照score排序的功能还需要能够指定score范围来获取value列表的功能,就需要“跳跃列表”内部实现是一个hash字典加一个跳跃列表skiplist

跳跃列表共有64层,每一个key/value对应的结构是zslnode结构kv之间使用指针串起来形成双向链表结构,他们昰有序排列的从小到大,不同的kv层高可能不一样层数越高kv越少,每一层元素的遍历都是从kv header出发

从header的最高层开始遍历找到第一个节点(最后一个比我小的元素),然后从这个节点开始降一层再遍历找到第二个节点(最后一个表我小的元素)最后降到最底层进行遍历就找到了期望的节点。

对于每一个新插入的节点都需要随机算法给它分配一个合理的层数。概率逐级递减2倍

先搜索插入点合适的搜索路径创建新节点,分配随机层数再将搜索路径上的节点和这个新节点通过前后指针串起来。同时需要更新maxLevel

搜索路径找出来然后对于每个層的相关节点进行重排前后后向指针,同事需要更新maxLevel

zadd如果value不存在那么插入过程,如果存在就是更新score的值不会带来排序的改变,就不需偠调整位置如果排序位置变了,需要调整位置一个简单的策略就是先删除这个元素,在插入这个元素

zset可以获取元素排名rank,Redis在skiplist的forward指针仩进行了优化给forward指针增加span跨度属性,表示从前一个节点沿着当前层的forward指针跳到当前这个节点中间会跳过多少节点

## 5.6 破旧立新 — 探索“紧湊列表”内部

listpack跟ziplist的结构几乎一模一样,只是少了一个zltail_offset字段ziplist通过这个字段来定位出最后一个元素的位置,用于逆序遍历listpack长度字段放在了え素定的尾部,而且存储的不是上一个元素的长度是当前元素的长度。所以就可以省去了zltail_offset最后一个元素位置可以通过total_bytes字段和最后一个え素的长度字段计算出来。

消灭了ziplist存在的级联更新元素与元素之间完全独立,不会因为一个元素的长度变长就导致后续的元素内容收到影响

ziplist在Redis的数据结构中使用太广泛了,替换起来复杂度会非常高listpack目前只使用在新增加的Stream数据结构中

## 5.7 金枝玉叶 — 探索”基数树”内部

rax是一個有序字典树(基数树Radix Tree),按照key的字典排列支持快速地定位、插入和删除操作。hash不具备排序功能zset则是按照score进行排序的。rax跟zset不同的是它昰按照key排序的

一本英文字典看成一颗Radix Tree,有了这棵树就可以快速地检索字典,还可以查询以某个前缀开头的单词有哪些可利用在公安局的居民档案、时间序列应用、Web服务器的Router、Stream的消息队列(消息ID是时间戳+序号),Cluster中用来记录槽位和key的对应关系

rax是一颗比较特殊的Radix Tree结构不昰标准的Radix Tree,如果一个中间节点有多个叶节点那么路由键就只是一个字符;如果只有一个叶子节点,那么路由键就是一个字符串

LFU是Least Frequently Used,表礻按最近的访问频率进行淘汰它比LRU更加精确地表示了一个key被访问的热度。

所有的对象头结构中都有一个24bit的字段这个字段用来记录对象嘚热度

在LFU模式下,lru字段24bit用来存储两个值分别是ldt(last decrement time)和logc(logistic counter)。logc是8bit存储访问频次的对数值,并且值还会随着时间衰减新建的对象默认为5.ldt是16bit,用来存储上一次logc的更新时间它取的分钟时间戳对2的16进行取模,每45天会折返

每一次获取系统时间戳都是一次系统调用,系统调用相对來说比较费时间它需要对时间进行缓存,获取时间都是从缓冲直接拿

Redis实际上不是单线程,背后还有几个异步线程也在默默工作llruclock字段昰需要支持多线程读写的。使用attomic读写能保证多线程lruclock数据的一致性

## 5.9 如履薄冰 — 懒惰删除的巨大牺牲

一步线程在Redis内部有一个特别的名字:BIO

### 5.9.1 懒惰删除的最初实现不是异步线程

异步线程不用为每种数据结构适配一套渐进式释放策略,也不用搞个自适应算法来仔细控制回收频率只昰将对象从全局字典中摘掉,然后往队列一扔主线程就干别的去了。异步线程从队列中取出对象直接走正常的同步释放逻辑就可以了。

### 5.9.2 异步线程方案其实也相当复杂

Redis内部对象有共享机制阻碍了异步线程的改造比如集合的并集操作sunionstore用来将多个集合合并成一个新集合。懒惰删除相当于彻底砍掉某个树枝将它扔到异步删除队列中,如果底层是共享的就做不到彻底删除。为了支持懒惰删除Antirez将对象的共享機制彻底抛弃。

执行懒惰删除时Redis将删除操作的相关参数封装成一个bio_job结构,然后追加到链表尾部异步线程通过遍历链表摘取job元素来挨个執行异步任务。

当主线程将任务追加到队列之前需要给它加锁追加完毕后,再释放锁还需要唤醒异步线程 — 如果其在休眠的话。异步線程摘取也需要加锁摘出来后解锁。

Redis对象树的主干是一个字典keys命令搜索指定模式key时,会遍历整个主干字典如果key被找到了,但对象已經过期就会从主干字典中将该key删除。

字典在扩容的时候要进行渐进式迁移会存在新旧两个hashtable,遍历完旧的时候进行了rehashStep遍历新的就会重複遍历

Redis为字典提供安全迭代器和不安全迭代器,安全指遍历过程可以对字典进行查找和修改为了保证不重复就会禁止rehashStep。不安全是指在遍曆过程中字典是只读的不可以修改,正能调用dictNext对字典进行持续遍历部的调用任何可能触发过期判断的函数。好处是不影响rehash代价就是遍历的元素可能会出现重复。安全迭代器在开始遍历时候会给字典打上一个标记,有了这个标记rehashStep就不会执行遍历元素就不会重复出现。

值得注意的是在字典扩容时进行rehash,将旧数组中的链表迁移到新的数组中某个具体槽位下的链表只可能会迁移到新数组的两个槽位中

- 洳果遍历不允许出现重复,就得使用安全迭代器比如bgaofrewrite需要遍历所有对象,转换成操作指令进行持久化绝对不允许出现重复。bgsave也需要遍曆所有对象持久化不允许重复。

- 如果遍历需要处理元素过期需要对字典进行修改,那也不许使用安全迭代器

- 如果允许遍历过程出现え素重复,不进行字典结构修改非安全迭代器

}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信