我是靠谱客的博主 高高灯泡,最近开发中收集的这篇文章主要介绍高并发核心技术Redis系列(八)--------企业级解决方案一、Redis企业级解决方案,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

目录

一、Redis企业级解决方案

1 RedisDesktopManager

2 Redis脑裂

2.1 概念

2.2 解决方案

3 缓存预热

 4 缓存穿透

4.1 概念

4.2 解决方案

5 缓存击穿

5.1 概念

5.2 解决方案

6 缓存雪崩

6.1 概念

6.2 解决方案

7 分布式锁

7.1 设置锁和过期时间

7.2 防止误删

7.3 保证删除原子性

8 消息队列

8.1 List消息队列

8.2 发布/订阅消息队列

9 数据一致性解决方案

 9.1 延时双删策略

10 企业级持久化解决方案

10.1 RDB的生成策略

10.2 AOF的生成策略

10.3 企业级的数据备份方案


一、Redis企业级解决方案

1 RedisDesktopManager

一款好用的Redis桌面管理工具,支持命令控制台操作,以及常用,查询key,rename,delete等操作。

RedisDesktopManager不支持集群操作。

1. 傻瓜式安装该工具

 2. 安装成功后,启动该工具

 3. 启动后,创建新的连接

 4. 创建连接成功

 5. 双击进入数据库,可以查看所有的key及value、过期时间,同时可以修改key或删除key

 6. 在数据库上右键可以选择新增key或筛选key

 筛选出来包含1的key

 注:对key更改后(新增、修改、删除),需要通过右键连接名->reload重新加载

2 Redis脑裂

2.1 概念


1. 假设现在有三台机器,分别安装了redis服务,结构如图

 2. 如果此时master服务器所在区域网络通信出现异常,导致和两台slave机器无法正常通信,但是和客户端的连接是正常的。那么sentinel就会从两台slave机器中选举其中一个作为新的master来处
理客户端请求。

 3. 这个时候,已经存在两台master服务器,client发送的数据会持续保存在旧的master服务器中,而新的master和slave中没有新的数据。如果一分钟以后,网络恢复正常,服务之间能够正常通信。此时,sentinel会把旧的master会变成新的master的slave节点。

 4. 问题出现了,我们都知道,slave会从master中同步数据,保持主从数据一致。这个时候,变成了slave节点的旧master会丢失掉通信异常期间从客户端接收到的数据。

2.2 解决方案

redis.conf配置参数:

min-replicas-to-write 1
min-replicas-max-lag 10

第一个参数表示最少的slave节点为1个
第二个参数表示数据复制和同步的延迟不能超过10秒
配置了这两个参数:如果发生脑裂:原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。

3 缓存预热

新启动的系统没有任何缓存数据,在缓存重建数据的过程中,系统性能和数据库负载都不太好,所以最好是在系统上线之前就把要缓存的热点数据加载到缓存中,这种缓存预加载手段就是缓存预热。

 4 缓存穿透

4.1 概念

如果某个key对应的数据不存在,而又未对该key做缓存,所以每次请求都会穿过缓存直接到数据库进行查询,并发量高的情况下进而导致数据库直接宕机,这就是缓存穿透。

 4.2 解决方案

1. 对空值缓存:如果一个查询返回的数据为空(不管数据是否存在),我们仍然把这个空结果缓存,设置空结果的过期时间会很短,最长不超过5分钟。


2. 设置白名单:使用bitmaps类型定义一个可以访问的名单,用户id作为偏移量,每次访问查询是否在白名单中,如果不存在,则拒绝访问。


3. 布隆过滤器:类似一个hash set,用来判断某个元素(key)是否在某个集合中。
和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个
存储一个标志,用来判断key是否在集合中。

5 缓存击穿

5.1 概念

某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

 5.2 解决方案

1. 加互斥锁:在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,其他线程直接查询缓存。


2. 热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。

6 缓存雪崩

6.1 概念

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。


和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。


 

 6.2 解决方案

1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
3. 设置热点数据永远不过期。

7 分布式锁

随着业务发展的需要,原单机部署的系统被演化成分布式集群系统,由于分布式系统多线程、多进程且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

  • 假设订单系统部署两台机器上,不同的用户都要同时买10台iphone,分别发了一个请求给订单系统。
  • 接着每个订单系统实例都去数据库里查了一下,当前iphone库存是12台。
  • 12台库存大于了要买的10台数量啊!
  • 于是每个订单系统实例都发送SQL到数据库里下单,然后扣减了10个库存,其中一个将库存从12台扣减为2台,另外一个将库存从2台扣减为-8台。

 分布式锁主流的实现方案:

