springboot整合redis
项目地址(gitee): https://gitee.com/qinenqi/onlinespringboot整合redis实现数据缓存引入依赖 (依赖放在了common公共服务中)<!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>
项目地址(gitee): https://gitee.com/qinenqi/online
springboot整合redis实现数据缓存
- 引入依赖 (依赖放在了common公共服务中)
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 修改配置文件、添加redis的配置(新建 application.yml)
spring:
redis:
open: true # 是否开启redis缓存 true开启 false关闭
database: 0
host: 192.168.8.132
port: 6379
password: # 密码(默认为空)
timeout: 6000ms # 连接超时时长(毫秒)
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
- 启动类添加注解: @EnableCaching
这个注解是为了触发一个post processor,这会扫描每一个spring bean,查看是否已经存在注解对应的缓存。如果找到了,就会自动创建一个代理拦截方法调用,使用缓存的bean执行处理。小编的理解是:添加了这个注解,就可以使用一些springboot提供的缓存注解了,如:@CacheConfig @Cacheable @CachePut @CacheEvict - 新建redis的配置文件: RedisConfig.java
import org.springframework.beans.factory.annotation.Autowired;
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.*;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置
* Redis 数据类型: String(字符串) Hash(哈希) List(列表) Set(集合) zset(sorted set:有序集合)
*
*/
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
/**
* Hash(哈希)
* @param redisTemplate
* @return
*/
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* String(字符串)
* @param redisTemplate
* @return
*/
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
/**
* List(列表)
* @param redisTemplate
* @return
*/
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
/**
* Set(集合)
* @param redisTemplate
* @return
*/
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
/**
* zset(sorted set:有序集合)
* @param redisTemplate
* @return
*/
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
- 新建 CouponServiceImpl.java
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.onlinecoupon.utils.RedisUtils;
import com.example.onlinecoupon.entry.CouponEntity;
import com.example.onlinecoupon.mapper.CouponMapper;
import com.example.onlinecoupon.service.CouponService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 1. 缓存穿透 是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候, 在缓存中找不到对应key的value,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次 无用的查询)。这样请求就绕过缓存直接查数据库。
* 解决方案:缓存空值
* 2. 缓存击穿 是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
* 解决方案:加互斥锁
*
* 3. 缓存雪崩 假如很多的缓存同一时刻失效,此时正好大量的请求进来了,有可能会发生同一时刻都去查询数据库,因此就发生了缓存雪崩问题
* 解决: 设置不同的过期时间
*
* 4. redis分布式锁: 当次服务部署多个时, 结合gateway,则会出现 redis 分部署 访问问题
* 解决: 添加分布式锁
*/
@Service
@Slf4j
public class CouponServiceImpl extends ServiceImpl<CouponMapper, CouponEntity> implements CouponService {
@Autowired
private CouponMapper couponMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ValueOperations<String, String> valueOperations;
@Autowired
private HashOperations<String, String, String> hashOperations;
@Autowired
private RedisUtils redisUtils;
/**
* 新增一条 优惠券信息
* @param couponEntity
*/
public void insertOne(CouponEntity couponEntity){
couponMapper.insert(couponEntity);
}
/**
* 根据id 查询数据
* ValueOperations<String, String>
* @param id
* @return
*/
public CouponEntity selectById01(Long id){
CouponEntity couponEntity = couponMapper.selectById(id);
String key = id.toString();
String value = redisUtils.objectParseString(couponEntity);
if(couponEntity != null){
valueOperations.set(key, value);
}
return couponEntity;
}
/**
* 根据id 查询数据
* HashOperations<String, String, String>
* @param id
* @return
*/
public CouponEntity selectById02(Long id){
String redisKey = "couponEntity";
String key = id.toString();
Boolean aBoolean = hashOperations.hasKey(redisKey, key);
CouponEntity couponEntity = new CouponEntity();
if(!aBoolean){
couponEntity = couponMapper.selectById(id);
if(couponEntity != null){
String value = redisUtils.objectParseString(couponEntity);
hashOperations.put(redisKey, key, value);
}
}else{
String valueRedis = hashOperations.get(redisKey, key);
couponEntity = (CouponEntity) redisUtils.stringParseObject(valueRedis, couponEntity);
}
return couponEntity;
}
/**
* 根据id 查询数据 放入缓存
* ValueOperations<String, String>
* @param id
* @return
*/
public CouponEntity selectById03(Long id){
String key = "couponEntity#" + id.toString();
CouponEntity couponEntity = new CouponEntity();
String valueReids = valueOperations.get(key);
if(StringUtils.isEmpty(valueReids)){
couponEntity = couponMapper.selectById(id);
log.info("我查询了数据库");
if(couponEntity != null){
String value = redisUtils.objectParseString(couponEntity);
valueOperations.set(key, value, 60, TimeUnit.MINUTES);
}
}else{
log.info("我查询了redis缓存");
couponEntity = (CouponEntity) redisUtils.stringParseObject(valueReids, couponEntity);
}
return couponEntity;
}
/**
* 根据id 查询数据
* 缓存击穿(加入某个缓存到期了,此时正好有1000个请求进来了, 就会出现缓存缓存击穿问题,Jmeter测试1000个请求,出现了9个请求查询数据库)
* 解决 加互斥锁, 加上锁之后, jmeter测试1000个请求,只要一个请求了数据库,解决了缓存击穿问题
* ValueOperations<String, String>
* @param id
* @return
*/
public CouponEntity selectById04(Long id){
String key = "couponEntity#" + id.toString();
CouponEntity couponEntity = new CouponEntity();
String valueReids = valueOperations.get(key);
if(StringUtils.isEmpty(valueReids)){
synchronized (this){
valueReids = valueOperations.get(key);
if(StringUtils.isEmpty(valueReids)){
couponEntity = couponMapper.selectById(id);
log.info("我查询了数据库");
// 如果couponEntity是 null, 则存入缓存10分钟,防止此id,一直查询数据库
if(couponEntity == null){
String value = redisUtils.objectParseString(couponEntity);
valueOperations.set(key, value, 10, TimeUnit.MINUTES);
}else{
String value = redisUtils.objectParseString(couponEntity);
valueOperations.set(key, value, 60, TimeUnit.MINUTES);
}
}else{
log.info("获取锁之后,我查询了redis缓存");
couponEntity = (CouponEntity) redisUtils.stringParseObject(valueReids, couponEntity);
}
}
}else{
log.info("我查询了redis缓存");
couponEntity = (CouponEntity) redisUtils.stringParseObject(valueReids, couponEntity);
}
return couponEntity;
}
/**
* 根据id 查询数据
* 缓存雪崩
* 解决 设置不同的过期时间
* ValueOperations<String, String>
* @param id
* @return
*/
public CouponEntity selectById05(Long id){
synchronized (this){
String key = "couponEntity#" + id.toString();
CouponEntity couponEntity = new CouponEntity();
String valueReids = valueOperations.get(key);
if(StringUtils.isEmpty(valueReids)){
valueReids = valueOperations.get(key);
if(StringUtils.isEmpty(valueReids)){
couponEntity = couponMapper.selectById(id);
log.info("我查询了数据库");
// 取 10 -20 中的随机数
int number = (int)(10 + Math.random()*(20 - 10 + 1));
long time = Long.parseLong(number+"");
if(couponEntity == null){ // 如果couponEntity是 null, 则存入缓存10分钟,防止此id,一直查询数据库
String value = redisUtils.objectParseString(couponEntity);
valueOperations.set(key, value, time, TimeUnit.MINUTES);
}else{
String value = redisUtils.objectParseString(couponEntity);
valueOperations.set(key, value, time, TimeUnit.MINUTES);
}
}else{
log.info("获取锁之后,我查询了redis缓存");
couponEntity = (CouponEntity) redisUtils.stringParseObject(valueReids, couponEntity);
}
}else{
log.info("我查询了redis缓存");
couponEntity = (CouponEntity) redisUtils.stringParseObject(valueReids, couponEntity);
}
return couponEntity;
}
}
/**
* 根据id 查询数据
* redis分布式锁
* 解决: 添加分布式锁
* @param id
* @return
*/
public CouponEntity selectById(Long id){
CouponEntity couponEntity = new CouponEntity();
String key = "lock";
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
// 1. 占分布式锁,去redis中占坑 置过期时间和占位必须是原子的
Boolean flag = valueOperations.setIfAbsent(key, uuid, 30, TimeUnit.SECONDS);
if(flag){ // 加锁成功
try { // 加锁成功,执行业务
couponEntity = selectById05(id);
}finally {
String value = valueOperations.get(key);
if(uuid.equals(value)){
redisTemplate.delete(key); // 删除锁
}
// String script = "if redis.call('get', KEYS[1])== ARGV[1] then returnredis.call('del', KEYS[1])else return 0 end";
// //删除锁
// Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
}else{ // 加锁失败,重试, synchronized
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
couponEntity = selectById(id);
}
return couponEntity;
}
}
selectById03() 解决缓存穿透问题
selectById04() 解决缓存击穿问题
selectById05() 解决缓存雪崩问题
selectById() 解决分布式情况下,redis的访问问题
- 注解方式实现缓存
6.1 在启动类上添加注解
@EnableCaching //开启注解式缓存
6.2 新建 CouponAnnotationServiceImpl.java
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.onlinecoupon.entry.CouponEntity;
import com.example.onlinecoupon.mapper.CouponMapper;
import com.example.onlinecoupon.service.CouponAnnotationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class CouponAnnotationServiceImpl extends ServiceImpl<CouponMapper, CouponEntity> implements CouponAnnotationService {
@Autowired
private CouponMapper couponMapper;
/**
* 用注解的方式 新增一条 优惠券信息
* @param couponEntity
*/
@Override
public void insertOne(CouponEntity couponEntity) {
couponMapper.insert(couponEntity);
}
/**
* 用注解的方式 根据id 查询数据
* @param id
* @return
*/
@Override
@Cacheable(cacheNames = "selectById")
public CouponEntity selectById(Long id) {
log.info("缓存注解 我查询了数据库");
CouponEntity couponEntity = couponMapper.selectById(id);
return couponEntity;
}
/**
* 用注解的方式 根据id 更新数据
* @param couponEntity
*/
@CachePut(value="selectById",key = "#couponEntity.id")
public CouponEntity updateCouponEntityById(CouponEntity couponEntity){
int i = couponMapper.updateById(couponEntity);
couponEntity = couponMapper.selectById(couponEntity.getId());
return couponEntity;
}
/**
* 根据id 删除缓存
* @CacheEvict(value="selectById", key = "#id") 删除对应的缓存
* @CacheEvict(value="selectById",allEntries = true,beforeInvocation = true)
* allEntries = true (是否删除该缓存名中所有数据,默认为false)
* beforeInvocation = true(缓存清除是否在方法之前执行,默认false,代表在方法执行之后执行)
* @param id
*/
// @CacheEvict(value="selectById", key = "#id")
@CacheEvict(value="selectById",allEntries = true,beforeInvocation = true)
public void deleteById(Long id){
int i = couponMapper.deleteById(id);
}
}
@Cacheable(cacheNames = “selectById”) 作用是主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 。(凡是你想要使用缓存的地方,加上该注解就能缓存)
cacheNames:一般是方法名 value:一般不写,默认是参数值
@CachePut:@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CachePut(value=“selectById”,key = “#couponEntity.id”) 需要指定key,要不然是不能更新缓存的
详细信息可以参考@CachePut 注解
@CacheEvict:作用是主要针对方法配置,能够根据一定的条件对缓存进行清空
详细信息可以参考 @CacheEvict 注解
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)