Redis
一、现有项目的缺陷【面试题】
1、数据缓存
数据缓存 |
---|
![]() |
2、分布式Session
分布式Session |
---|
![]() |
二、Redis
1、概述
- 非关系数据库【NoSQL:Not only SQL】
- 存储结构:key-value
- key:字符串
- value:5种数据类型【string,hash,list,set,zset】
- 存储介质:内存,可以持久化
- 单线程,NIO【同步非阻塞,多路复用】
2、安装
2.1 原始安装
安装包:redis-5.0.8.tar.gz
- 安装步骤
安装gcc环境和make命令
apt-get install gcc
apt-get install make
上传
redis-5.0.8.tar.gz
解压
tar -zxvf redis-5.0.8.tar.gz
进入到redis-5.0.8目录,执行
make
会编译出
redis-server
、redis-cli
cd redis-5.0.8
make
- 运行redis
进入到
redis-5.0.8/src
- 执行
./redis-server
./redis-server |
---|
![]() |
- 连接redis
进入到
redis-5.0.8/src
- 执行
./redis-cli
./redis-cli |
---|
![]() |
2.2 docker-compose安装
- docker-compose.yml
version: '3.1'
services:
redis:
image: daocloud.io/library/redis:5.0.8
restart: always
container_name: redis
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
- 通过
docker-compose up -d
启动容器- 自动启动redis的服务端
- 进入到容器内部,可以直接使用
redis-cli
连接
重点命令 |
---|
![]() |
3、连接redis的语法
-
./redis-cli
-
没有加任何参数,则连接的是本机,且端口也是默认的【6379】
-
完整语法
./redis-cli -h 127.0.0.1 -p 6379
-
三、Redis客户端工具
-
命令行工具
- redis-cli
-
图形化工具
- RedisDesktopManager【一个windows的安装包】
RedisDesktopManager
|
-
Java工具
- Jedis【Redis官方推荐使用Java连接Redis的API】
四、Redis的五种数据类型
Redis是一个key-value的数据结构
五种数据类型是指value的数据类型
-
五种数据类型
string:字符串
list:列表
set:集合
zset:有序集合【靠分数来排序】
hash:对象
五种数据类型 |
---|
![]() |
1、string
#设置值
#set key value
set name lucy
#结果,正确就返回OK
#获取值
#get key
get name
#结果,有就返回值,没有nil[null]
#返回key对应的value,再重新设置这个key的值
#getset key value
getset name lily
#设置多个键值对
#mset k1 v1 k2 v2 ...
mset sex 1 age 20 address hangzhou
#结果:OK
#获取多对key对应的value
#mget k1 k2 k3 ...
mget sex age address
#结果:依次返回得到数据
#设置key-value:如果key不存在,则设置,如果key存在,则什么都不做
#setnx key value
#结果:成功返回1,失败返回0
setnx phone 13577889900
#设置key的同时,设置有效时间
#setex key 秒数 value
setnx gender 10 1
#gender有效时间为10S
#查看剩余有效时间
#ttl key
ttl gender
结果:返回剩余有效时间,如果已经过期:返回-2,如果没有设置有效时间:返回-1
#计算key对应的value的长度
#strlen key
strlen phone
#结果为11,phone存的值是11位
#自增1,value是数值类型才能成功。如果key不存在,则直接以0为基础,进行自增
#incr key
incr age
#age本来是20,现在返回就是21
#自增指定数量,value是数值类型才能成功
incrby age 5
#age本来是20,现在返回就是25
#自减1,value是数值类型才能成功。
#decr key
decr age
#age本来是0,现在返回就是-1
#自减指定数量,value是数值类型才能成功
#decrby key
decrby age 20
#age本来是30,现在返回就是10
#在原先的基础之上进行字符串追加
#append key value
append address xiaoshan
#本来address的值是hangzhou,现在就是hangzhouxiaoshan
#为已经存在的key的设置有效时间,单位为秒
#expire key seconds
expire address 10
#为已经存在的address设置有效时间为10S
#为已经存在的key设置过期时间,单位是毫秒
pexpire key 毫秒
#查看指定key的剩余存活时间,单位是毫秒
pttl key
案例 |
---|
![]() |
![]() |
![]() |
![]() |
2、list
#左压栈
lpush key v1 v2 ...
#特征:存入的数据顺序跟读取顺序相反
#右压栈
rpush key v1 v2 ...
#特征:存入的数据顺序跟读取顺序相同
#查询列表中的数据
#下标从0开始,如果要读到最末尾,endIndex就是-1
#lrange key startIndex endIndex
lrange key 0 -1
#获取列表的长度
llen key
#从左边弹出一个元素[获取最左边的元素,再删除它]
lpop key
#从右边弹出一个元素
rpop key
#获取指定索引的元素
lindex key index
#从一个列表的右边弹出元素,再追到加另一个列表的左边
rpoplpush 源列表 目标列表
#移除count个的value值
lrem key count value
lrem names2 2 tom
#删除names2列表中2个tom
list操作 |
---|
![]() |
![]() |
![]() |
![]() |
![]() |
3、set
#存值
sadd myset aa bb cc
#返回存入的个数
#获取集合长度
scard myset
#获取集合元素
smembers myset
#判断元素是否在集合中
sismember myset cc
#随机弹出一个元素
spop myset
#随机弹出指定个数元素
spop myset 2
#差集
sdiff myset1 myset2
#交集
sinter myset1 myset2
#并集
sunion myset1 myset2
#将交集结果存入到另一个集合中
sdiffstore myset3 myset1 myset2
#将交集结果存入到另一个集合中
sinterstore myset3 myset1 myset2
#将并集结果存入到另一个集合中
sunionstore myset4 myset1 myset2
#将myset1中的aa元素移动到myset3
smove myset1 myset3 aa
set操作 |
---|
![]() |
![]() |
4、zset
#向有序集合中增加元素
#zadd 集合名 分数1 元素名1 分数2 元素名2 ...
zadd paihangbang 10 liubei 80 caocao 30 sunquan
#返回插入的数量
#获取集合中元素的数量
zcard paihangbang
#获取集合中所有的元素的名称
zrange paihangbang 0 -1
1) "liubei"
2) "sunquan"
3) "caocao"
#获取集合所有的元素,包括名称和分数
zrange paihangbang 0 -1 withscores
1) "liubei"
2) "10"
3) "sunquan"
4) "30"
5) "caocao"
6) "80"
#获取集合前两台元素的名称和分数
zrange paihangbang 0 1 withscores
1) "liubei"
2) "10"
3) "sunquan"
4) "30"
#为集合中指定的元素增加分数
zincrby paihangbang 50 liubei
#返回增加后的分数
#返回集合中指定元素的分数
zscore paihangbang sunquan
#把myzset1和myzset2根据key求出交集,并把value加在一起
ZINTERSTORE myzset3 2 myzset1 myzset2
#在不知道当前集合中最大值和最小值的情况下,可以获取前几位
#-inf:指最小值 +inf:最大值 limit 限定【跟mysql中使用一样】
#withscores是可以不写的,如果不写,则只返回元素名
#limit 0 2是可以不写的,如果不写,则按从小到大的顺序返回集合元素
zrangebyscore paihangbang -inf +inf withscores limit 0 2
#按从大到小的顺序进行排序,并获取前2位
zrevrangebyscore paihangbang -inf +inf withscores limit 0 2
#根据元素名删除元素
zrem key member [member...]
#获取集合中指定元素的索引
zrank paihangbang caocao
#删除指定分数区间的元素[10,20]
ZREMRANGEBYSCORE paihangbang 10 20
5、hash
#设置值,必须指定大key和小key
hset user name lisi
#设置多值
hmset user sex 1 age 20
#获取单个小key对应的value
hget user sex
#获取多个小key对应的value们
hmget user name age
#判断小key是否存在
hexists user age
#获取所有的小key
hkeys user
1) "name"
2) "sex"
3) "age"
#获取所有的小key对应的values
hvals user
1) "lisi"
2) "1"
3) "20"
#获取所有大key中的键值对
hgetall user
1) "name"
2) "lisi"
3) "sex"
4) "1"
5) "age"
6) "20"
#求长度
hlen user
#设置小key【小key不存在,则设置成功,如果存在,则什么都不做】
hsetnx user age 25
五、通用命令
- key相关的命令
#查找匹配规则的key, *:代表0到多个字符,?代表一个字符
keys *
keys ?tu*
#判断key是否存在
exists key
#删除key
del key
#设置已经存在的key的过期时间,单位为秒
expire key seconds
#为已经存在的key设置过期时间,单位是毫秒
pexpire key 毫秒
#移除指定key的生存时间,永久的持久化
PERSIST key
#获取指定的key的value的数据类型
type key
#移动指定key到别的数据库,dbindex是0-15
move key dbindex
- 数据库相关操作
#redis默认有16个数据库,分别是 db0 - db15
#查看当前数据库下有多少key
dbsize
#切换数据库,dbindex是0-15
select dbindex
#清空当前数据库,慎重
flushdb
#清空所有数据库中的数据,慎重
flushall
#实时监控Redis服务接收到的命令
monitor
- 查看redis具体指令的性能
#在src下执行
redis-benchmark set
redis-benchmark get
#set是写的性能
#gte是读的性能
#具体的性能跟redis服务器的硬件配置有关
redis-benchmark redis命令 |
---|
![]() |
六、开启原始安装Redis的访问权限
1、修改redis的核心配置文件【redis.conf】
- 路径:redis根目录下
- 修改内容如下
#关闭本地绑定【69行】
#bind 127.0.0.1
#关闭保护模式【88行】
把 protected-mode yes 改成 protected-mode no
#设置密码【507行】
requirepass java2107
2、启动redis【带着配置文件】
cd src
./redis-server ../redis.conf
使用RedisDesktopManager连接 |
---|
![]() |
3、使用Redis客户端连接服务器也需要指定密码
./redis-cli |
---|
![]() |
七、Jedis操作Redis
1、快速入门
1.1 创建Maven项目
1.2 导入依赖
<!-- 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>
1.3 写代码
@Test
public void setTest() throws Exception {
Jedis jedis = new Jedis("192.168.145.138", 6379);
jedis.auth("java2107");
//操作redis
jedis.set("username", "superbaby");
jedis.close();
}
@Test
public void getTest() throws Exception {
Jedis jedis = new Jedis("192.168.145.138", 6379);
jedis.auth("java2107");
String username = jedis.get("username");
System.out.println("username = " + username);
jedis.close();
}
1.4 测试
- 可以看到把值存入到了Redis,也可以获取出来
2、Jedis连接池
- JedisPool
@Test
public void jedisPoolTest() throws Exception {
//JedisPool(final GenericObjectPoolConfig poolConfig, final String host, int port,
// int timeout, final String password, final int database)
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.145.138", 6379, 3000, "java2107", 1);
Jedis jedis = jedisPool.getResource();
jedis.set("address", "杭州");
jedis.close();
}
- 抽取工具类
package com.qf.java2107.utils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* 工具类
* @author ghy
* @version 1.0
* @date 2022-02-16
**/
public class JedisPoolUtils {
private static String HOST = "192.168.145.138";
private static Integer PORT = 6379;
private static Integer TIMEOUT = 3000;
private static String PASSWORD = "java2107";
private static Integer DATABASE = 1;
private static JedisPool jedisPool = null;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPool = new JedisPool(jedisPoolConfig, HOST, PORT, TIMEOUT, PASSWORD, DATABASE);
}
public static Jedis getResource(){
return jedisPool.getResource();
}
}
3、操作其他数据类型
3.1 list
3.2 set
3.3 zset
3.4 hash
/**
* incr、decrBy
**/
@Test
public void incrDecrTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
Long age = jedis.incr("age");
System.out.println(age);
Long age1 = jedis.decrBy("age", 100);
System.out.println(age1);
jedis.close();
}
/**
* 通用命令
*
**/
@Test
public void otherTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
Boolean flag = jedis.exists("user:1001");
System.out.println("flag = " + flag);
if(flag) {
Long count = jedis.del("user:1001");
System.out.println(count);
}
jedis.close();
}
/**
* hash操作
**/
@Test
public void hashTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
User user = new User();
user.setId(1001);
user.setUsername("lucy");
user.setGender(1);
user.setAddress("九堡");
Map<String, String> userMap = new HashMap<>();
userMap.put("id", user.getId().toString());
userMap.put("username", user.getUsername());
userMap.put("gender", user.getGender().toString());
userMap.put("address", user.getAddress());
jedis.hmset("user:" + user.getId(), userMap);
Map<String, String> map = jedis.hgetAll("user:1001");
System.out.println("map = " + map);
jedis.close();
}
/**
* zset操作
**/
@Test
public void zsetTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
Map<String, Double> scoreMembers = new HashMap<>();
scoreMembers.put("liubei", 20.0);
scoreMembers.put("zhangfei", 2.0);
scoreMembers.put("guanyu", 15.0);
jedis.zadd("tuhaobang", scoreMembers);
//Set<Tuple> tuples = jedis.zrangeWithScores("tuhaobang", 0, -1);
Set<Tuple> tuples = jedis.zrevrangeWithScores("tuhaobang", 0, -1);
for (Tuple tuple : tuples) {
System.out.println(tuple.getElement() + ":" + tuple.getScore());
}
jedis.close();
}
/**
* set操作
**/
@Test
public void setTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
Long count = jedis.sadd("subjects", "java", "c", "php", "python", "html");
System.out.println(count);
Set<String> subjects = jedis.smembers("subjects");
System.out.println(subjects);
jedis.sadd("subjects2", "java", "c", "php");
Set<String> set1 = jedis.sdiff("subjects", "subjects2");
System.out.println("差集:" + set1);
Set<String> set2 = jedis.sinter("subjects", "subjects2");
System.out.println("交集:" + set2);
jedis.close();
}
/**
* list操作
**/
@Test
public void listTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
jedis.rpush("names", "lucy", "tom", "jack","mark");
jedis.expire("names", 5);
List<String> names = jedis.lrange("names", 0, -1);
System.out.println(names);
Thread.sleep(5500);
System.out.println("有效时间到了-------------");
names = jedis.lrange("names", 0, -1);
System.out.println(names);
jedis.close();
}
/**
*
**/
@Test
public void jedisPoolTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
String address = jedis.get("address");
System.out.println("address = " + address);
jedis.close();
}
4、保存对象的方式
4.1 String
- json数据
/**
* 存json
**/
@Test
public void saveObjectTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
User user = new User();
user.setId(1001);
user.setUsername("lucy");
user.setGender(1);
user.setAddress("九堡");
ObjectMapper objectMapper = new ObjectMapper();
String userString = objectMapper.writeValueAsString(user);
jedis.set("user:" + user.getId(), userString);
String json = jedis.get("user:1001");
System.out.println(json);
jedis.close();
}
4.2 字节数组
/**
* 取字节数组
**/
@Test
public void getByteArrayTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
byte[] bytes = jedis.get("user:1002".getBytes());
User user = (User) SerializationUtils.deserialize(bytes);
System.out.println("user = " + user);
jedis.close();
}
/**
* 存字节数组
**/
@Test
public void saveByteArrayTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
User user = new User();
user.setId(1002);
user.setUsername("李雷");
user.setGender(1);
user.setAddress("九堡");
//序列化
byte[] userBytes = SerializationUtils.serialize(user);
jedis.set(("user:" + user.getId()).getBytes(), userBytes);
jedis.close();
}
字节数组存储底层 |
---|
![]() |
5、批量存储之管道操作
没用管道 |
---|
![]() |
|
用了管理 |
---|
![]() |
/**
* 使用管道提升了近1000倍
* 442
* @throws Exception
*/
@Test
public void pipelinedSaveTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
ObjectMapper objectMapper = new ObjectMapper();
Pipeline pipelined = jedis.pipelined();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
User user = new User();
user.setId(i);
user.setUsername("李雷" + i);
user.setGender(i % 2);
user.setAddress("九堡" + i);
String userString = objectMapper.writeValueAsString(user);
pipelined.set("user:" + user.getId(), userString);
}
System.out.println("--->" + (System.currentTimeMillis()-start));
jedis.close();
}
/**
* 442501
**/
@Test
public void manySaveTest() throws Exception {
Jedis jedis = JedisPoolUtils.getResource();
ObjectMapper objectMapper = new ObjectMapper();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
User user = new User();
user.setId(i);
user.setUsername("李雷" + i);
user.setGender(i % 2);
user.setAddress("九堡" + i);
String userString = objectMapper.writeValueAsString(user);
jedis.set("user:" + user.getId(), userString);
}
System.out.println("--->" + (System.currentTimeMillis()-start));
jedis.close();
}
八、SpringBoot整合Redis
1、实现步骤
- 创建SpringBoot项目
- 导入依赖【spring-boot-starter-data-redis】
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置文件
spring:
redis:
host: 192.168.145.136
port: 6379
jedis:
pool:
max-active: 10
max-wait: 3000
min-idle: 5
enabled: true
max-idle: 10
- 操作
package com.qf.java2107.springbootredis.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SpringbootRedisDemoApplicationTests {
/**
* redisTemplate.opsForXxx().操作
* Xxx : 数据类型
*/
@Autowired
RedisTemplate redisTemplate;
@Test
void stringTest() throws Exception {
//参数一:key
//参数二:值
//参数三:有效时间
//参数四:有效时间单位
redisTemplate.opsForValue().set("username", "lilei", 3, TimeUnit.SECONDS);
//setnx
Boolean flag1 = redisTemplate.opsForValue().setIfAbsent("username", "lilei", 3, TimeUnit.SECONDS);
System.out.println(flag1);
String username = (String) redisTemplate.opsForValue().get("username");
System.out.println(username);
Thread.sleep(3500);
username = (String) redisTemplate.opsForValue().get("username");
System.out.println("3秒后:" + username);
Boolean flag2 = redisTemplate.opsForValue().setIfAbsent("username", "lilei", 30, TimeUnit.SECONDS);
System.out.println(flag2);
}
}
2、操作其他数据类型
list、set、zset、hash
package com.qf.java2107.springbootredis.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class SpringbootRedisDemoApplicationTests {
/**
* redisTemplate.opsForXxx().操作
* Xxx : 数据类型
*/
@Autowired
RedisTemplate redisTemplate;
/**
*
**/
@Test
public void otherTest() throws Exception {
Long age = redisTemplate.opsForValue().increment("age");
System.out.println(age);
}
/**
* hash操作
**/
@Test
public void hashTest() throws Exception {
Map<String,Object> userMap = new HashMap<>();
userMap.put("id", 1111);
userMap.put("username", "Hello");
userMap.put("gender", 1);
redisTemplate.opsForHash().putAll("user:" + userMap.get("id"), userMap);
Set keys = redisTemplate.opsForHash().keys("user:1111");
System.out.println("keys = " + keys);
List list = redisTemplate.opsForHash().multiGet("user:1111", keys);
System.out.println("list = " + list);
List values = redisTemplate.opsForHash().values("user:1111");
System.out.println("values = " + values);
Map map = redisTemplate.opsForHash().entries("user:1111");
System.out.println("map = " + map);
Boolean flag = redisTemplate.opsForHash().hasKey("user:1111", "gender");
System.out.println("flag = " + flag);
}
/**
* zset操作
*
**/
@Test
public void zsetTest() throws Exception {
redisTemplate.opsForZSet().add("tuhaobang", "sunwukong", 60);
redisTemplate.opsForZSet().add("tuhaobang", "zhubajie", 50);
redisTemplate.opsForZSet().add("tuhaobang", "laosha", 70);
Set<ZSetOperations.TypedTuple<String>> set = redisTemplate.opsForZSet().reverseRangeWithScores("tuhaobang", 0, -1);
for (ZSetOperations.TypedTuple<String> tuple : set) {
String name = tuple.getValue();
Double score = tuple.getScore();
System.out.println(name + ":" + score);
}
}
/**
* 操作set
*
**/
@Test
public void setTest() throws Exception {
Long count = redisTemplate.opsForSet().add("hobbies", "java", "c", "swim", "game", "douxin");
System.out.println("count = " + count);
Set set = redisTemplate.opsForSet().members("hobbies");
System.out.println(set);
Long count2 = redisTemplate.opsForSet().remove("hobbies", "c", "swim");
System.out.println("count2 = " + count2);
}
/**
* 操作list
**/
@Test
public void listTest() throws Exception {
List<String> names = new ArrayList<>();
names.add("Lucy");
names.add("Tom");
names.add("Lucy");
names.add("Jack");
Long count = redisTemplate.opsForList().rightPushAll("names", names);
System.out.println("count = " + count);
List<String> list = redisTemplate.opsForList().range("names", 0, -1);
System.out.println(list);
String value = (String) redisTemplate.opsForList().index("names", 2);
System.out.println("value = " + value);
}
}
3、序列化器
SpringBoot提供默认的
RedisTemplate
这个bean,继承一个RedisAccessor
类,该类实现了一个接口InitializingBean
,所以会自动调用afterPropertiesSet
方法,在该方法中设置了如果没有指定具体使用哪个序列化器,那么Redis在存储数据时,采用的都默认的序列化器
3.1 修改默认的序列化器
- 增加配置类
@Configuration
public class MyRedisConfig {
@Bean
public RedisTemplate<String, Object> myRedisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
- 测试
@Autowired
RedisTemplate myRedisTemplate;
/**
*
**/
@Test
public void myRedisTemplateTest() throws Exception {
myRedisTemplate.opsForValue().set("name", "张三");
}
/**
*
**/
@Test
public void Test() throws Exception {
Object name = myRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
RedisDesktopManager |
---|
![]() |
Redis |
---|
![]() |
3.2 使用RedisTemplate可以直接存储对象
/**
*
**/
@Test
public void getObjectTest() throws Exception {
Product product = (Product) redisTemplate.opsForValue().get("product:1011");
System.out.println(product);
}
/**
* RedisTemplate是可以直接存对象的,存储的key-value不是普通的字符串,而是经过jdk序列化器序列化后的东西
**/
@Test
public void saveObjectTest() throws Exception {
Product product = new Product();
product.setId(1011);
product.setProName("冰箱666");
product.setProDate(new Date());
product.setPrice(7777.77);
redisTemplate.opsForValue().set("product:" + product.getId(), product);
}
4、使用管道
@Test
public void saveManyTest() throws Exception {
long start = System.currentTimeMillis();
redisTemplate.executePipelined(new SessionCallback<String>() {
@Override
public String execute(RedisOperations redisOperations) throws DataAccessException {
for (int i = 0; i < 200000; i++) {
redisOperations.opsForValue().set("name" + i, "value"+i);
}
return null;
}
});
long end = System.currentTimeMillis();
System.out.println("-->" + (end - start) );
}
九、Redis的事务
1、MySQL中事务
- 开启事务
- 执行操作
- 提交或回滚事务
事务中所有的操作都成功,事务才会提交,一旦由某个操作失败,那么就回滚事务,回到事务开始前的状态
2、Redis中的事务
2.1 事务操作
- 开启事务【multi】
- 执行操作【执行的命令会被放到一个队列中,队列中的所有操作是在提交事务时执行的】
- 提交或回滚事务【exec/discard】
事务中Redis操作数据执行的命令是放在一个队列中,队列中的命令在事务提交时执行,这些命令在没有语法错误的情况下,是该成功的成功,该失败的失败
事务开启与提交 |
---|
![]() |
事务开启与回滚【什么都不做】 |
---|
![]() |
语法问题,会造成事务直接取消 |
---|
![]() |
2.2 Java实现Redis事务
/**
* 事务操作
**/
@Test
public void txTest() throws Exception {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
//开事务
redisOperations.multi();
redisOperations.opsForValue().set("k1", "v1");
redisOperations.opsForList().rightPush("names", "lucy");
redisOperations.opsForList().rightPush("names", "lily");
redisOperations.opsForValue().increment("k1");//失败
//提交事务
redisOperations.exec();
return null;
}
});
}
十、在Docker搭建Redis时使用Redis的配置文件
- 配置文件名称:redis.conf
- 配置文件路径:redis根目录下\redis.conf【原始安装Redis方式】
1、修改docker-compose.yml文件
version: '3.1'
services:
redis:
image: daocloud.io/library/redis:5.0.8
restart: always
container_name: redis
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
volumes:
- ./conf/redis.conf:/usr/local/redis/redis.conf
command: ["redis-server","/usr/local/redis/redis.conf"]
- 在docker-compose.yml文件同目录下创建
conf
目录,再在conf目录下创建redis.conf
文件
redis.conf文件 |
---|
![]() |
requirepass java2107
- 需要把原生的redis容器给down掉,重新up
docker-compose down
docker-compose up -d
2、在SpringBoot整合Redis的配置文件中需要加入密码
配置文件 |
---|
![]() |
十一、Redis持久化【面试题】
1、持久化概述
- 把Redis内存中的临时数据写入到磁盘中,永久的保存下来,以免数据丢失,这个过程被称为叫持久化
2、持久化方式
- RDB:基于快照的方式
- AOF:基于日志的方式
- 混合持久化【两种一起用】
3、RDB快照持久化机制
3.1 概述
- 基于快照的方式进行数据持久化,也是redis默认使用的持久化机制【4.0之前的版本】
3.2 redis.conf
Redis对RDB机制进行了综合性的测试
使用RDB机制既能保证redis的功能,同时也保证了redis的性能
3.3 触发RDB机制的规则
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
# 在一定时间范围内【以秒为单位】,发生变化的redis的key达到了一定的数量,就会进行一次保存【快照】
# Will save the DB if both the given number of seconds and the given number of write operations against the DB occurred.
# In the example below the behaviour will be to save:
# 900秒内,至少有一个key被改变了
# after 900 sec (15 min) if at least 1 key changed
# 300秒内,至少有10key被改变了
# after 300 sec (5 min) if at least 10 keys changed
# 60秒内,至少有10000个key被改变了
# fter 60 sec if at least 10000 keys changed
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
save 900 1
save 300 10
save 60 10000
- 900秒内,至少有一个key被改变了
- 300秒内,至少有10key被改变了
- 60秒内,至少有10000个key被改变了
手动触发RDB机制:当redis正常停止时,就会触发RDB机制
3.4 RDB触发后文件的保存在哪里?
- 文件名:dump.rdb,被保存在根目录下\src目录下
# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes
# Compress string objects using LZF when dump .rdb databases?
# For default that's set to 'yes' as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes
# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes
# The filename where to dump the DB
dbfilename dump.rdb
# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./
触发RDB机制后,就会把数据写入到一个在当前目录下名为dump.rdb的文件中
redis/src/dump.rdb |
---|
![]() |
- 可以查看dump.rdb文件的内容,发现是经过压缩的,存储的是二进制数据
3.5 使用docker-compose的方式来实现RDB
- docker-compose.yml
version: '3.1'
services:
redis:
image: daocloud.io/library/redis:5.0.8
restart: always
container_name: redis
environment:
- TZ=Asia/Shanghai
ports:
- 6379:6379
volumes:
- ./conf/redis.conf:/usr/local/redis/redis.conf
- ./data/:/data/
command: ["redis-server","/usr/local/redis/redis.conf"]
- 停止并删除原先的redis容器,再重开一个
- 进入redis-cli,执行写入【set】操作再执行
bgsave
指令【可以手动触发RDB机制】,会生成一个dump.rdb文件在data目录下,这个就是RDB机制进行快照保存后的文件
手动触发RDB【bgsave】 |
---|
![]() |
3.6 bgsave的执行逻辑
bgsave的执行逻辑 |
---|
![]() |
4、AOF日志持久化机制
4.1、概述
- redis会把发生变化的key的命令以日志的形式保存到一个文件中,默认AOF机制是关闭的
4.2、redis.conf
############################## APPEND ONLY MODE ###############################
# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.
appendonly no
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
#aof默认是关闭的,如果要开启,下面这个
no
改成yes
appendonly no
#aof机制保存数据的文件名:
appendonly.aof
appendfilename “appendonly.aof”
4.3、触发AOF时机
# The fsync() call tells the Operating System to actually write data on disk
# instead of waiting for more data in the output buffer. Some OS will really flush
# data on disk, some other OS will just try to do it ASAP.
#
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
#
# More details please check the following article:
# http://antirez.com/post/redis-persistence-demystified.html
#
# If unsure, use "everysec".
# appendfsync always
appendfsync everysec
# appendfsync no
#总是触发:每当一个key发生了变化 ,就会触发一次。触发的频率太高了,读写次数太频繁
appendfsync always
#每秒触发一次,是redis默认选择的方式
appendfsync everysec
#根据当前系统决定如何触发,一般是重启
appendfsync no
4.4、AOF文件重写的规则
- AOF持久化机制是以日志的形式来保存当前redis发生了变化的key的命令,当命令写入到AOF文件中时,随着命令的增加,文件会越来越大,大到一定时候,文件需要被重写【要对文件进行压缩】
# When the AOF fsync policy is set to always or everysec, and a background
# saving process (a background save or AOF log background rewriting) is
# performing a lot of I/O against the disk, in some Linux configurations
# Redis may block too long on the fsync() call. Note that there is no fix for
# this currently, as even performing fsync in a different thread will block
# our synchronous write(2) call.
#
# In order to mitigate this problem it's possible to use the following option
# that will prevent fsync() from being called in the main process while a
# BGSAVE or BGREWRITEAOF is in progress.
#
# This means that while another child is saving, the durability of Redis is
# the same as "appendfsync none". In practical terms, this means that it is
# possible to lose up to 30 seconds of log in the worst scenario (with the
# default Linux settings).
#
# If you have latency problems turn this to "yes". Otherwise leave it as
# "no" that is the safest pick from the point of view of durability.
no-appendfsync-on-rewrite no
# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
是否开启重写
no-appendfsync-on-rewrite no
#重写的比例:当重写过后的文件需要再次重写时,就会以100%比例重新设置重写的规则
#第一次重写【64mb】—重写后—> 60m — 第二次重写【到达120m后触发】----> 120m —> 240m
auto-aof-rewrite-percentage 100
#当AOF文件超过64mb时,会进行重写
auto-aof-rewrite-min-size 64mb
4.5 AOF演示
- 修改redis.conf配置文件
#开启AOF
appendonly yes
- 重启redis
docker-compose restart
- 监听appendonly.aof
#进入data目录,使用tail命令
cd data
tail -f appendonly.aof
- 另开一个客户端连接,向redis中写入数据
appendonly.aof文件中产生了内容
文件内容
*2 #下面的指令由2个单词组成,经过分析,就是执行了【select 0】选择0号数据库
$6 #第一个单词由6个字符组成
SELECT
$1 #第二个单词由1个字符组成
0
*3 #下面的指令由3个单词组成
$3 #第一个单词由3个字符组成
set
$2 #第二个单词由2个字符组成
k1
$2 #第三个单词由2个字符组成
v1
*3 #下面的指令由3个单词组成
$3 #第一个单词由3个字符组成
set
$4 #第二个单词由4个字符组成
name
$7 #第二个单词由7个字符组成
zhansan
4.6 压缩AOF文件
- 只需要在redis的客户端输入命令
bgrewriteaof
- aof文件被压缩了,压缩后的格式跟dump.rdb文件是一致,使用的是二进制存储
5、混合持久化
5.1 概述
- 同时开启了RDB和AOF
- Redis4.0版本后默认采用的就是混合持久化
5.2 redis.conf
# When rewriting the AOF file, Redis is able to use an RDB preamble in the
# AOF file for faster rewrites and recoveries. When this option is turned
# on the rewritten AOF file is composed of two different stanzas:
#
# [RDB file][AOF tail]
#
# When loading Redis recognizes that the AOF file starts with the "REDIS"
# string and loads the prefixed RDB file, and continues loading the AOF
# tail.
aof-use-rdb-preamble yes
#开启混合持久化
aof-use-rdb-preamble yes
- 混合持久化,会优先加载
appendonly.aof
的配置文件,再加载RDB,使用RDB加载时只会加载不同的数据。
6、RDB和AOF的选择
- RDB:
dump.rdb
文件小【因为存储的是二进制】,加载慢,读取效率低- 如果追求性能,对数据安全要求不高,可以选择RDB
- AOF:
appendonly.aof
文件大【因为在没重写前,使用的是命令存储】,如果开启AOF,会优加载appendonly.aof
,且读取效率比较高- 如果性能和安全双追求,那么选择混合持久化【开启AOF,RDB开不开都行】
十二、Redis高可用【集群搭建】
1、高可用概述
-
在分布式系统中,保证我们的应用能够做到4个9,甚至5个9
- 一天中,我们的应用99.99%可用,甚至99.999%可用
-
单节点在分布式系统中,会存在如下问题
- 单点故障
- 性能瓶颈
- 数据安全
单节点【一台服务器】 |
---|
![]() |
2、主从架构
主从架构 |
---|
![]() |
全量复制跟增量复制 |
---|
![]() |
- 全量复制:从节点通过RDB把主节点数据全部接收, 并写入到自己内存中。如果中间断掉了,那么重连后,会续传
- 增量复制:从节点跟主节点之间如果断开了,再次连接上的时候,就会执行增量复制,是把主节点新增加的数据接收并写入到内存中,而不是把全部数据再接收一遍。
2.1 搭建主从架构【docker】
2.1.1 docker-compose.yml
- 存放目录:/opt/docker/redis_master_slave/
version: "3.1"
services:
redis1:
image: daocloud.io/library/redis:5.0.8
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.8
restart: always
container_name: redis2
environment:
- TZ=Asia/Shanghai
ports:
- 7002:6379
volumes:
- ./conf/redis2.conf:/usr/local/redis/redis.conf
links:
#在从节点的配置文件中的master就相当于是redis1服务
- redis1:master
command: ["redis-server","/usr/local/redis/redis.conf"]
redis3:
image: daocloud.io/library/redis:5.0.8
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"]
2.2.2 三个节点的配置文件
存放目录:/opt/docker/redis_master_slave/conf
- redis1.conf
#bind 127.0.0.1
#关闭保护模式
protected-mode no
- redis2.conf 和 redis3.conf
#bind 127.0.0.1
#关闭保护模式
protected-mode no
#replicaof 主节点的ip 主节点的端口,这里的master是在yml文件中配置的主节点别名
replicaof master 6379
2.2.3 启动三个节点
- docker-compose up -d
2.2.4 分别进入到三个节点的容器内部,查看其信息
- docker exec -it 容器名称 bash
主节点【info replication】 |
---|
![]() |
![]() |
redis2从节点【info replication】 |
---|
![]() |
![]() |
redis3从节点【info replication】 |
---|
![]() |
3、哨兵机制
3.1 问题
- 在主从架构中,主节点负责读写,从节点只负责读
- 那么,如果主节点宕机了,那么整个Redis架构就不能执行写操作了
- 使用哨兵就可以解决这个问题
3.2 哨兵概述
哨兵工作原理 |
---|
![]() |
- 搭建哨兵
- https://www.cnblogs.com/JulianHuang/p/12650721.html
4、redis-cluster
4.1 工作原理
原理图 |
---|
![]() |
集群特点 |
---|
![]() |
4.2 搭建
4.2.1 准备docker-compose.yml文件
version: "3.1"
services:
redis1:
image: daocloud.io/library/redis:5.0.8
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.8
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.8
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.8
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.8
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.8
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"]
4.2.2 准备配置文件
- redis1.conf
# redis.conf
# 指定redis的端口号
port 7001
# 开启Redis集群
cluster-enabled yes
# 集群信息的文件
cluster-config-file nodes-7001.conf
# 集群的对外ip地址
cluster-announce-ip 192.168.145.136
# 集群的对外port
cluster-announce-port 7001
# 集群的总线端口
cluster-announce-bus-port 17001
其他5个配置文件,跟redis1.conf类似,只需要修改文件名及相关端口号即可 |
---|
![]() |
4.2.3 执行docker-compose up -d
4.2.4 进入任何一台redis节点,执行搭建集群的命令
docker exec -it redis5 bash
redis-cli --cluster create 192.168.145.136:7001 192.168.145.136:7002 192.168.145.136:7003 192.168.145.136:7004 192.168.145.136:7005 192.168.145.136:7006 --cluster-replicas 1
#192.168.145.136 redis节点的ip
4.2.5 需要输入yes
,后面就会自动搭建
4.3 redis-cli连接redis-cluster
#-c : 代表连接的是集群
redis-cli -c -h ip -p port
操作集群【会根据key计算出槽位,然后跳转到指定槽位对应的节点进入对应的操作】 |
---|
![]() |
4.4 Java连接redis-cluster
- application.yml
spring:
redis:
cluster:
nodes: 192.168.145.136:7001,192.168.145.136:7002,192.168.145.136:7003
- 测试
@SpringBootTest
class SpringbootRedisDemoApplicationTests {
@Autowired
RedisTemplate redisTemplate;
/**
*
**/
@Test
public void clusterTest() throws Exception {
redisTemplate.opsForValue().set("address", "李四");
String value = (String) redisTemplate.opsForValue().get("address");
System.out.println(value);
}
}
十三、Redis淘汰策略
1、概述
- 当Redis内存达到一定程序时【内存分配可以在配置文件指定】,Redis会对内存进入优先,以此来满足能够存入新的数据。这种行为就是Redis淘汰策略【机制】
2、内存管理
############################## MEMORY MANAGEMENT ################################
# Set a memory usage limit to the specified amount of bytes.
# When the memory limit is reached Redis will try to remove keys
# according to the eviction policy selected (see maxmemory-policy).
#设置内存使用限制到指定的字节数。当内存达到限制时,Redis将尝试删除键,根据选择的淘汰策略(参见maxmemory-policy)。
# If Redis can't remove keys according to the policy, or if the policy is
# set to 'noeviction', Redis will start to reply with errors to commands
# that would use more memory, like SET, LPUSH, and so on, and will continue
# to reply to read-only commands like GET.
#如果没有明确指定淘汰策略,Redis默认采用的是'noeviction', 只可以读,不能再写
# This option is usually useful when using Redis as an LRU or LFU cache, or to
# set a hard memory limit for an instance (using the 'noeviction' policy).
#
# WARNING: If you have replicas attached to an instance with maxmemory on,
# the size of the output buffers needed to feed the replicas are subtracted
# from the used memory count, so that network problems / resyncs will
# not trigger a loop where keys are evicted, and in turn the output
# buffer of replicas is full with DELs of keys evicted triggering the deletion
# of more keys, and so forth until the database is completely emptied.
#
# In short... if you have replicas attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for replica
# output buffers (but this is not needed if the policy is 'noeviction').
#
# maxmemory <bytes>
#如果指定最大内存,那么redis就会选择相应的淘汰策略【这里的淘汰策略是指LRU或者LFU】来进行key的删除,直到能够满足redis的最大内存够用
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.【一般会选择】
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
- volatile-lru:在内存不足时,Redis会在设置过了生存时间的key中干掉一个最近最少使用的key。key1:10m 只用1次 key2:10m只用了2次(建议使用这种)
- allkeys-lru:在内存不足时,Redis会在全部的key中干掉一个最近最少使用的key。
- volatile-lfu:在内存不足时,Redis会在设置过了生存时间的key中干掉一个最近最少频次使用的key。使用10个key,key1用了2次 key2 用了1次
- allkeys-lfu:在内存不足时,Redis会在全部的key中干掉一个最近最少频次使用的key。
- volatile-random:在内存不足时,Redis会在设置过了生存时间的key中随机干掉一个。
- allkeys-random:在内存不足时,Redis会在全部的key中随机干掉一个。
- volatile-ttl:在内存不足时,Redis会在设置过了生存时间的key中干掉一个剩余生存时间最少的key。
- noeviction:(默认)在内存不足时,直接报错。只读不写,写会报错
# LRU means Least Recently Used
# LFU means Least Frequently Used
- LRU 算法(Least Recently Used,最近最少使用)淘汰很久没被访问过的数据,以最近一次访问时间作为参考。
- LFU 算法(Least Frequently Used,最不经常使用)淘汰最近一段时间被访问次数最少的数据,以次数作为参考。
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# Note: with any of the above policies, Redis will return an error on write
# operations, when there are no suitable keys for eviction.
#
# At the date of writing these commands are: set setnx setex append
# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
# getset mset msetnx exec sort
#
# The default is:
#
# maxmemory-policy noeviction
# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. For default Redis will check five keys and pick the one that was
# used less recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs more CPU. 3 is faster but not very accurate.
#
# maxmemory-samples 5
# 取样基数,也就是在5key中去选择要删除的key
# Starting from Redis 5, by default a replica will ignore its maxmemory setting
# (unless it is promoted to master after a failover or manually). It means
# that the eviction of keys will be just handled by the master, sending the
# DEL commands to the replica as keys evict in the master side.
#
# This behavior ensures that masters and replicas stay consistent, and is usually
# what you want, however if your replica is writable, or you want the replica to have
# a different memory setting, and you are sure all the writes performed to the
# replica are idempotent, then you may change this default (but be sure to understand
# what you are doing).
#
# Note that since the replica by default does not evict, it may end using more
# memory than the one set via maxmemory (there are certain buffers that may
# be larger on the replica, or data structures may sometimes take more memory and so
# forth). So make sure you monitor your replicas and make sure they have enough
# memory to never hit a real out-of-memory condition before the master hits
# the configured maxmemory setting.
#
# replica-ignore-maxmemory yes
3、Redis在什么时候会使用淘汰策略
- 惰性删除:当读取一个key,发现这个key已经过期了,那么redis会去删除这个key
- 主动删除:redis会执行一个后台线程,在一定时间范围内根据淘汰策略去删除key
4、会删除哪些key
- 会删除过期的key
- 会不会删除没过期的key
- 根据淘汰策略有可能会删除没过期的key
5、redis如何选择要删除的key
- 可以指定删除的基数,默认值是5
根据淘汰策略,把redis的key分为5个一组,然后在5个key找到要删除的key,然后把它删掉
- Redis经过测试,发现5是效率还不错,定位删除key的精准度也还高
十四、使用Redis存在的问题
-
使用缓存的步骤
- 用户发送查询请求,去查询缓存
- 缓存
- 有:直接返回
- 没有:查询数据库
- 有:存入缓存,返回数据给用户
- 没有:返回null
1、穿透
1.1 概述
- 查询一个缓存跟数据库都不存在的数据【恶意攻击】
- 先查询缓存,再查询数据库
- 那么就会发现无论如何,最终一定要去访问数据库
- 模拟缓存穿透
/**
* 模拟缓存穿透
* @author ghy
* @version 1.0
* @date 2022-02-18
**/
@SpringBootTest
public class RedisChuanTouTest {
@Autowired
RedisTemplate redisTemplate;
/**
*
**/
@Test
public void chuanTouTest() throws Exception {
//数据库有10条记录,id分别为1-10
for (int i = 11; i <= 20; i++) {
Product product = (Product) redisTemplate.opsForValue().get("k" + i);
if(ObjectUtils.isEmpty(product)) {
System.out.println("从数据库查询");
//缓存中没有,去查询数据库【模拟查询数据库】
Product productDB = selectFromDB(i);
//放入缓存
redisTemplate.opsForValue().set("k" + i, productDB);
System.out.println(productDB);
continue;
}
System.out.println("从缓存中查询");
}
}
/**
* 数据库有10条记录,id分别为1-10
*
* 模拟从数据库查询
* @param id
* @return
*/
private Product selectFromDB(Integer id) {
if(id > 10) {
return null;
}
return new Product(id, id + "_pro", new Date(), 100D * id);
}
}
1.2 解决方案
- 方案一:如果数据库没有,就放一个无用的对象到缓存中,并设置过期时间,以便后期数据库会出现准备的数据放入到缓存中
方案一 |
---|
![]() |
- 方案二:布隆过滤器
布隆过滤器 |
---|
![]() |
向缓存中增加数据时,会通过布隆过滤器经过多个无偏hash函数对key进行计算,并对位数组长度取模,把每个得到值作为位数组的索引找到对应的位置,把值改成1
向查询时,通过布隆过滤器经过多个无偏hash函数对key进行计算,并对位数组长度取模,找到对应索引的值,判断是否至少有一个为0,那么就可以判断该key在缓存中一定不存在。那么如果都是1,也不代表在缓存一定存在,只是有可能存在。因为这些1有可能是别的那些key正为因为hash计算得到了相同索引位置进行修改为1的操作
- 具体实现
先向布隆过滤器中存储Redis的key,用于hash计算
@Test
public void chuanTouTest() throws Exception {
//1.创建Redisson的config对象
Config config = new Config();
//2.设置redis服务器信息
// config.useClusterServers().addNodeAddress("redis://172.16.253.67:6379","redis://172.16.253.67:6380")
config.useSingleServer().setAddress("redis://192.168.145.136:6379");
config.useSingleServer().setPassword("java2107");
//3.获得redisson对象
RedissonClient redisson = Redisson.create(config);
//4.使用布隆过滤器
RBloomFilter<Object> myBloom = redisson.getBloomFilter("myBloom");
//5.初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
myBloom.tryInit(100000000L,0.03);
//6.模拟从数据库获取数据向布隆过滤器中插入数据,只插入redis中存取数据的key
for (int i = 1; i <= 10; i++) {
Product product = selectFromDB(i);
myBloom.add("k" + product.getId());
}
//数据库有10条记录,id分别为1-10
for (int i = 11; i <= 20; i++) {
//布隆过滤器判断缓存中是否有数据
boolean result = myBloom.contains("k" + i);
if(result) {
//1.1 缓存中有可能有
Product product = (Product) redisTemplate.opsForValue().get("k" + i);
if(ObjectUtils.isEmpty(product)) {
System.out.println("从数据库查询---------->");
//缓存中没有,去查询数据库【模拟查询数据库】
Product productDB = selectFromDB(i);
if(!ObjectUtils.isEmpty(productDB)) {
//放入缓存
redisTemplate.opsForValue().set("k" + i, productDB);
}
continue;
} else {
System.out.println("从缓存中获取------------------");
}
} else {
//缓存中一定不存在
System.out.println("缓存中不存在----->");
}
}
}
2、击穿【失效】
2.1 概述
- 在同一时间点,缓存中有大量的key过期了,那么在这个时间节点,如果有很多的请求同时访问缓存时,发现缓存并没有数据,则会直接访问数据库,感觉就像缓存被击穿了
2.2 解决方案
- 热点数据永不过期
- 不同的key设置随机过期时间
3、雪崩
3.1 概述
- 缓存由于某些原因,导致在某个时间点不可用,那么这个时间点的所有请求都会直接打到数据库,可能会造成数据库宕机
- 集群
十五、应用场景
1、缓存中间件
- 秒杀【库存】
- 热点数据【京东首页】
2、分布式Session
- 单点登录【SSO】:是一个独立的系统,所有节点的登录与认证都是由这个节点来完成的
- 在单个节点进行过登录,那么在分布式的其他节点上也会直接登录
3、计数器
-
播放数、点击量、点赞数、关注数、阅读量
-
自增【incr】
#关注数
#关注
incr like:用户ID
#取消关注
decr like:用户ID
4、分布式ID
-
电商平台的订单号必须是唯一的
- UUID:有字母,没办法排序
- Redis的分布式ID
-
自增【incr】
抢购中【订单号生成】 |
---|
![]() |
5、排行榜
- 热点新闻
6、社交类需求
-
Set 可以实现交集,从而实现共同好友等功能,Set通过求差集,可以进行好友推荐,文章推荐。
微博微信点赞、收藏、标签
|
## 点赞
SADD like:{消息ID} {用户ID}
## 取消点赞
SREM like:{消息ID} {用户ID}
## 检查用户是否点过赞
SISMEMBER like:{消息ID} {用户ID}
## 获取点赞的用户列表
SMEMBERS like:{消息ID}
## 获取点赞用户数
SCARD like:{消息ID}
微博微关系 |
---|
![]() |
## A关注的人: ASet存在于redis数据库中的Set的key
ASet-> {B, D}
## C关注的人:
CSet--> {A, F, B, D}
## B关注的人:
BSet-> {A, C, F, D, E)
## A和C共同关注:
SINTER ASet CSet--> {B, D}
## A关注的人也关注他(C):
SISMEMBER BSet C
SISMEMBER DSet C
## A可能认识的人:
SDIFF CSet ASet->(A, F}
7、购物车
可以使用hash来存储 |
---|
![]() |
-
分为两类购物车
-
用户登录后的购物车
- cart:1
- 商品ID【联想拯救者Y7000 1001】 数量 2
- 商品ID【香奈尔2022款 1051】 数量 200
- cart:1
-
用户未登录的购物车
- cart:uuid
- 商品ID【联想拯救者Y7000 1001】 数量 1
- 商品ID【海尔冰箱 1097】 数量 1
uuid:生成后,会把这个值以cookie的形式发给客户端,以cart:uuid值为key存入到redis。将来就可以获取对应的cookie【得到uuid】,然后加cart:前缀到redis中去查找。找到:有这个车,没找到:就没这个车
- cart:uuid
-
-
有两个车了
- 合并操作
- 未登录的车合并到用户登录购物车
- 怎么合?
- 商品ID相同:数量相加,更新用户登录购物车中这个商品ID对应的数量
- 商品ID不同,增加这个商品对应的数据到已登录的车中
- 怎么合?
- 未登录的车合并到用户登录购物车
- 什么时候合并
- 登录后
- 异步合并【消息队列】
- 登录后
- 合并操作
# 购物车操作
## 添加商品
hset cart:1 1001 1
## 增加数量
hincrby cart:1 1001 1
## 几类总数
hlen cart:1
## 删除商品
hdel cart:1 1001
## 获取购物车所有商品
hgetall cart:1
8、分布式锁
传统锁 |
---|
![]() |
分布式锁 |
---|
![]() |
Redisson实现分布式锁的原理 |
---|
![]() |
- https://www.cnblogs.com/qdhxhz/p/11046905.html