1. Redis
Redis是一款基于内存的、使用K-V结构来实现读写数据的NoSQL非关系型数据库
基于内存的:Redis访问的数据都在内存中
- Redis的读写效率非常高
- 其实Redis也会自动的处理持久化,但是正常读写都是在内存中执行的
NoSQL:不涉及SQL语句,
No可理解为日常英语中的no(没有),也可理解为No Operation(不操作)非关系型数据库:不关心数据库中存储的是什么数据,几乎没有数据种类的概念,更不存在数据与数据之间的关联
通常,在项目中,Redis用于实现缓存!

当使用Redis后:
- 【优点】读取数据的效率会高很多
- 【优点】能够一定程度上保障缓解数据库的查询压力,提高数据库的安全性
- 【缺点】需要关注数据一致性问题,即Redis中的数据与MySQL中的数据是否一致,如果不一致,是否需要处理
2. Redis的基本操作
在Windows操作系统中,通过.msi安装包来安装的Redis,会自动注册Redis服务,开机会自动启动Redis,所以,Redis处于随时可用的状态。
可以在命令提示符窗口或终端窗口通过redis-cli命令,登录Redis控制台:
D:\IdeaProjects\jsd2207-csmall-product-teacher>redis-cli
127.0.0.1:6379>
当操作提示符变成 127.0.0.1:6379> 后,表示已经登录到Redis客户端的控制台。
在Redis客户端控制台中,可以通过ping命令实时检测Redis是否仍处理可用状态,如果Redis服务正常可用,将反馈PONG:
127.0.0.1:6379> ping
PONG
在Redis客户端控制台中,可以通过exit命令退出,以回到操作系统的终端:
127.0.0.1:6379> exit
D:\IdeaProjects\jsd2207-csmall-product-teacher>
在Redis客户端控制台中,可以通过set命令向Redis中存入值数据,例如:
127.0.0.1:6379> set name wangkejing
OK
在Redis客户端控制台中,可以通过get命令向Redis中存入值数据,例如:
127.0.0.1:6379> get name
"wangkejing"
127.0.0.1:6379> get email
(nil)
提示:以上使用的set命令,既是新增数据的命令(当Key尚且不存在时),也是修改数据的命令(当Key已经存在时),例如:
127.0.0.1:6379> set name fanchuanqi
OK
127.0.0.1:6379> get name
"fanchuanqi"
在Redis客户端控制台中,可以通过keys命令向Redis中存入值数据,此命令必须有参数,在参数中可以使用通配符,例如:
127.0.0.1:6379> keys username1
1) "username1"
127.0.0.1:6379> keys username0
(empty list or set)
127.0.0.1:6379> keys user*
1) "username1"
2) "username3"
3) "username2"
4) "username4"
127.0.0.1:6379> keys *
1) "username1"
2) "email1"
3) "email2"
4) "username3"
5) "name"
6) "username2"
7) "username4"
注意:keys命令会根据模式查找当前Redis中的Key,可能耗时较长,会导致“阻塞”,所以,在生产环境中,一般不允许使用此命令!
在Redis客户端控制台中,可以通过dbsize命令查看Redis中的数据的数量,例如:
127.0.0.1:6379> dbsize
(integer) 7
在Redis客户端控制台中,可以通过del命令删除Redis中指定Key的数据,例如:
127.0.0.1:6379> del username1
(integer) 1
在Redis客户端控制台中,可以通过flushall命令删除Redis中的数据,例如:
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
更多命令,可查阅资料,例如:https://blog.csdn.net/weixin_46742102/article/details/109483603
Redis中的传统数据类型有:string、list、hash、set、zset。
3. Redis编程
需要添加依赖项:
<!-- Spring Boot Data Redis的依赖项,用于实现Redis编程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在Spring系列框架中,Redis编程需要使用RedisTemplate工具类,此工具类应该事先创建、配置,并保存到Spring容器中,当需要使用时,自动装配此工具类的对象。
在根包下创建RedisConfiguration配置类,在此类中使用@Bean方法来配置RedisTemplate:
package cn.tedu.csmall.product.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.io.Serializable;
/**
* Redis配置类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Configuration
public class RedisConfiguration {
public RedisConfiguration() {
log.debug("创建配置类对象:RedisConfiguration");
}
@Bean
public RedisTemplate<String, Serializable> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
}
关于Redis中的list类型的数据,它是一种先进后出、后进先出的栈结构:

在Redis中操作list时,允许从左侧或右侧进行操作(请将此栈结构想像为横着的):



关于Redis读写数据,常用API大致如下:
package cn.tedu.csmall.product;
import cn.tedu.csmall.product.pojo.vo.AlbumStandardVO;
import lombok.extern.slf4j.Slf4j;
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.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
@SpringBootTest
public class RedisTests {
@Autowired
RedisTemplate<String, Serializable> redisTemplate;
// Redis编程相关API
// =====================
// 【RedisTemplate类】
// ValueOperations opsForValue() >>> 获取ValueOperations对象,操作Redis中的string类型时需要此对象
// ListOperations opsForList() >>> 获取ListOperations对象,操作Redis中的list类型时需要此对象
// Set<String> keys(String pattern) >>> 根据模式pattern搜索Key
// Boolean delete(String key) >>> 根据Key删除数据,返回成功与否
// Long delete(Collection<String> keys) >>> 根据Key的集合批量删除数据,返回成功删除的数据的数量
//
// 【ValueOperations类】
// void set(String key, Serializable value) >>> 向Redis中写入数据
// Serializable get(String key) >>> 读取Redis中的数据
@Test
void valueSet() {
String key = "username1";
String value = "王克晶";
//添加数据
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
ops.set(key, value);
}
@Test
void valueSetObject() {
String key = "album2022";
AlbumStandardVO album = new AlbumStandardVO();
album.setId(2022L);
album.setName("测试相册00001");
album.setDescription("测试简介00001");
album.setSort(998);
//添加对象数据
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
ops.set(key, album);
}
@Test
void valueGet() {
String key = "username1";
//取出数据
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
Serializable value = ops.get(key);
log.debug("从Redis中取出Key值为【{}】的数据,结果:{}", key, value);
}
@Test
void valueGetObject() {
String key = "album2022";
//取出对象数据
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
Serializable value = ops.get(key);
log.debug("从Redis中取出Key值为【{}】的数据", key);
log.debug("结果:{}", value);
log.debug("结果的数据类型:{}", value.getClass().getName());
}
@Test
void keys() {
String pattern = "*";
//根据字符串搜索key
Set<String> keys = redisTemplate.keys(pattern);
log.debug("根据模式【{}】搜索Key,结果:{}", pattern, keys);
}
@Test
void delete() {
String key = "username1";
//根据key删除数据
Boolean result = redisTemplate.delete(key);
log.debug("根据Key【{}】删除数据完成,结果:{}", key, result);
}
@Test
void deleteX() {
Set<String> keys = new HashSet<>();
keys.add("username2");
keys.add("username3");
keys.add("username4");
//根据key集合删除数据
Long count = redisTemplate.delete(keys);
log.debug("根据Key集合【{}】删除数据完成,成功删除的数据的数量:{}", keys, count);
}
@Test
void rightPush() {
String key = "stringList";
List<String> stringList = new ArrayList<>();
for (int i = 1; i <= 8; i++) {
stringList.add("string-" + i);
}
//右压列表数据
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
for (String s : stringList) {
ops.rightPush(key, s);
}
}
@Test
void size() {
String key = "stringList";
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
Long size = ops.size(key);
//根据Key读取列表的长度
log.debug("根据Key【{}】读取列表的长度,结果:{}", key, size);
}
@Test
void range() {
String key = "stringList";
long start = 0;
long end = -1;
//根据Key从0到1读取列表,结果长度
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
List<Serializable> list = ops.range(key, start, end);
log.debug("根据Key【{}】从【{}】到【{}】读取列表,结果长度:{}", key, start, end, list.size());
for (Serializable item : list) {
log.debug("列表项:{}", item);
}
}
}
4. 关于数据一致性问题的思考
当使用Redis缓存数据后,如果数据库中的数据发生了变化,此时,Redis中的数据会暂时与MySQL数据库中的数据并不相同,通常称之为“数据一致性”问题!对于此问题,只可能:
- 及时更新Redis缓存数据,使得Redis中的数据与MySQL数据库中的数据是一致的
- 放任数据不一致的表现,即Redis中的数据在接下来的一段时间里是不准确的,直至更新Redis中的数据,才会是准确的数据
如果及时更新Redis缓存数据,其优点是Redis缓存中的数据基本上是准确的,其缺点在于可能需要频繁的更新Redis缓存数据,本质上反复读MySQL、反复写Redis的操作,如果读取Redis数据的频率根本不高,则会形成浪费,并且,更新缓存的频率太高,也会增加服务器的压力,所以,对于增、删、改频率非常高的数据,可能不太适用此规则!
放任数据不一致的表现,其缺点很显然就是数据可能不准确,但是,其优点是没有给服务器端增加任何压力,需要注意:其实,并不是所有数据都必须时时刻刻都要求准确性的,某些数据即使不准确,也不会产生恶劣后果(例如热门话题排行榜,定期更新即可),或者,某些数据即使准确,也没有太多实际意义(例如热门时段的火车票、飞机票等,即使在列表中显示了正确的余量,也不一定能够成功购买)。
基于使用Redis缓存数据可能存在数据一致性问题,通常,使用Redis缓存的数据可能:
- 数据的增、删、改的频率非常低,查询频率相对更高(甚至非常高)
- 例如电商平台中的商品类别、品牌
- 无论采取即时更新Redis的策略,还是定期更新Redis的策略,都是可行的解决方案
- 对数据的准确性要求不高的数据
- 例如某些列表或榜单
- 通常使用定期更新Redis的策略,更新周期应该根据数据变化的频率及其价值来决定
4.1. 缓存品牌数据
通常,推荐将Redis的读写数据操作进行封装,则先在根包下创建repo.IBrandRedisRepository接口:
public interface IBrandRedisRepository {}
然后,在根包下repo.impl.BrandRedisRepositoryImpl创建以上接口的实现类:
@Slf4j
@Repository
public class BrandRedisRepositoryImpl implements IBrandRedisRepository {
public BrandRedisRepositoryImpl() {
log.debug("创建处理缓存的数据访问对象:BrandRedisRepositoryImpl");
}
}
接下来,应该在接口中添加读写数据的抽象方法,并在实现类中实现这些方法!
在IBrandRedisRepository接口中添加抽象方法:
package cn.tedu.csmall.product.repo;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import java.util.List;
/**
* 处理品牌缓存的数据访问接口
*
* @author java@tedu.cn
* @version 0.0.1
*/
public interface IBrandRedisRepository {
/**
* 品牌数据项在Redis中的Key前缀
*/
String BRAND_ITEM_KEY_PREFIX = "brand:item:";
/**
* 品牌列表在Redis中的Key
*/
String BRAND_LIST_KEY = "brand:list";
/**
* 所有品牌数据项的Key
*/
String BRAND_ITEM_KEYS_KEY = "brand:item-keys";
/**
* 向Redis中写入品牌数据
*
* @param brandStandardVO 品牌数据
*/
void save(BrandStandardVO brandStandardVO);
/**
* 向Redis中写入品牌列表
*
* @param brands 品牌列表
*/
void save(List<BrandListItemVO> brands);
/**
* 删除Redis中全部品牌数据,包括各品牌详情数据和品牌列表等
*
* @return 成功删除的数据的数量
*/
Long deleteAll();
/**
* 从Redis中读取品牌数据
*
* @param id 品牌id
* @return 匹配的品牌数据,如果没有匹配的数据,则返回null
*/
BrandStandardVO get(Long id);
/**
* 从Redis中读取品牌列表
*
* @return 品牌列表
*/
List<BrandListItemVO> list();
/**
* 从Redis中读取品牌列表
*
* @param start 读取数据的起始下标
* @param end 读取数据的截止下标
* @return 品牌列表
*/
List<BrandListItemVO> list(long start, long end);
}
并在BrandRedisRepositoryImpl中实现此方法:
package cn.tedu.csmall.product.repo.impl;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import cn.tedu.csmall.product.repo.IBrandRedisRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 处理品牌缓存的数据访问实现类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Repository
public class BrandRedisRepositoryImpl implements IBrandRedisRepository {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
public BrandRedisRepositoryImpl() {
log.debug("创建处理缓存的数据访问对象:BrandRedisRepositoryImpl");
}
@Override
public void save(BrandStandardVO brandStandardVO) {
String key = BRAND_ITEM_KEY_PREFIX + brandStandardVO.getId();
redisTemplate.opsForSet().add(BRAND_ITEM_KEYS_KEY, key);
redisTemplate.opsForValue().set(key, brandStandardVO);
}
@Override
public void save(List<BrandListItemVO> brands) {
String key = BRAND_LIST_KEY;
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
for (BrandListItemVO brand : brands) {
ops.rightPush(key, brand);
}
}
@Override
public Long deleteAll() {
// 获取到所有item的key
Set<Serializable> members = redisTemplate
.opsForSet().members(BRAND_ITEM_KEYS_KEY);
Set<String> keys = new HashSet<>();
for (Serializable member : members) {
keys.add((String) member);
}
// 将List和保存Key的Set的Key也添加到集合中
keys.add(BRAND_LIST_KEY);
keys.add(BRAND_ITEM_KEYS_KEY);
return redisTemplate.delete(keys);
}
@Override
public BrandStandardVO get(Long id) {
Serializable serializable = redisTemplate
.opsForValue().get(BRAND_ITEM_KEY_PREFIX + id);
BrandStandardVO brandStandardVO = null;
if (serializable != null) {
if (serializable instanceof BrandStandardVO) {
brandStandardVO = (BrandStandardVO) serializable;
}
}
return brandStandardVO;
}
@Override
public List<BrandListItemVO> list() {
long start = 0;
long end = -1;
return list(start, end);
}
@Override
public List<BrandListItemVO> list(long start, long end) {
String key = BRAND_LIST_KEY;
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
List<Serializable> list = ops.range(key, start, end);
List<BrandListItemVO> brands = new ArrayList<>();
for (Serializable item : list) {
brands.add((BrandListItemVO) item);
}
return brands;
}
}
完成后,可以调整“查询品牌列表”的业务,将原本从数据库中查询数据改为从Redis缓存中查询数据,则修改BrandServiceImpl中的list()方法:
@Override
public List<BrandListItemVO> list() {
log.debug("开始处理【查询品牌列表】的业务,无参数");
// return brandMapper.list();
return brandRedisRepository.list();
}
至于,查询品牌列表时将从Redis中查询数据。
4.2. 缓存预热
当启用项目时就将缓存数据加载到Redis缓存中,这种做法通常称之为“缓存预热”。
在Spring Boot项目中,自定义组件类,实现ApplicationRunner接口,此接口中的run()方法将在启动项目之后自动执行,可以通过此机制实现缓存预热。
在根包下创建preload.CachePreload类并实现缓存预热:
package cn.tedu.csmall.product.preload;
import cn.tedu.csmall.product.mapper.BrandMapper;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.repo.IBrandRedisRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
public class CachePreload implements ApplicationRunner {
@Autowired
private BrandMapper brandMapper;
@Autowired
private IBrandRedisRepository brandRedisRepository;
public CachePreload() {
log.debug("创建开机自动执行的组件对象:CachePreload");
}
// ApplicationRunner中的run()方法会在项目启动成功之后自动执行
@Override
public void run(ApplicationArguments args) throws Exception {
log.debug("CachePreload.run()");
log.debug("准备删除Redis缓存中的品牌数据……");
brandRedisRepository.deleteAll();
log.debug("删除Redis缓存中的品牌数据,完成!");
log.debug("准备从数据库中读取品牌列表……");
List<BrandListItemVO> list = brandMapper.list();
log.debug("从数据库中读取品牌列表,完成!");
log.debug("准备将品牌列表写入到Redis缓存……");
brandRedisRepository.save(list);
log.debug("将品牌列表写入到Redis缓存,完成!");
}
}
4.3. 手动更新缓存
由于在业务逻辑层已经实现“重建品牌缓存”的功能,在控制器中添加处理请求的方法,即可实现手动更新缓存:
// http://localhost:9080/brands/cache/rebuild
@ApiOperation("重建品牌缓存")
@ApiOperationSupport(order = 600)
@PostMapping("/cache/rebuild")
public JsonResult<Void> rebuildCache() {
log.debug("开始处理【重建品牌缓存】的请求,无参数");
brandService.rebuildCache();
return JsonResult.ok();
}
后续,客户端只需要提交请求,即可实现“重建品牌缓存”。
4.4. 按需加载缓存数据
假设当根据id获取品牌详情时,需要通过“按需加载缓存数据”的机制来实现缓存,可以将原业务调整为:
@Override
public BrandStandardVO getStandardById(Long id) {
log.debug("开始处理【根据id查询品牌详情】的业务,参数:{}", id);
// 根据id从缓存中获取数据
log.debug("将从Redis中获取相关数据");
BrandStandardVO brand = brandRedisRepository.get(id);
// 判断获取到的结果是否不为null
if (brand != null) {
// 是:直接返回
log.debug("命中缓存,即将返回:{}", brand);
return brand;
}
// 无缓存数据,从数据库中查找数据
log.debug("未命中缓存,即将从数据库中查找数据");
brand = brandMapper.getStandardById(id);
// 判断查询到的结果是否为null
if (brand == null) {
// 是:抛出异常
String message = "获取品牌详情失败,尝试访问的数据不存在!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
}
// 将查询结果写入到缓存,并返回
log.debug("从数据库查询到有效结果,将查询结果存入到Redis:{}", brand);
brandRedisRepository.save(brand);
log.debug("返回结果:{}", brand);
return brand;
}
5.Redis 强化
5.1.缓存使用原则
什么时候,什么样的数据能够保存在Redis中?
1.数据量不能太大
2.使用越频繁,Redis保存这个数据越值得
3.保存在Redis中的数据一般不会是数据库中频繁修改的
5.2.缓存淘汰策略
Redis将数据保存在内存中, 内存的容量是有限的
如果Redis服务器的内存已经全满,现在还需要向Redis中保存新的数据,如何操作,就是缓存淘汰策略
- noeviction:返回错误**(默认)**
如果我们不想让它发生错误,就可以设置它将满足某些条件的信息删除后,再将新的信息保存
allkeys-random:所有数据中随机删除数据
volatile-random:有过期时间的数据中随机删除数据
volatile-ttl:删除剩余有效时间最少的数据
科学↓
allkeys-lru:所有数据中删除上次使用时间距离现在最久的数据
volatile-lru:有过期时间的数据中删除上次使用时间距离现在最久的数据
allkeys-lfu:所有数据中删除使用频率最少的
volatile-lfu:有过期时间的数据中删除使用频率最少的
Time To Live (ttl)
Least Recently Used (lru)
Least Frequently Used (lfu)
5.3.缓存穿透
所谓缓存穿透,就是一个业务请求先查询redis,redis没有这个数据,那么就去查询数据库,但是数据库也没有的情况
正常业务下,一个请求查询到数据后,我们可以将这个数据保存在Redis
之后的请求都可以直接从Redis查询,就不需要再连接数据库了
但是一旦发生上面的穿透现象,仍然需要连接数据库,一旦连接数据库,项目的整体效率就会被影响
如果有恶意的请求,高并发的访问数据库中不存在的数据,严重的,当前服务器可能出现宕机的情况
解决方案:业界主流解决方案:布隆过滤器
布隆过滤器的使用步骤
1.针对现有所有数据,生成布隆过滤器,保存在Redis中
2.在业务逻辑层,判断Redis之前先检查这个id是否在布隆过滤器中
3.如果布隆过滤器判断这个id不存在,直接返回
4.如果布隆过滤器判断id存在,在进行后面业务执行
5.4.缓存击穿
一个计划在Redis保存的数据,业务查询,查询到的数据Redis中没有,但是数据库中有
这种情况要从数据库中查询后再保存到Redis,这就是缓存击穿
但是这个情况也不是异常情况,因为我们大多数数据都需要设置过期时间,而过期时间到时,这个数据就会从Redis中移除,再有请求查询这个数据,就一定会从数据库中再次同步
缓存击穿本身并不是灾难性的问题,也不是不允许发生的现象
5.5.缓存雪崩
上面讲到击穿现象
同一时间发生少量击穿是正常的
但是如果出现同一时间大量击穿现象就会如下图
所谓缓存雪崩,指的就是Redis中保存的数据,短时间内有大量数据同时到期的情况
如上图所示,本应该由Redis反馈的信息,由于雪崩都去访问了Mysql,mysql承担不了,非常可能导致异常
要想避免这种情况,就需要避免大量缓存同时失效
大量缓存同时失效的原因:通常是同时加载的数据设置了相同的有效期导致的
我们可以通过在设置有效期时添加一个随机数,这样就能够防止大量数据同时失效了
5.6.Redis持久化
Redis将信息保存在内存
内存的特征就是一旦断电,所有信息都丢失,对于Redis来讲,所有数据丢失后,再重新加载数据,就需要从数据库重新查询所有数据,这个操作不但耗费时间,而且对数据库的压力也非常大
而且有些业务是先将数据保存在Redis,隔一段时间和数据库同步的
如果Redis断电,这段时间的数据就完全丢失了!
为了防止Redis的重启对数据库带来额外的压力和数据的丢失,Redis支持了持久化的功能
所谓持久化就是将Redis中保存的数据,以指定方式保存在Redis当前服务器的硬盘上
如果存在硬盘上,那么断电数据也不会丢失,再启动Redis时,利用硬盘中的信息来回复数据
Redis实现持久化有两种策略
RDB:(Redis Database Backup)
RDB本质上就是数据库快照(就是当前Redis中所有数据转换成二进制的对象,保存在硬盘上)
默认情况下,每次备份会生成一个dump.rdb的文件
当Redis断电或宕机后,重新启动时,会从这个文件中恢复数据,获得dump.rdb中所有内容
实现这个效果我们可以在Redis的配置文件中添加如下信息
save 60 5
上面配置中60表示秒
5表示Redis的key被更新的次数
配置效果:1分钟内如果有5个及以上的key被更新,就启动rdb数据库快照程序
优点:
- 因为是整体Redis数据的二进制格式,数据恢复是整体恢复的
缺点:
- 生成的rdb文件是一个硬盘上的文件,读写效率是较低的
- 如果突然断电,只能恢复到最后一次生成的rdb中的数据
AOF(Append Only File):
AOF策略是将Redis运行过的所有命令(日志)备份下来,保存在硬盘上
这样即使Redis断电,我们也可以根据运行过的日志,恢复为断电前的样子
我们可以在Redis的配置文件中添加如下配置信息
appendonly yes
经过这个设置,就能保存运行过的指令的日志了
理论上任何运行过的指令都可以恢复
但是实际情况下,Redis非常繁忙时,我们会将日志命令缓存之后,整体发送给备份,减少io次数以提高备份的性能 和对Redis性能的影响
实际开发中,配置一般会采用每秒将日志文件发送一次的策略,断电最多丢失1秒数据
优点:
相对RDB来讲,信息丢失的较少
缺点:
因为保存的是运行的日志,所以占用空间较大
实际开发中RDB和AOF是可以同时开启的,也可以选择性开启
Redis的AOF为减少日志文件的大小,支持AOF rewrite
简单来说就是将日志中无效的语句删除,能够减少占用的空间
5.7.Redis存储原理
我们在编写java代码业务时,如果需要从多个元素的集合中寻找某个元素取出,或检查某个Key在不在的时候,推荐我们使用HashMap或HashSet,因为这种数据结构的查询效率最高,因为它内部使用了
"散列表"
下图就是散列表的存储原理

