Summer Blog

IT konekt 2019 Note

Clean Architecture

Video

Not too much detail too early.

A good architecture allows major decisions to be DEFERRED!

A good architecture maximizes the number of decisions NOT made.

You only test the parts of the application that you want to work.

Things hard to test:

  1. Anything at outer boundary of the system, like GUI
  2. It’s very difficult to test things if you don’t know the answer

Top 10 SQL Performance Tips & Tricks for Java Developers

Video Slide

Round trips

Auto commit

Bulk processing

Bind variables

Fetch size

Save point

KISS (Keep it simple SQL)

Parallel queries

SELECT /*+ PARALLEL */ id, create_date, first_name...

Explain plans

SQL wait events (Oracle only)

Redis Cluster Issue

Redis总结

基本使用

数据类型

类型 名称 应用
string 字符串 分布式锁,设置 key-value-ttl,考虑超时和可重入性
list 列表 异步消息队;利用 lrange 命令,做基于 Redis 的分页功能列
set 集合 利用 Set 的交集、并集、差集等操作
zset 有序集合 延迟队列,score 为延迟时间;限流器,score 为加入时间,移除加入时间超过与之的成员,zset 大小超过限流时拒绝请求
hash 字典  
setbit 位图 位图不是特殊的数据类型,而是将字符串看出 byte 数组后按位操作。用于记录 1/0 数据节约使用空间
hyperloglog   不精确的去重计数方案,标准误差 0.81%
bloom filter 布隆过滤器 检查一个值是否在集合里,有一定误差。当判断一个值存在时,可能不存在,但判断一个值不存在,就一定不存在
redis cell 限流器 使用漏斗算法
geo hash 地理模块 原理:将二维地理映射到一维 zset 中进行排序

事务

使用 MULTIEXEC DISCARD WATCH

Stream

Redis 5.0 后加入的支持多播的可持久化消息队列。它的实现参考了 Kafka 的设计,有消费组(customer group)和消费者(customer)概念,消费组通过维护游标指向已消费到的地方,消费组内的消费者是竞争关系,只会有一个消费者接到消息,消费组间相互独立。消费组会维护pending_ids记录没有被消费(没有收到 ack)的消息,保证至少一次的消费。stream 没有分区的概念,可以由客户端手动创建多个 stream 并设置路由规则实现分区。

xadd # 追加消息
xdel # 删除消息
xrange # 获取stream中的消息列表
xlen # 消息长度
del # 删除stream
xread # 独立消费,可以忽略消费者组概念,将stream当成普通list
xgroup create # 创建消费者组
xreadgroup # 消费组内消费

安全保护

  1. 危险指令用rename-command设置别名,只有通过这个别名才能请求
  2. 增加密码限制
  3. lua 脚本安全,禁止客户输入生产 lua 脚本
  4. SSL 代理,Redis 本身不支持 SSL 安全链接,需要通过代理使数据加密。官方推荐 spiped 工具

原理

I/O 模型

单线程,多路复用

Redis 为什么这么快

  1. C 语言效率更高
  2. 内存数据库,避免 I/O
  3. 单线程避免了上下文切换资源竞争
  4. I/O 多路复用技术处理网络 socket 连接
  5. 对象压缩,如果对象小,一维结构内存小于二维结构。ziplist 紧凑型字节数组,inset 紧凑型整数数组

持久化

通常在生产环境使用混合持久话方式,因为只用 rdb 可能会丢数据,只用 AOF 效率慢。在 Redis 重启时先加载 rdb 的内容,然后再增量重放 AOF 日志。

过期策略

LRU

Redis 内存超过内存使用限制时,需要执行淘汰策略,提供以下选择:

  1. noeviction默认策略。不会继续执行写请求,读、删请求可继续执行,这样保证数据不丢失,但可能造成线上业务不能持续进行
  2. volatile-lru淘汰设置了过期时间的 key 中最少使用的
  3. volatile-ttl淘汰设置了过期时间的 key 中剩余寿命(ttl)值最小的
  4. volatile-random随机淘汰设置了过期时间的 key
  5. allkey-lru淘汰所有 key 中最少使用的
  6. allkey-random随机淘汰所有 key

集群

主从

Redis 支持一主一从,一主多从的模式,从节点可以从主节点同步,也可以从其他从节点同步。Redis 可以通过wait指令指定命令的同步由异步变为同步,wait指令接收两个参数,节点数量N,等待时间t,表示指令需要等待N个节点同步完成,最长等待时间是t

