Redis学习笔记
1、NoSQL概述
1.1、为什么要用NoSQL
单机MySQL年代

90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够。那个时候,更多的去使用静态网页Html~服务器根本没有太大的压力。
思考一下,这种情况下:整个网站的瓶颈是什么?
- 数据量如果太大、一个机器放不下了
 - 数据的索引 ( B+ Tree ) ,一个机器内存也放不下
 - 访问量(读写混合),一个服务器承受不了
 
只要你开始出现以上的三种情况之一,那么你就必须要晋级
Memcached(缓存)+ MySQL+垂直拆分(读写分离)
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦。所以说我们希望减轻数据的压力,我们可以使用缓存来保证效率。
发展过程∶优化数据结构和索引 --> 文件缓存( IO ) --> Memcached(当时最热门的技术)

分库分表+水平拆分+MySQL集群
技术和业务在发展的同时,对人的要求也越来越高。
本质:数据库(读,写)
早些年MyISAM:表锁,十分影响效率,高并发下就会出现严重的锁问题,后来转战Innodb:行锁
慢慢的就开始使用分库分表来解决写的压力,MySQL在哪个年代推出了表分区,这个并没有多少公司使用,MySQL的集群,很好满足哪个年代的所有需求。

目前一个基本的互联网项目

为什么要用NoSQL
2010 -- 2020十年之间,世界已经发生了翻天覆地的变化,用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长(定位,也是一种数据,音乐,热榜),MySQL等关系型数据库就不够用了。MySQL有的使用它来存储一些比较大的文件(博客,图片)。数据库表很大,效率就低了。如果有一种数据库来专门处理这种数据,MySQL压力就变得十分小。这时候我们就需要使用NoSQL数据库的,Nosql 可以很好的处理以上的情况。
1.2、什么是NoSQL
关系型数据库:表格,行,列
NoSQL = Not Only SQL(不仅仅是SQL),泛指非关系型数据库的
随着 web2.0 互联网的诞生,传统的关系型数据库很难对付 web2.0 时代,尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL 在当今大数据环境下发展的十分迅速,Redis 是发展最快的,而且是我们当下必须要掌握的一个技术。很多的数据类型用户的个人信息,社交网络,地理位置。这些数据类型的存储不需要一个固定的格式,不需要多余的操作就可以横向扩展的,Map<String,Object> 使用键值对来控制。
NoSQL特点
解耦!
- 方便扩展(数据之间没有关系,很好扩展)
 - 大数据量高性能(Redis一秒写8万次,读取11万,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高)
 - 数据类型是多样型的(不需要事先设计数据库,随取随用,如果是数据量十分大的表,很多人就无法设计了)
 
传统 RDBMS 和 NoSQL:
传统的 RDBMS
- 结构化组织
- sQL
- 数据和关系都存在单独的表中
- 操作操作,数据定义语言
- 严格的一致性
- 基础的事务
- ......
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性,
- CAP 定理和 BASE(异地多活)
- 高性能,高可用,高可扩
- ......
了解:3V + 3高
大数据时代的3V:主要是描述问题的
- 海量Volume
 - 多样Variety
 - 实时Velocity
 
大数据时代的3高:主要是对程序的要求
- 高并发
 - 高可拓
 - 高性能
 
1.3、NoSQL的四大分类
KV键值对:
- 新浪:Redis
 - 美团:Redis + Tair
 - 阿里、百度:Redis + memecache
 
文档型数据库(bson 格式和 json 一样):
- MongoDB(一般必须要掌握)
- MongoDB 是一个基于分布式文件存储的数据库,C++编写,主要用来处理大量的文档
 - MongoDB 是一个介于关系型数据库和非关系型数据中中间的产品,MongoDB是非关系型数据库中功能最丰富,最像关系型数据库的。
 - ConthDB
 
 
列存储数据库:
- HBase
 - 分布式文件系统
 
图关系数据库:

- 他不是存图形,放的是关系,比如∶朋友圈社交网络,广告推荐
 - Neo4j,InfoGrid
 

2、Redis入门
Redis是什么?
Redis ( Remote Dictionary Server ),即远程字典服务。
是一个开源的使用 ANSI,C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库并提供多种语言的API。

redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
免费和开源,是当下最热门的 NoSQL 技术之一,也被人们称之为结构化数据库。
Redis能干嘛?
- 内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof)
 - 效率高,可以用于高速缓存
 - 发布订阅系统
 - 地图信息分析
 - 计时器、计数器(浏览量)
 
特性
- 多样的数据类型
 - 持久化
 - 集群
 - 事务
 
3、Linux下安装Redis
- 前往官网下载:https://redis.io/,并将 Redis 上传到Linux服务器的/opt目录下
 


- 解压安装包
 
tar -zxvf redis-6.2.5.tar.gz #解压

- 安装基本环境命令
 
yum install gcc-c++
make
make install
- 安装完成以后 Redis 默认安装路径 
/usr/local/bin 

- 复制配置文件夹到当前目录
 
cd /usr/local/bin
mkdir RedisConfig
cp /opt/redis-6.2.5/redis.conf RedisConfig

- redis默认不是后台启动,所以需要修改配置文件
 
cd RedisConfig
vim redis.conf

- 启动redis服务
 
cd /usr/local/bin
redis-server RedisConfig/redis.conf #通过配置文件启动
- 使用 
redis-cli测试是否正常启动 
redis-cli -h 127.0.0.1 -p 6379 #启动redis客户端
ping 
set name liuxiang
keys * #查看所有的K

- 查看redis的进程是否开启
 

- 关闭redis服务
 
#在打开redis客户端情况下执行
shutdown
exit
redis-benchmark官方自带的性能测试工具
#测试:100个并发连接,每个连接100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