槽位越多代表元素多的时候,查询性能越高,HashMap默认16个槽
Redis底层保存数据用的也是这样的散列表的结构
Redis将内存划分为16384个区域(类似hash槽)
将数据的key使用CRC16算法计算出一个值,取余16384
得到的结果是0~16383
这样Redis就能非常高效的查找元素了
5.8.Redis集群
Redis最小状态是一台服务器
这个服务器的运行状态,直接决定Redis是否可用
如果它离线了,整个项目就会无Redis可用
系统会面临崩溃
为了防止这种情况的发生,我们可以准备一台备用机
主从复制

也就是主机(master)工作时,安排一台备用机(slave)实时同步数据,万一主机宕机,我们可以切换到备机运行
缺点,这样的方案,slave节点没有任何实质作用,只要master不宕机它就和没有一样,没有体现价值
读写分离

这样slave在master正常工作时也能分担Master的工作了
但是如果master宕机,实际上主备机的切换,实际上还是需要人工介入的,这还是需要时间的
那么如果想实现发生故障时自动切换,一定是有配置好的固定策略的
哨兵模式:故障自动切换

哨兵节点每隔固定时间向所有节点发送请求
如果正常响应认为该节点正常
如果没有响应,认为该节点出现问题,哨兵能自动切换主备机
如果主机master下线,自动切换到备机运行

但是如果哨兵判断节点状态时发生了误判,那么就会错误将master下线,降低整体运行性能
所以要减少哨兵误判的可能性
哨兵集群

我们可以将哨兵节点做成集群,由多个哨兵投票决定是否下线某一个节点
哨兵集群中,每个节点都会定时向master和slave发送ping请求
如果ping请求有2个(集群的半数节点)以上的哨兵节点没有收到正常响应,会认为该节点下线
当业务不断扩展,并发不断增高时
分片集群
只有一个节点支持写操作无法满足整体性能要求时,系统性能就会到达瓶颈
这时我们就要部署多个支持写操作的节点,进行分片,来提高程序整体性能
分片就是每个节点负责不同的区域
Redis0~16383号槽,
例如
MasterA负责0~5000
MasterB负责5001~10000
MasterC负责10001~16383
一个key根据CRC16算法只能得到固定的结果,一定在指定的服务器上找到数据

有了这个集群结构,我们就能更加稳定和更加高效的处理业务请求了
为了节省哨兵服务器的成本,有些公司在Redis集群中直接添加哨兵功能,既master/slave节点完成数据读写任务的同时也都互相检测它们的健康状态
有额外精力的同学,可以自己查询Redis分布式锁的解决方案(redission)