1. 基于数据库实现分布式锁
2. 基于缓存(Redis等)
3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:

1. 性能:Redis最高
2. 可靠性:Zookeeper最高

7.1 设置锁和过期时间

1. 通过setnx上锁
由于setnx只有不存在该key的时候,可以设置成功,并返回1,否则设置失败,并返回0。
 

setnx lock A //获取锁,并对lock上锁
setnx lock B //其他服务器试图获取锁时,失败

2. 通过del释放锁

del lock //释放锁,此时其他服务器可以获取锁

 3. 如果锁一直不释放,需要增加过期时间,防止资源浪费。

expire lock 10 //给锁添加个过期时间

4. 如果在上锁之后,设置过期时间之前,服务器异常,就无法设置过期时间,可以在上锁的同时设置过期时间。

set lock 1 nx ex 10 //上锁的同时设置过期时间

7.2 防止误删

 避免误删情况出现,可以在加锁过程中添加一个加锁的唯一id,通过跟该id对比,阻止误删的情况出现。

//连接到自己的redis服务器
    Jedis jedis = new Jedis("192.168.56.31",6379);
    UUID uuid = UUID.randomUUID();
    jedis.setnx("lock",uuid.toString());

    /*
    执行业务代码
* */
    //释放锁的时候,通过uuid对比下是不是自己加的锁
    String lockuuid = jedis.get("lock");
    if (uuid.toString().equals(lockuuid))
   {
     
      //如果是自己加的锁,则释放
      jedis.del("lock");
   }

7.3 保证删除原子性

 Lua脚本
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Redis中引入lua的优势:

  • 减少网络开销:多个请求通过脚本一次发送,减少网络延迟
  • 原子操作:将脚本作为一个整体执行,中间不会插入其他命令,无需使用事务
  • 复用:客户端发送的脚本永久存在redis中,其他客户端可以复用脚本
  • 可嵌入性:可嵌入JAVA,C#等多种编程语言,支持不同操作系统跨平台交互

lua进行比较uuid,对比成功后删除键值对的代码:

if redis.call('get', KEYS[1]) == ARGV[1]
  then
  return redis.call('del', KEYS[1])
else
  return 0
end

if 中的比较如果是true , 那么 执行 del 并返回del结果;如果 if 结果为false 直接返回 0 。
其中的KEYS[1] , ARGV[1] 是参数,我们只调用 jedis 执行脚本的时候,传递这两个参数就可以了。
通过jedis执行lua脚本

//连接到自己的redis服务器
Jedis jedis = new Jedis("192.168.56.31",6379);

UUID uuid = UUID.randomUUID();

jedis.setnx("lock",uuid.toString());

String lockuuid = jedis.get("lock");

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return

redis.call('del',KEYS[1]) else return 0 end";// lua脚本,用来释放分布式锁

//第一个参数是lua脚本,第二个参数是需要判断的key,第三个参数是key所对应的value

jedis.eval(luaScript,Arrays.asList("lock"),Arrays.asList(lockuuid));

通过lua脚本进行比对删除,保证原子性操作,防止出现问题。

8 消息队列

8.1 List消息队列

List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型。

 生产者使用 lpush发布消息

lpush queue msg1
lpush queue msg2

 消费者这一侧,使用 rpop拉取消息:

rpop queue
rpop queue

 当队列中已经没有消息了,消费者在执行 RPOP 时,会返回 NULL。

一般编写消费者逻辑时,通过一个“死循环”实现,如果此时队列为空,那消费者依旧会频繁拉取消息,造成资源浪费。

while(true)
{
String msg = jedis.rpop("queue");
}

Redis 提供「阻塞式」拉取消息的命令:brpop / blpop,这里的 B 指的是阻塞(Block)。

 brpop key timeout:移除并返回最后一个值,同时需要传入一个超时时间(timeout),如果设置为0,则表示不设置超时,直到有新消息才返回,否则会在指定的超时时间后返回 NULL。

brpop queue 0//获取queue最后一个值,如果没有值,则一直等待

缺点:
不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,
即不支持多个消费者消费同一批数据
消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了


消费者(Customer):

Jedis jedis = new Jedis("192.168.56.31",6379);
System.out.println("开始监听");
while (true)
{
  List<String> msg =  jedis.brpop(0,"queue");
  System.out.println("接受消息:");
  //一般来说 一条消息分为两部分,第一部分是list的key,第二部分为value
  for (String m : msg)
 {
    System.out.print(m + " ");
 }
}