4、Redis基础知识
redis 默认有16个数据库,默认在第0个,通过命令 select 可以切换数据库
cd /usr/local/bin
redis-server RedisConfig/redis.conf #通过配置文件启动
redis-cli -p 6379 #启动客户端
select 3 #切换数据库
dbsize  #查看数据库大小
flushdb #清空当前数据库
flushall #清空全部数据库
Redis 默认端口是6379
Redis的核心是将全部数据放在内存当中,所以内存系统使用单线程去操作是效率最高的,如果采用多线程会导致上下文切换,这是一个耗时操作。
5、Redis五大数据类型
5.1、String(字符串)
set key value #添加key-value
get key #根据key获取value
keys * #查询全部key
mset key1 value1 key2 value2 ... #设置多个值
mget key1 key2 ... #获取多个值
exists key #判断key是否存在
strlen key #获取key的长度
del key #删除Key
append key value #对key进行追加
type key #检查某个key的类型
expire key seconds #指定key的生效时间
ttl key #检查key的剩余存活时间
pexpice key milliseconds #指定key的失效时间
persist key #撤销key的失效时间
incr key #自动加1
decr key #自动减1
incrby 10 #指定数值增加
decrby 10 #指定数值减少
getrange key start stop #截取 start-stop 的字符串
getrange key 0 -1 #截取全部字符串
setrange key offset value #替换第offset个字符为value
setex key seconds value #如果存在设置值和过期时间
setnx key value #如果不存在则设置(常用于分布式锁)
msetnx key1 value1 key2 value2 ... #如果不存在则设置多个值(原子性操作)
getset key value #组合命令先get再set,可以用来做更新的操作
mset user:1:name zhangshan user:1:age 3 #存储对象
mget user:1:name user:1:age #获取对象值
String 类型的 value 除了是字符串还可以是数字,使用 incr 命令可以做计数器。
5.2、List(列表)
在 redis 里面可以把 List 类型做成栈,阻塞队列等
List 是一个双端队列,先进去的后出来,头部插入的命令是L开头的,尾部插入目录是R开头的。
lpush key value #创建列表并从头部设置值	
lrange key start stop #获取列表中 start-stop 的值
lrange key 0 -1 #获取列表中所有的值
Rpush ket value #从尾部设置值
Lpop key #移除第一个元素
Rpop key #移除最后一个元素
lindex key index #通过下标获取某一个值
Llen key #返回list长度
lrem key count(移除几个) value #移除指定的值
Ltrim key start stop #截断,通过下标截取指定的长度
RpopLpush key1 key2 #组合命令,移除指定集合的最后一个元素并把它添加到新list中
lset key index value #设置指定位置的值(前提是key与下标元素必须存在)
linsert key before/after pivot value #在指定字符串的前或后插入值
list 实际上是一个双向链表,key不存在会创建链表,key存在会新增,空链表相当于不存在。
合理使用 list 可以当作消息队列: lpush Rpop 或者栈: lpush Lpop
5.3、Set(集合)
Set 值是不能重复的
sadd key value #创建集合并添加值
smembers key #查看set集合中的所有值
sismember key value #查看value在指定set中是否存在
scard key #查看set中值的个数
srem key value #移除set中的元素
srandmember key #随机抽选元素	
spop key #随机删除其中一个值
smove key1 key2 member #将集合1的指定值放到集合2
sdiff key1 key2 #差集(找不同的元素)
sinter key1 key2 #交集(找相同的元素,共同好友可以这样实现)
sunion key1 key2 #并集(合并两个集合)
5.4、Hash(哈希)
Map集合 key-value,本质和 String 类型没什么区别
hset myhash field value #创建集合并添加数据
hget myhash field #获取一个字段值
hexists myhash field #判断hash中指定字段是否存在
hdel myhash field #删除hash中指定的key字段
hgetall myhash #获取hash全部数据
hkeys myhash #获取hash中所有字段
hvals myhash #获取hash中所有value值
hlen myhash #获取hash表的字段数量
hmset myhash key field ... #设置hash的多个 key-value
hmget myhash key field ... #获取hash多个字段的值
hsetnx field value #设置hash一个字段只有当这个字段不存在时有效
hsetrlen myhash field #获取hash指定key的长度
hset myhash field 5 #指定增量
hincrby myhash field increment(增量) #为指定hash设置增量
hset user:1 name qinjiang #在hash中存储对象
hash适合存储经常变动的数据,尤其是用户信息之类的如: user:name,age
hash适合存储对象,String适合存储字符串
5.5、Zset(有序集合)
在set的基础上增加了一个值,有助于排序
set:key value
Zset:key score(数字) value
zadd key score value #添加一个值
zadd key score1 value1 score2 value2 ... #添加多个值
zrange key start stop #查看集合中 start-stop 的值
zrangebyscore key -inf(负无穷) +inf( 正无穷) withscores(显示排序参数)#升序排列
zrevrange key start stop withscores(显示排序参数) #降序排列
zrem key value #移除集合中的指定元素
zcard key #获取有序集合中的个数
zcount key start stop #获取指定区间的成员数量
有序集合适合用在需要排序的场景:排行榜,成绩单等
6、三种特殊数据类型
6.1、geospatial地理位置
适用于:朋友的定位,附近的人,打车距离计算等

- 添加地理位置
 
#规则:两极无法直接添加,一般会下载城市数据并通过Java程序添加
geoadd China:city longitude(经度) latitude(纬度) name(城市名)
geopos China:city name #获取指定的城市经纬度

- 计算两个给定位置之前的距离
 
geodist China:city name1 name2 m|km|ft|mi


- 查询指定位置和半径以内的所有地理信息
 
适用于:附近的人
georadius China:city longitude(经度) latitude(纬度) radius(半径) m|km|ft|mi(单位) [WITHCOORD](返回距离) [WITHDIST](返回经纬度) [WITHHASH] [COUNT count](返回数量)
- 查询指定元素和半径以内的所有地理信息
 
georandiusbymember China:city name radius(半径) m|km|ft|mi(单位)
- 返回一个或多个元素的geohash字符串
 
geohash China:city name1 name2 ...
geo的底层实现原理其实是基于Zset,可以通过Zset命令操作geo
例如:删除地理位置和查看地图中全部的元素
zrem China:city name1 name2 ...
zrange China:city 0 -1
6.2、Hyperloglog基数统计
A={1,2,3,4,5,6,5,6} 基数=6
基数:可以接受误差且不重复的元素
Hyperloglog是一种基数统计的算法
业务:网站的UV(同一个人访问网页多次也只能算作一个人)
传统解决方式:set集合,不允许存重复元素。缺点是如果保存了大量用户,处理起来非常麻烦。
Hyperloglog:优点内存占用小,固定占用12kb,可以存2^64的数据,官方提供的错误率只有0.81%,可以忽略不计。
- 添加数据
 
