NoSQL:Not Only SQL(不仅仅是SQL)
NoSQL的四大分类
KV键值对:
-
新浪:Redis
-
美团:Redis + Tair
-
阿里、百度:Redis + memecache
文档型数据库:
- MongoDB(一般必须要掌握):
MongoDB是一个基于分布式文件存储的数据库,C++编写,主要用来处理大量的文档!
MengoDB是一个介于关系型数据库和非关系型数据中中间的产品!MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的!
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图关系数据库
- 它存的不是图形,放的是关系,比如:朋友圈社交网络
redis
Redis ( Remote Dictionary Server ),即远程字典服务
作用:
- 内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof )
- 效率高,可以用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(浏览量!)
- ........
建议使用linux,windows已经停止更新
特点:
- 没有固定的查询语言
- 键值对存储
- 最终一致性
- CAP定理和BASE
- 高性能,高可用,高扩展(一秒写8万次,读11万)
大数据时代的3V:主要是描述问题的
- 海量Volume
- 多样Variety
- 实时Velocity
大数据时代的3高:主要是对程序的要求
- 高并发
- 高可扩
- 高性能
王坚:阿里云的这群疯子
多隆:Isearch
reids测试工具(官方自带)
redis-benchmark
redis-benchmark -c 100 -n 100000
以我电脑为例
redis有16个数据库,默认使用的是一个
select [数据库编号]
flushdb:清除当前数据库的所有数据
flushall:清除所有数据库的所有数据
为什么Redis的端口号是6379?
redis的作者早年看电视节目,觉得电视上的一个女明星说的一些话很愚蠢,而且当时redis的作者很喜欢造梗,就把这个女明星的名字"MERZ"当成了愚蠢的代名词,之后为了给Redis取端口号,就把MERZ对应手机键盘上的数字当成了端口号,也就是后来的6379。
为什么Redis是单线程的?
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
Redis为什么单线程还这么快?
核心: redis是将所有的数据全部放在内存中的,多线程是CPU进行上下文切换,对于内存系统来说,这个切换时要耗时的,所以说使用单线程去操作效率就是最高的。
Redis五大基本数据类型
String(字符串)
expire [键值] [过期时间/秒]:设置过期时间
ttl [键值]:查看过期时间
type [键值]:查看类型
exists [键值]:查看键值是否存在
append [键值] [值]:追加字符串
strlen [键值]:查看长度
incr [键值]:加一
incrby [键值] [步长]:加步长的值
decr [键值]:减一
decrby [键值] [步长]:减步长的值
getrange [键值] [开始下标] [结束下标(包含)]:截取指定范围内的下标的数据
setrange [键值] [开始下标] [替换的字符]:替换字符串
setex(set with expire):设置过期时间
setnx(set if not exists):不存在再设置(分布式锁中会常常使用)
getset [键值] [值]:先get再set,如果不存在值,则返回nil,如果存在值先获取原来的值,再设置值
使用场景:
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
List(列表)
可用将List当成栈、队列、阻塞队列
所有list命令都是以l开头的
lset [键值] [下标] [值]:往指定下标添加元素
lpush [键值] [值]:将一个元素插入到列表的头部
rpush [键值] [值]:将一个元素插入到列表的尾部
lpop [键值] [值]:移除第一个元素
rpop [键值] [值]:移除最后一个元素
lrange [键值] [开始下标] [结束下标]:获取范围内的元素
lindex [键值] [下标]:获取下标的元素
llen [键值]:返回列表的长度
lrem [键值] [移除元素的个数] [移除哪个元素]:移除元素
ltrim [键值] [开始下标] [结束下标]:截取哪些元素
rpoplpush [操作的键值] [移动到哪个键值]:先移除再添加
linsert [键值] [before/after] [值] :将指定的value元素添加到元素的前面还是后面
- 消息队列(lpush rpop)
- 栈(lpush lpop)
Set(集合)
set的值不能重复
sadd [key] [value]:添加
smembers [key]:查看元素的值
sismember [key] [value]:判断值是否存在
scard [key]:判断元素的值的个数
srem [key] [value]:移除指定元素
srandmember [key] [个数] :随机抽取指定个数的元素
spop [key] :随机删除一些set集合中的元素
smove [key1] [key2] [value]:将一个元素的值移动到一个元素中
sdiff [key1] [key2]:差集
sinter [key1] [key2]:交集
sunion [key1] [key2]:并集
使用场景:
- 微博,B站(共同关注)
- 数字集合类(差集、并集、交集)
- 共同爱好、推荐好友
Hash(哈希)
key<key,value>
所有的hash是以h开头的
hset [key] [field] [value]:set一个具体的key-value
hmset [key] [field] [value]:set多个具体的key-value
hgetall [key]:获取全部的数据
hdel [key] [field]:删除指定的key的字段
hlen [key]:获取hash表中指定key的长度
hexists [key] [field]:判断指定key对于的字段是否存在
hkeys [key] :只获取所有的字段
hvals [key] 只获取所有的value
hincrby [key] [field] [步长]:自增或自减步长
hsetnx [key] [field] [value]:如果不存在则可以设置,存在不能设置
使用场景:
- hash变更的数据 user name age,更适合于对象的存储,String更加适合字符串的存储
zset(有序集合)
再set的基础上,增加了一个值,set k1 v1 zset k1 score1 v1
zadd [key] [编号] [value] :添加一个值
zrangebyscore [key] -inf +inf:根据标号排序,从最小到最大
zrangebyscore [key] -inf +inf withscores:显示全部用户,并且附带成绩
zrevrange [key] 0 -1:从大到小进行排序
zrem [key] [value]:移除有序集合中的指定元素
zcard [key]:获取有序集合中的个数
zcount [key] 1 3 :获取指定区间的成员数量
使用场景:
- 排序,班级成绩排序,工资排序
- 重要消息,带权重进行判断
- 排行榜应用实现,取top N
三种特殊数据类型
geospatial:地理位置
再redis3.2版本就推出了
可以推算两地的距离,方圆半径的人等等
注意:
- 我们一般会下载城市数据,直接通过java程序一次性导入
- 有效的经度从-180度到180度
- 有效的纬度从-85.05112878度到85.05112878度
- 当坐标位置超出上述指定范围时,该命令将会返回一个错误
- geo底层的实现原理其实就是zset,我们可以使用zset命令来操作geo
geoadd [key] [经度] [纬度] [名称]:添加带经纬度的数据
geopos [key] [名称]:获取指定城市的经度和纬度
geodist [key] [名称1] [名称2] [单位]:获取两个位置的的距离
- m表示单位为米
- km表示单位为千米
- mi表示单位为英里
- ft表示单位为英尺
georadius [key] [经度] [纬度] [半径] [单位] [withdist] [withcoord] [count 个数]:找出半径范围内的人
- withdist:显示到中心的距离
- withcoord:显示他人的定位信息
- count 个数:显示多少人
georadiusbymember [key] [名称] [距离]:找出指定元素周围的其他元素
geohash [key] [名称]:返回一个或多个位置元素的geohash(11个字符的geohash字符串)
- 将二维的经纬度转换为一维的字符串,字符串越相近,则距离就越相近
hyperloglog(基数统计)
基数:多个集合中不重复的元素数量,可以接受误差
可以用来作为网页的UV(一个人访问一个网站多次,但是还是算作一个人 )
传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标准判断!
优点︰占用的内存是固定,2^64不同的元素的技术,只需要12kb内存,如果用内存角度来比较hyperloglog是首选
只有0.81%错误率,可以忽略不记
pfadd [key] [元素1] [元素2] ...:添加元素
pfcount [key] :统计元素个数
pfmerge [合并后的key] [比较的key1] [比较的key2]:合并两组元素,并集
- 如果可以容错,可以使用hyperloglog
- 如果不允许容错,可以使用set
bitmaps(位存储)
按位存储 0 1 0 1 0 1,只有两个状态的都可以使用
bitmaps位图,数据结构,都是操作二进制进行记录的,就只有0和1两个状态
使用场景:
- 统计用户信息,活跃、不活跃,登录、未登录,打卡
setbit:添加数据
bitcount [key]:1的个数
事务
redis的单条命令是保证事务的,但是redis的事务不保证原子性
redis没有隔离级别的概念,只有在执行命令的时候才会执行
Redis事务本质∶一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行!
一次性、顺序性、排他性
队列 set set set 执行
redis的事务:
- 开启事务(mulit)
- 命令入队()
- 执行事务(exec)
discard(取消事务)
redis监控
watch [key]:监视对象,可以当坐redis的乐观锁操作
unwatch:如果发现事务执行失败,就先解锁,然后在监视
SpirngBoot整合Redis
- jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池,更像BIO模式
- lettuce:采用nEetty,实例可以再多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据,更像NIO模式
注意redis对象都需要序列化
可以自定义序列化方式,默认是jdk
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
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);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
所有传递的对象都要序列化
使用RedisTemplate对象
- opsForValue():操作String类型
- opsForList():操作List类型
- opsForSet():操作Set类型
- opsForHash():操作Hash类型
- opsForZSet():操作ZSet类型
- opsForGeo():操作Geo类型
- opsForHyperLogLog():操作HyperLogLog类型
一般在公司都会有一个工具类简化开发步骤
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.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final 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;
}
}
}
redis.conf详解
unit大小,并且对大小写不敏感
可以导入其他配置
网络
bind 127.0.0.1 #绑定ip
protected-mode yes #保护模式
port 6379 #端口设置
通用 GENERAL
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
logfile "" #日志的文件位置名
databases 16 #数据库的数量,默认是16个
always-show-logo yes #是否显示logo
快照 SNAPSHOTTING
持久化,在规定的时间内,执行了多少次操作,则会持久化到文件 .rdb .aof
redis是内存数据库,如果没有持久化,那么数据断电及失!
#如果900s/15分钟内,如果至少有一个1 key进行了修改,我们及进行持久化操作
save 900 1
#如果300s/5分钟内,如果至少10 key进行了修改,我们及进行持久化操作
save 300 10
#如果60s/1分钟内,如果至少10000 key进行了修改,我们及进行持久化操作
save 60 10000
stop-writes-on-bgsave-error yes #持久化如果出错是否还需要继续工作
rdbcompression yes #是否压缩rdb文件,需要消耗一些cpu资源
rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验
dir ./ #rdb文件保存目录
REPLICATION 复制
SECURITY 安全
requirepass [密码] #设置redis的密码,默认没有密码
限制 CLIENTS
maxclients 10000 #设置能连接上redis的最大客户端的数量
maxmemory <bytes> #redis设置的最大内存容量
maxmemory-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 模式 aof配置
appendonly no #默认不开启aof模式,默认使用的是rdb方式持久化的,在大部分情况下,rdb足够用了
appendfilename "appendonly.aof" #持久化aof文件名字
# appendfsync always #每次修改都会sync。消耗性能
appendfsync everysec #每秒执行一次sync,可能会丢失这1s的数据!
# appendfsync no #不执行 sync,这个时候操作系统自己同步数据,速度最快!
Redis的持久化
RDB(Redis DataBase)
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。Redis会单独创建(( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置!
rdb保存的文件是dump.rdb
触发机制:
- save的规则满足的情况下,会自动触发rdb规则
- 执行flushall命令,也会触发我们的rdb规则!
- 退出redis,也会产生rdb文件
- 备份就自动生成一个dump.rdb
恢复:只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据!
优点:
- 适合大规模的数据恢复!
- 对数据的完整性要不高!
缺点:
- 需要一定的时间间隔进程操作,如果redis意外宕机了,这个最后一次修改数据就没有的了
- fork进程的时候,会占用一定的内容空间
- 持久化的过程中,如果主进程有修改的数据,它会复制一个4k大小的分页,这个操作对子进程来说是透明的,所有持久化的数据是修改之前的数据,会发生数据丢失
AOF(Append Only File)
将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
AOF保存的是appendonly.aof
默认是不开启的,我们要手动开启
如果这个aof文件有错误,这时redis时启动不了的,我们需要修复这个aof文件,redis给我们提供了一个工具redis-check-aof --fix [文件名]
重写规则:
- aof默认的就是文件的无限追加,文件会越来越大
- 如果aof文件大于64m,fork会开启一个新的进程来将我们的文件进行读重写
优点:
- 每一次修改都同步,文件的完整会更加好
- 适合做灾难性的误删除的紧急恢复
缺点:
- 相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
- Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化
- 每秒同步一次,可能会丢失一秒的数据
Redis发布订阅
消息队列
Redis 发布订阅(pub/sub)是一种消息通信模式︰发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统
Redis客户端可以订阅任意数量的频道。
订阅/发布消息图
订阅者:
subscribe [频道]:订阅一个频道
发送者:
publish [频道] [消息] :发送消息
使用场景:
- 实时消息系统!
- 事实聊天(频道当做聊天室,将信息回显给所有人即可!)
- 订阅,关注系统都是可以的
稍微复杂的我们就使用消息中间件MQ
Redis主从复制
主从复制就是将一台Redis服务器的数据,复制到其他的Redis服务器上,前者称为主节点(master/leader),后者称为从节点(slave/follower)
注意:数据的复制都是单向的,只能由主节点到从节点,master以写为主,slave以读为主
主从复制,读写分离,80%的情况下都是在进行读操作,减缓服务器的压力!架构中经常使用
主从复制的作用主要包括:
- 数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复∶当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余
- 负载均衡∶在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量
- 高可用(集群)基石∶除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础
单台Redis最大使用内存不应该超过20G
Redis默认就是一个主机
一主二从
slaveof [ip] [端口]
真实的从主配置应该在配置文件中配置,这样的话是永久的,我们这里使用的是命令,暂时的,当从机重启,从机又会变成主机,也能重新获取到主机写入的数据
主机负责写,从机不能写只能读
如果主机宕机了,从机依然连接到主机,当主机回来了,从机依旧能获取到主机新写入的数据,
复制原理
Slave启动成功连接到master后会发送一个sync同步命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行,我们的数据一定可以在从机中看到
链路
上一个从机是下一个从机的主机
slaveof no one:如果主机断开了,我们可以使用此命令让自己变成主机,其他的节点就可以手动连接到这个最近的主节点(哨兵模式没出现前的解决方案)
哨兵模式
当主机断开后,Redis会自动的把一个从机选举成主机,投票选举。哨兵是一个独立的进程
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通
过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
一般我们会给哨兵也配置集群,防止哨兵死亡
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程(默认30秒),仅仅是哨兵1主观的认为主服务器不可用,这个果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。换主机,这个过程称为客观下线。
配置
sentinel.conf
#sentinel monitor被监控的名称host port 1
sentinel monitor myredis 127.0.0.1 63791
#后面的这个数字1,代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机!
如果Master节点断开了,这个时候就会从从机中随机选择一个服务器(这里面有一个投票算法)
如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点:
- Redis不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择
Redis的缓存穿透和缓存雪崩
缓存穿透
用户发送请求发现缓存没有数据,然后去数据库查,结果数据库也没有数据,这就叫缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿
热点数据正好过期,在恢复的过程中发生大量请求,这就叫缓存击穿
解决方案
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
加互斥锁
分布式锁∶使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
缓存雪崩
redis在某一时间段,缓存集中过期失效,或者说redis宕机
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。