上回讲到了基本校验和统一参数返回.
是不是以为我这章要记录mybatis啊?~
不好意思,这章记录redis.
因为现在数据库那部分不是我做的,哈哈哈~
言回正传.
5.Redis
5.1 Redis是什么,为什么要用?
Redis是一种非关系型数据库,用key-value存储数据。
不同于MYSQL之类的关系型数据库,有字段,属性,实体之类的概念,但是Redis没有,我大概是把它看做成Java的Map来看待的。不过是一个超无敌无敌好用的HashMap.
为什么要用,这问题就牵扯到它的特性了.
速度快:
由于Redis是将数据放缓存
,所以它每次拿数据并不用像MySQL一样去磁盘里面去拿,在查询数据上,也就快很多。所以很多时会拿来做
缓存中间件
原子性:由于操作是原子性,要么做就做完,要么不做完。很多操作可以不考虑线程安全问题
所以很适合数据库。
那么它最大的作用是什么呢!减少数据库压力!!!
你想想,如果每一个请求都是对数据库进行查询,数据库累的得死,再猛也要给喘口气啊。那就想了,我能不能查一次,然后存在一个地方
,用的时候就去哪里取,等要更新的时候再到数据库取。
这就是redis了!而且它里面的数据类型很多,无论java,python啥的都能用。如果还是不能理解它是一个什么东西,先去看一下关系型数据库和非关系型数据库。
5.2 配置Spring boot的redis
- 事前准备
和网上大多数的教程一样,但是很多人不写版本,让人搞来搞去很难受。这里就我自己实践的将自己的一些理解记录下来.
先安装Redis
第一节去下载吧,都一样
然后去到自己windows服务里面,看下Redis有没有开,我第一次是没有开启,然后手动开的,然后设为自动启动。
然后安装Redis Desktop Manager
但是现在新版本要收费了!
所以去搜一个旧版本然后不升级就好了,先用着吧。
我们偷偷摸摸就好了,别告诉别人
为什么要安装,因为你机子没有!这是一个服务器~!spring boot 可没有义务帮你!
- 配置spring boot
添加依赖
加入灵魂~
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
添加配置文件
,这里网上很多教程分不清spring boot 1.*版本和2.*版本,我这里用的2.*版本,就直接在properties文件里面配置就完成了!
#Redis的服务器地址
spring.redis.host=localhost
#Redis服务器连接端口
spring.redis.port=6379
# Redis数据库索引(默认为 0 )
spring.redis.database= 0
spring.redis.database1= 1
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000
- 基本案例
然后我们新建类,继承CachingConfigureSupport用来配置我们的redis实例。在存入的时候要记得序列化里面的值,否则将会很在可视化里面基本都是乱码,很难看里面到底有什么东西。
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{
/**
* 自定义生成redis-key
*
* @return
*/
@Bean
public KeyGenerator wiselyKeyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
@Autowired
private LettuceConnectionFactory factory;
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate() {
// 关闭共享链接
factory.setShareNativeConnection(false);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
setSerializer(redisTemplate);
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
private void setSerializer(RedisTemplate<String, Object> template) {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//指定序列化类型,如果不指定就是纯json,java解析就是一个LinkHashMap类型的key-value类型
//指定了就会自动的转化为ArrayList和model,app
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key也采用String序列化
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
}
这里就相当于我们将整个redis配置就弄完了,毕竟我们不会再每个代码里面去重复放入取出操作,这里我们用工具类
即可了。
@Component
public class RedisCache {
private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String, Object> gredisTemplate;
@Autowired
@Qualifier("redisTemplateTwo")
private RedisTemplate<String, Object> gredisTemplate_second;
//这个缓存时间最好用一定时间内的随机数,防止缓存雪崩
private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
//返回此缓存将指定键映射到的值,
//*通常指定返回值将被强制转换为的类型。
public <T> T get(Object key, Class<T> type) {
T Return;
RedisTemplate redisTemplate = gredisTemplate;
ValueOperations opsForValue = redisTemplate.opsForValue();
Return = (T)opsForValue.get(key);
return Return;
}
public Object get(String key){
RedisTemplate redisTemplate = gredisTemplate;
if(key == null){
return null;
}else{
return redisTemplate.opsForValue().get(key);
}
}
public void put(String key, Object value) {
RedisTemplate redisTemplate = gredisTemplate;
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
}
//从缓存中删除对应的key
public void evict(String key) {
RedisTemplate redisTemplate = gredisTemplate;
redisTemplate.delete(key);
logger.debug("Remove cached query result from redis");
}
public void evict(String... key) {
RedisTemplate redisTemplate = gredisTemplate;
if(key !=null &&key.length>0){
if(key.length ==1){
redisTemplate.delete(key);
}else{
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
logger.debug("Remove cached query result from redis");
}
//发生更新时清楚缓存
// public void clear() {
// RedisTemplate redisTemplate = gredisTemplate;
// redisTemplate.execute((RedisCallback) connection -> {
// connection.flushDb();
// return null;
// });
// logger.debug("Clear all the cached query result from redis");
// }
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key){
return gredisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
gredisTemplate.expire(key, time, TimeUnit.MINUTES);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 判断key存不存在
* @param key 键
* @return return即存在
*/
public boolean HasKey(String key){
boolean ReturnBoolean = false;
try{
ReturnBoolean = gredisTemplate.hasKey(key);
}catch (Exception e){
e.printStackTrace();
}
return ReturnBoolean;
}
/**
* 哈希 添加
* @param key
* @param hashKey
* @param value
*/
public void hmSet(String key, Object hashKey, Object value){
HashOperations<String, Object, Object> hash = gredisTemplate.opsForHash();
hash.put(key,hashKey,value);
expire(key,EXPIRE_TIME_IN_MINUTES);
}
/**
* 哈希获取数据
* @param key
* @param hashKey
* @return
*/
public Object hmGet(String key, Object hashKey){
HashOperations<String, Object, Object> hash = gredisTemplate.opsForHash();
return hash.get(key,hashKey);
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
gredisTemplate.opsForHash().delete(key, item);
}
/**
* 列表添加
* @param k
* @param v
*/
public void lPush(String k,Object v){
ListOperations<String, Object> list = gredisTemplate.opsForList();
list.rightPush(k,v);
}
/**
* 列表获取
* @param k
* @param l
* @param l1
* @return
*/
public List<Object> lRange(String k, long l, long l1){
ListOperations<String, Object> list = gredisTemplate.opsForList();
return list.range(k,l,l1);
}
public void ChangeDb(int db){
//这个是共享连接的
// RedisConnection redisConnection = gredisTemplate.getConnectionFactory().getConnection();
// DefaultStringRedisConnection stringRedisConnection = new DefaultStringRedisConnection(redisConnection);
// stringRedisConnection.select(db);
//改成lettuceConnectionFactory,
// LettuceConnectionFactory jedisConnectionFactory = (LettuceConnectionFactory) gredisTemplate
// .getConnectionFactory();
// jedisConnectionFactory.setDatabase(db);//这个好像线程不安全噢
// jedisConnectionFactory.resetConnection();
// gredisTemplate.setConnectionFactory(jedisConnectionFactory);
RedisTemplate redisTemplate = gredisTemplate_second;
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set("key2", 1000, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
}
}
当然这些工具类只是一部分,大家也可以自己加油扩展。
- 分库redis
OK,这里我们能看到在配置文件中,我们的配置数据库的索引默认是0.而redis默认有16个db,索引从0-15.如果我们不改变配置,那么存的东西一直都在db0中,对于小系统可能没有什么问题,但是如果系统大了,把所有东西都存在db0中,我总感觉有点不对劲,所以如何将数据存在db1-15中也重要。
在这里,我们先稍微想一想,存入数据操作你会放在主线程吗
,如果所有操作放在主线程,那么可想而知如果用户量开始稍微大一点,你的服务器就负担很大了,所以存入写入基本操作都在线程中
。那我们代码中切换所存入的db,产生的问题就是要加锁,因为redis切库本身是线程不安全的
,那我们换一种思路。产生多个db,redis实例,每个实例对应一个Base不就完了嘛~在我们配置类中,多加一个Bean.
SpringBoot 2.x 之后连接redis驱动默认使用的是lettuce,而非之前的jedis。很多教程现在还是用的jedis,所以坑还是稍微有点多的0 0.不过这里我有一点不明白的是,如果我直接自动注入为什么不可以切换实例的数据库~
先加上依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
加上配置代码
/**
* 第二个redis
* @return
*/
@Bean(name = "redisTemplateTwo")
public RedisTemplate<String, Object> redisTemplateTwo() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
setSerializer(redisTemplate);
redisTemplate.setConnectionFactory(getStringRedisTemplate(1));
return redisTemplate;
}
@Bean
public GenericObjectPoolConfig getPoolConfig(){
// 配置redis连接池
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(maxActive);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setMaxWaitMillis(maxWait);
return poolConfig;
}
private LettuceConnectionFactory getStringRedisTemplate(int database) {
// 构建工厂对象
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
configuration.setPassword(RedisPassword.of(password));
LettucePoolingClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(timeout)).poolConfig(getPoolConfig()).build();
LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration, clientConfiguration);
// 设置使用的redis数据库
factory.setDatabase(database);
// 重新初始化工厂
factory.afterPropertiesSet();
return factory;
}
这里因为在同一个配置类中的Bean有很多,spring会分不清喔,所以要加上Name.然后再setDatabase()函数中去设置自己想去存储的db即可。那工具类就需要定义好不同的实例,这样不同的实例就会存储到不同的db了
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String, Object> gredisTemplate;
@Autowired
@Qualifier("redisTemplateTwo")
private RedisTemplate<String, Object> gredisTemplate_second;
public void put(String key, Object value) {
RedisTemplate redisTemplate = gredisTemplate;
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
}
public void put2(String key, Object value) {
RedisTemplate redisTemplate = gredisTemplate_second;
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
}
结尾
好了,这里先总结到这里。
Spring boot的redis功能主要是能减少很多数据库的压力。所以这个功能点还是很有用而且需要的。
当然redis还需要很多解决的问题,我这里自己记录一下,继续有时间的话可以继续解决这些问题。
缓存倾斜
集群模式
主从机制
哨兵(烧饼)模式
新的一年,新的开始
回来填坑了,打工是真的困又累。干巴爹,打工人!
缓存穿透:
缓存穿透是指查询一个一定不存在的数据
,这样无论查询多少次,缓存就一定不存在,穿透就会不断的打在数据库上,这个时候缓存就失去了意义,在流量大的时候,甚至会让DB挂掉。
基于存在这种可能性,一般会有两种方案:缓存空值和布隆过滤器。
- 缓存空值
缓存空值就是在查询为空时,也将空值缓存下来,因为空值不是业务数据,也会占用缓存控件,所以设置一个较短的过期时间。伪代码如下:
Object nullValue = new Object();
try {
Object valueFromDB = getFromDB(uid); //从数据库中查询数据
if (valueFromDB == null) {
cache.set(uid, nullValue, 10); //如果从数据库中查询到空值,就把空值写入缓存,设置较短的超时时间
} else {
cache.set(uid, valueFromDB, 1000);
}
} catch(Exception e) {
cache.set(uid, nullValue, 10);
}
这种方法比较简单粗暴,但是如果缓存系统有大量的空值,会浪费缓存的存储空间,如果都被这种空值占满了,还会使得一些真正有用的用户信息被剔除。
- 布隆过滤器
“你可以相信布隆~~”
布隆过滤器底层是一个超级大的 bit 数组,默认值都是 0 ,一个元素通过多个hash函数映射到这个 bit 数组上,并且将 0 改成 1。
虽然布隆过滤器存在一定的误判,不在数据库的元素也有可能被判断在布隆过滤器上,但是不在布隆过滤器中的元素一定不存在数据库中
在服务启动的时候,先把数据查询条件,如数据ID映射到布隆过滤器上,看是否存在,如果不存在就返回控制了,存在的才继续查询数据库和缓存。这样就解决了缓存透传的问题了。应用问题我就不写了,不太会,不过可以去看看大佬的。
可以说的是,把大量的数据已经打入布隆过滤器了,这样一些恶意透传就会被过滤掉 了。
大佬的应用
缓存击穿:
缓存击穿(雪崩),雪崩和击穿都一个key或多个key的区别而已。是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大。解决方案如下:
- 缓存失效时间分散
在原有的失效时间基础上增加一个基础值,比如1-5分钟随机,这样每一个缓存的过期时间重复率会降低,集体灾难的几率也会降低。 - 互斥锁
当缓存失效的时候,不立即去数据库读取
,而是先用Redis的setnx先去设置一个互斥锁,操作成功后进行数据库读取。伪代码如下
还有一个热点数据永不过期的,我觉得不太合理,所以就在这里不介绍了。
缓存倾斜
缓存倾斜和大量请求数据库差不多,就是对某个key请求过量,使得缓存服务器挂掉了。只是缓存服务器挂掉了,跟数据库没啥关系
- 客户端解决
把热点key放到客户端存储,设置过期时间,我们解决不了就让客户端去解决吧。 - 服务端解决
把这个key复制出多个子key,value一样,查询的时候用hash取模分配到不同的子key去。分摊压力,