Back

redis笔记

redis

Redis本质上是一个Key-Value类型的内存数据库,支持保存多种数据结构,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。
因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。

Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

配置

  • daemonize:是否以后台进程运行,默认为no。Windows下不支持修改 。Linux平台下可以改为yes,这样就不用为了启动Redis而单独保留一个shell窗口。
  • pidfile:如以后台进程运行,则需指定一个pid,默认为/var/run/redis.pid。Windows下不支持修改。
  • bind:绑定主机IP,默认值为127.0.0.1(注释)
  • protected-mode:以保护模式运行,默认yes
  • port: 监听端口,默认为6379
  • timeout: 超时时间,默认为300(秒)
  • loglevel: 日志记录等级,有4个可选值,debug,verbose(默认值),notice,warning
  • logfile: 日志记录方式,默认值为stdout
  • databases: 可用数据库数,默认值为16,默认数据库为0
  • save : 指出在多长时间内,有多少次更新操作,就将数据同步到数据文件。这个可以多个条件配合,比如默认配置文件中的设置,就设置了三个条件。
    • save 900 1 900秒(15分钟)内至少有1个key被改变
    • save 300 10 300秒(5分钟)内至少有10个key被改变
    • save 60 10000 60秒内至少有10000个key被改变
  • rdbcompression: 存储至本地数据库时是否压缩数据,默认为yes
  • dbfilename: 本地数据库文件名,默认值为dump.rdb
  • dir: 本地数据库存放路径,默认值为 ./
  • slaveof: 当本机为从服务时,设置主服务的IP及端口(注释)
  • masterauth: 当本机为从服务时,设置主服务的连接密码(注释)
  • requirepass 连接密码(注释)
  • maxclients: 最大客户端连接数,默认不限制(注释)
  • maxmemory : 设置最大内存,达到最大内存设置后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理后,任到达最大内存设置,将无法再进行写入操作。(注释)
  • appendonly: 是否在每次更新操作后进行日志记录,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认值为no
  • appendfilename: 更新日志文件名,默认值为appendonly.aof(注释)
  • appendfsync: 更新日志条件,共有3个可选值。no表示等操作系统进行数据缓存同步到磁盘,always表示每次更新操作后手动调用fsync()将数据写到磁盘,everysec表示每秒同步一次(默认值)。
  • vm-enabled: 是否使用虚拟内存,默认值为no
  • vm-swap-file: 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
  • vm-max-memory: 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。默认值为0。

命令

  • info replication 查看信息,本机角色(master、slave)、ip、端口、状态

哨兵配置

# 这个是Redis6379配置内容,其他文件同理新增然后改一下端口即可,26380 26381
#当前Sentinel服务运行的端口
port 26381
bind 127.0.0.1
# 哨兵监听的主服务器 
sentinel monitor mymaster 127.0.0.1 6379 2
# 3s内mymaster无响应,则认为mymaster宕机了
sentinel down-after-milliseconds mymaster 3000
#如果10秒后,mysater仍没启动过来,则启动failover  
sentinel failover-timeout mymaster 10000  
# 执行故障转移时, 最多有1个从服务器同时对新的主服务器进行同步
sentinel parallel-syncs mymaster 1
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码,没有的话不用设置
sentinel auth-pass mymaster 123456

启动哨兵命令:redis-server.exe sentinel.conf –sentinel

redis的数据类型,以及每种数据类型的使用场景

(一)String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。

(二)hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。

(三)list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。

(四)set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。 另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

(五)sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。还可以用来做延时任务(分布式之延时任务方案解析)、范围查找。

项目中使用redis

  • 响应请求获取数据时经过redis查询(如果没有则访问数据库,并将数据存储至redis),达到极速响应;
  • 高并发的情况,可能减少数据库压力。

redis单线程工作模型优势

  1. 纯内存操作
  2. 单线程操作,避免了频繁的上下文切换
  3. 采用了非阻塞I/O多路复用机制(单个线程,跟踪每个I/O流的状态,来管理多个I/O流。)

使用redis存在的问题

  • 缓存和数据库双写一致性问题
  • 缓存雪崩问题
  • 缓存击穿问题
  • 缓存的并发竞争问题

redis的过期策略以及内存淘汰机制

  1. 为什么不用定时删除策略?
    定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
  2. 定期删除+惰性删除是如何工作的呢?
    定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
  3. 采用定期删除+惰性删除就没其他问题了么?
    不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

在redis.conf中有一行配置

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)

1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

使用Redis实现分布式锁

//伪代码
//当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;
//当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
if(setnx(key,1) == 1){
    //设置锁超时
    expire(key,30)
    try {
        do something ......
    } finally {
        //解锁
        del(key)
    }
}

上面的伪代码中,存在着三个致命问题:

  1. setnx和expire的非原子性
    设想一个极端场景,当线程A执行setnx,成功得到了锁,setnx刚执行成功,还未来得及执行expire指令,节点1(线程A)挂掉了。
    这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。
    解决方法:使用set(key,1,30,NX)取代 setnx

  2. del 导致误删
    又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是30秒。
    如果某些原因导致线程A执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。
    随后,线程A执行完了任务,线程A接着执行del指令来释放锁。
    但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。
    解决方法:可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。

    String threadId = Thread.currentThread().getId()
    set(key,threadId ,30,NX)
    //解锁
    if(threadId .equals(redisClient.get(key))){
        del(key)
    }
    

    但是,这样做又隐含了一个新的问题,判断和释放锁是两个独立操作,不是原子性。 使用用Lua脚本来实现:

    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));
    
  3. 出现并发的可能性 还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。
    我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
    当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。
    守护线程从第29秒开始执行,每20秒执行一次。
    当线程A执行完任务,会显式关掉守护线程。
    另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。
    这把锁到了超时的时候,没人给它续命,也就自动释放了。

REF

分布式之redis复习精讲
Redis的那些最常见面试问题
什么是分布式锁?

comments powered by Disqus