项目地址(gitee): https://gitee.com/qinenqi/online
springboot整合redis实现数据缓存

  1. 引入依赖 (依赖放在了common公共服务中)
 <!--    redis    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 修改配置文件、添加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       # 连接池中的最小空闲连接
  1. 启动类添加注解: @EnableCaching
    这个注解是为了触发一个post processor,这会扫描每一个spring bean,查看是否已经存在注解对应的缓存。如果找到了,就会自动创建一个代理拦截方法调用,使用缓存的bean执行处理。小编的理解是:添加了这个注解,就可以使用一些springboot提供的缓存注解了,如:@CacheConfig @Cacheable @CachePut @CacheEvict
  2. 新建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();
    }
}

  1. 新建 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的访问问题

  1. 注解方式实现缓存
    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 注解

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