redis笔记

Redis全面笔记:安装、集群与缓存问题
本文是Redis笔记,介绍了Redis特点、与MemoryCache对比,涵盖安装使用、常用配置、数据类型等内容。重点阐述集群模式,如主从复制、哨兵模式、cluster集群搭建与使用。还提及Java连接Redis方法,以及数据一致性和缓存穿透、雪崩、击穿等问题的解决方案。

redis笔记

简介

redis是一个高性能的key-value数据库,有一下3个特点:

  • 支持数据的持久化;
  • 提供字符串、hash、列表、无序集合、有序集合5种数据类型;
  • 支持备份,即master-slave模式;

Redis与MemoryCache比较:

  • 性能差不多,redis单线程,MemoryCache多线程,并发处理好;
  • MemoryCache只支持字符串;
  • redis支持持久化;

安装使用

linux下安装:
下载->解压->进入根目录->执行make && make install;
linux下运行:redis-server redis.conf
windows下运行:redis-server.exe redis.windows.conf;
连接:redis-cli.exe -h 127.0.0.1 -p 6379
若要外部访问,则需要在防火墙中开放相应端口,即在/etc/sysconfig/iptables文件中参照开放22端口的方法,复制一行,改为6379,然后重启防火墙(service iptables restart);

常用配置

daemonize no  #守护线程方式运行,默认非守护
protected-mode yes  #保护模式,禁用外网访问,默认开启
pidfile /var/run/redis.pid  #守护时,pid保存路径
port 6379  #端口
bind  127.0.0.1  #绑定主机ip
timeout 300   #客户端闲置多久关闭连接,0时关闭该功能
loglevel verbose  #日志级别,共四个:debug、verbose、notice、warning;
logfile stdout  #日志记录方式
databases 16  #数据库数量,通过select切换
save 900 1
save 300 10
save 60 10000  #多长时间有多少更新时,同步到数据库,以上时默认配置;
rdbcompression yes  #保存时是否压缩,默认LZF压缩
dbfilename dump.rdb  #保存的文件名
dir ./  #保存的目录
slave of ip port  #本机为slave时,指定master的ip、port
requirepass foobared  #连接密码
maxclients 128  #最大客户端数量,默认无限制
maxmemory <bytes>  #最大内存限制
appendonly no  #是否每次更新记录日志
appendfilename appendonly.aof  #日志文件名
appendfsync everysec  #日志更新条件:no、always、everysec
vm-enabled no  #是否启用虚拟内存机制
vm-swap-file /tmp/redis.swp  #虚拟内存文件全名
vm-max-memory 0  #大于该值的value数据存到虚拟内存
vm-page-size 32  #分页大小
vm-pages  #分页数量
vm-max-threads 4  #swap文件的访问线程数
include /path/to/local.conf  #从其他文件读取配置

数据类型

  • 字符串:二进制安全,可以存任何数据,比如图片,最大512M;
  • 哈希:键值对集合,适合存对象;
  • 列表:双向链表,增删快;适合排行榜、消息队列等场景;
  • 无序集合Set:哈希实现,不重复,并提供了求交集、并集、差集的功能;
  • 有序集合:Set集合加了权重参数,适合排行榜,带权重的消息队列。

命令

redis命令详情参见:https://www.runoob.com/redis/redis-commands.html

redis桌面客户端工具:https://github.com/qishibo/AnotherRedisDesktopManager/releases

小结:每种数据类型都有自己的操作命令,且一般操作哈希的以H开头,操作列表以l或r或b开头,操作集合以s开头,有序集合以z开头。

发布订阅

订阅者订阅后,就可以收到发布者推送的消息;subscribe xxx订阅xxx,publish xxx发布到xxx。subscribe前加un表示退订,加p表示按正则匹配频道。

事务

只保证三点:①exec前,命令只放入队列缓存;②事务执行后,即使执行失败,仍然继续;③命令执行时,不会插入其他客户端命令。reids中,单个命令是原子的,事务不能保证原子性。

集群

分片对象

jedis分片对象:启动多个redis节点,每个节点处理一部分数据;

自定义的分片计算:假设有n个结点,按key的hash与上Integer最大值得到一个正整数,在对n求余,得到的余数为对应redis节点;

jedis的分片计算:jedis内部封装了hash一致性算法,开发者只需要通过JedisShardInfo创建连接池即可使用;具体分片连接池代码示例见java连接redis;

