• 1

  • 1

【Java劝退师】Redis 知识脑图 - 分布式缓存

1星期前

Reids

Redis

分布式缓存

一、使用场景

1. 数据库缓存

缓存 - 原始数据的复制级,用于快速访问

将访问过的内容进行缓存,再次访问先找缓存,缓存命中返回数据,不命中找数据库,回填缓存。

2. 提高系统响应

Redis数据是放在内存中,数据库数据大多放在硬盘中,内存的访问速度远大于硬盘,相较于数据库可以瞬间处理大量读/写请求。

3. 做 Session 分离

当架构使用 Tomcat 集群,并且使用 Tomcat 做 Session 复制,将产生 (1) 复制时性能损耗 (2) 无法保证实时同步 问题,因此可以采取将 Session 统一放在 Redis 中,这样多个 Tomcat 就可以共享 Session 消息。

4. 乐观锁

使用 Redis 的 watch + incr 实现。

jedis1.watch(redisKey);
String redisValue = jedis1.get(redisKey);
int valInteger = Integer.valueOf(redisValue);
String userInfo = UUID.randomUUID().toString();

// 没有秒完
if (valInteger < 20) {
    Transaction tx = jedis1.multi();
    tx.incr(redisKey);
    List list = tx.exec();
    // 秒成功 失败返回空list而不是空
    if (list != null && list.size() > 0) {
    	System.out.println("用户:" + userInfo + ",秒杀成功!当前成功人数:" + (valInteger + 1));
    }
    // 版本变化,被别人抢了。
    else {
    	System.out.println("用户:" + userInfo + ",秒杀失败");
    }
}
// 秒完了
else {
	System.out.println("已经有20人秒杀成功,秒杀结束");
}
复制代码

5. 分布式锁 (悲观锁)

若需要控制多个进程( JVM ) 并发的时序性(串型化),可以采用 Redis 的 setnx 实现。

当 value 不存在时则赋值,属于原子操作

127.0.0.1:6379> setnx name zhangf # 如果name不存在赋值
(integer) 1
127.0.0.1:6379> setnx name zhaoyun # 再次赋值失败
(integer) 0
127.0.0.1:6379> get name
"zhangf"
复制代码
127.0.0.1:6379> set age 18 NX PX 10000 # 如果不存在赋值 有效期10秒
OK
127.0.0.1:6379> set age 20 NX # 赋值失败
(nil)
127.0.0.1:6379> get age # age失效
(nil)
127.0.0.1:6379> set age 30 NX PX 10000 # 赋值成功
OK
127.0.0.1:6379> get age
"30"
复制代码

自己实现分布式锁

/**
* 使用redis的set命令实现获取分布式锁
* @param lockKey 可以就是锁
* @param requestId 请求ID,保证同一性 uuid+threadID
* @param expireTime 过期时间,避免死锁
* @return
*/
public boolean getLock(String lockKey,String requestId,int expireTime) {
    // NX: 保证互斥性
    // hset 原子性操作 只要lockKey有效 则说明有进程在使用分布式锁
    String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
    if("OK".equals(result)) {
    	return true;
    } 
    return false;
}
复制代码
public static boolean releaseLock(String lockKey, String requestId) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(lockKey),Collections.singletonList(requestId));
    if (result.equals(1L)) {
        return true;
    } 
    return false;
}
复制代码

使用 Redission 实现分布式锁

public class DistributedRedisLock {
    
    //从配置类中获取redisson对象
    private static Redisson redisson = RedissonManager.getRedisson();
    private static final String LOCK_TITLE = "redisLock_";
    
    //加锁
    public static boolean acquire(String lockName){
        //声明key对象
        String key = LOCK_TITLE + lockName;
        //获取锁对象
        RLock mylock = redisson.getLock(key);
        //加锁,并且设置锁过期时间3秒,防止死锁的产生 uuid+threadId
        mylock.lock(2,3,TimeUtil.SECOND);
        //加锁成功
        return true;
    }
    
    //锁的释放
    public static void release(String lockName){
        //必须是和加锁时的同一个key
        String key = LOCK_TITLE + lockName;
        //获取所对象
        RLock mylock = redisson.getLock(key);
        //释放锁(解锁)
        mylock.unlock();
    }
    
}
复制代码

6. MyBatis 的二级缓存

二、锁的解释

  1. 悲观锁 - 我认为会出问题,所以先上锁 - 性能差

    (1) synchronized (2) 数据库中的行锁、表锁

  2. 乐观锁 - 谁都可以来,但是我成功了,你就成功不了 - 性能高

    秒杀场景

三、缓存的读写模式

1. Cache Aside Pattern - Redis

(1) 读: 先读缓存,缓存没有读数据库,取出数据放数缓存,并返回响应。

(2) 更: 先更新数据库,再删除缓存。

​ 不更新缓存的原因:

​ <1> 缓存如果是一个复杂结构(hash、list),会需要遍历,增加复杂度

​ <2> 只更新不删除有机率出现脏读情况

2. Read/Write Through Pattern - Guava Cache

应用进程只操作缓存,缓存操作数据库。

3. Write Behind Caching Pattern - EVCache

