一、Redis介绍
1.1 数据库压力过大
由于用户量增大,请求数量也随之增大,数据压力过大
1.2 数据不同步
多台服务器之间,数据不同步
1.3 传统锁失效
多台服务器之间的锁,已经不存在互斥性了。
2.1 NoSQL介绍
Redis就是一款NoSQL。
NoSQL -> 非关系型数据库 -> Not Only SQL。
Key-Value:Redis。。。
文档型:ElasticSearch,Solr,Mongodb。。。
面向列:Hbase,Cassandra。。。
图形化:Neo4j。。。
除了关系型数据库都是非关系型数据库。
NoSQL只是一种概念,泛指非关系型数据库,和关系型数据库做一个区分。
2.2 Redis介绍
有一位意大利人,在开发一款LLOOGG的统计页面,因为MySQL的性能不好,自己研发了一款非关系型数据库,并命名为Redis。Salvatore。
Redis(Remote Dictionary Server)即远程字典服务,Redis是由C语言去编写,Redis是一款基于Key-Value的NoSQL,而且Redis是基于内存存储数据的,Redis还提供了多种持久化机制,性能可以达到110000/s读取数据以及81000/s写入数据,Redis还提供了主从,哨兵以及集群的搭建方式,可以更方便的横向扩展以及垂直扩展。
二、Redis的安装
方法一:使用源码的方式安装
## 首先,将redis的安装包上传到服务器上,我们将其存放到usr/local中
## 第一:安装gcc环境
yum -y install gcc-c++
## 第二:解压redis源码包
tar -zxvf redis-3.2.6.tar.gz
## 第三:编译redis源码(进入到Reids目录)
make
## 第四:安装redis
make install PREFIX=/usr/local/redis -- 指定安装到某个路径下面
Reids默认是使用前台进程启动,所以要操作Redis需要重新再开一个窗口
## 第五:将redis源码包中的redis.conf配置文件复制到/usr/local/redis3/bin/下
方法二:使用Docker-Compose安装
version: '3.1'
services:
redis:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
使用redis-cli连接Redis
进去Redis容器的内部
docker exec -it 容器id bash (不使用容器启动不需进入)
在容器内部,使用redis-cli连接
执行语句认证
auth 【认证密码】
Redis配置相关
将daemonize由no改为yes // reids后台模式启动
bind 127.0.0.1 // 取消本机绑定
requirepass root //设置root账号登录密码启动redis ./redis-server redis.conf
关闭 ./redis-cli shutdown-h 指定ip -p 指定端口 -a 指定密码 -c 连接集群结点时使用,此选项可防止moved和ask异常
三、Redis存储数据的结构
常用的5种数据结构:
- key-string:一个key对应一个值。 最常用的,一般用于存储一个值。
- key-hash:一个key对应一个Map。存储一个对象数据的。
- key-list:一个key对应一个列表。使用list结构实现栈和队列结构。
- key-set:一个key对应一个集合。交集,差集和并集的操作。
- key-zset:一个key对应一个有序的集合。排行榜,积分存储等操作。
四、Redis常用命令
2.1 string常用命令
string常用操作命令
#1. 添加值
set key value
#2. 取值
get key
#3. 批量操作
mset key value [key value...]
mget key [key...]
#4. 自增命令(自增1)
incr key
#5. 自减命令(自减1)
decr key
#6. 自增或自减指定数量
incrby key 【自增值】
decrby key 【自减值】
#7. 设置值的同时,指定生存时间(每次向Redis中添加数据时,尽量都设置上生存时间)
setex key 【生存时间】 value
#8. 设置值,如果key已存在不添加且返回0,不存在添加且返回1
setnx key value
#9. 在key对应的value后,追加内容
append key value
#10. 查看value字符串的长度
strlen key
#11、删除key
del key
4.3 hash常用命令
hash常用命令
#1. 存储数据
hset key 【字段】 【字段值】
如:hset user name kobe
#2. 获取数据,重复的话会进行覆盖
hget key 【字段】
#3. 批量操作
hmset key 【字段】 【字段值】 [【字段】 【字段值】 ...]
hmget key 【字段】 [【字段】 ...]
#4. 自增(指定自增的值)
hincrby key 【字段】 【自增值】
#5. 设置值,如果key已存在不添加且返回0,不存在添加且返回1
hsetnx key 【字段】 value
#6. 检查【字段】是否存在,存在返回1,不存在返回0
hexists key 【字段】
#7. 删除key的字段,可以删除多个
hdel key 【字段】 [字段 ...]
#8. 获取当前hash结构中的全部字段和值
hgetall key
#9. 获取当前hash结构中的全部字段
hkeys key
#10. 获取当前hash结构中的全部value
hvals key
#11. 获取当前hash结构中【字段】的数量
hlen key
4.4 list常用命令
list常用命令
#1. 存储数据(从左侧插入数据,从右侧插入数据)
lpush key value [value ...]
rpush key value [value ...]
#2. 存储数据,将一个值插入到已存在的列表头部,列表不存在时操作无效。
lpush key value //插入一个值到左头部
rpush key value //插入一个值到右头部
lpushx key value ... //插入多个值到左头部
rpushx key value ... //插入多个值到右头部
#3. 修改数据(在存储数据时,指定好你的索引位置,覆盖之前索引位置的数据,index超出整个列表的长度,也会失败)
lset key index value
#4. 弹栈方式获取数据(左侧弹出数据,从右侧弹出数据) 即返回并删除
lpop key
rpop key
#5. 获取指定索引范围的数据(start从0开始,stop输入-1,代表最后一个,-2代表倒数第二个)
lrange key start stop
#6. 获取指定索引位置的数据
lindex key index
#7. 获取整个列表的长度
llen key
#8. 删除列表中的数据(他是删除当前列表中的count个value值,count > 0从左侧向右侧删除,count < 0从右侧向左侧删除,count == 0删除列表中全部的value)
lrem key count value
#9. 保留列表中的数据(保留你指定索引范围内的数据,超过整个索引范围被移除掉)
ltrim key start stop
#10. 将一个列表中最后的一个数据,插入到另外一个列表的头部位置
rpoplpush list1 list2
4.5 set常用命令
set常用命令
#1. 存储数据
sadd key value [value ...]
#2. 获取数据(获取全部数据,是无序的)
smembers key
#3. 随机弹出一个数据,count代表弹出数据的个数,count默认为1
spop key [count]
#4. 交集(取多个set集合交集)
sinter set1 set2 ...
#5. 并集(获取全部集合中的数据)
sunion set1 set2 ...
#6. 差集(获取多个第一个key中与第二个key不一样的数据)
sdiff set1 set2 ...
# 7. 删除数据
srem key member [member ...]
# 8. 查看当前的set集合中是否包含这个值
sismember key member
4.6 zset的常用命令
zset常用命令
#1. 添加数据(score必须是数值。member不允许重复的。)
zadd key score member [score member ...]
#2. 增加member的分数(如果member是存在于key中的,正常增加分数,如果memeber不存在,这个命令就相当于zadd)
zincrby key 【修改的值】 member
#3. 查看指定的member的分数
zscore key member
#4. 获取zset中数据的数量
zcard key
#5. 根据score的范围查询member数量
zcount key min max
#6. 删除zset中的成员
zrem key member [member...]
#7. 根据分数从小到大排序,获取指定范围内的数据(withscores如果添加这个参数,那么会返回member对应的分数)
zrange key start stop [withscores]
#8. 根据分数从大到小排序,获取指定范围内的数据(withscores如果添加这个参数,那么会返回member对应的分数)
zrevrange key start stop [withscores]
4.7 key常用命令
key常用命令
#1. 查看Redis中的全部的key(pattern:* ,xxx*,*xxx)
keys pattern
#2. 查看某一个key是否存在(1 - key存在,0 - key不存在)
exists key
#3. 删除key
del key [key ...]
#4. 设置key的生存时间,单位为秒,单位为毫秒,设置还能活多久
expire key second
pexpire key milliseconds
set name admin ex 10;# 添加key同时设置时间
#6. 查看key的剩余生存时间,单位为秒,单位为毫秒(-2 - 当前key不存在,-1 - 当前key没有设置生存时间,具体剩余的生存时间)
ttl key
pttl key
#7. 移除key的生存时间(1 - 移除成功,0 - key不存在生存时间,key不存在)
persist key
#8. 选择操作的库
select 0~15
#9. 移动key到另外一个库中
move key db
4.8 库的常用命令
db常用命令
#1. 清空当前所在的数据库
flushdb
#2. 清空全部数据库
flushall
#3. 查看当前数据库中有多少个key
dbsize
#4. 查看最后一次操作的时间
lastsave
#5. 实时监控Redis服务接收到的命令
monitor
#6. 切换数据库
select 【数据库的值】
五、Redis整合Spring以及Redis的管道技术
5.1 Jedis连接Redis
5.1.1 创建Maven工程
idea创建
5.1.2 导入需要的依赖
<dependencies>
<!-- 1、 Jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 2、 Junit测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- 3、 Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
</dependencies>
5.1.3 测试
public class Demo1 {
@Test
public void set(){
//1. 连接Redis
Jedis jedis = new Jedis("192.168.199.109",6379);
//2. 操作Redis - 因为Redis的命令是什么,Jedis的方法就是什么
jedis.set("name","李四");
//3. 释放资源
jedis.close();
}
@Test
public void get(){
//1. 连接Redis
Jedis jedis = new Jedis("192.168.199.109",6379);
//2. 操作Redis - 因为Redis的命令是什么,Jedis的方法就是什么
String value = jedis.get("name");
System.out.println(value);
//3. 释放资源
jedis.close();
}
}
public class Demo3 {
// 存储对象 - 以String形式存储
@Test
public void setString(){
//1. 连接Redis
Jedis jedis = new Jedis("192.168.199.109",6379);
//2.1 准备key(String)-value(User)
String stringKey = "stringUser";
User value = new User(2,"李四",new Date());
//2.2 使用fastJSON将value转化为json字符串
String stringValue = JSON.toJSONString(value);
//2.3 存储到Redis中
jedis.set(stringKey,stringValue);
//3. 释放资源
jedis.close();
}
// 获取对象 - 以String形式获取
@Test
public void getString(){
//1. 连接Redis
Jedis jedis = new Jedis("192.168.199.109",6379);
//2.1 准备一个key
String key = "stringUser";
//2.2 去Redis中查询value
String value = jedis.get(key);
//2.3 将value反序列化为User
User user = JSON.parseObject(value, User.class);
//2.4 输出
System.out.println("user:" + user);
//3. 释放资源
jedis.close();
}
}
5.2 Spring整合Redis
依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.8.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
配置
<!-- 配置redis连接池对象 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大空闲数 -->
<property name="maxIdle" value="50" />
<!-- 最大连接数 -->
<property name="maxTotal" value="100" />
<!-- 最大等待时间 -->
<property name="maxWaitMillis" value="20000" />
</bean>
<!-- 配置redis连接工厂 -->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<!-- 连接池配置 -->
<property name="poolConfig" ref="poolConfig" />
<!-- 连接主机 -->
<property name="hostName" value="192.168.193.66" />
<!-- 端口 -->
<property name="port" value="6379" />
<!-- 密码 -->
<!--<property name="password" value="root" />-->
</bean>
<!-- 配置redis模板对象 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<!-- 配置连接工厂 -->
<property name="connectionFactory" ref="connectionFactory"/>
<!-- 配置Redis key系列化方式为spring-->
<property name="keySerializer" ref="stringRedisSerializer"/>
</bean>
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
测试
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testString(){
redisTemplate.opsForValue(); // 字符串类型
redisTemplate.opsForHash(); // hash类型
redisTemplate.opsForList(); // lit类型
redisTemplate.opsForSet(); // set类型
redisTemplate.opsForZSet(); // zset类型
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("age","20");
Object age = valueOperations.get("age");
System.out.println(age);
}
5.3 Redis的管道操作
因为在操作Redis的时候,执行一个命令需要先发送请求到Redis服务器,这个过程需要经历网络的延迟,Redis还需要给客户端一个响应。
如果我需要一次性执行很多个命令,上述的方式效率很低,可以通过Redis的管道,先将命令放到客户端的一个Pipeline中,之后一次性的将全部命令都发送到Redis服务,Redis服务一次性的将全部的返回结果响应给客户端。
@Test
public void test3(){ // 5.56
ValueOperations valueOperations = redisTemplate.opsForValue();
for(int i =0;i<10000;i++){
valueOperations.set("key_"+i,"val_"+i);
}
}
@Test
public void test4(){ // 153m
ValueOperations valueOperations = redisTemplate.opsForValue();
redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Object execute(RedisOperations<K, V> redisOperations) throws DataAccessException {
for(int i =0;i<10000;i++){
valueOperations.set("key_"+i,"val_"+i);
}
return null;
}
});
}
5.4 Redis的应用
5.4.1 准备用户登录的项目
spring-redis.xml
<!-- 配置redis连接池对象 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大空闲数 -->
<property name="maxIdle" value="50" />
<!-- 最大连接数 -->
<property name="maxTotal" value="100" />
<!-- 最大等待时间 -->
<property name="maxWaitMillis" value="20000" />
</bean>
<!-- 配置redis连接工厂 -->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<!-- 连接池配置 -->
<property name="poolConfig" ref="poolConfig" />
<!-- 连接主机 -->
<property name="hostName" value="192.168.193.66" />
<!-- 端口 -->
<property name="port" value="6379" />
<!-- 密码 -->
<!--<property name="password" value="root" />-->
</bean>
<!-- 配置redis模板对象 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<!-- 配置连接工厂 -->
<property name="connectionFactory" ref="connectionFactory"/>
<!-- 配置Redis key系列化方式为spring-->
<property name="keySerializer" ref="stringRedisSerializer"/>
</bean>
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
5.4.2准备docker-compose.yml文件
version: "3.1"
services:
nginx:
image: daocloud.io/library/nginx:latest
restart: always
container_name: nginx
ports:
- 80:80
volumes:
- ./nginx_conf:/etc/nginx/conf.d
tomcat1:
image: daocloud.io/library/tomcat:7.0.59-jre8
restart: always
container_name: tomcat1
ports:
- 8081:8080
environment:
- TZ=Asia/Shanghai
volumes:
- ./tomcat1/webapps:/usr/local/tomcat/webapps
- ./tomcat1/logs:/usr/local/tomcat/logs
- ./tomcat1/rbaclog:/opt/ssm/log
tomcat2:
image: daocloud.io/library/tomcat:7.0.59-jre8
restart: always
container_name: tomcat2
ports:
- 8082:8080
environment:
- TZ=Asia/Shanghai
volumes:
- ./tomcat2/webapps:/usr/local/tomcat/webapps
- ./tomcat2/logs:/usr/local/tomcat/logs
- ./tomcat2/rbaclog:/opt/ssm/log
mysql:
image: daocloud.io/library/mysql:5.7.24
restart: always
container_name: mysql
ports:
- 3306:3306
environment:
- MYSQL_ROOT_PASSWORD=root
- TZ=Asia/Shanghai
volumes:
- ./mysql_data:/var/lib/mysql
command:
--character-set-server=utf8
5.4.3修改认证功能
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "/login")
public String login(String username, String password, HttpSession session, Model model, HttpServletResponse response){
User user = userServie.login(username, password);
if(user != null){
String key = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(key,user);
Cookie cookie = new Cookie(Constant.SESSION_USER,key);
cookie.setPath("/");
cookie.setMaxAge(60*60*24);
response.addCookie(cookie);
return "redirect:/index.jsp";
}else{
model.addAttribute("msg","用户名获密码错误。。");
return "login";
}
}
5.4.4 修改过滤器信息
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
// 2.判断
if(isLogin(httpServletRequest)){
return true;
}else{
httpServletRequest.setAttribute("msg","你还没有登录,请先登录");
httpServletRequest.getRequestDispatcher("/login.jsp").forward(httpServletRequest,httpServletResponse);
return false;
}
}
private boolean isLogin(HttpServletRequest httpServletRequest) {
String cookie_value = null;
Cookie[] cookies = httpServletRequest.getCookies();
if(cookies != null && cookies.length> 0){
for(int i =0;i<cookies.length;i++){
Cookie cookie = cookies[i];
if(Constant.SESSION_USER.equals(cookie.getName())){
cookie_value = cookie.getValue();
break;
}
}
}
if(cookie_value == null){
return false;
}
Object o = redisTemplate.opsForValue().get(cookie_value);
if(o != null){
return true;
}
return false;
}
5.4.5 修改权限过滤
@Aspect // 表示这个是一个切面
@Component // 让spring容器扫描到
public class SysAdminPermissionAOP {
@Autowired
private HttpSession session;
@Autowired
private RedisTemplate redisTemplate;
public SysAdminPermissionAOP(){
System.err.println("=================================");
}
@Around("@annotation(permission)") // 只有调用加了@Permissiond方法的时候才会进入到这个方法里面
public Object hanlderPermission(ProceedingJoinPoint joinPoint,Permission permission){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String uid = CookieUtils.getCookieValue(Constants.SESSION_USER);
SystemAdmin systemAdmin = (SystemAdmin) redisTemplate.opsForValue().get(uid);
if(systemAdmin == null){
throw new BusinessException(10003,"你还没有登录");
}
Set<String> perSet = systemAdmin.getPerSet();
// 2.获取用户正在访问的资源的权限
String value1 = permission.value();
// 3.判断是否包含
if(!perSet.contains(value1)){
throw new BusinessException(10003,"你没有权限访问这个资源。。。。");
}
Object proceed = null;
try {
Object[] args = joinPoint.getArgs(); // 获取正在调用方法的参数
proceed = joinPoint.proceed(args); // 调用Controller
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
5.4.6 重新部署
重新打成war包,复制到数据卷路径中即可
六、Redis的事务
6.1 redis的事务
6.1.1 Redis事务的特点
redis事务的特点
1、redis的开启事务后,每次的操作都会放在一个队列中
2、redis开启事务后,当执行了一个错误的指令,事务不能正常提交
3、开启事务后,最后调用了回滚的命令,队列中的命令就会丢弃,不执行
redis的监控
- monitor
redis开启事务
- multi
redis的事务提交
- exec
redis的事务回滚
- discard
6.2 redis的监听机制
解决问题:如果开启事务后,对某个key进行多次修改,只有当事务提交后才会做修改,但是有可能其他事务也在提交,就会存在值被覆盖
试验步骤:给name赋初始值admin并且使用watch开启监听,开启事务,在事务内对name进行修改,在修改的同时另一客户端进行对name进行修改,当事务进行提交时,发现事务并不能提交,这就是redis的监听机制底层是基于乐观锁实现的
1、set name admin
2、watch name ## 对name进行监听
3、mulit
4、set name 1
5、set name 2
6、exec ## 在事务提交之前,有其他事务对name修改了,这时候事务是提交失败的。
6.3 在spring中操作事务
一定要使用executePipelined方法,这样才能保证多个事务使用的是同一个连接
/**
* 测试redis的事务
*/
@Test
public void testTransaction(){
//获取到的是一个连接
redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
//1、开启事务
redisTemplate.multi();
try {
//2、发送命令
redisTemplate.opsForValue().set("username","kobe");
//3、事务提交
redisTemplate.exec();
}catch (Exception e){
e.printStackTrace();
//3、发生异常,事务回滚
redisTemplate.discard();
}
return null;
}
});
}
6.4、Redis持久化机制
作用:因为redis的数据是放在缓存中的,当redis停止或者关闭,数据就丢失了,所以可以使用持久化机制存储数据
Redis会定期的把内存中的数据持久化到硬盘上,当reids服务关闭后,重启Redis会自动把硬盘的数据恢复到内存中继续使用。可以用shutdown命令关闭Redis服务来进行测试。Redis存在两种持久化方式RDB和AOP。
6.4.1 RDB
RDB(快照)是记录当前Reids一瞬间的内存结构,以文件的形式保存的硬盘上,默认存储在bin目录下的dump.rdb目录下,redis会存在默认的持久化策略
快照相关命令:
- save: -- 手动进行快照,这个过程是前台快照,在执行的快照的的过程中,redis是拒绝写命令的。
- bgsave:手动进行快照,这个过程是后台快照,相当于redis在后天启动了一个新的线程进行存储,这期间,redis接收到的指令会放在一个缓冲区中,当后台线程执行完毕后,redis会把缓冲区中的数据再次进行存储
RDB是Redis默认的持久化机制
RDB持久化文件,速度比较快,而且存储的是一个二进制的文件,传输起来很方便。
RDB无法保证数据的绝对安全。
6.4.2 RDB相关配置
RDB持久化的时机:
save 900 1:在900秒内,有1个key改变了,就执行RDB持久化。
save 300 10:在300秒内,有10个key改变了,就执行RDB持久化。
save 60 10000:在60秒内,有10000个key改变了,就执行RDB持久化。
dir ./ 快照文件存放的位置
rdbchecksum yes 回复快照时检查快照的完整性
dbfilename dump.rdb 快照文件的名称(可以把这个文件发送给别人,启动后缓存就会有的对应的数据)
stop-writes-on-bgsave-error yes 因为bgsave命令是启动一个新的线程执行快照,当前线程还是接收写命令的,如果新线程在执行快照的过程中出现错误是否停止写命令,默认是yes,这样用户可以感知到执行快照失败。
6.4.2 AOF
AOF相当于将所有的写入和删除命令追加记录在一个文件中,当redis启动时,redis会重新执行这些命令达到数据还原,当AOF和RDB同时启动时,服务器启动时会优先读取AOF
6.4.3 AOF配置
appendonly no # 是否开启只追加文件
appendfilename "appendonly.aof" # 只追加文件的名称
# 只追加文件记录评率
保存的策略
appendfsync always # 每次执行写操作后记录到只追加文件中(绝对安全)
appendfsync everysec # 每秒中记录一次只追加文件(默认值)
# appendfsync no # 不主动记录只追加文件,需要手动记录
auto-aof-rewrite-min-size 64mb # 只追加文件的大小,超过64兆新建一个文件记录命令
# 使用AOF也有可能在丢失最后一秒的数据,当最后一秒服务器异常,还来不及存储就挂了
AOF持久化机制默认是关闭的,Redis官方推荐同时开启RDB和AOF持久化,更安全,避免数据丢失。
AOF持久化的速度,相对RDB较慢的,存储的是一个文本文件,到了后期文件会比较大,传输困难。
AOF相对RDB更安全,推荐同时开启AOF和RDB。
6.4.5 AOF和RDB的区别
AOF持久化机制默认是关闭的,Redis官方推荐同时开启RDB和AOF持久化,更安全,避免数据丢失。
AOF持久化的速度,相对RDB较慢的,存储的是一个文本文件,到了后期文件会比较大,传输困难。
AOF相对RDB更安全,推荐同时开启AOF和RDB。
6.4.6注意事项
同时开启RDB和AOF的注意事项:
如果同时开启了AOF和RDB持久化,那么在Redis宕机重启之后,需要加载一个持久化文件,优先选择AOF文件。
快照:记录和回复的速度快,数据的安全性不高(因为他是按照时间来记录的)
只追加文件:记录和恢复的速度慢(命令一旦很多久执行慢),数据安全性高(因为最多只丢失1s的数据)
如果Reids只是作为缓存服务器话,快照和只追加文件都可以关闭,这样可以大大的提高Redis读写性能,如果对数据要求安全性很高,则两个都可以开启。
七、redie的集群
1、Redis为什么要集群
解决单体故障
处理高并发2、有状态/无状态
redis集群是有状态的3、主从复制
数据库备份方案4、读写分离
提供Reids的性能5、哨兵机制
监控主,主挂了后进行选举
从服务器选举冲一台当主
主从赋值原理图,当项目的数据写到主redis服务器的时候,主redis服务器会发送广播,将所有数据赋值到从的redis服务器中,哨兵负责检测主redie是否正常,当检测到主不正常时,哨兵会进行选举,将一台从redis服务器当成主服务器
7.1 Redis的主从架构架构的搭建
编写docker-compose.yml文件
version: "3.1"
services:
redis1:
# 镜像
image: daocloud.io/library/redis:5.0.7
restart: always
# 容器名称
container_name: redis1
# 配置时区
environment:
- TZ=Asia/Shanghai
# 配置映射端口
ports:
- 7001:6379
# 配置数据卷
volumes:
- ./conf/redis1.conf:/usr/local/redis/redis.conf
# 容器启动执行的默认命令
command: ["redis-server","/usr/local/redis/redis.conf"]
redis2:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis2
environment:
- TZ=Asia/Shanghai
ports:
- 7002:6379
volumes:
- ./conf/redis2.conf:/usr/local/redis/redis.conf
# 映射reids1的地址为master
links:
- redis1:master
command: ["redis-server","/usr/local/redis/redis.conf"]
redis3:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis3
environment:
- TZ=Asia/Shanghai
ports:
- 7003:6379
volumes:
- ./conf/redis3.conf:/usr/local/redis/redis.conf
links:
- redis1:master
command: ["redis-server","/usr/local/redis/redis.conf"]
# redis2和redis3从节点的redis2.conf和redis3.conf配置 相当于配置主redis的地址,其中master为之前映射主reids服务地址
replicaof master 6379
7.1.1 主从赋值过程
1.从节点保存主节点信息
2.主从建立socket连接
3.从节点发送ping命令,等待主节点回应
4.权限验证,比如密码校验
5.主从连接正常后,开始同步数据集,首次建立复制,是全量复制的方式
6.持续的主从复制,后续主节点发生数据变更,会继续给从节点发送命令,此处采用增量复制
6.4.2 全量复制
全量复制Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
1)从服务器连接主服务器,发送SYNC命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
6.4.3 增量复制
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
总结增量同步就是当 master 服务器有数据更新的时候,会立刻同步到所有的 slave 服务器
6.4.5 全量复制和增量复制的区别
发生事件的时机不同,全量复制发生的时机是节点初始化的时候,增量复制发生的时机是在节点初始化完成后数据发生改变
7.2 Redis的哨兵机制
哨兵可以帮助我们解决主从架构中的单点故障问题,
哨兵机制是一种容灾方案。哨兵:实则是一个在特殊模式下的Redis服务器,里面存储的是自己本身的信息,主服务器的信息,从服务器的信息。用一个或者多个哨兵来监视主服务器(也就是进行写操作的服务器)是否在正常执行任务,一旦哨兵发现主服务器不可用时,就找到一个合适的从服务器成为主服务器。
哨兵是对Redis的系统的运行情况的监控,它是一个独立进程,功能有二个:
监控主数据库和从数据库是否运行正常;
主数据出现故障后自动将从数据库转化为主数据库;
修改了以下docker-compose.yml,为了可以在容器内部使用哨兵的配置
version: "3.1"
services:
redis1:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis1
environment:
- TZ=Asia/Shanghai
ports:
- 7001:6379
volumes:
- ./conf/redis1.conf:/usr/local/redis/redis.conf
- ./conf/sentinel1.conf:/data/sentinel.conf
command: # 覆盖容器启动的默认命令
- /bin/sh
- -c
- |
redis-sentinel /data/sentinel.conf
redis-server /usr/local/redis/redis.conf
redis2:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis2
environment:
- TZ=Asia/Shanghai
ports:
- 7002:6379
volumes:
- ./conf/redis2.conf:/usr/local/redis/redis.conf
- ./conf/sentinel2.conf:/data/sentinel.conf
links:
- redis1:master
command:
- /bin/sh
- -c
- |
redis-sentinel /data/sentinel.conf
redis-server /usr/local/redis/redis.conf
redis3:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis3
environment:
- TZ=Asia/Shanghai
ports:
- 7003:6379
volumes:
- ./conf/redis3.conf:/usr/local/redis/redis.conf
- ./conf/sentinel3.conf:/data/sentinel.conf
links:
- redis1:master
command:
- /bin/sh
- -c
- |
redis-sentinel /data/sentinel.conf
redis-server /usr/local/redis/redis.conf
/bin/sh是指此脚本使用/bin/sh来解释执行,其后面根的是此解释此脚本的shell的路径。/bin/sh和/bin/bash效果类似。
使用/bin/bash -c指定将命令转为一个完整命令执行 redis的哨兵默认端口号为26379
配置哨兵的配置文件,每个哨兵都有一个配置文件:sentinel.conf文件,可以在redis源码的conf目录下进行寻找
主redis哨兵的配置文件
# 哨兵需要后台启动
daemonize yes
# 指定Master节点(主服务器)的ip和端口(主)
sentinel monitor master localhost 6379 2
# 指定Master节点的ip和端口(从)
# 例子表示的是声明该Sentinel监控的master的地址为marster,端口号为6379,最后一个2表示的意思是# 当集群中有2个Sentinel认为master宕机了或者1个Sentinel有2次认为master宕机了,就会真正认为该master彻底宕# 机了。
# sentinel monitor master master 6379 2
# 哨兵每隔多久监听一次redis架构
sentinel down-after-milliseconds master 10000
从redis哨兵的配置文件
# 哨兵需要后台启动
daemonize yes
# 指定Master节点的ip和端口(主)
# sentinel monitor master localhost 6379 2
# 指定Master节点的ip和端口(从)
# 例子表示的是声明该Sentinel监控的master的地址为marster,端口号为6379,最后一个2表示的意思是# 当集群中有2个Sentinel认为master宕机了或者1个Sentinel有2次认为master宕机了,就会真正认为该master彻底宕# 机了。
sentinel monitor master master 6379 2
# 哨兵每隔多久监听一次redis架构
sentinel down-after-milliseconds master 10000
6.6 Redis的集群
Redis集群在保证主从加哨兵的基本功能之外,还能够提升Redis存储数据的能力。
Redis集群架构图 |
---|
准备yml文件
# docker-compose.yml
version: "3.1"
services:
redis1:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis1
environment:
- TZ=Asia/Shanghai
ports:
- 7001:7001
- 17001:17001
volumes:
- ./conf/redis1.conf:/usr/local/redis/redis.conf
command: ["redis-server","/usr/local/redis/redis.conf"]
redis2:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis2
environment:
- TZ=Asia/Shanghai
ports:
- 7002:7002
- 17002:17002
volumes:
- ./conf/redis2.conf:/usr/local/redis/redis.conf
command: ["redis-server","/usr/local/redis/redis.conf"]
redis3:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis3
environment:
- TZ=Asia/Shanghai
ports:
- 7003:7003
- 17003:17003
volumes:
- ./conf/redis3.conf:/usr/local/redis/redis.conf
command: ["redis-server","/usr/local/redis/redis.conf"]
redis4:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis4
environment:
- TZ=Asia/Shanghai
ports:
- 7004:7004
- 17004:17004
volumes:
- ./conf/redis4.conf:/usr/local/redis/redis.conf
command: ["redis-server","/usr/local/redis/redis.conf"]
redis5:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis5
environment:
- TZ=Asia/Shanghai
ports:
- 7005:7005
- 17005:17005
volumes:
- ./conf/redis5.conf:/usr/local/redis/redis.conf
command: ["redis-server","/usr/local/redis/redis.conf"]
redis6:
image: daocloud.io/library/redis:5.0.7
restart: always
container_name: redis6
environment:
- TZ=Asia/Shanghai
ports:
- 7006:7006
- 17006:17006
volumes:
- ./conf/redis6.conf:/usr/local/redis/redis.conf
command: ["redis-server","/usr/local/redis/redis.conf"]
配置文件
# redis.conf
# 指定redis的端口号
port 7001
# 开启Redis集群
cluster-enabled yes
# 集群信息的文件
cluster-config-file nodes-7001.conf
# 集群的对外ip地址
cluster-announce-ip 192.168.40.100
# 集群的对外port
cluster-announce-port 7001
# 集群的总线端口
cluster-announce-bus-port 17001
启动了6个Redis的节点。
随便跳转到一个容器内部,使用redis-cli管理集群
redis-cli --cluster create 192.168.40.100:7001 192.168.40.100:7002 192.168.40.100:7003 192.168.40.100:7004 192.168.40.100:7005 192.168.40.100:7006 --cluster-replicas 1
八、Redis常见问题【重点
】
8.1 key的生存时间到了,Redis会立即删除吗?
不会立即删除。
定期删除:Redis每隔一段时间就去会去查看Redis设置了过期时间的key,会再100ms的间隔中默认查看3个key。
惰性删除:如果当你去查询一个已经过了生存时间的key时,Redis会先查看当前key的生存时间,是否已经到了,直接删除当前key,并且给用户返回一个空值。
8.2 Redis的淘汰机制
在Redis内存已经满的时候,添加了一个新的数据,执行淘汰机制。
- volatile-lru:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少使用的key。
- allkeys-lru:在内存不足时,Redis会再全部的key中干掉一个最近最少使用的key。
- volatile-lfu:在内存不足时,Redis会再设置过了生存时间的key中干掉一个最近最少频次使用的key。
- allkeys-lfu:在内存不足时,Redis会再全部的key中干掉一个最近最少频次使用的key。
- volatile-random:在内存不足时,Redis会再设置过了生存时间的key中随机干掉一个。
- allkeys-random:在内存不足时,Redis会再全部的key中随机干掉一个。
- volatile-ttl:在内存不足时,Redis会再设置过了生存时间的key中干掉一个剩余生存时间最少的key。
- noeviction:(默认)在内存不足时,直接报错。
LRU,即:最近最少使用淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的。
LFU,即:最不经常使用淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的。
8.3 缓存的常问题
8.3.1 缓存穿透问题
出现原因:
key对应的数据在数据库并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的商品id获取商品信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方式一:设置空值:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。
解决方式二:拦截:如用户鉴权校验,id做基础校验,id<=0的直接拦截;
8.3.2 缓存击穿问题
出现原理:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方式:
1、设置热点数据永远不过期(爆款商品访问次数最多)。
2、布隆过滤器,可以判断某样东西一定不存在或者可能存在。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。
System.out.println("ABCDEa123abc".hashCode()); // 165374702
System.out.println("ABCDFB123abc".hashCode()); // 165374702
3、锁机制
8.3.3 缓存雪崩问题
出现原理:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
比如,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
解决方式:
1、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
2、设置热点数据永不过期
8.3.4 缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;