hash一致性算法:取0到2^32-1的区间,将节点信息字符串通过hash映射到该区间的一个节点,然后将key也通过hash映射到该区间一个节点,然后顺时针找最近的redis节点,则该key就在该节点处理;该算法具有以下特点:

  • 区间弧段随着节点数增多而减少,扩容时迁移数据量随之减少;
  • 算法还引入虚拟节点,默认为160×节点权重值(权重默认1),这样虚拟节点越多,平衡性越好;故还可以通过调整权重值来调整数据处理比例;

主从复制

上面分片式的分布式集群不能保证数据的高可用,当有节点故障时,不能实现自动故障转移从而导致缓存雪崩,主从复制则是集群高可用的基础结构之一;

主从复制结构:即一个master主节点下有多个slave从节点,主节点数据可以读写,从节点工作在只读模式,并从主节点同步数据;redis支持1主多从,多级主从,一般最多两级主从,6个从节点;下面以1主2从为例搭建:

  • 准备3个redis节点,3个redis.conf配置文件;
  • 若从节点还有从节点,则将从节点只读关闭(slave-read-only no);
  • 启动三个节点;
  • 登录到子节点,执行命令:slaveof 主节点ip 端口;(若在子节点中添加了配置:slaveof ip port,则不需要执行该命令)
  • 主从复制完成;
    但是该形式的主从复制结构只能完成数据同步,无法转移故障;

哨兵模式

哨兵是redis提供的监听主从结构的第三方进程,所有相同的监听哨兵形成集群管理主从结构;即调用info replication命令查看主从信息,同时定时对主节点进行心跳检测,一旦发现主节点宕机则会投票选举新的主节点,并通知主从结构中所有节点更新配置;

宕机机制与选举:选举使用的是过半机制,即集群中超过半数的哨兵判断主节点宕机了才真正宕机,主节点宕机后哨兵会投票选举新的主节点,票数操作一半的当选;(选举依据:网络连接正常->5秒内回复过INFO命令->10×down-after-milliseconds内与主连接过的->从服务器优先级->复制偏移量->运行id较小)

当哨兵集群无法正常工作时,是需要重新搭建哨兵环境的,哨兵的配置文件在运行过程中可能会发生改变,重搭环境时,需要在此配好这些文件;

哨兵模式的集群搭建:

  • 启动主节点:redis-server redis.conf;
  • 启动从节点:redis-server redis.conf;
  • 修改哨兵配置文件,启动哨兵进程:redis-sentinel sentinel.conf;
  • 哨兵集群搭建完成;

哨兵集群解决了分布式集群中单个数据分片的高可用问题,但集群分片之间互不相通;

附:sentinel.conf配置文件说明:

# bind 127.0.0.1 192.168.1.1
# 守护进程模式
daemonize yes
# 保护模式,默认是yes,改为no
protected-mode no
# 端口
port 26379
# sentinel announce-ip <ip>
# sentinel announce-port <port>
# sentinel announce-ip 1.2.3.4
#工作路径
dir /tmp
# 日志文件路径和文件名(填相对路径时,是相对的工作路径)
logfile "./sentinel.log"
# 配置主节点名称、ip、端口、主观下线票数(哨兵集群最少哨兵个数)
# ip不要填127.0.0.1,会造成代码访问失效
sentinel monitor mymaster 192.168.1.1 6379 2
# 主节点名称和密码
sentinel auth-pass mymaster 1234
# master或slave多长时间(默认30秒)不能使用后标记为s_down状态。
sentinel down-after-milliseconds mymaster 30000
#执行故障转移时,最多有多少个从服务器同时对新的主服务器进行同步
sentinel parallel-syncs mymaster 1
# 投票失败后过多久继续投票,默认3minutes,改为3s方便测试
sentinel failover-timeout mymaster 3000
# sentinel notification-script mymaster /var/redis/notify.sh
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

cluster集群

redis3.0之后,出现了redis-cluster的集群计数,解决了之前集群中的分片互相不相关的问题;

特点
  • 节点两两之间互通,使用二进制协议通讯来优化传输速度;
  • 哨兵进程消失;监听和投票工作交个master完成;
  • 客户端可以不关心数据分配,由集群内部处理;
  • 引入hash槽:所有key不直接和节点信息绑定,而是绑定槽道号(slot),每个主节点各自维护维护一批槽道号,如果槽道号迁移,key也一并迁移(微调);
槽道