pfadd key value1 value2 ...
- 统计总数
 
pfcount key
- 合并分支
 
pfmerge key1 key2

如果允许容错就一定可以使用Hyperloglog,如果不允许容错,就使用set或者其他的数据类型。
6.3、Bitmap位图
Bitmap 是一种数据结构,适合存储只有两种状态的数据,如:打卡,是否登录等等
Bitmap 都是通过操作二进制位来进行记录的,只有0和1两种状态
- 添加数据
 
setbit key offset value(0或1)

- 获取数据
 
getbit key offset

- 统计数据
 
bitcount key [start end] #默认统计全部的1

7、事务
Redis 事务本质:一组命令的集合,一个事务中的所有命令都回被序列化,在事务执行的过程中,会按照顺序的执行。一次性、顺序性、排他性
---队列 set set set 执行----
Redis 的事务没有隔离级别的概念
所有命令在事务中,都没有直接被执行,而是发起了执行命令 Exec 才会全部被执行。
Redis 的单条命令是保证原子性的,而事务是不保证原子性的
Redis 的事务:
- 开启事务 
multi - 命令入队 (要执行的命令入队)
 - 开启执行 
Exec - 放弃事务 
Discard,一旦执行取消命令,之前队列中的命令都不会被执行 
7.1、事务的异常
正常执行事务

编译型异常(代码有问题,命令有错),事务中所有的命令都不会被执行

运行时异常(如:1/0),如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的
,错误命令抛出异常

7.2、Redis监视器实现乐观锁
悲观锁:主要用于保护数据的完整性。当多个事务并发执行时,某个事务对数据应用了锁,则其他事务只能等该事务执行完了,才能进行对该数据进行修改操作。简而言之,悲观锁无论执行什么操作都会加锁,其效率是极其低下的。
乐观锁:是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
Redis 的乐观锁是通过监视器 watch 实现的
当事务并发执行并提交的时候,监视器 watch 会去检测 key 是否被改变,从而决定当前事务是否被正常执行。



8、Jedis
Jedis 是 Redis 官方推荐的 java 连接开发工具。
使用 Java 操作 Redis 中间件,如果你要使用 java 操作 Redis,那么一定要对 Jedis 十分的熟悉。
8.1、Jedis 连接 Redis
- 创建项目并导入依赖
 
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
<!--阿里官方提供json转换工具包  -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
</dependency>
- Jedis 连接 Redis
 
- 注释IP绑定,如果不注释,则 redis 只能某个IP访问,然后关闭保护模式。
 

- 需要保证防火墙6379端口打开
 

- 阿里云的安全组策略端口也需要打开
 

- 测试
 
import redis.clients.jedis.Jedis;
public class Test {
    public static void main(String[] args) {
        // new Jedis对象即可 host为服务区公网ip
        Jedis jedis = new Jedis("120.55.60.109",6379);
        System.out.println(jedis.ping());
    }
}

8.2、测试常用API
- String(字符串)
 
/**
 * String类型的API
 */
@Test
public void RedisStringTest() {
    System.out.println(jedis.keys("*"));//查看数据库中的所有Key
    jedis.flushDB();//清空当前数据库
    System.out.println("判断某个值是否存在:"+jedis.exists("username"));
    jedis.set("username", "username");
    jedis.set("password", "password");//设置值
    jedis.set("meney","1");
    System.out.println("获取key的长度:"+jedis.strlen("username"));
    System.out.println("对某个key的值进行追加:"+jedis.append("username", "helloword"));
    System.out.println("获取key的值:"+jedis.get("username"));
    System.out.println("数字自增:"+jedis.incr("meney"));
    System.out.println("重命名key:"+jedis.rename("username", "username:helloword"));
    System.out.println("返回当前key的数目:"+jedis.dbSize());
    System.out.println("查看键存储的值类型:"+jedis.type("password"));
    System.out.println("设置过期时间:"+jedis.setex("meney", 30, "100"));
}
- List(列表)
 
