• 5.1 Redis集群模式


     
    

     引子

            前面我们介绍了主从复制以及哨兵,它们可以提高读的并发,但是单个master容量有限,数据达到一定程度会有瓶颈,这个时候可以通过水平扩展为多master-slave成为集群。

            那么接下来我们就介绍redis-cluster:它可以支撑多个master-slave,支持海量数据,实现高可用与高并发。

            哨兵模式其实也是一种集群,它能够提高读请求的并发,但是容错方面可能会有些问题,比如master同步数据给slave的时候,这其实是异步复制吧,这个时候master挂了,那么slave上的数据就没有master新,数据同步需要时间的,1-2秒的数据会丢失。master恢复并转换成slave后,新数据则丢失。

    特点

    1.  每个节点知道彼此之间的关系,也会知道自己的角色,当然他们也会知道自己存在于一个集群环境中,它们彼此之间可以交互和通信,比如ping  pong。那么这些关系都会保存到某个配置文件中,每个节点都有,这个我们在搭建的时候会做配置的。
    2. 客户端要和建立连接的话,只需要和其中一个建立关系就行。
    3. 某个节点挂了,也是通过超过半数的节点来进行的检测,客观下线后主从切换,和我们之前在哨兵模式中提到的是一个道理。
    4. Redis中存在很多的插槽,又可以称之为槽节点,用于存储数据,这个先不管,后面再说。

    集群容错

            构建Redis集群,需要至少3个节点作为master,以此组成一个高可用的集群,此外每个master都需要配备一个slave,所以整个集群需要6个节点,这也是最经典的Redis集群,也可以称之为三主三从,容错性更佳。所以在搭建的时候需要有6台虚拟机。

    • 集群也可以在单服务器构建,称之为伪集群,但是生产环境肯定是真的,所以建议用6台。
    • 克隆后务必关闭Redis。

    构建Redis集群配置

    redis.conf配置

    # 开启集群模式
    cluster-enabled yes
    # 每一个节点需要有一个配置文件,需要6份。每个节点处于集群的角色都需要告知其他所有节点,彼此知道,这个文件用于存储集群模式下的集群状态等信息,这个文件是由redis自己维护,我们不用管。如果你要重新创建集群,那么把这个文件删了就行
    cluster-config-file nodes-201.conf
    # 超时时间,超时则认为master宕机,随后主备切换
    cluster-node-timeout 5000
    # 开启AOF
    appendonly yes

    启动6个redis实例

    1. 启动6台
    2. 如果启动过程出错,把rdb等文件删除清空

    创建集群

    #####
    # 注意1:如果你使用的是redis3.x版本,需要使用redis-trib.rb来构建集群,最新版使用C语言来构建了,这个要注意
    # 注意2:以下为新版的redis构建方式
    #####

    # 创建集群,主节点和从节点比例为1,1-3为主,4-6为从,1和4,2和5,3和6分别对应为主从关系,这也是最经典用的最多的集群模式
    redis-cli --cluster create ip1:port1 ip2:port2 ip3:port3 ip4:port4 ip5:port5 ip6:port6 --cluster-replicas 1

    slots:槽,用于装数据,主节点有,从节点没有。

    检查集群信息

    redis-cli  --cluster  check  192.168.25.64:6380

    springboot集成Redis集群配置:

    1. spring:
    2. redis:
    3. password: 1TdhblkFcdhx2a
    4. cluster:
    5. nodes: 192.168.1.201:6379,192.168.1.202:6379,192.168.1.203:6379,192.168.1.204:6379,192.168.1.205:6379,192.168.1.206:6379

    Redis缓存穿透、雪崩方案与批量查询的优化设计

    Redis缓存穿透

    什么是缓存穿透?

    热点key在缓存中查无数据,转向数据库也查无,大量的请求就会直接打在数据库上,给数据库增加压力。例如商品分类

    解决方案:

    1. 布隆过滤器

    2. 缓存空对象

    缓存穿透出现的原因:

    例子:

           用户查询数据,如果缓存中没有数据,就会去数据库中查询,然后再加入缓存中,之后的请求就可以在缓存中查询了;

            如果用户在查询的时候,使用非法请求,每次查询到的都是数据库中没有的数据,这样就无法存入缓存之中了;

            这样就会导致,如果用户连续发送这种数据库中根本不存在值的查询,这种请求上千上万次,这种不经过缓存而直接请求数据库,就被称为缓存穿透。

    实例:

    不为空才放入缓存之中(这样有可能会造成缓存穿透)

    解决方案: 不管是否为空,都将其放入缓存

    (为防止占用过多无效key占用存储,为这种类型的key设置一个过期时间 [5-10分钟])

    为空的key设置过期时间,不为空的key设置永久存在

    就算将来这种key的值会有值了, set方法是会去覆盖的,所以无需担心

     
    

    布隆过滤器

    相当于在Redis前加了一层过滤器,用户请求去查询数据,如果缓存中和数据库中都没有数据,

    那么布隆过滤器中也必定没有,这样请求经过过滤器得知数据库中没有数据,那么请求就不会访问缓存和数据库了;

    将key 存放在 二进制数组中,一个二进制单位可以存放一个或者多个key,这种方式查询起来key是否存在是非常快且迅速的; (1就是存在 key的二进制单位,0就是一个key都没存放)

    但是尽量不要用,因为存在缺陷:

    1. 如果删除了数据库和redis中的数据,但是布隆过滤器中的数据是没法删除的,因为一个二进制单位中可能绑定有多个key,没法达到修改二进制单位为0而达到删除效果;

    2. 布隆过滤器存在误判的概念,二进制数组越大,误差率越低;

    3. 使用布隆过滤器,代码复杂度会增大, 维护难度会增大,(维护一个集合,这个集合中存在很多的key)

    4. 集群和分布式环境下,布隆过滤器需要和redis结合使用,数据需要保存到redis中,保证各个节点都要可用;

    布隆过滤器基本原理

    1、初始化所有数组值都为0

    2、一个数值,更具hash算法,更改三个数值。如subCat:9999本不存在,但hash后发现hash的三个值,都是1,则会误判这个值是存在的。因此布隆过滤器可以判断一个值绝对不存在,但是不能判断一个值是否一定存在。

    3、删除键是不能删的,因为可能有重复键值。

    Redis缓存雪崩
    

    什么是缓存雪崩

    • 缓存的key是可以设置过期时间的;

    • 如果同一个时刻,有大面积的key过期失效;

    • 恰恰这个时候,又有大量的请求进来;

    这个时候就会出现大量请求不经过缓存,而直接落到数据库上,造成数据库崩溃,这种现象就称为缓存雪崩。

    如何预防雪崩

    对于这种现象,想要完全解决是没法实现的,只能预防,去缓解雪崩这一现象;

    1. 可以设置key永不过期;

      1. 有些数据不存储在数据库中,可以设置有效时间,比如验证码;

      2. 其他一些数据,第一次需要经过数据库查询的就设置为永不过期,后期可以手动的进行过期删除;

    2. 尽量让缓存key,过期时间错开

      1. 每次服务器重启初始化的时候,大量的数据加载到缓存中,其中也包括这些设置了过期时间的缓存;

      2. 如果每种类型的数据过期时间都设置一样的话,有可能会导致过期时间一到,大面积key失效,造成雪崩

        1. 使用可以给每种key都设置成不同的过期时间,尽量避开同一时间大量缓存key失效;

      3. 多缓存结合

        1. 比如: 请求进来先查询Redis, 如果没有再查询其他缓存库,最终都没有再查询数据库,这样可以有效降低缓存雪崩出现的概率;

        2. 越后面,的缓存中的key,设置的过期时间越久

    3. 采购第三方Redis

       

    Redis批量查询的优化

    批量查询mget   
    1. /**
    2. * 批量查询 mget
    3. * @param keys
    4. * @return
    5. */
    6. @GetMapping("/mget")
    7. public Object mget(String... keys) {
    8. List<String> keysList = Arrays.asList(keys);
    9. return redisOperator.mget(keysList);
    10. }
    1. /**
    2. * 批量查询,对应mget
    3. * @param keys
    4. * @return
    5. */
    6. public List<String> mget(List<String> keys) {
    7. return redisTemplate.opsForValue().multiGet(keys);
    8. }

    批量查询pipeline

    当客户端client与Redis建立连接后,在一段时间内,不会马上关闭管道连接;

    管道批量查询支持的类型更加丰富;

    mget这种,一次批量查询只能处理单个类型的批量数据;

    1. /**
    2. * 批量查询 pipeline
    3. * @param keys
    4. * @return
    5. */
    6. @GetMapping("/batchGet")
    7. public Object batchGet(String... keys) {
    8. List<String> keysList = Arrays.asList(keys);
    9. return redisOperator.batchGet(keysList);
    10. }
    1. /**
    2. * 批量查询,管道pipeline
    3. * @param keys
    4. * @return
    5. */
    6. public List<Object> batchGet(List<String> keys) {
    7. // nginx -> keepalive
    8. // redis -> pipeline
    9. List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
    10. @Override
    11. public String doInRedis(RedisConnection connection) throws DataAccessException {
    12. StringRedisConnection src = (StringRedisConnection)connection;
    13. for (String k : keys) {
    14. src.get(k);
    15. }
    16. return null;
    17. }
    18. });
    19. return result;
    20. }
    RedisOperator工具类代码:
    1. import org.springframework.beans.factory.annotation.Autowired;
    2. import org.springframework.dao.DataAccessException;
    3. import org.springframework.data.redis.connection.RedisConnection;
    4. import org.springframework.data.redis.connection.StringRedisConnection;
    5. import org.springframework.data.redis.core.RedisCallback;
    6. import org.springframework.data.redis.core.StringRedisTemplate;
    7. import org.springframework.stereotype.Component;
    8. import java.util.List;
    9. import java.util.Map;
    10. import java.util.Set;
    11. import java.util.concurrent.TimeUnit;
    12. /**
    13. * @Title: Redis 工具类
    14. * @author lvxiaosha
    15. */
    16. @Component
    17. public class RedisOperator {
    18. // @Autowired
    19. // private RedisTemplate<String, Object> redisTemplate;
    20. @Autowired
    21. private StringRedisTemplate redisTemplate;
    22. // Key(键),简单的key-value操作
    23. /**
    24. * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
    25. *
    26. * @param key
    27. * @return
    28. */
    29. public long ttl(String key) {
    30. return redisTemplate.getExpire(key);
    31. }
    32. /**
    33. * 实现命令:expire 设置过期时间,单位秒
    34. *
    35. * @param key
    36. * @return
    37. */
    38. public void expire(String key, long timeout) {
    39. redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    40. }
    41. /**
    42. * 实现命令:INCR key,增加key一次
    43. *
    44. * @param key
    45. * @return
    46. */
    47. public long incr(String key, long delta) {
    48. return redisTemplate.opsForValue().increment(key, delta);
    49. }
    50. /**
    51. * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
    52. */
    53. public Set<String> keys(String pattern) {
    54. return redisTemplate.keys(pattern);
    55. }
    56. /**
    57. * 实现命令:DEL key,删除一个key
    58. *
    59. * @param key
    60. */
    61. public void del(String key) {
    62. redisTemplate.delete(key);
    63. }
    64. // String(字符串)
    65. /**
    66. * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key
    67. *
    68. * @param key
    69. * @param value
    70. */
    71. public void set(String key, String value) {
    72. redisTemplate.opsForValue().set(key, value);
    73. }
    74. /**
    75. * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
    76. *
    77. * @param key
    78. * @param value
    79. * @param timeout
    80. * (以秒为单位)
    81. */
    82. public void set(String key, String value, long timeout) {
    83. redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    84. }
    85. /**
    86. * 实现命令:GET key,返回 key所关联的字符串值。
    87. *
    88. * @param key
    89. * @return value
    90. */
    91. public String get(String key) {
    92. return (String)redisTemplate.opsForValue().get(key);
    93. }
    94. /**
    95. * 批量查询,对应mget
    96. * @param keys
    97. * @return
    98. */
    99. public List<String> mget(List<String> keys) {
    100. return redisTemplate.opsForValue().multiGet(keys);
    101. }
    102. /**
    103. * 批量查询,管道pipeline
    104. * @param keys
    105. * @return
    106. */
    107. public List<Object> batchGet(List<String> keys) {
    108. // nginx -> keepalive
    109. // redis -> pipeline
    110. List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
    111. @Override
    112. public String doInRedis(RedisConnection connection) throws DataAccessException {
    113. StringRedisConnection src = (StringRedisConnection)connection;
    114. for (String k : keys) {
    115. src.get(k);
    116. }
    117. return null;
    118. }
    119. });
    120. return result;
    121. }
    122. // Hash(哈希表)
    123. /**
    124. * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
    125. *
    126. * @param key
    127. * @param field
    128. * @param value
    129. */
    130. public void hset(String key, String field, Object value) {
    131. redisTemplate.opsForHash().put(key, field, value);
    132. }
    133. /**
    134. * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
    135. *
    136. * @param key
    137. * @param field
    138. * @return
    139. */
    140. public String hget(String key, String field) {
    141. return (String) redisTemplate.opsForHash().get(key, field);
    142. }
    143. /**
    144. * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
    145. *
    146. * @param key
    147. * @param fields
    148. */
    149. public void hdel(String key, Object... fields) {
    150. redisTemplate.opsForHash().delete(key, fields);
    151. }
    152. /**
    153. * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
    154. *
    155. * @param key
    156. * @return
    157. */
    158. public Map<Object, Object> hgetall(String key) {
    159. return redisTemplate.opsForHash().entries(key);
    160. }
    161. // List(列表)
    162. /**
    163. * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
    164. *
    165. * @param key
    166. * @param value
    167. * @return 执行 LPUSH命令后,列表的长度。
    168. */
    169. public long lpush(String key, String value) {
    170. return redisTemplate.opsForList().leftPush(key, value);
    171. }
    172. /**
    173. * 实现命令:LPOP key,移除并返回列表 key的头元素。
    174. *
    175. * @param key
    176. * @return 列表key的头元素。
    177. */
    178. public String lpop(String key) {
    179. return (String)redisTemplate.opsForList().leftPop(key);
    180. }
    181. /**
    182. * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
    183. *
    184. * @param key
    185. * @param value
    186. * @return 执行 LPUSH命令后,列表的长度。
    187. */
    188. public long rpush(String key, String value) {
    189. return redisTemplate.opsForList().rightPush(key, value);
    190. }
    191. }

  • 相关阅读:
    Web3 游戏发展趋势的 5 个预测
    一文搞清楚Java中的方法、常量、变量、参数
    五、资源控制器
    通讯网关软件014——利用CommGate X2HTTP实现HTTP访问OPC Server
    蓝桥杯国奖一等奖,经历回顾
    【Vue3从零开始-实战】S10:Toast弹窗组件开发
    SpringBoot 2.18升级到2.7.5, 踩到的坑
    zeno使用方法笔记
    关于SQLSERVER触发器的一个问题
    Imitation Learning(模仿学习)
  • 原文地址:https://blog.csdn.net/Xx13624558575/article/details/126581421