RedisTemplate 操作 Reids
代码示例-添加依赖
pom.xml 中添加 Redis 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
代码示例-StringRedisTemplate 工具类
/**
* @Description: redis工具类
* @Date: 2018/12/3 17:05
* @Author: lxz
*/
@Component
public class RedisComponent {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public boolean hasKey(String key) {
return this.stringRedisTemplate.hasKey(key);
}
/**
* 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
*
* @param key
* @return
*/
public long ttl(String key) {
return stringRedisTemplate.getExpire(key);
}
/**
* 实现命令:expire 设置过期时间,单位秒
*
* @param key
* @return
*/
public void expire(String key, long timeout) {
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:INCR key,增加key一次
*
* @param key
* @return
*/
public long incr(String key, long delta) {
return stringRedisTemplate.opsForValue().increment(key, delta);
}
/**
* 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
*/
public Set<String> keys(String pattern) {
return stringRedisTemplate.keys(pattern);
}
/**
* 实现命令:DEL key,删除一个key
*
* @param key
*/
public void del(String key) {
stringRedisTemplate.delete(key);
}
// String(字符串)
/**
* 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
*
* @param key
* @param value
*/
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*
* @param key
* @param value
* @param timeout (以秒为单位)
*/
public void set(String key, String value, long timeout) {
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:GET key,返回 key所关联的字符串值。
*
* @param key
* @return value
*/
public String get(String key) {
return (String)stringRedisTemplate.opsForValue().get(key);
}
// Hash(哈希表)
/**
* 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
*
* @param key
* @param field
* @param value
*/
public void hset(String key, String field, Object value) {
stringRedisTemplate.opsForHash().put(key, field, value);
}
/**
* 实现命令:HGET key field,返回哈希表 key中给定域 field的值
*
* @param key
* @param field
* @return
*/
public String hget(String key, String field) {
return (String)stringRedisTemplate.opsForHash().get(key, field);
}
/**
* 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
*
* @param key
* @param fields
*/
public void hdel(String key, Object... fields) {
stringRedisTemplate.opsForHash().delete(key, fields);
}
/**
* 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
*
* @param key
* @return
*/
public Map<Object, Object> hgetall(String key) {
return stringRedisTemplate.opsForHash().entries(key);
}
// List(列表)
/**
* 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long lpush(String key, String value) {
return stringRedisTemplate.opsForList().leftPush(key, value);
}
/**
* 实现命令:LPOP key,移除并返回列表 key的头元素。
*
* @param key
* @return 列表key的头元素。
*/
public String lpop(String key) {
return (String)stringRedisTemplate.opsForList().leftPop(key);
}
/**
* 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long rpush(String key, String value) {
return stringRedisTemplate.opsForList().rightPush(key, value);
}
}
StringRedisTemplate 其它的操作方法
- opsForValue:操作 Key Value 类型
- opsForValue:操作 Key Value 类型
- opsForList:操作 List 类型
- opsForHash:操作 Hash 类型
- opsForSet:操作 Set 类型
- opsForZSet:操作 opsForZSet 类型
代码示例-StringRedisTemplate 示例
@Autowired
private RedisComponent redisComponent;
@RequestMapping("/test")
public void test() {
redisComponent.set("stringRedisTemplate-key", "stringRedisTemplate-value");
}
Repository 操作Redis
定义一个数据存储的实体类,添加注解@RedisHash,@Id
- @RedisHash 是 Hash 的名称
- @Id 能够自动生成亦可以赋值
代码示例-数据存储实体类
/**
* @Description: 数据存储的实体类
* @Date: 2018/12/3 18:01
* @Author: lxz
*/
@RedisHash("persons")
public class Person implements Serializable {
@Id
private Long id;
private String firstName;
private String lastName;
// 注意,配置了初始化序列号记得实体类中增加无参构造,否则会报错: springdataredis Could not read JSON: Cannot construct instance of...
public Person() {}
public Person(Long id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "Person{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}';
}
}
代码示例-定义 Repository 接口
/**
* @Description: Repository 接口
* @Date: 2018/12/3 18:04
* @Author: lxz
*/
public interface PersonRepository extends CrudRepository<Person, Long> {}
代码示例-Repository 操作redis
@RequestMapping("/test")
public void test() {
Person person1 = new Person((long)1, "罗 ", "小小");
Person person2 = new Person((long)2, "罗 ", "大大");
Person person3 = new Person((long)3, "罗 ", "超大");
personRepository.save(person1);
// 注意,保存到 Redis 中会变成两部分,一部分是一个set,里面存储的是所有数据的 ID值
Object o1 = personRepository.findById(person1.getId()).get();
// 设置过期时间。注意,设置set不会影响到下面hash值
redisComponent.expire("persons", 20);
System.out.println(o1.toString());
List<Person> personList = new ArrayList<>();
personList.add(person1);
personList.add(person2);
personList.add(person3);
personRepository.saveAll(personList);
// 设置过期时间,设置Id为1的过期时间
redisComponent.expire("persons:1", 20);
Object o2 = personRepository.findById(person2.getId()).get();
System.out.println(o2.toString());
long count = personRepository.count();
System.out.println(count);
List<Long> idList = new ArrayList<>();
idList.add((long)1);
idList.add((long)3);
personRepository.findAllById(idList).forEach(System.out::println);
}
数据图-Repository 操作redis
Spring Cache 利用注解方式实现缓存数据,能够使用SpEL定义缓存的Key及多种条件判断
代码示例-在启动类中添加注解@EnableCaching
@SpringBootApplication
@EnableCaching
public class MyFamilyCacheApplication {
public static void main(String[] args) {
SpringApplication.run(MyFamilyCacheApplication.class, args);
}
}
常用的注解
- @Cacheable:用于查询的时候缓存数据
- @CachePut:用于对数据修改的时候修改缓存中的数据
- @CacheEvict:用于对数据删除的时候清除缓存中的数据
- @Caching 将多种缓存操作分组
- @CacheConfig 类级别的缓存注解,允许共享缓存名称
自定义Redis的配置
- 初始化redisTemplate,设置默认的序列化方式为 Json
- 初始化cacheManager,设置默认的序列化方式为 Json;缓存的默认超时时间
- 配置缓存 Key 的自动生成方式
代码示例-Redis的配置
/*****
* @Description: redis配置文件【注意】配置了初始化序列号记得实体类中增加无参构造,否则会报错: springdataredis Could not read JSON: Cannot construct
* instance of...
* @Date: 2018/12/3 17:08
* @Author: lxz
*/
@Configuration
public class MyRedisConfig extends CachingConfigurerSupport {
private Logger logger = LoggerFactory.getLogger(MyRedisConfig.class);
/**
* @Description 初始化redisTemplate,设置默认的序列化方式为 Json
* @Params [redisConnectionFactory]
* @Return
* @CreateTime 2018/12/5
* @Author lxz
*/
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
return redisTemplate;
}
/**
* @Description 初始化cacheManager,设置默认的序列化方式为 Json;缓存的默认超时时间
* @Params [redisConnectionFactory]
* @Return
* @CreateTime 2018/12/5
* @Author lxz
*/
@Primary
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 缓存配置对象
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.ofMinutes(30L)) // 设置缓存的默认超时时间:30分钟
.disableCachingNullValues() // 如果是空值,不缓存
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())) // 设置key序列化器
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((valueSerializer()))); // 设置value序列化器
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
private RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
/**
* @Description 配置缓存 Key 的自动生成方式,这里是用类名+方法名+参数来生成缓存的 Key
* @Params []
* @Return
* @CreateTime 2018/12/5
* @Author lxz
*/
@Override
@Bean
public KeyGenerator keyGenerator() {
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();
}
};
}
/**
* @Description保证 redis 服务器出现连接等问题的时候不影响程序的正常运行, 使得能够出问题 时不用缓存, 继续执行业务逻辑去查询
* @Params []
* @Return
* @CreateTime 2018/12/6
* @Author lxz
*/
@Override
@Bean
public CacheErrorHandler errorHandler() {
// 异常处理,当Redis发生异常时,打印日志,但是程序正常走
logger.info("初始化 -> [{}]", "Redis CacheErrorHandler");
CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
logger.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
logger.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
logger.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
logger.error("Redis occur handleCacheClearError:", e);
}
};
return cacheErrorHandler;
}
}
代码示例-Dao层
/**
* @Description: spring Cache 缓存数据
* @Date: 2018/12/4 16:56
* @Author: lxz
*/
@Repository
@CacheConfig(cacheNames = "person")
public class PersonDao {
//使用MyRedisConfig中配置的key
//@Cacheable(value = "get", keyGenerator = "keyGenerator")
@Cacheable(value = "get", key = "'key-' + #randomInt")
public Person get(int randomInt) {
Person person = new Person((long)randomInt, "我是姓" + randomInt, "我是名" + randomInt);
return person;
}
}
代码示例-Service层
@Autowired
private PersonDao personDao;
public void test() {
int randomInt = new Random().nextInt(10);
personDao.get(randomInt);
Person person = personDao.get(randomInt);
System.out.println(person.toString());
}
数据图-EnableCaching
自定义缓存工具类
- 代码省略~~~
缓存穿透
什么是缓存穿透?
- 缓存可以说是我们对数据库的一道保护墙,缓存穿透相当于是冲破了我们的保护墙, 每个缓存都有一个缓存的 Key, 当相同的 Key 过来时,我们就直接取缓存中的数据返回给调用方,而不用去查询数据库,如果调用方传来的永远都是我们缓存中不存在的 Key,这样每 次都需要去数据库中查询一次,就会导致数据库压力增大,这样缓存就失去意义了,这就是所谓的缓存穿透。
缓存穿透的危害
- 当大量的请求过来时, 首先会从缓 存巾去寻找数据,当缓存中没有对应的数据时又转到了数据库中去寻找,瞬时数据库的压
力会很大,相当于没有用到缓存,同时还增加了去缓存中查找数据的时间。
解决方案
- 如果查询数据库也为空的时候,把这个 key 缓存起来,这样在下次请求过来的时候就可以走缓存了。 当然这种方案有个弊端,那就是请求过来的 key 必须大部分相同,如果受到攻击的话,每次的 key 肯定不是固定的,只要不固定 key,这个方案就没用。
- 可以用缓存 k町的规则来做一些限制,当然这种只适合特定的使用场景,比如我们 查询商品信息,我们商品存储在 Mongodb 中 , Mongodb 有一个 id 是自动生成的, 它有一定的生成规则,如果是直接根据 id 查询商品,在查询之前我们可以对这个 id 做认证,看是不是符合规范,当不符合的时候就直接返回默认的值,既不用去缓存 中查询,也不用操作数据库了。 这种方案可以解决一部分问题,使用场景比较少。
- 利用布隆过滤器来实现对缓存 key 的检验,需要将所有可能缓存的数据 Hash 到一个 足够大的 BitSet 中,在缓存之前先从布隆过滤器中判断这个 key 是否存在,然后做 对应的操作
缓存雪崩
什么是缓存雪崩?
- 就是在某一时刻,大量缓存同时失效导致所有请求都去查询数据库,导致数 据库压力过大,然后挂掉的情况。 缓存穿透比较严重的时候也会导致缓存雪崩的发生。
缓存雪崩的危害
- 缓存雪崩最乐观的情况是存储层能抗住,但是用户体验会受到影响,数据返回慢,当压力过大时会导致存储层直接挂掉,整个系统都受影响。 对于要做到 99.99% 高可用的产品,是绝对不允许缓存雪崩的发生。
解决方案
- 缓存存储高可用, 比如 Redis 集群,这样就能防止某台 Redis 挂掉之后所有缓存丢失 导致的雪崩问题。
- 缓存失效时间要设计好,不同的数据有不同的有效期,尽量保证不要在同一时间失 效,统一去规划有效期,让失效时间分布均匀即可。
- 对于一些热门数据的持续读取,这种缓存数据也可以采取定时更新的方式来刷新缓 存,避免自动失效。
- 服务限流和接口限流,如果服务和接口都有限流机制,就算缓存全部失效了,但是 请求的总量是有限制的,可以在承受范围之内,这样短时间内系统响应慢点,但不 至于挂掉,影响整个系统。
- 从数据库获取缓存需要的数据时加锁控制 ,本地锁或者分布式锁都可以。 当所有请 求都不能命中缓存,这就是我们之前讲的缓存穿透,这时候要去数据库中查询,如 果同时并发的量大,也是会导致雪崩的发生,我们可以在对数据库查询的地方进行 加锁控制,不要让所有请求都过去,这样可以保证存储服务不挂掉。