/**
 * List类型的API
 */
@Test
public void RedisListTest() {
    System.out.println("设置值:"+jedis.lpush("list:Key", "v1"));
    System.out.println("设置值:"+jedis.lpush("list:Key", "v2"));
    System.out.println("从尾部设置值:"+jedis.rpush("list:Key", "v3"));
    System.out.println("在指定字符串后面插入值:"+jedis.linsert("list:Key", LIST_POSITION.AFTER, "v1", "hmp"));
    System.out.println("获取全部的值:"+jedis.lrange("list:Key", 0, -1));
    System.out.println("通过下标截取指定长度:"+jedis.ltrim("list:Key", 0, 1));
}
- Set(集合)
 
/**
 * set类型的API 
 */
@Test
public void RedisSetTest() {
    System.out.println("设置key1值:"+jedis.sadd("set:key1","1","2","3","4","5"));
    System.out.println("设置key2值:"+jedis.sadd("set:key2","1","2","4","5","6"));
    System.out.println("查看key中的所有值:"+jedis.smembers("set:key1"));
    System.out.println("查看set中值的个数:"+jedis.scard("set:key1"));
    System.out.println("移除key中的元素:"+jedis.srem("set:key1", "1"));
    System.out.println("差集:"+jedis.sdiff("set:key1","set:key2"));
    System.out.println("交集:"+jedis.sinter("set:key1","set:key2"));
    System.out.println("并集:"+jedis.sunion("set:key1","set:key2"));
}
- Hash(哈希)
 
/**
 * Hash类型的API
 */
@Test
public void RedisHashTest() {
    System.out.println("设置值:"+jedis.hset("hash:key", "key1", "v1"));
    jedis.hset("hash:key", "key2", "v2");
    jedis.hset("hash:key", "key3", "v3");
    System.out.println("获取指定key值:"+jedis.hget("hash:key", "key1"));
    System.out.println("获取全部的值:"+jedis.hgetAll("hash:key"));
    System.out.println("获取hash的长度:"+jedis.hlen("hash:key"));
}
- Zset(有序集合)
 
/**
 * Zset类型的API
 */
@Test
public void RedisZsetTest() {
    System.out.println("设置值:"+jedis.zadd("zset:key", 1, "v1"));
    jedis.zadd("zset:key", 2, "v2");
    jedis.zadd("zset:key", 3, "v4");
    jedis.zadd("zset:key", 4, "v3");
    System.out.println("查看有序集合中的值:"+jedis.zrange("zset:key", 0, -1));
    System.out.println("升序排列:"+jedis.zrangeByScore("zset:key", Double.MIN_VALUE, Double.MAX_VALUE));
    System.out.println("降序排列:"+jedis.zrevrangeByScoreWithScores("zset:key", Double.MAX_VALUE, Double.MIN_VALUE));
    System.out.println("获取有序集合中的个数:"+jedis.zcard("zset:key"));
}
8.3、Jedis操作事务
/**
 * 通过jedis去操作事务
 */
@Test
public void RedisTransactionTest() {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("username", "xiaowangba");
    jsonObject.put("password", "password");
    Transaction multi = jedis.multi();//开启事务
    try {
        multi.set("user", jsonObject.toJSONString());
        multi.exec();//执行事务
    } catch (Exception e) {
        multi.discard();//放弃事务
        e.printStackTrace();
    }finally {
        System.out.println("事务执行成功再次获取值:"+jedis.get("user"));
        jedis.close();//关闭连接
    }
}
9、SpringBoot整合Redis
SpringBoot2.X之后,底层由原来的 Jedis 被替换为 Lettuce
Jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用 Jedis Pool 连接池,更像 BIO 模式。
Lettuce:采用Netty,实例可以在多个线程中进行共享,不存在线程不安全的情况。可以减少线程数据,更新 NIO 模式。
9.1、SpringBoot整合步骤
- 导入依赖
 
<!-- springBoot整合redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置连接信息
 
spring:
   redis:
      host: 120.55.60.109
      port: 6379
