Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它支持存储的 value 类型相对更多,包括 String(字符串)、List(列表)、Set(集合)、Sorted Set(有序集合) 和 Hash(哈希)。在此基础上,Redis 支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中。Redis 可以周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
**官方下载地址:**https://redis.io/download
windos版本(长时间停更,不建议使用) 需要到github 下载
解压
tar -zxf redis-4.0.9.tar.gz
安装gcc
yum install gcc-c++ 6.0 之后版本要求 需要升级 gcc到 9.0+
安装redis
make
make install
默认安装到 /usr/loccal/bin 下
拷贝配置文件 cp redis-config 到 自己指定文件夹下
修改daemonize 的值为 yes 改为后台启动
开启服务端 redis-server /指定的文件路径/redis.conf
开启客户端 redis-cli -p 6379 使用ping 测试连接
查看 redis 服务开启情况 ps -ef|grep redis
性能测试 redis-benchmark -p 6379 -c 100 -n 100000 含义:6379端口 100并发 100000数据
6.0前只是单线程 6.0后支持多线程
不管是单线程或者是多线程都是为了提升Redis的开发效率,因为Redis是一个基于内存的数据库,还要处理大量的外部的网络请求,这就不可避免的要进行多次IO。好在Redis使用了很多优秀的机制来保证了它的高效率。
为什么redis 单线程这么快? 官方:100000+的QPS 不比key-value 的Memecache
cpu->内存->硬盘的速度
redis数据都是存放在内存的,多线程cpu切换有性能损耗
(1)IO多路复用
一旦受到网络请求就会在内存中快速处理,由于绝大多数的操作都是纯内存的,所以处理的速度会非常地快。也就是说在单线程模式下,即使连接的网络处理很多,因为有IO多路复用,依然可以在高速的内存处理中得到忽略。
(2)可维护性高
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题。单线程模式下,可以方便地进行调试和测试。
(3)基于内存,单线程状态下效率依然高
基于内存而且使用多路复用技术,单线程速度很快,又保证了多线程的特点。因为没有必要使用多线程。
读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间,如果把网络读写做成多线程的方式对性能会有很大提升。
Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。
Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率。
之前用单线程是因为基于内存速度快,而且多路复用有多路复用的作用,也就是足够了,现在引入是因为在某些操作要优化,比如删除操作,因此引入了多线程。
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
string 使用场景 除了字符串可以是数字 可以做计数器、统计多单位的数量uid:{2323}:follow 0、粉丝数、对象缓存
list 可以完成 栈 队列 阻塞队列
所有命令都是L开头的
小结
消息排队 、队列、栈
set 中的值不能重复
所有命令都是s开头的
Map集合 ,key-map 和string 类型没有太大区别
所有命令都是h开头的
应用 : 变更的值 user name zhj user age 22 尤其是经常变动信息的保存 更适合对象的存储#
set 的基础上加了一个值 set k v | zset k score v
所有命令都是z开头的
案例思路: set排序 班级成绩、工资表
普通消息 重要消息 加权
排行榜 TOP 10
朋友的定位,附近的人,打车距离
geoadd china:city 116.40 39.90 beijing 添加(经度、纬度、城市) 一般通过java程序添加
有效的经度-180到180 有效纬度 -85.05112878到85.05112878
geopos china:city beijing 获取指定城市的经度纬度
geopos china:city beijing shanghai 获取多个指定城市的经度纬度 得先存入
geodist china:city beijing shanghai km 北京到上海的距离 单位km
m 米 km 千米 mi 英里 ft 英尺
georadius china:city 110 30 1000 km 110,30 附近1000km的城市
georadius china:city 110 30 10000 km withdist 附近10000km的城市 和 据中心点的距离
georadius china:city 110 30 10000 km withcoord 110,30 附近10000km的城市 和 坐标
georadius china:city 110 30 10000 km withcoord count 1 110,30 附近10000km的城市 和 坐标 中选一个
georadiusbymember china:city beijing 1500 km 距离北京1500km 内 的城市
geohash china:city beijing shanghai 将两个地方的坐标转化为一维的字符串 字符串差值越小越近
zrange china:city 0 -1 所有值
zrem china:city beijing 移除元素
基数统计算法
基数 不重复的元素
A{1,3,5,7,8,9} B{1,3,5,7,8} 基数 = 5 ,可以接受误差
网站 的 UV 一个人访问网站多次,但还是一个人
传统的方式 set 保存用户id 然后统计set中的元素数量作为标准判断 如果存大量id 就有问题 目的是为了计数而不是保存id
0.81% 的错误率 可以忽略不计
必须容许容错才可以用
位运算 两种状态的 0 1 位图
疫情 : 01010110 统计疫情感染人数 | 活跃 不活跃 | 登录 不登录
127.0.0.1:6379> getbit sign 1
(integer) 1
127.0.0.1:6379> setbit sign 2 1
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0
127.0.0.1:6379> getbit sign 4
Redis 事物 不保证原子性,没有隔离级别的概念 (单条保证原子性)
Redis 事物本质是: 一组命令的集合 所有命令都会被序列化,在事物执行过程中会顺序执行!
一次性 顺序性 所有事物中的命令只有在执行事物时才会执行
redis 的事物
事物中出错
编译出错:代码有问题! 命令出错,所有都不会执行
运行时异常: 错误命令,报错 其它命令正常执行
很悲观,认为什么时候都会出现问题,什么时候都加锁 降低性能
很乐观,认为什么时候都不会有问题,不加锁!更新数据时会判断,是否有人动过数据,可以用一个version来验证
redis 的监控测试
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi # 事物正常结束 数据期间没有发生变化
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
127.0.0.1:6379>
#################################################################
#### 客户端一
127.0.0.1:6379> watch money # 监视money 相当于乐观锁
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 10
QUEUED
127.0.0.1:6379> incrby out 10
QUEUED
#### 客户端二
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> unwatch # 放弃监视 解锁
OK
#### 再来一次 加锁 执行事物 如果监视的对象不发生变化,事物执行成功
Redis官方推荐的 java连接工具 使用java操作redis 必会工具
// 阿里云需要绑定私网ip 在redis配置文件中 才可以访问可以
public static void main(String[] args) {
Jedis jedis = new Jedis("ip地址", 6379);
// 之前的指令都是方法
System.out.println(jedis.ping());
System.out.println("清空数据: " + jedis.flushDB());
System.out.println("设置name: " + jedis.set("name","zhj"));
System.out.println("获得name: " + jedis.get("name"));
jedis.close();
}
SpingBoot 整合 通过 SpringData 可以连接 Redis MongoDB JDBC
SpingBoot 2.x 之后 将原来的jedis 改为 lettuce
jedis: 采用直连,多个线程操作不安全,想避免使用jedis pool 连接池 !BIO
lettue: 采用Netty,实例可以在多线程共享,不存在线程不安全情况 ,更像 NIO
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"} // 可以自定义 去替换默认的
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 默认的RedisTemplate 没有过多设置 redis对象都是需要序列化的
// 两个泛型都是Object
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean // 由于spring 使用频繁,单独提取一个模板
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 默认的RedisTemplate 没有过多设置 redis对象都是需要序列化的
// 两个泛型都是Object
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 序列化配置
// json 的 序列化
Jackson2JsonRedisSerializer Jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(Jackson2JsonRedisSerializer);
template.setHashValueSerializer(Jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
package com.zhj.redis.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @PackageName:com.zhj.redis.utils
* @ClassName:RedisUtil
* @date:2020/7/29 0029 12:06
**/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
启动的时候就是通过配置文件启动的
大小写不敏感
包含 include /path/to/local.conf
bind 127.0.0.1 绑定ip 阿里云绑定内网ip
protected-mode yes 保护模式
port 6379 端口
daemonize yes 以守护进程方式运行,默认是no,我们需要自己开启为yes
pidfile /var/run/redis_6379.pid 如果我们以后台方式运行,我们就需要一个pid文件
日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing) #测试开发
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) #生产
# warning (only very important / critical messages are logged)
loglevel notice
# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile "" # 文件名,为空标准的输出
database 16 数据库数量,默认16
always-show-logo yes 是否总显示logo
持久化,在规定的时间内,执行了多少次操作,则会持久化到文件.rdb .aof
redis 是内存数据库,如果没有持久化,断电数据丢失
# 如果900s内,至少有一个key进行了修改,我们及进行持久化操作
save 900 1
# 如果300s内,至少有10个key进行了修改,我们及进行持久化操作
save 300 10
# 如果60s内,至少有10000个key进行了修改,我们及进行持久化操作
save 60 10000
# 我们之后学习持久化,会自定义这个测试
stop-writes-on-bgsave-error yes #持久化如果出错,是否还需要继续工作
rdbcompression yes # 是否压缩rdb文件,会消耗cpu资源
rdbchecksum yes # 保存rdb文件,进行错误校验
dir ./ #rdb保存的目录
复制
安全
config get requirpass # 得到密码 默认没有
config set requirpass “123456” # 设置密码
auth 123456 登录
限制
maxclients 10000 # 设置能连接上redis的最大客户端数
maxmemory # redis 配置最大内存容量
maxmemooy-policy noeviction # 内存达到上线的处理策略
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
3、volatile-random:随机删除即将过期key
4、allkeys-random:随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
APPEND Only 模式
appendonly no # 默认是不开启aof模式的,默认rdb方式持久化的,在大部分情况下rdb完全够用
appendfilename “apppendonly.aof” #持久化文件名字
appendfsync always # 每次修改都会sync,消耗性能
appendfsync everysec # 每秒执行一次sync 可能会丢失这一秒数据
appendfsync no # 不执行sync ,这个时候系统自己同步数据,速度最快
面试和工作持久化都是重点!
RDB 和 AOF 在指定时间间隔内将内存写入磁盘,恢复是将磁盘文件读入内存
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据的完整性不是非常敏感,那么RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置。
RDB 保存的文件是 dump.rdb
生产环境会备份该文件
配置; save 60 5 # 只要60秒内修改了5次key,就会触发RDB规则
触发机制
恢复RDB文件
优点:
缺点:
所有命令保留下来,相当于历史记录
AOF默认保存的是 appendonly.aof
以日志形式来记录每个操作,将redis执行过程记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文家重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次已完成数据恢复工作。
append only yes 默认是no不开启,改为yes启动
append filename "appendonly.aof" 持久化文件名字
appendfsync alawys 每次修改都会sync 消耗性能
appendfsync everysec 每秒一次sync ,可能会丢1秒数据
appendfsync no 不执行sync 系统自己同步数据,速度最快
# 重写规则 默认无限制增加
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb 大于64重写
优点:
缺点:
发布订阅(pub/sub) 线程之间通信 队列 生产者(发布者) 消费者 (订阅者)
网络聊天室、及时通信、实时消息系统、订阅关注
复杂的用消息中间件做
主从复制,将一台Redis服务器的数据,复制的其他Redis服务器,前者称为主节点(master/leader),后者称为从结点(slave/follower);数据的复制是单向的,只能由主节点到从结点。master以写为主,slave以读为主,
默认情况下每一台Redis服务器都是主结点,且一个主结点可以有多个或没有从结点,但一个从结点只能有一个主结点
主从复制的作用:
一般在项目中都会是集群,保证高可用,最少三台(一主二从)
原因:
主从复制,读写分离,80%的情况下是读操作,减缓服务器压力,架构经常使用,一主二从
3个Redis服务
修改:端口号、pid、log、dump.rdb文件名
一主二从配置:从 slaveof 127.0.0.1 6379
主机写,从机读,主机的所有数据都会复制到从机 从机不可以写入值
测试:
原理:
slave 启动成功连接到master后会发送一个sync同步命令
master 接到命令,启动后台的存盘进程,同时收集所接收到的用于修改的命令,在后台进程执行完毕后,master将传送整个数据文件到slave,并完成一次完全同步
全量复制:slave服务在接收到数据文件,将其存盘并加载到内存
增量复制:master继续将新的所有收集到的命令修改一次传给slave,完成同步
但是只要重新连接master,一次完全同步将自动执行
链式的
1主-----1从/2主-------2从
slaveof no one 主机断开让自己成为主机
宕机后手动配置:
由于服务器宕机,需要手动重新配置,还会使服务在一段时间不可用,费时费力,所以一般情况下更推荐哨兵模式,redis 2.8 开始正式提供 Sentinel(哨兵)
自动版,检测到主机故障,自动将从库变成主库
哨兵模式是一种特殊模式,首先 Redis 提供了哨兵命令,哨兵是一个独立的进程,作为进程,他会独立运行,原理是哨兵通过发送命令,等到Redis服务响应,从而监控多个实例
正常配 多哨兵模式
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1的主观认为主服务器不可用,这个现象称主关下线。当后边的哨兵也检测到主服务不可用,并且数量到一定值时,那么哨兵之间就会进行一次投票,投票结果是由一个哨兵发起的,进行failover 【故障转移】操作。切换成功后,会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程叫客观下线。
配置哨兵配置文件 sentinel.conf
sentinel moitor 被监控名称 主机 端口号 1 代表主机挂掉投票,票数多的做主机
sentinel moitor myredis 127.0.0.1 6379 1 # 哨兵模式最主要的配置
查看哨兵日志
主机回来,回归并从机下,当一个从机
优点:
缺点:
哨兵配置:
# redis.conf
# 使得Redis服务器可以跨网络访问
bind 0.0.0.0
# 设置密码
requirepass "123456"
# 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
slaveof 192.168.11.128 6379
# 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
masterauth 123456
# 配置哨兵
# 在Redis安装目录下有一个sentinel.conf文件,copy一份进行修改
# 端口
port 26379
# 是否后台启动
daemonize yes
# pid文件路径
pidfile /var/run/redis-sentinel.pid
# 日志文件路径
logfile "/var/log/sentinel.log"
# 定义工作目录
dir /tmp
# 定义Redis主的别名, IP, 端口,这里的2指的是需要至少2个Sentinel认为主Redis挂了才最终会采取下一步行为
sentinel monitor mymaster 127.0.0.1 6379 2
# 如果mymaster 30秒内没有响应,则认为其主观失效
sentinel down-after-milliseconds mymaster 30000
# 如果master重新选出来后,其它slave节点能同时并行从新master同步数据的台数有多少个,显然该值越大,所有slave节点完成同步切换的整体速度越快,但如果此时正好有人在访问这些slave,可能造成读取失败,影响面会更广。最保守的设置为1,同一时间,只能有一台干这件事,这样其它slave还能继续服务,但是所有slave全部完成缓存更新同步的进程将变慢。
sentinel parallel-syncs mymaster 1
# 该参数指定一个时间段,在该时间段内没有实现故障转移成功,则会再一次发起故障转移的操作,单位毫秒
sentinel failover-timeout mymaster 180000
# 不允许使用SENTINEL SET设置notification-script和client-reconfig-script。
sentinel deny-scripts-reconfig yes
面试高频,工作常用,保证系统高可用
用户想查询一个数据,发现缓存中没有,也就是缓存没有命中,于是向持久层去查询,发现也没,于是本次查询失效。当用户很多的时候,缓存没有命中,都去请求持久层数据库,这就会造成很大压力,致使相当于缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式储存,在控制层先进行校验,不符合则丢弃,从而避免了底层存储系统的查询能力,
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后在访问这个数据将会从缓存中获取,保护了后端的数据源
存在两个问题:
注意:缓存击穿和缓存穿透最大的区别是,缓存击穿是一个非常热的key,扛着超高的并发,大量并发集中一个点访问,当key失效瞬间,有大量请求并发访问,直接访问数据库,就相当于击穿了这堵墙
缓存过期期间,会同时访问数据库库来查询最新数据,并写回缓存,期间数据库压力过大
解决方案
设置热点数据不过期
加互斥锁
分布式锁,使用分布式锁,保证每一个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁考验极大。
某一个时间段,缓存集中失效,Redis服务宕机
集中过期,倒不是最致命的,最怕服务器某个结点宕机或断网,因为自然形成的缓存雪崩,一定是某个时间段集中创建缓存,这个时候数据库也是可以顶住压力大,无非就是对数据库产生周期性的压力而已,而服务器宕机,对数据库服务的压力是不可预知的,可能瞬间压垮
停掉一些不必要的服务,为核心服务提供服务资源
解决方案
redis 高可用
异地多活,多设几台服务器
限流降级
数据预热
正式访问前,先自己访问一遍,将热点数据放入缓存