what is redis
- Redis是一个开源的,基于内存的数据结构存储服务器,被用作数据库、缓存、消息代理。它支持的数据结构类型有
strings,hashes,lists,sets,sorted sets,bitmaps,hyperloglogs,geospatial,Stream。Redis有内置的复制,lua脚本,LRU(Least Recently Used最近最少使用)淘汰,事务,磁盘持久化,并提供高可用的Redis Sentinel哨兵和自动分区的Redis Cluster集群方案。
Redis的安装与配置
- 下载:download.redis.io/releases/re…
- 解压:
tar -zxvf redis-5.0.5.tar.gz -C /usr/local - 安装 yum intsall gcc-c++
- 修改核心配置文件
protected-mode no(改为不保护,否则远程访问不了)bind 127.0.0.1(注释掉,否则只能本机ip访问)daemonize yes(改为yes表示后台启动redis)requirepass 123456: 配置是否需要密码dir /usr/local/dataredis持久化目录
- 拷贝redis启动脚本
redis_init_script到/etc/init.d/目录下
cp /usr/local/redis-5.0.5/utils/redis_init_script /etc/init.d/
- 修改redis启动脚本
REDISPORT=6379
EXEC=/usr/local/redis-5.0.5/src/redis-server
CLIEXEC=/usr/local/redis-5.0.5/src/redis-cli
PIDFILE=/var/run/redis_${REDISPORT}.pid
CONF="/usr/local/redis-5.0.5/redis.conf"
- 启动redis
- cd /etc/init.d/
- ./redis_init_script start
- 配置redis开机自动启动
- 设置redis开机自启动,修改 redis_init_script,添加如下内容
#chkconfig: 22345 10 90
#description: Start and Stop redis
- 执行 chkconfig redis_init_script on
Redis客户端
- 概述:
redis-cli(Redis Command Line Interface)是Redis自带的基于命令行的Redis客户端,用于和Redis服务端交互,我们可以使用该客户端来执行redis的各种命令。 - 指定IP和端口连接redis:
./redis-cli -h 127.0.0.1 -p 6379 - redis默认为16个库,redis默认自动使用0号库,我们可以使用
select db切换库 - 查看当前数据库中key的数目:
dbsize - 删除所有库的数据:
flushall - 删除当前库的数据:
flushdb - 查看redis服务器的统计信息:
info
Redis图形界面客户端
- mac 推荐AnotherRedisDesktopManager
- 下载地址
Redis数据类型 redisdoc.com/
string 类型
- 字符串类型是redis最基本的数据类型,它能够存储任何形式的字符串,包括二进制数据,byte字节等,也就是说string类型能存储任何形式的数据,你可以用它来存储序列化后的对象,json,图片视频等,一个key下的最大存储是512M 1.1 暂时理解:只是名字叫string,是redis的基本类型,能存任何数据。
- 常用命令
2.1
SET:设置值SET name zhangsan2.2GET:获取值GET name2.3INCR:将值自增1,如果值不存在自动插入,可以用来做计数器。 2.4DECR: 将值自减1,如果值不存在自动插入,可以用来做计数器。
list 类型
- 列表类型可以存储一个字符串列表,按照数据加入的顺序排序,它非常方便地在往列表左右两端添加元素(左边称为头部,右边称为尾部)。
- 列表类型内部使用双向链表实现,所有头尾插入删除元素为0(1)
LPUSH x x x:一直往头部插入x x x元素RPUSH x x x:向尾部追加x x x元素LRANGE 0 -1:获取范围内的值,-1表示全部。LPOP RPOP: 弹出头尾元素。LTRIM: 对list修剪,保留指定区间内的元素。- list虽然有序,且可重复,但是链表的特点获取中间元素依然是0(n)。
set类型
- set类型是一个
无顺序的字符串集合,集合中每个元素都不能重复,多次添加同一个元素,集合中只会有一个该元素。 - set类型内部使用的是
散列哈希表,所以获取任何元素都是0(1),且值不能重复。就把值当作key来用,内部的值都是固定的一个值,和Set<>一样 - 集合的主要功能是 交集,并集,差集等
3.1
sadd key x x x:添加元素 3.2SPOP key:移除并返回集合中随机的元素,抽奖用。 3.3SCARD key: 返回集合的数量 3.4smembers key: 遍历集合的全部元素 3.5sismember key value检查是否包含这个元素。 3.5srem key value:删除集合中某个元素.
zset类型, 有序集合类型
- zset类型与set相似,也是一个
无重复元素的集合,不同的是zset的每个元素会关联一个分数,这个分数用于对集合元素进行排序; zset中的元素是唯一的,但是每个元素的分数是可以重复的; - 使用场景:实时的数据排行榜,每次数据的更新会更新分数,提高/降低排名。
- 常用操作
zadd key score menber:添加元素zrange 0 -1 [withscores]:查看所有元素[和其分数]zrem key value:删除指定的值
hash类型
1.Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
过期key自动删除(自动清理)的原理
- 消极方法(passive way)
- 在key被访问时如果发现它已经失效,那么就删除它;
- 积极方法(active way)
- 周期性地从设置了过期时间的key中选择一部分过期的key删除 对于那些从未被查询的key,即便它们已经过期,消极方式不会清除,所以Redis需要周期性地随机测试一些key,已过期的key将会被删掉。
- Redis每秒会进行10次操作,具体的流程:
- 随机测试 20 个带有过期信息的key;
- 删除其中已经过期的key;
- 如果超过25%的key被删除,则重复执行步骤1;
Lettuce 客户端
- Lettuce是一个可伸缩线程安全的 redis客户端,多个线程可以共享同一个redis connection,它底层使用了优秀的Netty NIO框架实现网络通信。
- Lettuce 异步方式操作redis
// 创建客户端链接 redis://[password@]host:port/[database num]
RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379/0");
// 返回链接对象
StatefulRedisConnection<String, String> connect = redisClient.connect();
// 获取异步操作命令
RedisAsyncCommands<String, String> async = connect.async();
// 设置数据
RedisFuture<String> future = async.set("name", "zhangsan");
// 阻塞获取 // result = OK
String result = future.get();
System.out.println("result = " + result);
RedisFuture<String> name = async.get("name");
// s1 = zhangsan
String s1 = name.get();
System.out.println("s1 = " + s1);
// 自增
RedisFuture<Long> rs = async.incr("kk1");
System.out.println("rs.get() = " + rs.get());
RedisFuture<String> kk1 = async.get("kk1");
String s = kk1.get();
System.out.println("s = " + s);
redis 热门数据 分页排序应用
- 案例:热门事件、热门微博等评论,不能直接用数据库。
- 实现思路,zset + hash组合, 或者 zset + string组合
redis 排行榜场景
- 使用zset + string 组合
- zset中存储一个 商品id,使用分数排行
- 然后再根据 商品id 缓存具体商品的数据。
ZINCRBY saleTop 1000 product_id (可以搞成商品id)
ZINCRBY saleTop 120 product_id
ZINCRBY saleTop 50 product_id
ZINCRBY saleTop 8 product_id
ZREVRANGEBYSCORE saleTop +inf -inf (+inf -inf # 逆序排列所有成员)
ZREVRANGE saleTop 0 -1 (从大到小)
ZRANGE saleTop 0 -1 (从小到大)
时间线场景:最近新帖,好友动态,时间轴等
- 这类信息是按照固定的时间顺序排列,可以使用列表(list)或者有序列表zset来存储;
LPUSH user:1021_active '{datetime:578555454,title:涨薪了,content:加油}'
LPUSH user:1021_active '{datetime:57885555,title:欢度国庆,content:放假了}'
LRANGE user:1021_active 0 10
计数类场景
- 比如计数器,用户未读消息个数、关注数、粉丝数、经验值等等,这类消息适合
- 以Redis中的散列(hash)结构进行存储。
HSET user:1021 follower 50
HINCRBY user:1021 follower 1 --增加一个粉丝
HGETALL user:1021
- 字符串存储
用string类型存储计数类的也可以
//id是1021的用户的粉丝数
Key -->user:follower:1021
Value -> 用string类型存储也可以
#现在要增加一个粉丝,那么执行:
incr user:follower:1021
分布式唯一ID
- 开发中常见的生成全局唯一标识的需求,支付、订单、红包、优惠券、跟踪号等等;
- 通过Redis原子操作命令
INCR和INCRBY(redis自增)实现递增,同时可使用Redis集群提高吞吐量,集群后每台Redis的初始值为1,2,3,4,5,步长为5;
缓存穿透
- 大量不存在的Key的请求,由于没有缓存,就会开始查数据库,但是数据库也没有该数据,不会重新缓存,就会造成大量查询,如受到一些攻击,造成大量空命中。
缓存击穿
- 高并发条件下,对于热点数据(一般地,80%的情况下都是访问某些热点数据,也就是访问某些热点key,其他key访问会比较少),当数据失效的一瞬间,或者刚开始时缓存中还没有对数据进行缓存,所有请求都被发送到数据库去查询,数据库被压垮。
- 解决方案一:使用分布式锁,第一个获得锁的线程才会去查询数据库,其他使用类似单例
双重检测锁的方式。 - 解决方案二:对即将过期的数据主动刷新,如分布式定时任务。
缓存雪崩
- 缓存雪崩是指比如我们给所有的数据设置了同样的过期时间,然后在某一个历史性时刻,整个缓存的数据全部过期了,然后瞬间所有的请求都落到数据库,数据库被压垮,或者是缓存发生故障,导致所有的请求都落入到数据库,数据库被压垮。
- 缓存雪崩核心意思:缓存不能用了(缓存没有或缓存服务器宕机)
- 解决方案
- 事前:保证redis服务器高可用(集群模式或主从加哨兵)
- 事中:本地cache缓存+限流&降级,避免数据库被压垮。
- 事后:redis数据持久化,快速恢复数据。
- 限流&降级:单位时刻内超过一定的请求数,开启限流,调用back方法返回兜底数据,避免查数据库。(比如保证1ms只有1000个请求查询数据库)
缓存和数据库双写不一致
- 我们在项目中大量使用缓存,主要是利用其高并发和高性能特性,一般情况下,我们是这样使用缓存。
- 缓存的读取操作:先判断是否有缓存,有缓存直接返回给前端,无缓存,从数据库查询数据,如果数据库查询结果为空,则直接返回给前端,如果数据库查询结果不为空,则将数据写入缓存再返回给前端;
- 缓存的更新操作:可以分为:
- 先更新数据库,再更新缓存;(不行)
- 先更新缓存,再更新数据库;(不行)
- 先更新数据库,再删除缓存;
- 先删除缓存,再更新数据库;(国内推荐)
(1)先更新数据库,再更新缓存(不行)
从线程安全角度,此方案是有问题的: 同时有请求A和请求B进行更新操作,那么会出现
- 线程A更新了数据库
- 线程B更新了数据库
- 线程B更新了缓存
- 线程A更新了缓存
- 这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存,这就导致了脏数据,因此不考虑。
先更新缓存,再更新数据库(不考虑,可能也是不存在的
- 这种情况不考虑,先查数据库,查到了数据,才有数据往缓存更新,没有数据,无法对缓存更新;
先更新数据库,再删除缓存
- 国外有一本书《Cloud Design Patterns》提出了一个缓存更新套路,名为Cache-Aside pattern指出:
- 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中;
- 命中:应用程序从cache中取数据,取到后返回;
- 更新:先把数据存到数据库中,成功后,再让缓存失效(删除)。
- 另外, Facebook他们有一次演讲中提到,他们用的是先更新数据库,再删缓存的策略。(一般情况下遵循Cache-Aside pattern套路是没有问题的)
如果缓存删除失败了怎么办
- 解决方案:重试机制;
- 消息队列延迟消息;
- 解决方案:延时双删策略;(和上面一样)
- 删除缓存;
- 更新数据库;
- 休眠200毫秒;
- 再删除缓存;
先删除缓存,再更新数据库
- 该方案会导致不一致,比如同时有一个请求A进行更新操作,另一个请求B进行查询操作。
- 请求A进行写操作,先删除缓存
- 请求B查询发现缓存不存在
- 请求B去数据库查询得到旧值
- 请求B将旧值写入缓存
- 请求A将新值写入数据库
- 那么这个将导致数据不一致,如果不给缓存设置过期时间,该数据将永远都是脏数据。
- 解决方案:双删策略。
- 删除缓存;
- 更新数据库;
- 休眠200毫秒;(休眠是当心B的新数据还没有写入数据库)
- 再删除缓存;
Redis 使用管道提升性能(发送批量命令)
- Redis服务是一种C/S模型,即客户端发起请求,服务端处理并返回结果给客户端,如果Redis客户端要发送很多条请求,后面的请求需要等待前面的请求处理完后才能进行处理,而且每个请求都存在往返时间,即使redis性能极高,当数据量足够大,也会极大影响性能,所以Redis为了改进该问题,引入了管道技术:可以在服务端未及时响应的时候,客户端也可以继续发送命令请求,做到客户端和服务端互不影响,服务端并最终返回所有服务端的响应,大大提高了C/S模型交互的响应速度;(比如批量对Redis操作)
- 使用方法
Jedis jedis = JedisPoolUtil.getInstance().getResource();
// 管道开始
Pipeline pipelined = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipelined.incr("number");
}
// 同步数据
pipelined.sync();
Redis持久化之RDB
- Redis Database(RDB),是指在指定的时间间隔内将内存中的数据集快照写入磁盘,数据恢复时将快照文件直接再读到内存。
- RDB方式的数据持久化,仅需在redis.conf文件中配置即可;
- 配置文件搜索 SNAPSHOTTING 部分,
- 配置格式:save
save 900 1 // 900秒内发生 1次变化执行持久化
save 300 10 // 300秒内发生 10次变化执行持久化
save 60 10000 // 60秒内发生 10000次变化执行持久化
- Redis 默认开启RDB方式
RBD内部实现原理
- 当满足条件时,Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,等到持久化过程都结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何关于持久化相关的IO操作的,这就确保了不影响主进程,保证Redis极高的性能;
- 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效,RDB的缺点是最后一次持久化后的数据可能丢失;
- Redis会在以下几种情况下对数据进行快照持久化
- 根据配置时间规则进行自动快照;
- 用户执行SAVE或者BGSAVE命令;
- 执行FLUSHALL命令;
- 执行复制(replication)时;
AOF方式
- Append-only File(AOF)Redis每次接收到一条改变数据的命令时,它将把该命令写到一个AOF文件中(只记录写操作,不记录读操作),当Redis重启时,它通过执行AOF文件中所有的命令来恢复数据。
- AOF是在RDB之后出现的一种技术,这种方式的持久化让redis的数据不会丢失;
AOF文件重写
- AOF 文件会在操作过程中变得越来越大。比如,如果你做一百次加法计算,最后你只会在数据库里面得到最终的数值(k 100),但是在你的 AOF 里面会存在 100 次记录,其中 99 条记录对最终的结果是无用的;但 Redis 支持在不影响服务的前提下在后台重构 AOF 文件,让文件得以整理变小;