- 测试
 
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringBootRun.class)
public class TestSpringBootRedis {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    /**
	 * 操作不同的数据类型
	 */
    @Test
    public void RedisTemplateTest1() {
        redistemplate.opsForValue();//操作字符串
        redistemplate.opsForList();//操作List
        redistemplate.opsForSet();//操作Set
        redistemplate.opsForZSet();//操作Zset
        redistemplate.opsForHash();//操作Hash
    }
    /**
	 * 常用方法可以直接通过RedisTemplate进行操作
	 */
    @Test
    public void RedisTemplateTest2() {
        redistemplate.multi();//开启事务
        redistemplate.exec();//提交事务
        redistemplate.discard();//终止事务
        redistemplate.watch("");//乐观锁-监听key
        redistemplate.type("");//查看值类型
    }
    /**
	 * 获取连接
	 */
    @Test
    public void RedisTemplateTest3() {
        RedisConnection connection = redistemplate.getConnectionFactory().getConnection();
        connection.flushDb();//清空当前数据库
        connection.flushAll();//清空所有数据库
    }
}
9.2、自定义RestTemplate
Redis 的对象都需要进行序列化 Serializable,不序列化容易产生中文乱码
RedisTemplate 的源码中有四个序列化默认配置:

继续往下可以看到在源码中默认才有的是 JDK 序列化方式,JDK序列化就会导致字符串转译(中文乱码)

解决这一问题可以使用 Json 序列化方式,就需要自己定义 RestTemplate 配置类
创建一个 Bean 加入容器,就会触发 RedisTemplate 上的条件,注解使默认的 RedisTemplate失效
固定模板拿来即用:
@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all") //压制警告
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        // 将template 泛型设置为 <String, 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);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String序列化配置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        /*
		 * 序列化设置
		 */
        // key、hash的key 采用 String序列化方式
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // value、hash的value 采用 Jackson 序列化方式
        template.setValueSerializer(RedisSerializer.json());
        template.setHashValueSerializer(RedisSerializer.json());
        //把所有配置设置进去
        template.afterPropertiesSet();
        return template;
    }
}
9.3、编写RestTemplate工具类
在实际的工作中,不会经常使用 redistemplate.opsForValue().get(key) 等原生的方法调用 Redis,过程过于繁琐且重复性代码过多。
所以会使用编写工具类的方式,方便调用API。
工具类的编写参考博客:https://www.cnblogs.com/zhzhlong/p/11434284.html
10、redis.conf配置文件详解
- 配置文件 unit 单位对大小写不敏感
 

- 可以包含(配置)别的配置文件
 

- 绑定IP,默认只能本机连接(可注释)
 

- 是否开启保护模式与端口号
 

- 后台启动,默认为no,如果设置为后台启动,需要指定pid文件
 

- 日志与输出的日志文件名
 

- 默认数据库数量16,是否总是显示 LOGO
 

- RDB持久化相关配置
 



- 主从复制
 

- 安全相关
 

#redis的密码可以通过命令行进行设置
config set requirepass 123456
auth 123456 #登录
config get requirepass
- 客户端相关
 



redis 中的默认的过期策略是 volatile-lru
maxmemory-policy 的六种方式
    1. volatile-lru #只对设置了过期时间的key进行LRU(默认值)
    2. allkeys-lru #删除lru算法的key
    3. volatile-random #随机删除即将过期key
    4. allkeys-random #随机删除
    5. volatile-ttl #删除即将过期的
    6. noeviction #永不过期,返回错误
使用命令设置内存淘汰策略:
config set maxmemory-policy noeviction
- AOF持久化相关配置
 


11、Redis的持久化策略
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失,所以Redis 提供了持久化功能。
11.1、RDB(Redis DataBase)持久化
Redis 默认的就是 RDB,一般情况下不需要修改 RDB 配置,RDB 保存的文件是 dump.rdb
RDB其实就是把数据以快照的形式保存在磁盘上。
快照:顾名思义可以理解为拍照一样,把整个内存数据映射到硬盘中,保存一份到硬盘,因此恢复数据起来比较快,把数据映射回去即可,不像AOF,一条条的执行操作命令。
RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb

Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
- 
触发机制
- 
save 的规则满足的条件下,会触发 RDB 规则
 - 