即每一个redis的集群节点的内存中保存两个组成槽道的数据结构,由两部分内存数据组成计算逻辑;分别是位序列和索引数组,范围都是[0, 16384(2^14)];这两种数据的功能如下:

  • 位序列(私有):每一位都是二进制,1表示在当前节点,0表示不在当前节点;这样就可以直接通过下标值快速判断当前节点能否处理;
  • 索引数组(公有):除了位序列外,每个元素还指向了对应节点信息对象的引用,在判断不在当前节点处理时,则会redirect到正确节点;
搭建

集群的搭建需要使用ruby语言环境(ruby -v查询是否安装),redis内部包含一个用ruby语言编写好的命令文件redis-trib.rb,简化了集群的搭建、扩容、管理等;下面示例搭建一个结构为3个master(集群至少需要三个主节点)和3个slave的集群(注意:在重新搭建集群时要先删除aof、dump、node等文件):

  • 修改redis.conf配置文件,修改内容为:
# 注释bind
# 关闭保护模式
protected-mode no
# 修改端口号
port 8000
# 后台运行
daemonize yes
# 日志文件路径(相对redis根目录)
logfile "./cluster8000.log"
# 持久化文件名
dump dump8000.rdb
# 改为使用aof方式持久化
appendonly yes 
appendfilename "appendonly8000.aof"
# 持久化同步策略:no、always、everysec
appendfsync everysec
# 开启集群
cluster-enabled yes
# 节点文件名
cluster-config-file ./nodes8000.conf
# 请求超时时间(默认15秒)
cluster-node-timeout 15000
  • 拷贝六份配置文件,并修改端口号和相关的文件名("%s/8000/8001/g");
  • 启动六个节点:redis-server xxxx.conf;
  • 将redis/src下的redis-trib.rb拷贝到/usr/local/bin目录下;
  • 如果节点设置的有密码,则用find搜索clients.rb文件,然后在类似/usr/lib/ruby/gems/1.8/gems/redis-3.2.1/lib/redis/client.rb的文件中添加**:password => “1234”**:
  • 创建集群:redis-trib.rb create ip1:port1 ip2:port (–replicas 1);(只添加三个主节点,–replicas 1代表自动为每一个master至少分配一个slave)
  • 添加剩下的三个从节点;
  • 集群搭建完成。
使用

集群搭建完成后,其登录命令为:redis-cli -c -h ip -p port(-c代表集群方式登录);登录后就可以查看和管理集群了;

cluster info;查看集群状态,包含状态、槽道、大小、节点数、发送接收大小等信息,其中epoch是集群的逻辑计算时间,与集群的变化和事件有关,数字越大代表配置或操作越新,该值整个集群保持一致;

cluster nodes:查看集群节点信息,包含id、ip:port、角色及状态、主节点id、管理的槽道号范围等信息;

动态添加新节点:redis-trib.rb add-node args;参数为新节点的ip:port和集群中任意节点的ip:port;添加从节点时–slave 和–master-id 必须同时配置就且指定主节点id;默认添加的新主节点没有分配槽道,需要通过命令redis-trib.rb reshard ip:port迁移,参数为任意旧节点;(注意:不能添加有节点数据和缓存数据的节点,也不能迁移有数据的槽道)

删除节点:redis-trib.rb del-node ip:port id,参数为集群中节点的ip,端口和id;(注意:只能删除没有槽道的主节点或slave节点)

平衡slot:redis-trib.rb rebalance --weight id=权重值 ip:port,权重值默认为1,该命令可以修改权重值,在根据权重值修改槽道号范围;

附:两种持久化模式,

  • aof:实时记录所有写命令,数据保存在aof文件;小量数据性能是rdb一半,海量时差不多;
  • dump:调用save命令时,保存当前内存数据到dump文件;
  • 同时开启时,aof优先级更高;
redis原命令

搭建:

  • 修改好配置文件,启动6个redis;
  • -c方式登录任意节点执行握手命令:cluster meet ip port;(是其他节点的ip和port)
  • 观察集群节点,此时还没有分配槽道;
  • 分配槽道号:cluster addsolts 槽道号;(槽道号为一个或多个数字)
# 推荐使用shell脚本分配
#分配节点1
for i in {1..5461}; do redis-cli -h 10.9.17.153 -c -p 8000 cluster addslots $i;done
# 分配节点2······
  • 添加从节点:登录希望成为从节点的节点执行cluster replicate 主节点id命令;
  • 集群搭建完成。