应用进程只更新缓存,缓存"异步"批量更新到数据库 - 不能实时同步,甚至可能会丢数据。

四、 数据类型

1. String 字符串

  1. 字符串

  2. 数字

  3. 浮点数

  4. 乐观锁 - watch + incr

  5. 分布式锁 - setnx

2. List 列表

存储有序、可重复元素,获取头部、尾部纪录极快。

  1. 栈、队列
  2. 用户列表、商品列表、评论列表

3. Set 集合

无序、唯一元素。

  1. 关注的用户
  2. 随机抽奖 ( spop命令 )

4. SortedSet 有序集合

元素唯一,可按分数排序。底层实现: 跳跃表

  1. 点击排行榜
  2. 销量排行榜
  3. 关注排行榜

5. Hash 散列表

String 类型的 field、value 映射表。

  1. 对象
  2. 表数据映射

6. Bitmap 位图

value 只能是 0 或 1。

  1. 用户签到
  2. 统计活跃用户
  3. 查找用户在线状态

7. Geo 地理位置

使用Z阶曲线、Base32编码、GeoHash算法,保存经纬度。

  1. 纪录地理位置
  2. 计算距离
  3. 附近的人

8. Stream 数据流

持久化消息队列。

五、过期、淘汰策略

1. maxmemory

  1. 默认: 禁止驱逐 - 可作为DB使用 - 数据太多可能导致崩溃
  2. 推荐设置物理内存的 3/4

注: 如果设置了 maxmemory 则 maxmemory-policy 要配置

2. maxmemory-policy (删除策略)

  1. 定时删除

    使用定时器删除过期时间的 Key - 不推荐

  2. 惰性删除

    访问 Key 发现已过期,则删除

  3. 主动删除 - 随机挑选键值对,使用遍历太耗时

    (1) no-enviction - 不删除 (默认)

    (2) allkeys-lru - 使用时间最远 (看使用的时间) - 通常采用

    (3) volatile-lru - 从已设置过期时间的数据,挑选使用时间最远

    (4) allkeys-lfu - 最近最少使用 (看使用次数)

    (5) volatile-lfu - 从已设置过期时间的数据,挑选最近最少使用

    (6) allkeys-random - 随机 - 希望请求压力平均分布时采用

    (7) volatile-random - 从已设置过期时间的数据,随机挑选

    (8) volatile-ttl - 挑选 TTL 值最小

Redis 默认采用 惰性删除 + 主动删除

3. expire (TTL)

数据存活时间

六、持久化

目的是为了快速恢复数据而非存储数据。AOF 记录过程,RDB只管结果

1. RDB 快照 (默认)

流程: 父进程 fork 子进程 (此时父进程阻塞),子进程创建 RDB 文档,根据父进程内存生成快照文档,并对原有文档进行原子替换。

优点:

  1. 使用二进制压缩,空间小,方便传输

缺点:

  1. 无法保证数据完整,将丢失快照以后的所有数据
  2. fork 子进程过程阻塞,若数据太大,将导致短时间无法响应请求 (如需避免须关闭RDB,启用AOF)

2. AOF 操作日志

将运行的命令记录到 AOF 文档中

优点:

数据安全,不丢失数据

缺点:

性能低

保存模式 (硬盘)

  1. AOF_FSYNC_NO : 不保存

    只有当 (1) Redis 关闭 (2) AOF 功能关闭 (3) 操作系统写缓存刷新 才会将AOF存到硬盘

  2. AOF_FSYNC_EVERYSEC : 每秒保存一次 (默认)

    最多丢失2秒钟数据

  3. AOF_FSYNC_ALWAYS : 一条命令保存一次 (不推荐)

    最多丢失一条命令数据

重写

将 AOF 文件内命令进行删除与合并,对文件进行瘦身,且整个过程绝对安全

七、Redis 弱事务

开启事务后 (1) 语法错误 - 命令队列清除 (2) 运行错误[类型错误..等] - 正确的命令 运行,不回滚 【性能考量】

1. Redis 的 ACID

Atomicity (原子性) : 一个队列中的命令,要么运行,要么不运行。

Consistency (一致性): 事务运行前、后状态必须是一致的。Redis 是 AP 模型,集群中不能保证实时一致,只能保证最终一致。

Isolation (隔离性) : 命令是顺序运行,但在一个事务中,有可能运行其他客户端的命令。

Durability (持久性) : 有持久化,但不保证数据完整性。

2. 事务命令

当被监视的字段被其他客户端更动之后,监视后开启的命令队列将被清空

  1. watch : 监视key (客户端内共享)
  2. multi : 开启事务,后续命令将放入命令队列中
  3. exec : 运行命令队列
  4. discard : 清除命令队列
  5. unwatch : 清除监视key (客户端内共享)
127.0.0.1:6379> watch s1 # 监视key(客户端内共享) 当被监视的字段被其他客户端更动之后,监视后开启的命令队列将被清空
OK
127.0.0.1:6379> multi # 开启事务,后续命令将放入命令队列中
OK
127.0.0.1:6379> set s1 555 # 此时在没有exec之前,通过另一个命令窗口对监控的s1字段进行修改
QUEUED
127.0.0.1:6379> exec # 运行命令
(nil)
127.0.0.1:6379> get s1 # 因为命令队列被清空,导致命令运行失败,此处只能查找到其他客户端修改后的结果
222
127.0.0.1:6379> unwatch # 清除监视key
OK
复制代码