执行
flushall命令,会触发 RDB 规则 - 
redis 关机时,会触发 RDB 规则
 
 - 
 - 
恢复机制
 
将 rdb 文件保存到 redis 启动目录下就可以了,当 redis 启动时会去检查 dump.rdb 文件并恢复其中的数据。
#通过命令查看rdb文件保存路径
config get dir
- 优点
 
大规模存储数据时,效率非常高
- 缺点
 
- 需要一定的时间间隔进行保存操作
 - 如果 redis 意外宕机,最后一次修改的数据会丢失
 - fork 进程会占用一定的内存空间
 
11.2、AOF(Append Only File)持久化
AOF 会将每一个收到的写命令都通过 write 函数追加到文件中
将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍。

以日志的形式来记录每个写操作,将 Redis 执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF 保存的是 appendonly.aof 文件
- 启动AOF持久化
 
在 Redis 的配置文件中 AOF 是默认不启用的,如果需要使用 AOF 持久化需要在 redis.conf 配置文件中修改。

- 测试AOF
 

vim 打开 appendonly.aof 文件,会发现里面就是进行了一些操作的记录

- 如果AOF文件有问题,会导致Redis启动出错
 


如果这个 aof 文件有错位,这时候 redis 是启动不起来的吗,我们需要修复这个 aof 文件。
redis 给我们提供了一个工具 redis-check-aof --fix

- 重写规则说明
 
aof 模式是对文件无限追加,会导致文件越来越大。在 redis.conf 配置文件中可以配置重写规则。

如果文件大于64mb,会 fork 一个新进程来将我们的文件重写
- 优点
 
- 每一次修改都同步,保证了数据的完整性
 - 默认情况下AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据
 
- 缺点
 
- 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
 - AOF运行效率也比RDB慢
 
11.3、扩展
- RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储
 - AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 Redis 协议追加保存每次写的操作到文件末尾,Redis 还能对 AOF 文件进行后台重写,使得 AOF文件的体积不至于过大。
 - 只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
 - 同时开启两种持久化方式
- 在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF文件保存的数据集要比 RDB 文件保存的数据集要完整。RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。
 - 那要不要只使用 AOF 呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。
 
 
11.4、性能建议
- 
因为RDB文件只用作后备用途,建议只在 Slave(从机)上持久化 RDB 文件,而且只要15分钟备份一次就够了,只保留
save 900 1这条规则。 - 
如果 Enable AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的 AOF文件就可以了,代价一是带来了持续的 I0,二是 AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
 - 
如果不 Enable AOF,仅靠 Master-Slave Repllcation(主从复制)实现高可用性也可以,能省掉一大笔IO,也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件,载入较新的那个,微博就是这种架构。
 
12、Redis发布订阅
Redis 发布订阅(publsub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。

下图展示了频道 channel1,以及订阅这个频道的三个客户端―— client2、client5 和 client1 之间的关系:

当有新消息通过 PUBLISH 命令发送给频道 channel1 时,这个消息就会被发送给订阅它的三个客户端:

- 命令
 
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。

- 测试
 
- 订阅端
 

- 发送端
 

- 原理
 
Redis 是使用C实现的,通过分析 Redis 源码里的 pubsublc 文件,了解发布和订阅机制的底层实现。籍此加深对Redis 的理解。Redis 通过 PUBLISH、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 channel,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定channel 的订阅链表中。通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

13、Redis主从复制
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower),数据的复制是单向的,只能由主节点复制到从节点。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
主从复制的作用主要包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
 - 故障恢复︰当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余。
 - 负载均衡∶在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载,尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
 - 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
 
一般来说,要将 Redis 运用于工程项目中,只使用一台 Redis 是万万不能的(宕机),原因如下:
- 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
 - 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。
 
电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。对于这种场景,我们可以使如下这种架构:

13.1、Redis集群环境搭建
- 通过命令查看当前库的信息
 
info replication

- 复制三个配置文件
 

- 修改如下配置文件
 
- 端口
 - pid 名字
 - log文件名字
 - dump.rdb名字
 
