前置知识
NoSQL VS SQL
Redis特征
redis6.0有多线程,但是仅针对网络请求处理,核心的命令执行还是单线程
运行命令
1. 前台启动
redis-server
2. 后台运行
// 启动服务
redis-server --service-start
// 停止服务
redis-server --service-stop
客户端启动
1. redis-cli -h 地址 -p 端口号 -a 密码
2. redis-cli -h 地址 -p 端口号
AUTH 密码
Redis
数据结构
通用命令
String
key命名
Hash
List
Set
SortedSet
GEO
BitMap
HyperLogLog
⭐五种基本数据类型的使用场景
- String:
存储常规数据(token)、需要计数、分布式锁(setnx)
- List:
信息流(最新动态)、消息队列
- Set:
存放的数据不能重复(点赞收藏)、获取多个数据源交集、并集和差集(共同好友)、随机获取数据源中的元素
- Hash:
存储对象数据(用户信息)
- sorted set:
随机获取数据源中的元素根据某个权重进行排序(排行榜)
存储的数据有优先级或者重要程度
Redis的java客户端
Jedis
1. 引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
2. 创建jedis对象,建立连接
private Jedis jedis;
@BeforeEach
void setUp() {
//连接jedis
jedis = new Jedis("192.168.150.101",6379);
//设置密码
jedis.auth("123456")
//设置库,一共有16个,默认为0
jedis.select(0);
}
3. 使用Jedis,方法名与Redis命令一样
//操作String
@Test
public void test() {
// 插入数据
String res = jedis.set("name", "zhangsan");
System.out.println("res = " + res);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
//操作hash
@Test
public void test2() {
jedis.hset("hash","name","zhangsan");
jedis.hset("hash","age","11");
Map<String, String> map = jedis.hgetAll("hash");
System.out.println(map);
}
4. 释放资源
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}
因为Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此使用Jedis连接池代替Jedis的直连方式
public class JedisConnecitonFactiory {
private static final JedisPool jedispool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//最大连接数
jedisPoolConfig.setMaxTotal(8);
//最大空闲连接
jedisPoolConfig.setMaxIdle(8);
//最小空闲连接
jedisPoolConfig.setMinIdle(0);
//设置最长等待时间 ms
jedisPoolConfig.setMaxWaitMillis(2000);
//1000 是等待连接redis的超时时间
jedispool = new JedisPool(jedisPoolConfig,"192.168.119.120",6379,1000,"123456");
}
public static Jedis getJedis() {
return jedispool.getResource();
}
}
SpringDataRedis
API
操作
1. 引入依赖
<!-- redis启动类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2. 配置文件
spring:
redis:
host: 192.168.150.101
port: 6379
password: 123321
# springboot默认使用lecctuce 如果要使用jedis 需要自己引入jedis的依赖
lettuce:
pool:
# 最大连接
max-active: 8
# 最大空闲连接
max-idle: 8
# 最小空闲连接
min-idle: 0
# 最大等待时长,没有等到会报错
max-wait: 100ms
3. 测试
@SpringBootTest
class SpringbootdataRedisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("name","大虎");
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
}
自定义序列化
RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的:
可读性差且占用内存,因此需要自定义序列化
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
// redis配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 1 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 2 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 3 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// 4.1 设置Key的序列化
// key和 hashkey 采用String序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 4.2 设置Value的序列化
// value 和 hashvalue 采用JSON序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 5 返回
return template;
}
}
StringRedisTemplate
@Test
void contextLoads2() {
User user = new User("虎哥",33);
redisTemplate.opsForValue().set("user:10",user);
user =(User) redisTemplate.opsForValue().get("user:10");
System.out.println("user = " + user);
}
得到结果:
为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销
因此为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。
SpringDataRedis提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式
@SpringBootTest
class RedisStringTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 存字符串
*/
@Test
void testString() {
// 写入一条String数据
stringRedisTemplate.opsForValue().set("verify:phone:13600527634", "124143");
// 获取string数据
Object name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
// springmvc中默认使用的JSON序列化工具
private static final ObjectMapper mapper = new ObjectMapper();
/**
* 存对象
*/
@Test
void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("虎哥", 21);
// 手动序列化!!!!!!!!!!
String json = mapper.writeValueAsString(user);
// 写入数据
stringRedisTemplate.opsForValue().set("user:200", json);
// 获取数据
String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
// 手动反序列化!!!!!!!!!!
User user1 = mapper.readValue(jsonUser, User.class);
System.out.println("user1 = " + user1);
}
/**
* 存哈希
*/
@Test
void testHash() {
// hash存用的是put 取一个用的是get
stringRedisTemplate.opsForHash().put("user:400", "name", "虎哥");
stringRedisTemplate.opsForHash().put("user:400", "age", "21");
// 一次性把所有的key value都取出来 用entries
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
System.out.println("entries = " + entries);
}
}
缓存更新策略
- Cache Aside(旁路缓存)策略;
- Read/Write Through(读穿 / 写穿)策略;
- Write Back(写回)策略;
1. Cache Aside(旁路缓存)
应用程序直接与「数据库、缓存」交互,并负责对缓存的维护
2. Read/Write Through(读穿 / 写穿)
应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。
3. Write Back(写回/异步缓存写入)
在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的(表示被修改过),然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
缓存问题
1. 缓存穿透
当有大量这样的请求到来时,数据库的压力骤增
布隆过滤器可以看作是由 位数组 和 哈希函数 组成的一种数据结构
每个元素只占1bit,只能为0或1
会误判,把不是在集合中的元素判断为处在集合中。
2. 缓存雪崩
3. 缓存击穿
缓存重建:redis未命中,到数据库查询并重新更新到缓存中
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
逻辑过期:
不设置 ttl(避免key失效),而是添加一个字段expire(表示过期时间),在逻辑上进行判断
互斥锁:
保证只有一个请求会落到数据库上
后台异步更新缓存:
不给热点数据设置过期时间,由后台异步更新缓存。或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存和数据库数据一致性
1. 先更新数据库再删除缓存
删除失败导致数据不一致怎么办?
① 引入消息队列,重试,将删除缓存的操作加到消息队列中
② 更新数据库成功,就会产生一条变更日志,记录在 binlog 里。订阅 MySQL binlog(伪装成数据库的一个从节点),拿到具体要操作的数据,再操作缓存
2. 延迟双删(解决 先删除缓存再更新数据库 数据不一致性)
删除缓存 -> 更新数据库 -> 睡眠 -> 删除缓存
睡眠:为了确保请求 A 在睡眠的时候,请求 B 能够在这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除B写入的缓存
缺点:延时时间难以把控,删除可能失败
3. 先更新数据库再更新缓存:
怎么解决数据不一致?/ 怎么保证双写一致性:
① 更新前加分布式锁; ② 更新完缓存时,给缓存加上较短的过期时间