3. Lua 脚本

具备强原子性,脚本运行过程,不允许插入新的命令,故运行时间应尽量短

八、优化

  1. 避免大key ( value > 100K ) → 拆为小 key
  2. 避免使用 key*、hgetall ...等全量操作
  3. RDB改为AOF,甚至关闭
  4. 添加多条数据时使用管道pipeline
  5. 使用 Hash 存储
  6. 限制内存大小,避免出现 swap 或 OOM 错误

九、高可用

1. 主从复制 (读写分离)

  1. 主可写,从不可写

  2. 主挂了,从不能为主

  3. 从服务器使用 replicaof 命令开启

2. 哨兵

当主服务器下线,Sentinel 将从服务器升级为主服务器。

初始化

Sentinel 向 Master 发送 info 命令,取得 Slave 服务器地址,之后向 Master、Slave 发送 info 命令,获得 Redis 状态。

Sentinel 向 Master、Slave 订阅 :hello 频道,并向频道发送自身消息,让 Sentinel 之间可以互相感知。

Sentinel Leader 选举

Sentinel 每秒向 Redis 发送心跳连接,若无回应则视为主观下线,并向其他 Sentinel 发送查找命令,若有 quorum 数量的 Sentinel 都认为该 Redis 下线,将被判定为客观下线

当 Master 客观下线,将使用 Raft 协议选出 Leader Sentinel 运行 Redis 的故障转移

Raft

选举开始,所有节点都是 Follower。如果收到 RequestVote (投票给我) 、AppendEntries (已选出Leader) 的请求,则保持 Follower 状态。

一段时间(随机)内没收到请求,则将身分转换为 Candidate 开始竞选 Leader,如果获得过半票数则成为 Leader。

如果最后未选出 Leader,则 Term + 1,开启下一轮选举。

故障转移

  1. 选出 Slave 取代原 Master ,并让其他 Slave 复制新 Master

    选择标准 (1) slave-priority 最高 (2) 复制偏移量最大 (3) run_id 最小[重启最少次]

  2. 向客户端返回新 Master 地址

  3. 更新所有 Redis 的 redis.conf、sentinel.conf

3. Codis (Proxy)

**优点: **

  1. 客户端透明,和 Codis 交互与和 Redis 交互相同
  2. 支持在线数据迁移
  3. 支持高可用 (Redis、Proxy)
  4. 数据自动均衡分配
  5. 支持 1024 个 Redis 实例

缺点:

  1. 某些命令不支持
  2. 只有一个 Codis,性能将下降20%
  3. 采用自有的 Redis 分支,与原版不同步

4. Redis Cluster

优势

  1. 高性能

    多主节点、负载均衡、读写分离

  2. 高可用

    主从复制、Raft选举

  3. 易扩展

    添加、移除节点,不须停机

    数据分片

  4. 原生

    不需要其他代理或工具,和单机 Redis 完全兼容

失效判定

  1. 半数以上主节点当机 (无法投票)
  2. 某个分区的主、从节点同时当机 (slot槽不连续)

副本飘移

集群中拥有最多从机的节点组,漂移到单点的主从节点组

十、高并发问题

1. 缓存穿透

查找缓存中 Key 不存在的数据,会穿透缓存去查数据库,导致DB压力过大

解决方案:

  1. 结果为空也进行缓存,TTL 设短一些,且在对数据库 insert 数据后清除缓存

    问题: 缓存空值将占用空间

  2. 使用布隆过滤器。先使用布隆过滤器查找 Key 是否存在,不存在则返回,存在再查缓存与DB

2. 缓存雪崩

大量缓存在同一时刻失效,导致客户端直接查找DB,造成DB压力过大

解决方案:

  1. 让 Key 的失效期分散
  2. 设置二级缓存(本地缓存) - 可能存在数据不一致问题
  3. 高可用(读写分离)

3. 缓存击穿

热Key失效,导致DB某个纪录瞬间被大量访问

解决方案:

  1. 使用分布式锁 setnx,让其他线程处于等待状态,来保证DB安全
  2. Key 不设置超时时间,过期策略使用 validate-lru

4. 数据一致性 - 延迟双删

  1. 更新DB后删除缓存,读数据再填充缓存
  2. 2秒后再删除一次缓存
  3. 设置缓存过期时间 10秒 or 1小时
  4. 若缓存删除失败,则记录到日志,并用脚本提取后删除 (1天)

5. 大Key

  1. Value 是 String 类型,可以存到 MongoDB 或 CDN 上,如果必须用 Redis,则单独存储,并且采一主多从架构。
  2. hash、set、zset、list 类型,元素过多,可以将 Key 进行 Hash 取模后生成新 Key,将 Key 进行分拆
  3. 删除使用 unlink 而非 del 命令
免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

java

1

相关文章推荐

未登录头像

暂无评论