vim redis80.conf
port 6380 #修改端口号
pidfile /var/run/redis_6380.pid #修改指定pid文件
logfile "6380.log" #修改输出日志文件名
dbfilename dump6380.rdb #修改持久化文件名
- 修改完毕之后,启动我们的3个 redis 服务器,可以通过进程信息查看
 

13.2、复制原理
- 设置主机命令
 
redis-cli -p 6380 #登录从机客户端
slaveof 127.0.0.1 6379 #设置6379为主机


通过命令设置的主从节点具有暂时性,从机重启以后会恢复成主机。
真实的从主配置应该在配置文件中配置,这样的话是永久的,我们这里使用的是命令,暂时的。

- 细节
 
- 从机不能写操作,只能读取操作
 - 主机设置的值,会被从机自动保存
 - 如果主机宕机,从机依旧在连接主机只是没有写操作了,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息
 - 从机宕机重启以后,依旧可以看到主机设置的全部数据
 
如果主机宕机,手动配置一个从机当主机。
如果主机断开了连接,我们可以使用 slaveof no one 让自己变成主机,其他的节点就可以手动连接到最新的这个主节点(手动)。如果这个时候主机回来了,那就只能重新使用命令配置主从关系了。
slaveof no one #从机设置成主机
- 复制原理
 
Slave 启动成功连接到 master 后会发送一个 sync 同步命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到 slave,并完成一次完全同步。
全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave,完成同步
但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行。
14、哨兵模式
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis2.8开始正式提供了 Sentinel (哨兵)架构来解决这个问题。
手动配置的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。
其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

- 作用
 
- 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器。
 - 当哨兵监测到 master 宕机,会自动将 slave 切换成 master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
 
然而一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover(故障转移)操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
- 哨兵模式的基础配置
 
- 创建 sentinel.conf 配置文件、
 
cd /usr/local/bin/RedisConfig
vim sentinel.conf #创建哨兵配置文件
sentinel moitor myredis 127.0.0.1 6379 1 #myredis是被监控名称

- 启动哨兵
 
cd /usr/local/bin
redis-sentinel RedisConfig/sentinel.conf #启动哨兵

- 测试哨兵模式
 
如果 Master 节点断开了,这个时候就会从从机中随机选择一个服务器!(这里面有一个投票算法 )
这里我们关闭6379主机,查看两个从机状态,发现6381从机变为了主机。如果6379回来了,也只能作为6381的从机,这就是哨兵模式的规则。




- 哨兵模式的优缺点
 
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
 - 主从可以切换,故障可以转移,系统的可用性就会更好
 - 哨兵模式就是主从模式的升级,手动到自动,更加健壮
 
缺点:
- Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦
 - 实现哨兵模式的配置其实是很麻烦的,里面有很多选择
 
- 哨兵模式的全部配置
 
# Example sentinel.conf
 
# 哨兵sentinel实例运行的端口 默认26379
port 26379
 
# 哨兵sentinel的工作目录
dir /tmp
 
# 哨兵sentinel监控的redis主节点的 ip port 
# master-name  可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1
 
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
 
 
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
 
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
#这个数字越小,完成failover所需的时间就越长,
#但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
#可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
 
 
 
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面: 
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。  
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
 
# SCRIPTS EXECUTION
 
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
 
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
 
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。 
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
15、缓存穿透与雪崩
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。
15.1、缓存穿透
缓存穿透的概念很简单,用户想要查询一个数据,发现 redis 内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
- 解决方案
 
- 布隆过滤器
 
布隆过滤器是一种数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。

- 缓存空对象
 
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。

但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键。
 - 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
 
15.2、缓存击穿
这里需要注意和缓存击穿的区别,缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
- 解决方案
 
- 设置热点数据永不过期
 
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
- 加互斥锁 
setnx 
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
15.3、缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效或Redis宕机。
产生雪崩的原因之一,比如马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
- 解决方案
 
- redis高可用
 
这个思想的含义是,既然 redis 有可能挂掉,那我多增设几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活)
- 限流降级
 
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 数据预热
 
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

                
            
        
浙公网安备 33010602011771号