目录结构如下:
Redis是一个高性能的key-value数据库。Redis对数据的操作都是原子性的。
优点:
缺点:对join或其他结构化查询的支持就比较差。
将用户socket对应的文件描述符(file description)注册进epoll,然后epoll帮你监听哪些socket上有消息到达。当某个socket可读或者可写的时候,它可以给你一个通知。只有当系统通知哪个描述符可读了,才去执行read操作,可以保证每次read都能读到有效数据。这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的复用指的是复用同一个线程。
Redis支持五种数据类型:
字符串类型的值可以是字符串、数字或者二进制,但值最大不能超过512MB。
常用命令:set, get, incr, incrby, desr, keys, append, strlen
SET name tysonGET name
INCR num //若键值不是整数时,则会提示错误。INCRBY num 2 //增加指定整数DESR num //递减数字INCRBY num 2.7 //增加指定浮点数
keys list* 列出匹配的key
APPEND name " dai" 追加值
STRLEN name 获取字符串长度
MSET name tyson gender male 同时设置多个值
MGET name gender 同时获取多个值
GETBIT name 0 获取0索引处二进制位的值
FLUSHDB 删除当前数据库所有的key
FLUSHALL 删除所有数据库中的key
SETNX key value:当key不存在时,将key的值设为value。若给定的key已经存在,则SETNX不做任何操作。
SETEX key seconds value:比SET多了seconds参数,相当于SET KEY value + EXPIRE KEY seconds,而且SETEX是原子性操作。
redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan。
scan的缺点:在scan的过程中如果有键的变化(增加、删除、修改),遍历过程可能会有以下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键。
scan命令用于迭代当前数据库中的数据库键:SCAN cursor [MATCH pattern] [COUNT count]
scan 0 match * count 10 //返回10个元素
SCAN相关命令包括SSCAN 命令、HSCAN 命令和 ZSCAN 命令,分别用于集合、哈希键及有序集合。
SET password 666EXPIRE password 5TTL password //查看键的剩余生存时间,-1为永不过期SETEX password 60 123abc //SETEX可以在设置键的同时设置它的生存时间
EXPIRE时间单位是秒,PEXPIRE时间单位是毫秒。在键未过期前可以重新设置过期时间,过期之后则键被销毁。
在Redis 2.6和之前版本,如果key不存在或者已过期时返回-1。
从Redis2.8开始,错误返回值的结果有如下改变:
TYPE 命令用于返回 key 所储存的值的类型。
127.0.0.1:6379> type NEWBLOGlist
常用命令:hset, hget, hmset, hmget, hgetall, hdel, hkeys, hvals
HSET car price 500 //HSET key field valueHGET car price
同时设置获取多个字段的值
HMSET car price 500 name BMWHMGET car price nameHGETALL car
使用 HGETALL 命令时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型。
HSETNX car price 400 //当字段不存在时赋值,HSETNX是原子操作,不存在竞态条件
HKEYS car //获取keyHVALS car //获取valueHLEN car //长度
常用命令:lpush, rpush, lpop, rpop, lrange, lrem
添加和删除元素
LPUSH numbers 1RPUSH numbers 2 3LPOP numbersRPOP numbers
获取列表片段
LRANGE numbers 0 2LRANGE numbers -2 -1 //支持负索引 -1是最右边第一个元素LRANGE numbers 0 -1
向列表插入值
首先从左到右寻找值为pivot的值,向列表插入value
LINSERT numbers AFTER 5 8 //往5后面插入8LINSERT numbers BEFORE 6 9 //往6前面插入9
删除元素
LTRIM numbers 1 2 删除索引1到2以外的所有元素
LPUSH常和LTRIM一起使用来限制列表的元素个数,如保留最近的100条日志
LPUSH logs $newLogLTRIM logs 0 99
删除列表指定的值
LREM key count value
1. count < 0, 则从右边开始删除前count个值为value的元素 2. count > 0, 则从左边开始删除前count个值为value的元素 3. count = 0, 则删除所有值为value的元素 `LREM numbers 0 2`
其他
LLEN numbers //获取列表元素个数LINDEX numbers -1 //返回指定索引的元素,index是负数则从右边开始计算LSET numbers 1 7 //把索引为1的元素的值赋值成7
常用命令:sadd, srem, smembers, scard, sismember, sdiff
集合中不能有相同的元素。
增加/删除元素
SADD letters a b cSREM letters c d
获取元素
SMEMBERS lettersSCARD letters //获取集合元素个数
判断元素是否在集合中 SISMEMBER letters a
集合间的运算
SDIFF setA setB //差集运算SINTER setA setB //交集运算SUNION setA setB //并集运算
三个命令都可以传进多个键 SDIFF setA setB setC
其他
SDIFFSTORE result setA setB 进行集合运算并将结果存储
SRANDMEMBER key count
随机获取集合里的一个元素,count大于0,则从集合随机获取count个不重复的元素,count小于0,则随机获取的count个元素有些可能相同。
SPOP letters
常用命令:zadd, zrem, zscore, zrange
zadd zsetkey 50 e1 60 e2 30 e3
Zset(sorted set)是string类型的有序集合。zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是Zset每个元素都会关联一个double(超过17位使用科学计算法表示,可能丢失精度)类型的分数,通过分数来为集合中的成员进行排序。zset的成员是唯一的,但分数(score)可以重复。
有序集合和列表相同点:
有序集合和列表不同点:
增加/删除元素
时间复杂度OlogN。
ZADD scoreboard 89 Tom 78 SophiaZADD scoreboard 85.5 Tyson //支持双精度浮点数ZREM scoreboard TysonZREMRANGEBYRANK scoreboard 0 2 //按照排名范围删除元素ZREMRANGEBYSCORE scoreboard (80 100 //按照分数范围删除元素,"("代表不包含
获取元素分数
时间复杂度O1。
ZSCORE scoreboard Tyson
获取排名在某个范围的元素列表
ZRANGE命令时间复杂度是O(log(n)+m), n是有序集合元素个数,m是返回元素个数。
ZRANGE scoreboard 0 2ZRANGE scoreboard 1 -1 //-1表示最后一个元素ZRANGE scoreboard 0 -1 WITHSCORES //同时获得分数
获取指定分数范围的元素
ZRANGEBYSCORE命令时间复杂度是O(log(n)+m), n是有序集合元素个数,m是返回元素个数。
ZRANGEBYSCORE scoreboard 80 100ZRANGEBYSCORE scoreboard 80 (100 //不包含100ZRANGEBYSCORE scoreboard (60 +inf LIMIT 1 3 //获取分数高于60的从第二个人开始的3个人
增加某个元素的分数
时间复杂度OlogN。
ZINCRBY scoreboard 10 Tyson
其他
ZCARD scoreboard //获取集合元素个数,时间复杂度O1ZCOUNT scoreboard 80 100 //指定分数范围的元素个数ZRANK scoreboard Tyson //按从小到大的顺序获取元素排名ZREVRANK scoreboard Tyson //按从大到小的顺序获取元素排名
Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作,可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1。
bitmap的长度与集合中元素个数无关,而是与基数的上限有关。假如要计算上限为1亿的基数,则需要12.5M字节的bitmap。就算集合中只有10个元素也需要12.5M。
HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
基数:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数即不重复元素为5。
应用场景:独立访客(unique visitor,uv)统计。
SDS定义:
struct sdshdr { // 记录 buf 数组中已使用字节的数量 // 等于 SDS 所保存字符串的长度 int len; // 记录 buf 数组中未使用字节的数量 int free; // 字节数组,用于保存字符串 char buf[];};
C 字符串 SDS 获取字符串长度的复杂度为 O(N) 。 获取字符串长度的复杂度为 O(1) 。 API 是不安全的,可能会造成缓冲区溢出。 API 是安全的,不会造成缓冲区溢出。 修改字符串长度 N 次必然需要执行 N 次内存重分配。 修改字符串长度 N 次最多需要执行 N 次内存重分配。 只能保存文本数据。 可以保存文本或者二进制数据。 可以使用所有 <string.h> 库中的函数。 可以使用一部分 <string.h> 库中的函数。
字典使用hashtable作为底层实现。键值对的值可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。
typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next;} dictEntry;
整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 它可以保存类型为 int16_t 、 int32_t 或者 int64_t 的整数值, 并且保证集合中不会出现重复元素。
ziplist是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。每个压缩列表节点都由 previous_entry_length 、 encoding 、 content 三个部分组成。
节点的 previous_entry_length 属性以字节为单位, 记录了压缩列表中前一个节点的长度。 节点的 encoding 属性记录了节点的 content 属性所保存数据的类型以及长度。有两种编码方式,字节数组编码和整数编码。
压缩列表的从表尾向表头遍历操作就是使用这一原理实现的: 只要我们拥有了一个指向某个节点起始地址的指针, 那么通过这个指针以及这个节点的 previous_entry_length 属性, 程序就可以一直向前一个节点回溯, 最终到达压缩列表的表头节点。
跳表可以看成多层链表,它有如下的性质:
Redis 的对象系统还实现了基于引用计数技术的内存回收机制: 当程序不再使用某个对象的时候, 这个对象所占用的内存就会被自动释放; 另外, Redis 还通过引用计数技术实现了对象共享机制, 这一机制可以在适当的条件下, 通过让多个数据库键共享同一个对象来节约内存。
字符串对象的编码可以是 int 、 raw 或者 embstr 。
值 编码 可以用 long 类型保存的整数。 int 可以用 long double 类型保存的浮点数。 embstr 或者 raw 字符串值, 或者因为长度太大而没办法用 long 类型表示的整数, 又或者因为长度太大而没办法用 long double 类型表示的浮点数。 embstr 或者 raw
hash类型内部编码有两种:
使用 ziplist 作为 hash 的底层实现时,添加元素的时候,同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后。
使用场景:记录博客点赞数量。hset MAP_BLOG_LIKE_COUNT blogId likeCount,key为MAP_BLOG_LIKE_COUNT,field为博客id,value为点赞数量。
列表list类型内部编码有两种:
Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现。
使用场景:
集合对象的编码可以是 intset 或者 hashtable 。
有序集合的编码可以是 ziplist 或者 skiplist 。当有序集合的元素个数小于128,同时每个元素的值都小于64字节时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。否则,使用skiplist作为有序集合的内部实现。
string:1、常规key-value缓存应用。常规计数如微博数、粉丝数。2、分布式锁。
hash:存放结构化数据,如用户信息(昵称、年龄、性别、积分等)。
list:热门博客列表、消息队列系统。使用list可以构建队列系统,比如:将Redis用作日志收集器,多个端点将日志信息写入Redis,然后一个worker统一将所有日志写到磁盘。
set:1、好友关系,微博粉丝的共同关注、共同喜好、共同好友等;2、利用唯一性,统计访问网站的所有独立ip 。
zset:1、排行榜;2、优先级队列。
切换数据库:select 1。Redis默认配置中是有16个数据库。0号数据库和15号数据库之间的数据没有任何关联,可以存在相同的键。不建议使用Redis多数据库功能,可以在一台机器上部署多个Redis实例,使用端口号区分,实现多数据库功能。
flushdb/flushall命令用于清除数据库,两者的区别的是flushdb只清除当前数据库,flushall会清除所有数据库。如果当前数据库键值数量比较多,flushdb/flushall存在阻塞Redis的可能性,并且这两个命令会将所有数据清除,一旦误操作后果不堪设想。
LPUSH myList 4 8 2 3 6SORT myList DESC
LPUSH letters f l d n cSORT letters ALPHA
BY参数
LPUSH list1 1 2 3SET score:1 50SET score:2 100SET score:3 10SORT list1 BY score:* DESC
GET参数
GET参数命令作用是使SORT命令的返回结果是GET参数指定的键值。
SORT tag:Java:posts BY post:*->time DESC GET post:*->title GET post:*->time GET #
GET #返回文章ID。
STORE参数
SORT tag:Java:posts BY post:*->time DESC GET post:*->title STORE resultCache
EXPIRE resultCache 10 //STORE结合EXPIRE可以缓存排序结果
事务的原理是将一个事务范围内的若干命令发送给Redis,然后再让Redis依次执行这些命令。
事务的生命周期:
DISCARD:放弃事务,即该事务内的所有命令都将取消
一个事务范围内某个命令出错不会影响其他命令的执行,不保证原子性:
127.0.0.1:6379> multiOK127.0.0.1:6379> set a 1QUEUED127.0.0.1:6379> set b 1 2QUEUED127.0.0.1:6379> set c 3QUEUED127.0.0.1:6379> exec1) OK2) (error) ERR syntax error3) OK
事务里的命令执行时会读取最新的值:
WATCH命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行(类似于乐观锁)。执行EXEC命令之后,就会自动取消监控。
127.0.0.1:6379> watch nameOK127.0.0.1:6379> set name 1OK127.0.0.1:6379> multiOK127.0.0.1:6379> set name 2QUEUED127.0.0.1:6379> set gender 1QUEUED127.0.0.1:6379> exec(nil)127.0.0.1:6379> get gender(nil)
UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。
使用一个列表,让生产者将任务使用LPUSH命令放进列表,消费者不断用RPOP从列表取出任务。
BRPOP和RPOP命令相似,唯一的区别就是当列表没有元素时BRPOP命令会一直阻塞连接,直到有新元素加入。 BRPOP queue 0 //0表示不限制等待时间
BLPOP queue:1 queue:2 queue:3 0 如果多个键都有元素,则按照从左到右的顺序取元素
PUBLISH channel1 hiSUBSCRIBE channel1UNSUBSCRIBE channel1 //退订通过SUBSCRIBE命令订阅的频道。
PSUBSCRIBE channel?* 按照规则订阅 PUNSUBSCRIBE channel?* 退订通过PSUBSCRIBE命令按照某种规则订阅的频道。其中订阅规则要进行严格的字符串匹配,PUNSUBSCRIBE *无法退订channel?*规则。
缺点:在消费者下线的情况下,生产的消息会丢失。
使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
Redis支持两种方式的持久化,一种是RDB的方式,一种是AOF的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将命令记录下来。一般将两者结合使用。
RDB 是 Redis 默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个dump.rdb文件。Redis 重启会加载dump.rdb文件恢复数据。
RDB持久化的过程(执行SAVE命令除外):
Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。通过RDB方式的持久化,一旦Redis异常退出,就会丢失最近一次持久化以后更改的数据。
触发RDB快照:
优点:Redis加载RDB恢复数据远远快于AOF的方式。
缺点:
AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly参数启用appendonly yes。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。
默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过appendfsync参数设置同步的时机。
appendfsync always //每次写入aof文件都会执行同步,最安全最慢,只能支持几百TPS写入,不建议配置appendfsync everysec //保证了性能也保证了安全,建议配置appendfsync no //由操作系统决定何时进行同步操作
重写机制:
随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
优点: (1)AOF可以更好的保护数据不丢失,一般AOF会每秒去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据。 (2)AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。 缺点 (1)对于同一份文件AOF文件比RDB数据快照要大。 (2)不适合写多读少场景。 (3)数据恢复比较慢。
RDB和AOF如何选择 (1)仅使用RDB这样会丢失很多数据。 (2)仅使用AOF,因为这一会有两个问题,第一通过AOF恢复速度慢;第二RDB每次简单粗暴生成数据快照,更加安全健壮。 (3)综合AOF和RDB两种持久化方式,用AOF来保证数据不丢失,作为恢复数据的第一选择;用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,可以使用RDB进行快速的数据恢复。
redis的复制功能是支持多个数据库之间的数据同步。主数据库可以进行读写操作,当主数据库的数据发生变化时会自动将数据同步到从数据库。从数据库一般是只读的,它会接收主数据库同步过来的数据。一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。
redis-server //启动Redis实例作为主数据库 redis-server --port 6380 --slaveof 127.0.0.1 6379 //启动另一个实例作为从数据库 slaveof 127.0.0.1 6379SLAVEOF NO ONE //停止接收其他数据库的同步并转化为主数据库。
Redis在2.8及以上版本使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制。
全量复制:一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。
部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。
通过redis的复制功能可以实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。很多场景下对数据库的读频率大于写,当单机的Redis无法应付大量的读请求时,可以通过复制功能建立多个从数据库节点,主数据库负责写操作,从数据库负责读操作。这种一主多从的结构很适合读多写少的场景。
持久化的操作比较耗时,为了提高性能,可以建立一个从数据库,并在从数据库进行持久化,同时在主数据库禁用持久化。
当master节点奔溃时,可以手动将slave提升为master,继续提供服务。
通过哨兵机制可以自动切换主从节点。哨兵是一个独立的进程,用于监控redis实例的是否正常运行。
客户端连接redis的时候,先连接哨兵,哨兵会告诉客户端redis主节点的地址,然后客户端连接上redis并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。
/** * 测试Redis哨兵模式 * @author liu */public class TestSentinels { @SuppressWarnings("resource") @Test public void testSentinel() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(10); jedisPoolConfig.setMaxIdle(5); jedisPoolConfig.setMinIdle(5); // 哨兵信息 Set<String> sentinels = new HashSet<>(Arrays.asList("192.168.11.128:26379", "192.168.11.129:26379","192.168.11.130:26379")); // 创建连接池 JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels,jedisPoolConfig,"123456"); // 获取客户端 Jedis jedis = pool.getResource(); // 执行两个命令 jedis.set("mykey", "myvalue"); String value = jedis.get("mykey"); System.out.println(value); }}
集群用于分担写入压力,主从用于灾难备份和高可用以及分担读压力。
主从复制存在不能自动故障转移、达不到高可用的问题。 哨兵模式解决了主从复制不能自动故障转移、达不到高可用的问题,但还是存在主节点的写能力、容量受限于单机配置的问题。 cluster模式实现了Redis的分布式存储,每个节点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题。
节点取余分区。使用特定的数据,如Redis的键或用户ID,对节点数量N取余:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。 优点是简单性。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。
一致性哈希分区:为系统中每个节点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。 这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。
Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。
Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线(pfail)和客观下线(fail)。
Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。
在Redis中执行Lua脚本有两种方法:eval和evalsha。
eval 命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。
//第一个参数是lua脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second1) "key1"2) "key2"3) "first"4) "second"
Redis还提供了evalsha命令来执行Lua脚本。首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和。Evalsha 命令根据给定的 sha1 校验和,执行缓存在服务器中的脚本。
script load命令可以将脚本内容加载到Redis内存中。
redis 127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'""232fd51614574cf0867b83d384a5e898cfd24e5a"redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0"hello moto"
使用evalsha执行Lua脚本过程如下:
1、Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
2、Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
限制接口访问频率。
在Redis维护一个接口访问次数的键值对,key是接口名称,value是访问次数。每次访问接口时,会执行以下操作:
private String buildLuaScript() { return "local c" + "\nc = redis.call('get',KEYS[1])" + "\nif c and tonumber(c) > tonumber(ARGV[1]) then" + "\nreturn c;" + "\nend" + "\nc = redis.call('incr',KEYS[1])" + "\nif tonumber(c) == 1 then" + "\nredis.call('expire',KEYS[1],ARGV[2])" + "\nend" + "\nreturn c;";}String luaScript = buildLuaScript();RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());
Redis 客户端与服务端之间的通信协议是在TCP协议之上构建的。
Redis Monitor 命令用于实时打印出 Redis 服务器接收到的命令,调试用。
redis 127.0.0.1:6379> MONITOR OK1410855382.370791 [0 127.0.0.1:60581] "info"1410855404.062722 [0 127.0.0.1:60581] "get" "a"
Redis原生提供慢查询统计功能,执行slowlog get{n}命令可以获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。慢查询队列长度默认128,可适当调大。
Redis客户端执行一条命令分为4个部分:发送命令;命令排队;命令执行;返回结果。慢查询只统计命令执行这一步的时间,所以没有慢查询并不代表客户端没有超时问题。
Redis提供了slowlog-log-slower-than(设置慢查询阈值,单位为微秒)和slowlog-max-len(慢查询队列大小)配置慢查询参数。
相关命令:
showlog get n //获取慢查询日志slowlog len //慢查询日志队列当前长度slowlog reset //重置,清理列表
慢查询解决方案:
redis客户端执行一条命令分4个过程: 发送命令-〉命令排队-〉命令执行-〉返回结果。使用Pipeline可以批量请求,批量返回结果,执行速度比逐条执行要快。
使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。
原生批命令(mset, mget)与Pipeline对比:
缓存和DB之间怎么保证数据一致性: 读操作:先读缓存,缓存没有的话读DB,然后取出数据放入缓存,最后响应数据 写操作:先删除缓存,再更新DB 为什么是删除缓存而不是更新缓存呢?
先删除缓存,再更新DB,同样也有问题。假如A先删除了缓存,但还没更新DB,这时B过来请求数据,发现缓存没有,去请求DB拿到旧数据,然后再写到缓存,等A更新完了DB之后就会出现缓存和DB数据不一致的情况了。
解决方法:采用延时双删策略。更新完数据库之后,延时一段时间,再次删除缓存,确保可以删除读请求造成的缓存脏数据。评估项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。
public void write(String key,Object data){ redis.delKey(key); db.updateData(data); Thread.sleep(1000);//确保读请求结束,写请求可以删除读请求造成的缓存脏数据 redis.delKey(key);}
可以将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,加大吞吐量。
当删缓存失败时,也会就出现数据不一致的情况。
解决方法:
图片来源:https://tech.it168.com