主从同步有两种方式:

  1. 增量同步:主节点将执行的命令存在 buffer 中,buffer 是循环记录的,然后异步的将指令同步到从节点,从节点执行命令并更新自己同步的偏移量
  2. 快照同步:主节点生成 rdb 快照,从节点同步 rdb 快照,完成后再同步复制 buffer。这个操作也可以不生成 rdb,主节点直接将内存中的数据通过网络传输给从节点

Redis Sentinel

Redis Sentinel 是 Redis 集群管理的工具,类似 zookeeper 集群,客户端通过 Sentinel 链接 Redis 可以实现无感切换主节点,用以保证一定的可用性。Sentinel 切换主节点时,可能产生数据丢失,因为有些主节点执行了的命令没来的及同步到从节点。

Codis

Codis 是 Redis 的集群方案之一。Redis 需要集群的原因:

  1. 单节点内存过大会导致 rdb 变大,进一步导致主从节点全量同步时间加长,恢复时间变长
  2. Redis 是单线程工作的,只能利用单 CPU

Codis 原理:

缺点:

  1. 增加网络开销
  2. 增加同步机制 zookeeper 的维护
  3. 同步带来的性能消耗,以及单个集合类型的数据的大小限制
  4. 非官方项目新功能同步慢

Redis Cluster

Redis 官方集群支持,去中心化的集群,每个节点管理一部分 key,共同组成一个对等集群。

原理:

源码

字符串

Redis 中的字符串使用SDS(Simple Dynamic String)结构。

struct SDS<T>{
  T capacity; // 数组容量
  T len; // 当前长度
  byte flags;
  byte[] content; // 内容
}

这里用 T 的泛型,是因为当字符串较短时可以使用长度更短的byteshort减少内存的占用。Redis 规定字符串长度不能超过 512M。创建字符串时lencapacity指定值是相同的,因为一般不会append修改字符串。

append

append方法会在现有字符串后面追加新的内容,追加时会计算当前容量是否够用,不够用会扩容,并复制当前值到新的对象中。扩容策略在字符串小于 1M 时使用加倍策略,之后只会多追加 1M。

embstr/raw

字符串有两种存储方式embstrraw,当长度超过 44 时,使用rawembstrSDS对象头结构和数据存储在一起,因为一次申请最大是 64 字节的空间,其中对象头占用 19 字节,1 字节站位符null,剩余就是 44 字节。当大于 44 字节采用raw结构,对象头和数据会分开存储。

字典

字典在 Redis 应用很广泛,不仅是hash结构的数据会用到,所有的 key/value 组成全局字典,带有过期时间的 key/ttl 组成过期时间字典,zset集合中的 value/score 也是字典。字典的数据结构类似hashmap

refresh

随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。

字典在refresh时需要至少O(n)的时间复杂度,Redis 采用渐进式迁移方法避免耗时阻塞程序。每次操作(增删查改)时首先会判断当前字典是否有正在执行的渐进式refresh,如果有就帮助执行一次。Redis 也会设置定时辅助进行渐进式refresh

列表

压缩列表

快速列表

跳跃列表

紧凑列表

缓存

缓存读写策略

  1. Cache Aside(旁路缓存)策略:在更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
    1. 问题:
      1. 很小几率的缓存不一致
      2. 数据库主从同步延迟,造成新写入的数据没有写入从库,查询不到,写不进缓存
    2. 解决:
      1. 写数据库同时,加锁写入缓存
      2. 写数据库同时,写缓存,设置较小的缓存过期时间
  2. Read/Write Through(读穿 / 写穿)策略:这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。
    1. Write Through 的策略:先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。可以选择两种“Write Miss”方式:一个是“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到数据库中;另一个是“No-write allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到数据库中。
    2. Read Through 策略:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。
    3. 问题:Write Through 策略中写数据库是同步的,这对于性能来说会有比较大的影响,因为相比于写缓存,同步写数据库的延迟就要高很多了。
  3. Write Back(写回)策略:这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。

缓存穿透

请求穿透缓存,直接访问数据库。两种解决方案:

  1. 回种空值:当我们从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。建议在使用的时候应该评估一下缓存容量是否能够支撑。如果需要大量的缓存节点来支持,那么就无法通过通过回种空值的方式来解决,这时可以考虑使用布隆过滤器。
  2. 布隆过滤器:把集合中的每一个值按照提供的 Hash 算法算出对应的 Hash 值,然后将 Hash 值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从 0 改成 1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为 1 就认为这个元素在集合中,否则则认为不在集合中。两个由于 hash 碰撞有关的缺陷:
    1. 它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中
    2. 不支持删除元素
  3. 极热点缓存穿透可以通过后台加载、设置分布式锁控制穿透的数量