文章目录
- 10. redis配置mysql实战优化[重要]
- 11. redis之缓存击穿、缓存穿透、缓存雪崩
- 12. redis实现分布式session
10. redis配置mysql实战优化[重要]
// 最初实现
@Override
@Transactional
public Product createProduct(Product product) {
productRepo.saveAndFlush(product);
jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product))
return product;
}
@Override
@Transactional
public Product updateProduct(Product product) {
productRepo.saveAndFlush(product);
jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product))
return product;
}
@Override
public Product getProduct(Long productId) {
// 1. 先查redis
String productRedis = jedis.get(SystemConstants.REDIS_KEY_PREFIX + productId);
if (!StringUtil.isBlank(productRedis)) {
return gson.fromJson(productRedis, Product.class);
}
// 2. redis没有,再查mysql数据库
Product productMysql = productRepo.findByProductId(productId);
if (productMysql != null) {
// 3. 数据库有,则更新redis数据
jedis.set(SystemConstants.REDIS_KEY_PREFIX + productMysql.getProductId(), gson.toJson(productMysql));
}
// 4. 返回mysql数据库数据
return productMysql;
}
小公司并发量不大的情况下,问题不是很大,但是大公司高并发量,会出现大量问题,列举如下:
存在的问题:
1. 缓存容量小问题:几百G的海量数据不可能一直都放到redis缓存中,大大降低redis(<10G)作为内存数据库的效率
解决方案:设置固定过期时间,比如说一天,虽然一开始redis数据量很大,但是一天之后,会有大量数据失效,达到冷热数据的分离。
jedis.set(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), gson.toJson(product));
jedis.expire(SystemConstants.REDIS_KEY_PREFIX + product.getProductId(), SystemConstants.REDIS_KEY_EXPIRED_TIME);
2. 缓存击穿问题:虽然设置了过期时间,仍然会出现缓存击穿问题, 即单个热点key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样(缓存无数据/数据库有数据)
解决方案:设置随机过期时间
jedis.expire(SystemConstants.REDIS_KEY_PREFIX + productId, genRandomExpiredTime(5));
public Integer genRandomExpiredTime(Integer random) {
return SystemConstants.REDIS_KEY_EXPIRED_TIME + new Random().nextInt(random) * 60 * 60;
}
3. 缓存穿透问题:用户访问的数据既不在缓存当中,也不在数据库中,按道理说数据库都没有这个数据,就不能一直来查数据库了,防止黑客恶意攻击。
解决方案一:缓存空值(null)或默认值 + 过期时间
在数据库查询不存在时,将其缓存为空值(null)或默认值,缓存失效时间一般设置为5分钟之内,当数据库被写入或更新该key的新数据时,缓存必须同时被刷新,避免数据不一致。
@Override
public Product getProduct(Long productId) {
String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
// 1. 先查redis
String productRedis = jedis.get(redisId);
if (!StringUtil.isBlank(productRedis)) {
// 判断缓存是否是默认值,避免缓存穿透
if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
jedis.expire(redisId, genRandomExpiredTime(3));
return null;
}
jedis.expire(redisId, genRandomExpiredTime(5));
return gson.fromJson(productRedis, Product.class);
}
// 2. redis没有,再查mysql数据库
Product productMysql = productRepo.findByProductId(productId);
if (productMysql != null) {
// 3. 数据库有,则更新redis数据
jedis.set(redisId, gson.toJson(productMysql));
jedis.expire(redisId, genRandomExpiredTime(5));
} else {
// 缓存空或默认值 + 过期时间,避免缓存穿透
jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);
jedis.expire(redisId, genRandomExpiredTime(3));
}
return productMysql;
}
4. 突发性热点缓存重建导致数据库系统压力倍增:也就是说某一数据本来是冷数据,存储在数据库中,突然出现大量访问,redis还没缓存该数据,因此需要大量查询数据库并重建缓存,也就是以下代码重复执行,要是只执行一次就好了。
if (!StringUtil.isBlank(productRedis)) {
// 3. 数据库有,则更新redis数据
jedis.set(redisId, gson.toJson(productMysql));
jedis.expire(redisId, genRandomExpiredTime(5));
}
解决方案一:DCL双端检锁机制
但仍然存在以下问题,一方面synchronized锁住的是单个JVM,若是该web项目集群部署,则在每个JVM都需要锁一次,另一方面,假如productId=101是热点数据会被锁住,但是其他数据productId=202也需要排队等待,效率降低。
解决方案二:分布式锁setnx
但仍然存在redis缓存和mysql数据库数据不一致问题
解决方案三:锁优化-读写锁
5. 缓存雪崩:在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。但当缓存中大量热点缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,造成系统崩溃等情况,这就是缓存雪崩。
解决方案:
1. key均匀失效: 将key的过期时间后面加上一个随机数(比如随机1-5分钟),让key均匀的失效。
2. 双key策略: 主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
3. 构建缓存高可用集群
// 解决方案一:DCL双端检锁机制
@Override
public Product getProduct(Long productId) {
String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
// 1. 先查redis
String productRedis = jedis.get(redisId);
if (!StringUtil.isBlank(productRedis)) {
// 判断缓存默认值,避免缓存穿透
if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
jedis.expire(redisId, genRandomExpiredTime(3));
return null;
}
jedis.expire(redisId, genRandomExpiredTime(5));
return gson.fromJson(productRedis, Product.class);
}
Product productMysql = null;
synchronized (this) {
// 2. DCL再查redis,因为只要有一次查询数据库操作,redis就已经有缓存数据了
productRedis = jedis.get(redisId);
if (!StringUtil.isBlank(productRedis)) {
if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
jedis.expire(redisId, genRandomExpiredTime(3));
return null;
}
jedis.expire(redisId, genRandomExpiredTime(5));
return gson.fromJson(productRedis, Product.class);
}
// 3. redis还是没有,再查mysql数据库
productMysql = productRepo.findByProductId(productId);
if (productMysql != null) {
// 4. 数据库有,则更新redis数据【可能出现突发性热点缓存重建导致数据库系统压力倍增】
jedis.set(redisId, gson.toJson(productMysql));
jedis.expire(redisId, genRandomExpiredTime(5));
} else {
// 缓存空或默认值 + 过期时间,避免缓存穿透
jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);
jedis.expire(redisId, genRandomExpiredTime(3));
}
}
return productMysql;
}
// 解决方案二:分布式锁setnx
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.0</version>
</dependency>
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
// 集群部署:分布式锁
public Product getProduct2(Long productId) {
String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
// 1. 先查redis缓存
Product product = getProductFromRedis(redisId);
if (product != null) {
return product;
}
// 分布式锁RLock确保锁住特定的productId,不影响其他productId,解决所有问题
RLock lock = redisson.getLock(SystemConstants.LOCK_HOT_CACHE_PREFIX + productId);
lock.lock(); // 等价于setnx(SystemConstants.LOCK_HOT_CACHE_PREFIX + productId, value)
// 2. DCL再查redis,因为只要有一次查询数据库操作,redis就已经有缓存数据了
Product productMysql = null;
try {
product = getProductFromRedis(redisId);
if (product != null) {
return product;
}
// 3. redis还是没有,最后查mysql数据库
productMysql = getProductFromMysql(productId);
} finally {
lock.unlock();
}
return productMysql;
}
private Product getProductFromRedis(String redisId) {
Product product = null;
String productRedis = jedis.get(redisId);
if (!StringUtil.isBlank(productRedis)) {
if (productRedis.equals(SystemConstants.REDIS_DEFAULT_CACHE)) {
// 缓存中存在,却是缓存默认值,也就是数据库没有数据,设置过期时间,避免缓存穿透
jedis.expire(redisId, genRandomExpiredTime(3));
return new Product(); // 特殊情况
}
// 缓存中存在,也是正常值
jedis.expire(redisId, genRandomExpiredTime(5));
product = gson.fromJson(productRedis, Product.class);
}
return product;
}
private Product getProductFromMysql(Long productId) {
String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
Product productMysql = productRepo.findByProductId(productId);
if (productMysql != null) {
// 数据库有,则同步更新redis缓存数据【但是可能出现突发性热点缓存重建导致数据库系统压力倍增,也就是这段代码大量执行】
jedis.set(redisId, gson.toJson(productMysql));
jedis.expire(redisId, genRandomExpiredTime(5));
} else {
// 数据库没有,则设置默认值缓存 + 过期时间,避免缓存穿透
jedis.set(redisId, SystemConstants.REDIS_DEFAULT_CACHE);
jedis.expire(redisId, genRandomExpiredTime(3));
}
return productMysql;
}
// 解决方案三:锁优化-读写锁
public Product getProductByReadWriteLock(Long productId) {
String redisId = SystemConstants.REDIS_KEY_PREFIX + productId;
// 1. 先查redis缓存
Product product = getProductFromRedis(redisId);
if (product != null) {
return product;
}
// 加写锁
ReadWriteLock readWriteLock = redisson.getReadWriteLock(SystemConstants.LOCK_HOT_UPDATE_PREFIX + productId);
Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
// 2. DCL再查redis,因为只要有一次查询数据库操作,redis就已经有缓存数据了
Product productMysql;
try {
product = getProductFromRedis(redisId);
if (product != null) {
return product;
}
// 3. 加读锁 读数据库
ReadWriteLock readWriteLock2 = redisson.getReadWriteLock(SystemConstants.LOCK_HOT_UPDATE_PREFIX + productId);
Lock readLock = readWriteLock2.readLock();
readLock.lock();
productMysql = getProductFromMysql(productId);
readLock.unlock();
} finally {
writeLock.unlock();
}
return productMysql;
}
11. redis之缓存击穿、缓存穿透、缓存雪崩
- 缓存击穿-缓存无数据/数据库有数据
单个热点key失效的瞬间,持续的大并发请求就会击破缓存,直接请求到数据库,好像蛮力击穿一样。这种情况就是缓存击穿(Cache Breakdown)。
1. 使用互斥锁(Mutex Key)
只让一个线程构建缓存,其他线程等待构建缓存执行完毕,重新从缓存中获取数据。
2. 热点数据设置随机过期时间,后台异步更新缓存,适用于不严格要求缓存一致性的场景。
-
缓存穿透-缓存无数据/数据库无数据
缓存穿透(cache penetration)是用户访问的数据既不在缓存当中,也不在数据库中。但出于容错的考虑,如果从数据库查询不到数据,则无法写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。
解决方案:
方案一:缓存空值(null)或默认值 + 过期时间
在数据库查询不存在时,将其缓存为空值(null)或默认值,缓存失效时间一般设置为5分钟之内,当数据库被写入或更新该key的新数据时,缓存必须同时被刷新,避免数据不一致。
方案二:业务逻辑前置校验
在业务请求的入口处进行数据合法性校验,检查请求参数是否合理、是否包含非法值、是否恶意请求等,提前有效阻断非法请求。比如,根据年龄查询时,请求的年龄为-10岁,这显然是不合法的请求参数,直接在参数校验时进行判断返回。
方案三:使用布隆过滤器请求白名单
写入数据时,使用布隆过滤器进行标记(相当于设置白名单),业务请求发现缓存中无对应数据时,可先通过查询布隆过滤器判断数据是否在白名单内,如果不在白名单内,则直接返回空或失败。
方案四:用户黑名单限制
当发生异常情况时,实时监控访问的对象和数据,分析用户行为,针对故意请求、爬虫或攻击者,进行特定用户的限制;
-
缓存雪崩-缓存无数据/数据库有数据
在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。但当缓存中大量热点缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,造成系统崩溃等情况,这就是缓存雪崩(Cache Avalanche)。
解决方案:
1. key均匀失效: 将key的过期时间后面加上一个随机数(比如随机1-5分钟),让key均匀的失效。
2. 双key策略: 主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值。
3. 构建缓存高可用集群(针对缓存服务故障情况)
12. redis实现分布式session
基于redis的分布式session实现,依赖于前台请求中携带的cookie和后台生成的token。大致原理可以分为以下步骤:
1,前端请求目标方法,拦截器判断请求头中是否携带cookie。
2,如果请求头中携带cookie,则取出cookie并查询redis中该cookie是否过期。
如果没有过期,则放行让该请求去请求目标方法;
如果已经过期,重新登陆
3,如果请求头中,没有携带cookie,则跳转到登录方法(同时携带当前请求的链接作为登录后的回调地址)
4,进行登录,登录完毕生成指定的token存入redis中,生成cookie设置到response中。
5,登录成功之后前端通过回调继续请求目标方法。