Java架构之redis集群使用场景

  1. 性能和并发、分布式
    1. 让所有请求先访问缓存,如果缓存有数据,就不访问数据库,没有数据再访问数据库,适用于sql’请求次数多,但结果不经常改变
  2. 缺点
    1. 高并发下缓存与数据库一致性的问题
        1. 原因:先删缓存后更新数据库,删缓存成功更新数据失败,导致缓存没有数据,db是旧数据;先更新数据成功后删缓存失败,导致缓存是旧数据
        2. 解决:分布式锁、选择先更新数据再删缓存+消息队列弥补删缓存失败重试
          1. Setex key seconds value设置key并设置过期时间
          2. Expire key value设置过期时间,防止死锁,如果key是已经过期将会自动删除key
          3. Del key删除key
          4. 如果一个商品的key存入到缓存中,说明该商品被上锁了,执行完方法后解锁,最好是在访问数据库时加锁
        3. 分为最终一致性和强一致性。如果对数据有强一致性要求,不能放缓存。Redis所作的一切,只能保证最终一致性,方案也只能降低不一致发生的概率,无法完全避免
    2. 缓存雪崩、缓存击穿、缓存并发竞争问题
      1. 大并发项目才会遇到
      2. 缓存击穿:黑客故意请求缓存中不存在的数据,所有请求都怼到数据库上
        1. 互斥锁,缓存失效时,先获得锁再请求数据库,没得到锁则休眠一段时间重试
      3. 缓存雪崩:缓存同一时间大面积失效
        1. 给缓存的失效时间,加上一个随机值,避免集体失效
  3. 单线程的redis为什么很快
    1. 属于内存操作,单线程操作避免了频繁的上下文切换,采用了非阻塞io多路复用机制
    2. 非阻塞io多路复用机制
      1. 一个线程,根据每个socket的io流的状态,来管理多个io流。Io多路复用程序会同时监听多个socket,当被监听的socket准备好执行accept、read、write、close等操作,与这些操作对应的文件事件就会产生,io多路复用器会把所有产生事件的socket压入一个队列种,然后有序的每次仅一个socket的方式传送给文件事件分派器,文件事件分派其接收到socket之后会根据socket产生的事件类型调用对应的事件处理器进行处理

  1. 文件事件处理器
    1. Socket、
    2. io多路复用程序、
    3. 文件事件分派器dispather、
    4. 事件处理器handler
      1. 连接应答处理器:处理客户端连接请求
      2. 命令请求处理器:处理客户端传递的命令,如set、ipush等
      3. 命令回复处理器:用户返回客户端命令执行结果,如set、get
    5. 事件种类
      1. ae_readable:与两个事件处理器结合使用
      2. ae_writeable:当服务器有数据需要回传给客户端时,服务端将命令回复处理器与socket的ae_writeable事件关联起来
      3. 客户端与服务端交互过程
  2. 代码实现
    1. 请求监听器
      1. public class Acceptor implements Runnable { 
      2. private final ServerSocketChannel ssc; 
      3. private final Selector selector; 
      4. public Acceptor(Selector selector, ServerSocketChannel ssc) { 
      5.  this.ssc=ssc; 
      6.  this.selector=selector; 
      7. @Override 
      8. public void run() { 
      9. try { 
      10. SocketChannel sc= ssc.accept(); // 接受client連線請求 
      11. if(sc!=null) { 
      12. sc.configureBlocking(false); // 設置為非阻塞 
      13. SelectionKey sk =sc.register(selector, SelectionKey.OP_READ); // SocketChannel向selector註冊一個OP_READ事件,然後返回該通道的key 
      14. selector.wakeup(); // 使一個阻塞住的selector操作立即返回 
      15. sk.attach(new TCPHandler(sk, sc)); // 給定key一個附加的TCPHandler對象 
    2. 事件派发器
      1. public class TCPReactor implements Runnable { 
      2.   
      3.     private final ServerSocketChannel ssc; 
      4.     private final Selector selector; 
      5.   
      6.     public TCPReactor(int port) throws IOException { 
      7.         selector = Selector.open(); 
      8.         ssc = ServerSocketChannel.open(); 
      9.         InetSocketAddress addr = new InetSocketAddress(port); 
      10.         ssc.socket().bind(addr); // 在ServerSocketChannel綁定監聽端口 
      11.         ssc.configureBlocking(false); // 設置ServerSocketChannel為非阻塞 
      12.         SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT); // ServerSocketChannel向selector註冊一個OP_ACCEPT事件
      13. sk.attach(new Acceptor(selector, ssc)); // 給定key一個附加的Acceptor對象 
      14.     } 
      15.   
      16.     @Override 
      17.     public void run() { 
      18.         while (!Thread.interrupted()) { // 在線程被中斷前持續運行 
      19.             try { 
      20.                 if (selector.select() == 0) // 若沒有事件就緒則不往下執行 
      21.                     continue; 
      22.             } catch (IOException e) { 
      23.                 // TODO Auto-generated catch block 
      24.                 e.printStackTrace(); 
      25.             } 
      26. Set<SelectionKey> selectedKeys = selector.selectedKeys(); // 取得所有已就緒事件的key集合 
      27.             Iterator<SelectionKey> it = selectedKeys.iterator(); 
      28.             while (it.hasNext()) { 
      29.     dispatch((SelectionKey) (it.next())); // 根據事件的key進行調度 
      30.                 it.remove(); 
      31.             } 
      32.         } 
      33.     } 
      34.      
      35.     private void dispatch(SelectionKey key) { 
      36.         Runnable r = (Runnable) (key.attachment()); // 根據事件之key綁定的對象開新線程 
      37.         if (r != null) 
      38.             r.run(); 
      39.     } 
      40.   
      41. }  
    3. 事件处理器
      1. public class TCPHandler implements Runnable { 
      2.   
      3.     private final SelectionKey sk; 
      4.     private final SocketChannel sc; 
      5.   
      6.     int state;  
      7.   
      8.     public TCPHandler(SelectionKey sk, SocketChannel sc) { 
      9.         this.sk = sk; 
      10.         this.sc = sc; 
      11.         state = 0; // 初始狀態設定為READING 
      12.     } 
      13.   
      14.     @Override 
      15.     public void run() { 
      16.         try { 
      17.             if (state == 0) 
      18.                 read(); // 讀取網絡數據 
      19.             else 
      20.                 send(); // 發送網絡數據 
      21.   
      22.         } catch (IOException e) { 
      23.             System.out.println("[Warning!] A client has been closed."); 
      24.             closeChannel(); 
      25.         }  
      26.     } 
      27.       
      28.     private void closeChannel() { 
      29.         try { 
      30.             sk.cancel(); 
      31.             sc.close(); 
      32.         } catch (IOException e1) { 
      33.             e1.printStackTrace(); 
      34.         } 
      35.     } 
      36.   
      37.     private synchronized void read() throws IOException {   
      38.         byte[] arr = new byte[1024]; 
      39.         ByteBuffer buf = ByteBuffer.wrap(arr); 
      40.           
      41.         int numBytes = sc.read(buf); // 讀取字符串 
      42.         if(numBytes == -1) 
      43.         { 
      44.             System.out.println("[Warning!] A client has been closed."); 
      45.             closeChannel(); 
      46.             return; 
      47.         } 
      48.         String str = new String(arr); // 將讀取到的byte內容轉為字符串型態 
      49.         if ((str != null) && !str.equals(" ")) { 
      50.             process(str); // 邏輯處理 
      51.             System.out.println(sc.socket().getRemoteSocketAddress().toString() 
      52.                     + " > " + str); 
      53.             state = 1; // 改變狀態 
      54.             sk.interestOps(SelectionKey.OP_WRITE); // 通過key改變通道註冊的事件 
      55.             sk.selector().wakeup(); // 使一個阻塞住的selector操作立即返回 
      56.         } 
      57.     } 
      58.   
      59.     private void send() throws IOException  { 
      60.         // get message from message queue 
      61.           
      62.         String str = "Your message has sent to " 
      63.                 + sc.socket().getLocalSocketAddress().toString() + "\r\n"; 
      64.         ByteBuffer buf = ByteBuffer.wrap(str.getBytes()); // wrap自動把buf的position設為0,所以不需要再flip() 
      65.   
      66.         while (buf.hasRemaining()) { 
      67.             sc.write(buf); // 回傳給client回應字符串,發送buf的position位置 到limit位置為止之間的內容 
      68.         } 
      69.           
      70.         state = 0; // 改變狀態 
      71.         sk.interestOps(SelectionKey.OP_READ); // 通過key改變通道註冊的事件 
      72.         sk.selector().wakeup(); // 使一個阻塞住的selector操作立即返回 
      73.     }   
      74. 客户端代码
      75. public class Client { 
      76.     public static void main(String[] args) { 
      77.         String hostname=args[0]; 
      78.         int port = Integer.parseInt(args[1]); 
      79.         try { 
      80.             Socket client = new Socket(hostname, port); // 連接至目的地 
      81. PrintWriter out = new PrintWriter(client.getOutputStream()); 
      82. BufferedReader in=new BufferedReader(new InputStreamReader(client.getInputStream())); 
      83. BufferedReader stdIn=new BufferedReader(new InputStreamReader(System.in)); 
      84. String input; 
      85.             while((input=stdIn.readLine()) != null) { // 讀取輸入 
      86.                 out.println(input); // 發送輸入的字符串 
      87.                 out.flush(); // 強制將緩衝區內的數據輸出 
      88.                 if(input.equals("exit")) 
      89.                 { 
      90.                     break; 
      91.                 } 
      92.                 System.out.println("server: "+in.readLine()); 
      93.             } 
      94.             client.close(); 
      95.         } catch (UnknownHostException e) { 
      96.             System.err.println("Don't know about host: " + hostname); 
      97.         } catch (IOException e) { 
      98.             System.err.println("Couldn't get I/O for the socket connection"); 
  1. Redis的瓶颈不是cpu,而是内存或者带宽
  2. 数据类型
    1. String、hash、list、set、sorted set
  3. 过期策略:Redis采用的是定期删除+惰性删除策略
    1. 默认每隔100ms检查,是否有过期的key,有酒删除,但是并不是将所有key都检查一次,而是随机抽取检查,因此定期删除会导致很多key到时间没删除
    2. 于是,惰性删除配合酒完美了。只要客户端请求了key,如果是过期的酒会自动删除
    3. 如果定期删除没删除key,客户端也没有即时去请求key,redis的内存也还是会越来越高,这样就会采用内存淘汰机制,再redis.conf有一行配置#maxmemory-policy volatile-lru就是配置内存淘汰策略的
      1. Noeviction:当内存不足以容纳新写入数据时,新写入操作会报错
      2. Allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key,推荐使用
      3. Allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,
      4. Volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key,这种情况一般把redis既当作缓存,又做持久化存储的时候才用,不推荐
      5. Volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key,不推荐
      6. Volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除,不推荐
    4. 如果没有设置expire的key,不满足先决条件,那么以上的策略行为和noeviction没区别
  4. Redis三种集群方式
    1. 主从复制
      1. 包含一个主数据库,一个或多个从数据库实例,
      2. 客户端可对主数据库进行读写,怼从数据库进行读,主数据写入的数据会实时自动同步给从数据库
      3. 工作机制:
        1. 从数据库启动后,向主数据库发送sync命令,主数据库接收到命令后通过bgsave保存快照,并使用缓冲区记录快照这段时间执行的写命令
        2. 主数据库将保存的快照文件发送给从数据库,并继续记录执行的写命令
        3. 从数据库接收到快照文件后,加载快照文件,载入数据
        4. 主数据库快照发送完后开始向从数据库发送缓冲区的写命令,保持数据一致性
      4. 部署:
        1. Redis.conf主要配置
          1. Protected-mode no #关闭保护模式
          2. Prot 6379 设置监听端口
          3. Timeout 30客户端连接空闲多久后端口连接
          4. Daemonize yes后台运行
          5. Pidfile redis_9379.pid
          6. Logfile redis.log
          7. #以下持久化配置
          8. Save 900 1 #900秒至少一次写操作则执行bgsave进行rdb持久化
          9. Save 300 10
          10. Save 60 10000
          11. #如果禁用rdb持久化,可以添加save
          12. Rdbcompression yes 是否怼rdb文件进行压缩,建议no
          13. Dofilename dump.rdb rdb文件名称
          14. Dir /redis/datas #rdb文件保存路径,aof也保存在这里
          15. #aof配置
          16. Appendonly yes
          17. Appendfsync everysec
          18. #设置密码
          19. Requirepass 123456
        2. 配置主从复制只需调整salve的配置即可
          1. Replicaof 127.0.0.1:6379 #master的ip,prot
          2. Masterauth 1234556 #master的密码
          3. Replica-serve-stale-data no #如果slave无法与master同步,设置为从不可读方便监控脚本
        3. 启动主从数据库
          1. Redis-server master.conf
          2. Redis-server salve1.conf
          3. Redis-server salve2.conf
          4. 从主写,从从读
        4. 执行info replication可以查看连接该为数据库的其他库连接信息
    2. 哨兵模式
    3. Redis-cluster集群
      1. 特点:客户端与redis节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点
      1. 引入主从复制模式,一个主节点对应一个或多个从节点,当主节点宕机,就会启用从节点
      2. 当其他主节点ping一个主节点时,如果半数以上的主节点超时,那么认为主节点宕机了,就会切换到从节点
      3. 部署
        1. 复制6个redis,再每个reidis的redis.conf中配置
          1. Port 7100 #7100,7200,7300,7400,7500,7600
          2. Daemonize yes #后台运行
          3. Pidfile redis_7100.pid #pidfile对应7100,7200,7300,7400,7500,7600
          4. Cluster-enabled yes #开启集群模式
          5. Masterauth password #如果设置了密码,指定master密码
          6. Cluster-config-file nodes_7100.conf #集群的配置文件,对应6个
          7. Cluster-node-timeout 15000 请求超时,默认15秒
        2. 启动6个实例
          1. Redis-server redis_7100.conf 对应6个节点
        3. 通过命令将6个节点组成一个三主节点三从节点的集群
          1. Redis-cli-cluster create –cluster-replicas 1 127.0.0.1:7100
          2. 127.0.0.1:7200 127.0.0.1:7300 127.0.0.1:7400 127.0.0.1:7500
          3. 127.0.0.1:7600 -a password
        4. 连接7100设置一个值
          1. Redis-cli -p 7100 -c – a password  c表示集群
          2. Cluster nodes可查看集群的节点信息
          3. 通过7200 :kill -9 pid杀死进程来验证集群的高可用,重新进入集群执行cluster nodes可以看到7200fail了,但是7400成了master。重新启动7200,就成了slave从节点
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值