空槽道迁移:目标节点导入:cluster setslot 槽道号 importing 源节点id;源节点导出:cluster setslot 槽道号 migrating 目标节点id;(一次只能迁移一个!);通知过半主节点槽道迁移了:cluster setslot 槽道号 node 主节点id;对于非空槽道迁移,则是在通知其他节点前加两步:确定指定槽道所包含的keys:cluster getkeysinslot 槽道号 范围(需要在槽道号对应的节点上执行命令),迁移key:migrate 目标ip port “” 0 500 keys k1 k2 k3···

Java连接redis

一、直接在代码中连接redis:

@Test
public void  test01(){
    Jedis jedis = new Jedis("192.168.140.101", 6379);
    jedis.auth("1234");
    //业务代码······
    jedis.close();
}

二、使用连接池直接在代码中连接:

@Test
public void  test02(){
    JedisPool pool = new JedisPool("192.168.140.101", 6379);
    Jedis jedis = pool.getResource();
    jedis.auth("1234");
    //业务代码······
    jedis.close();
    pool.close();
}

三、分片连接池连接:

@Test
public void  test03(){
    List<JedisShardInfo> infos = new ArrayList<>();
    JedisShardInfo info = new JedisShardInfo("192.168.140.101", 6379);
    info.setPassword("1234");
    infos.add(info);
    //更多分片节点······
    ShardedJedisPool pool = new ShardedJedisPool(
            new JedisPoolConfig(), infos);
    ShardedJedis jedis = pool.getResource();
    //业务代码······
    jedis.close();
    pool.close();
}

四、连接哨兵:

@Test
public void testSentinel(){
    Set<String> sentinels = new HashSet<>();
    sentinels.add("192.168.140.101:26379");
    sentinels.add("192.168.140.101:26380");
    sentinels.add("192.168.140.101:26381");
    JedisSentinelPool jedisSentinelPool = 
            new JedisSentinelPool("mymaster", sentinels, "1234");
    Jedis jedis = jedisSentinelPool.getResource();
    //业务代码······
    jedis.close();
}

五、连接redis集群:

//只需要连接集群中的任意一个节点,会自动查询到其他节点信息;
//同时操作key时,还会计算槽道号,从而得到处理的主机;
@Test
public void testCluster02() throws IOException {
    JedisCluster jedisCluster = new JedisCluster(
            new HostAndPort("192.168.140.101", 8004),
            0, 500, 3, "1234",
            new JedisPoolConfig());
    //业务代码······
    jedisCluster.close();
}

数据一致性

情况一:更新数据库后执行清空缓存异常,导致缓存中仍然是旧的数据;解决办法是:每次更新数据时要先清空缓存,再更新数据库,若清空缓存失败,则暂时不更新;

情况二:高并发情况下,清了缓存,还没来得及更新数据库就被其他查询请求将旧数据存放到了缓存;解决:使用队列,每次更新操作之前先将key入缓存队列,其他查询在查询之前先检查缓存队列,若非空,则正常操作,否则直接查询数据库—>优化解决:创建固定长度的列表,更新之前先计算Hash取余的值,然后将key放在该值对应的索引位置上,有其他查询时,同样先计算Hash取余值,根据该值去列表查询是否存在,若不存在,则正常操作,否则直接查询数据库。

缓存问题

缓存穿透

指查询一个一定不存在的数据,而查不到的数据又不会写入缓存,所以每次这个查询都要去数据库,失去了缓存的意义,大流量时还可能会使数据库无法承受,其解决方案有两种:

  1. 空结果也进行缓存;
  2. 采用布隆过滤器;

缓存雪崩

一般是由于我们设置缓存时采用了相同的过期时间,从而导致缓存某一时刻同时失效,使得数据出现周期性的高压力,其解决方案是给每个key设置为一个随机的过期时间;

缓存击穿

当某个热点key缓存失效时,这时大量的访问会直接请求数据库,造成数据库压力,热点key应该具有以下特点:

  1. 这种key的访问量可能非常大;
  2. 这个key的缓存构建需要一定的时间;

其解决方案有:

  1. 设置永不过期:
  2. 设置真正的永不过期;
  3. 把过期时间设置在value中,查询时发现过期了则通过一个后台线程重新构建;
  4. 也可以在预计会有热点访问之前先手动访问一次;
  5. 使用互斥锁,只让一个线程构建,其他线程必须等待其构建完成;
  6. 提前互斥锁,发现过期时延长1秒过期时间,同时重新构建缓存;
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值