生产者(Producer):

Jedis jedis = new Jedis("192.168.56.31",6379);
Scanner sc = new Scanner(System.in);
while (true)
{
  System.out.println("输入发送的消息:");
  String msg = sc.next();
  jedis.lpush("queue",msg);
}

8.2 发布/订阅消息队列

Redis 提供了 PUBLISH / SUBSCRIBE 命令,来完成发布、订阅的操作。

 多个消费者,同时消费同一批数据。

//多个客户端同时订阅queue频道
SUBSCRIBE queue

通过生产者,发布一条消息。

PUBLISH queue msg1

客户端接收到消息

SUBSCRIBE queue
// 收到新消息
1) "message"
2) "queue"
3) "msg1"

使用 Pub/Sub 这种方案,既支持阻塞式拉取消息,还很好地满足了多组消费者,消费同一批数据的业务需求。
但是该方案会引起消息丢失

  • 消费者下线
  • Redis 宕机

消费者:通过jedis订阅频道,需要一个JedisPubSub子类对象,并重写onMessage方法用于接受消息

public class Customer extends JedisPubSub {
  public void onMessage(String channel, String message) {
    System.out.println("接收到消息:" + channel + ":" + message);
 }
  public static void main(String[] args) {
    Jedis jedis = new Jedis("192.168.56.31",6379);
    //通过jedis订阅频道,需要一个JedisPubSub子类对象,并重写onMessage方法用于接受消息
    jedis.subscribe(new Customer(),"queue");
 }
}

生产者:

Jedis jedis = new Jedis("192.168.56.31",6379); //第一个参数是ip地址,第二个参数是端口
Scanner sc = new Scanner(System.in);
while (true)
{
 System.out.println("输入发送的消息:");
 String msg = sc.next();
 jedis.publish("queue",msg);
}

9 数据一致性解决方案

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存
(Redis)和数据库(MySQL)间的数据一致性问题。

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
例:
1. 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
2. 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情
况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

 9.1 延时双删策略

1. 先删除缓存。
2. 再写数据库。
3. 休眠500毫秒;
4. 再次删除缓存。

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。
缺点:结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

10 企业级持久化解决方案

在企业中不要仅仅使用RDB,因为那样会导致丢失很多数据。
也不要仅仅使用AOF,因为那样有两个问题:

  1. 通过AOF做冷备,没有RDB做冷备,来的恢复速度更快;
  2. RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug。

综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择;
用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复。
如果RDB在执行snapshotting操作,那么redis不会执行AOF rewrite;如果redis再执行AOF rewrite,那么就不会执行RDB snapshotting。
如果RDB在执行snapshotting,此时用户执行BGREWRITEAOF命令,那么等RDB快照生成之后,才会去执行AOF rewrite。

10.1 RDB的生成策略


如果希望能确保RDB最多丢1分钟的数据,那么尽量就是每隔1分钟都生成一个快照。不过到底是10000条执行一次RDB,还是1000条执行一次RDB,这个根据需要根据自己的应用和业务的数据量来确定。


10.2 AOF的生成策略


AOF一定要打开,fsync方式选择everysec。一般可能会调整的参数可能就是下面俩参数了

auto-aof-rewrite-percentage 100

 就是当前AOF大小膨胀到超过上次100%,上次的两倍。

就是当前AOF大小膨胀到超过上次100%,上次的两倍。

根据自己的数据量来定,16mb,32mb。

10.3 企业级的数据备份方案

RDB非常适合做冷备,每次生成之后,就不会再有修改。
数据备份方案:
   1. 写定时调度脚本去做数据备份。
   2. 每小时都copy一份rdb的备份,到一个目录中去,仅仅保留最近48小时的备份。
   3. 每天都保留一份当日的rdb的备份,到一个目录中去,仅仅保留最近1个月的备份。
   4. 每次copy备份的时候,都把太旧的备份给删了。
   5. 每天晚上将当前服务器上所有的数据备份,发送一份到远程的云服务上去。

最后

以上就是高高灯泡为你收集整理的高并发核心技术Redis系列(八)--------企业级解决方案一、Redis企业级解决方案的全部内容,希望文章能够帮你解决高并发核心技术Redis系列(八)--------企业级解决方案一、Redis企业级解决方案所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(39)

评论列表共有 0 条评论

立即
投稿
返回
顶部