前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Springboot整合Redis缓存机制

Springboot整合Redis缓存机制

作者头像
算法之名
发布2020-10-26 16:29:06
1.3K0
发布2020-10-26 16:29:06
举报
文章被收录于专栏:算法之名算法之名算法之名

先放几个必要的依赖吧

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>2.9.0</version>
</dependency>
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.1.2</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>8.0.11</version>
</dependency>
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid</artifactId>
   <version>1.0.29</version>
</dependency>
<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>27.0.1-jre</version>
</dependency>
<dependency>
   <groupId>com.hazelcast</groupId>
   <artifactId>hazelcast-all</artifactId>
   <version>3.10.1</version>
</dependency>

配置文件

spring:
  application:
      name: redis-caching
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/redis_caching?useSSL=FALSE&serverTimezone=GMT%2B8
    username: root
    password: ****
    type: com.alibaba.druid.pool.DruidDataSource
    filters: stat
    maxActive: 20
    initialSize: 1
    maxWait: 60000
    minIdle: 1
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: select 'x'
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    maxOpenPreparedStatements: 20
  redis:
    host: 127.0.0.1
    port: 6379
    password: ****
    timeout: 10000
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1
server:
  port: 8080
#mybatis
mybatis-plus:
  mapper-locations: classpath*:/mybatis-mappers/*
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.guanjian.rediscaching.model
  global-config:
    #数据库相关配置
    db-config:
      #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: INPUT
      logic-delete-value: -1
      logic-not-delete-value: 0
    banner: false
  #原生配置
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    jdbc-type-for-null: 'null'

配置类

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;

    @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 JedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName(host);
        factory.setPort(port);
        factory.setPassword(password);
        return factory;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheManager cacheManager =RedisCacheManager.create(factory);
        // Number of seconds before expiration. Defaults to unlimited (0)
//        cacheManager.setDefaultExpiration(10); //设置key-value超时时间
        return cacheManager;
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate template = new StringRedisTemplate(factory);
        setSerializer(template); //设置序列化工具,这样ReportBean不需要实现Serializable接口
        template.afterPropertiesSet();
        return template;
    }

    private void setSerializer(StringRedisTemplate template) {
        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);
        template.setValueSerializer(jackson2JsonRedisSerializer);
    }
}
@Configuration
public class DruidConfig {
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    @Primary
    public DataSource dataSource() {
        return new DruidDataSource();
    }
}

实体类

@Data
@TableName("city")
public class City implements Serializable{
    @TableId
    private Integer id;
    private String name;
}

dao

@Mapper
public interface CityDao extends BaseMapper<City> {
}

service

public interface CityService extends IService<City> {
}
@Service
public class CityServiceImpl extends ServiceImpl<CityDao,City> implements CityService{
}

controller

@RestController
public class CityController {
    @Autowired
    private CityService cityService;

    @GetMapping("/findbyid")
    @Cacheable(cacheNames = "city_info",key = "#id")
    public City findCityById(@RequestParam("id") int id) {
        return cityService.getById(id);
    }

    @PostMapping("/save")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City saveCity(@RequestBody City city) {
        cityService.save(city);
        return city;
    }

    @GetMapping("/deletebyid")
    @CacheEvict(cacheNames = "city_info",key = "#id")
    public boolean deleteCityById(@RequestParam("id") int id) {
        return cityService.removeById(id);
    }

    @PostMapping("/update")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City updateCity(@RequestBody City city) {
        City cityQuery = new City();
        cityQuery.setId(city.getId());
        QueryWrapper<City> wrapper = new QueryWrapper<>(cityQuery);
        cityService.update(city,wrapper);
        return city;
    }
}

测试

我们在数据库中有一个city的表,其中有一条数据

而redis中任何数据都没有

此时我们查询第一个Rest接口

后端日志为

2020-09-30 06:06:12.919 DEBUG 1321 --- [nio-8080-exec-4] c.g.rediscaching.dao.CityDao.selectById  : ==>  Preparing: SELECT id,name FROM city WHERE id=? 
2020-09-30 06:06:12.920 DEBUG 1321 --- [nio-8080-exec-4] c.g.rediscaching.dao.CityDao.selectById  : ==> Parameters: 1(Integer)
2020-09-30 06:06:12.945 DEBUG 1321 --- [nio-8080-exec-4] c.g.rediscaching.dao.CityDao.selectById  : <==      Total: 1

此时我们查询redis中如下

可见我们在没有写任何redis代码的同时,就将数据存储进了redis

此时我们再此查询

则后端日志没有打印SQL语句,说明再次查询是从redis中获取而不是mysql中获取的。

此时我们测试第二个Rest接口

此时数据库中多出一条数据

我们再来看redis中的数据

查询第二条数据可得

现在我们来删除第二条数据

数据库中第二条数据被删除

同时我们在redis中可以看到第二条数据也被删除了

现在我们来修改第一条数据

数据库中同时更新了数据

redis中的数据依然存在

此时我们重新查询第一条数据

后端日志中也没有相应的查询SQL语句,之前的日志如下

2020-09-30 06:32:57.729 DEBUG 1349 --- [nio-8080-exec-3] c.g.rediscaching.dao.CityDao.insert      : ==>  Preparing: INSERT INTO city ( id, name ) VALUES ( ?, ? ) 
2020-09-30 06:32:57.730 DEBUG 1349 --- [nio-8080-exec-3] c.g.rediscaching.dao.CityDao.insert      : ==> Parameters: 2(Integer), 武汉(String)
2020-09-30 06:32:57.735 DEBUG 1349 --- [nio-8080-exec-3] c.g.rediscaching.dao.CityDao.insert      : <==    Updates: 1
2020-09-30 06:38:04.042 DEBUG 1349 --- [io-8080-exec-10] c.g.rediscaching.dao.CityDao.deleteById  : ==>  Preparing: DELETE FROM city WHERE id=? 
2020-09-30 06:38:04.043 DEBUG 1349 --- [io-8080-exec-10] c.g.rediscaching.dao.CityDao.deleteById  : ==> Parameters: 2(Integer)
2020-09-30 06:38:04.047 DEBUG 1349 --- [io-8080-exec-10] c.g.rediscaching.dao.CityDao.deleteById  : <==    Updates: 1
2020-09-30 06:40:09.723 DEBUG 1349 --- [nio-8080-exec-3] c.g.rediscaching.dao.CityDao.update      : ==>  Preparing: UPDATE city SET name=? WHERE id=? 
2020-09-30 06:40:09.728 DEBUG 1349 --- [nio-8080-exec-3] c.g.rediscaching.dao.CityDao.update      : ==> Parameters: 北京(String), 1(Integer)
2020-09-30 06:40:09.733 DEBUG 1349 --- [nio-8080-exec-3] c.g.rediscaching.dao.CityDao.update      : <==    Updates: 1

现在我们来给缓存设置过期时间

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;

    @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 JedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName(host);
        factory.setPort(port);
        factory.setPassword(password);
        return factory;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        Random random = new Random();
        return new RedisCacheManager(
                RedisCacheWriter.nonLockingRedisCacheWriter(factory),
                //未设置过期策略的在20分钟内过期
                getRedisCacheConfigurationWithTtl(1140 + random.nextInt(60)),
                // 指定 key 策略
                getRedisCacheConfigurationMap()
        );
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate template = new StringRedisTemplate(factory);
        setSerializer(template); //设置序列化工具,这样ReportBean不需要实现Serializable接口
        template.afterPropertiesSet();
        return template;
    }

    private void setSerializer(StringRedisTemplate template) {
        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);
        template.setValueSerializer(jackson2JsonRedisSerializer);
    }

    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new ConcurrentHashMap<>();
        Random random = new Random();
        redisCacheConfigurationMap.put("city_info", getRedisCacheConfigurationWithTtl(540 + random.nextInt(60)));
        return redisCacheConfigurationMap;
    }

    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
        Jackson2JsonRedisSerializer<Object> 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.serializeValuesWith(
                RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(jackson2JsonRedisSerializer)
        ).entryTtl(Duration.ofSeconds(seconds));

        return redisCacheConfiguration;
    }
}

通过查看redis的键的过期时间,我们可以看到

它是用的指定键的过期时间

此时我们调整RedisConfig的内容,将指定的city_info改掉

private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
    Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new ConcurrentHashMap<>();
    Random random = new Random();
    redisCacheConfigurationMap.put("abcd", getRedisCacheConfigurationWithTtl(540 + random.nextInt(60)));
    return redisCacheConfigurationMap;
}

此时我们会使用默认的20分钟过期时间

此时我们可以看到,它使用的就是默认所有键都相同的20分钟过期时间。

现在我们来增加防止缓存高并发的功能

缓存高并发的一般性原则可以参考建立缓存,防高并发代码demo

现在我们要通过标签来完成这个功能,新增一个标签

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {
}

新增一个Redis工具类,包含了分布式锁的实现

@Component
public class RedisUtils {
    @Autowired
    private RedisTemplate redisTemplate;
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /**
     * 写入缓存
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    /**
     * 写入缓存设置时效时间
     */
    public boolean set(final String key, Object value, Long expireTime , TimeUnit timeUnit) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, timeUnit);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入缓存设置时效时间,仅第一次有效
     * @param key
     * @param value
     * @param expireTime
     * @param timeUnit
     * @return
     */
    public boolean setIfAbsent(final String key, Object value, Long expireTime , TimeUnit timeUnit) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.setIfAbsent(key,value,expireTime,timeUnit);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
    /**
     * 批量删除对应的value
     */
    public void remove(final String... keys) {
        for (String key : keys) {
            remove(key);
        }
    }
    /**
     * 批量删除key
     */
    public void removePattern(final String pattern) {
        Set<Serializable> keys = redisTemplate.keys(pattern);
        if (keys.size() > 0){
            redisTemplate.delete(keys);
        }
    }
    /**
     * 删除对应的value
     */
    public void remove(final String key) {
        if (exists(key)) {
            redisTemplate.delete(key);
        }
    }
    /**
     * 判断缓存中是否有对应的value
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }
    /**
     * 读取缓存
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }
    /**
     * 哈希 添加
     */
    public void hmSet(String key, Object hashKey, Object value){
        HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
        hash.put(key,hashKey,value);
    }
    /**
     * 哈希获取数据
     */
    public Object hmGet(String key, Object hashKey){
        HashOperations<String, Object, Object>  hash = redisTemplate.opsForHash();
        return hash.get(key,hashKey);
    }
    /**
     * 列表添加
     */
    public void lPush(String k,Object v){
        ListOperations<String, Object> list = redisTemplate.opsForList();
        list.rightPush(k,v);
    }
    /**
     * 列表获取
     */
    public List<Object> lRange(String k, long l, long l1){
        ListOperations<String, Object> list = redisTemplate.opsForList();
        return list.range(k,l,l1);
    }
    /**
     * 集合添加
     */
    public void add(String key,Object value){
        SetOperations<String, Object> set = redisTemplate.opsForSet();
        set.add(key,value);
    }
    /**
     * 集合获取
     */
    public Set<Object> setMembers(String key){
        SetOperations<String, Object> set = redisTemplate.opsForSet();
        return set.members(key);
    }
    /**
     * 有序集合添加
     */
    public void zAdd(String key,Object value,double scoure){
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        zset.add(key,value,scoure);
    }
    /**
     * 有序集合获取
     */
    public Set<Object> rangeByScore(String key,double scoure,double scoure1){
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        return zset.rangeByScore(key, scoure, scoure1);
    }

    /**
     * 尝试获取锁 立即返回
     *
     * @param key
     * @param value
     * @param timeout
     * @return
     */
    public boolean lock(String key, String value, long timeout) {
        return setIfAbsent(key,value,timeout,TimeUnit.MILLISECONDS);
    }

    /**
     * 以阻塞方式的获取锁
     *
     * @param key
     * @param value
     * @param timeout
     * @return
     */
    public boolean lockBlock(String key, String value, long timeout) {
        long start = System.currentTimeMillis();
        while (true) {
            //检测是否超时
            if (System.currentTimeMillis() - start > timeout) {
                return false;
            }
            //执行set命令
            //1
            Boolean absent = setIfAbsent(key,value,timeout,TimeUnit.MILLISECONDS);
            //其实没必要判NULL,这里是为了程序的严谨而加的逻辑
            if (absent == null) {
                return false;
            }
            //是否成功获取锁
            if (absent) {
                return true;
            }
        }
    }

    /**
     * 解锁
     * @param key
     * @param value
     * @return
     */
    public boolean unlock(String key, String value) {
        RedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_LUA,Long.class);
        Long result = (Long) redisTemplate.execute(redisScript,Collections.singletonList(key),value);
        //返回最终结果
        return RELEASE_SUCCESS.equals(result);
    }
}

实现一个AOP,用于拦截缓存过期高并发

/**
 * aop实现拦截缓存过期时的高并发
 *
 * @author 关键
 */
@Aspect
@Component
public class LockAop {
    @Autowired
    private RedisUtils redisUtils;

    @Around(value = "@annotation(com.guanjian.rediscaching.annotation.Lock)")
    public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Cacheable cacheableAnnotion = methodSignature.getMethod().getDeclaredAnnotation(Cacheable.class);
        String[] cacheNames = cacheableAnnotion.cacheNames();
        String idKey = cacheableAnnotion.key();
        String[] paramNames = methodSignature.getParameterNames();
        if (paramNames != null && paramNames.length > 0) {
            Object[] args = joinPoint.getArgs();
            Map<String,Object> params = new HashMap<>();
            for (int i = 0; i < paramNames.length; i++) {
                params.put(paramNames[i],args[i]);
            }
            idKey = idKey.substring(1);
            String key = cacheNames[0] + "::" + params.get(idKey).toString();
            if (!redisUtils.exists(key)) {
                if (redisUtils.lock(key + "lock","id" + params.get(idKey).toString(),3000)) {
                    Object res = joinPoint.proceed();
                    try {
                        return res;
                    } finally {
                        redisUtils.unlock(key + "lock","id" + params.get(idKey).toString());
                    }
                }else {
                    LocalDateTime now = LocalDateTime.now();
                    Future<Object> future = CompletableFuture.supplyAsync(() -> {
                        while (true) {
                            if (redisUtils.exists(key)) {
                                return redisUtils.get(key);
                            }
                            if (LocalDateTime.now().isAfter(now.plusSeconds(3))) {
                                return null;
                            }
                        }
                    });
                    try {
                        return future.get(3000,TimeUnit.MILLISECONDS);
                    } catch (Exception e) {
                        e.printStackTrace();
                        return null;
                    }
                }
            }else {
                return redisUtils.get(key);
            }
        }
        throw new IllegalArgumentException("参数错误");
    }
}

最后将标签添加到查询方法上面

@RestController
public class CityController {
    @Autowired
    private CityService cityService;

    @GetMapping("/findbyid")
    @Cacheable(cacheNames = "city_info",key = "#id")
    @Lock
    public City findCityById(@RequestParam("id") int id) {
        return cityService.getById(id);
    }

    @PostMapping("/save")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City saveCity(@RequestBody City city) {
        cityService.save(city);
        return city;
    }

    @GetMapping("/deletebyid")
    @CacheEvict(cacheNames = "city_info",key = "#id")
    public boolean deleteCityById(@RequestParam("id") int id) {
        return cityService.removeById(id);
    }

    @PostMapping("/update")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City updateCity(@RequestBody City city) {
        City cityQuery = new City();
        cityQuery.setId(city.getId());
        QueryWrapper<City> wrapper = new QueryWrapper<>(cityQuery);
        cityService.update(city,wrapper);
        return city;
    }
}

现在我们来增加布隆过滤器来防治恶意无效访问

在该缓存系统中存在一个问题,那就是当用户查询了数据库中不存在的id的时候,缓存系统依然会将空值添加到redis中。如果有恶意用户通过工具不断使用不存在的id进行访问的时候,一方面会对数据库造成巨大的访问压力,另一方面可能会把redis内存撑破。

比方说我们访问一个不存在的id=5的时候

Redis依然会被写入,查出来是NullValue

代码实现(请注意,该实现依然存在漏洞,但是可以杜绝大部分的恶意访问)

先写一个标签

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Bloom {
}

在RedisConfig中添加一个布隆过滤器的Bean

@Bean
public BloomFilter<String> bloomFilter() {
    return BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000000,0.0003);
}

建立一个任务调度器,每一分钟获取一次数据库中的id值写入布隆过滤器中

@Component
public class BloomFilterScheduler {
    private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
    @Autowired
    private BloomFilter<String> bloomFilter;
    @Autowired
    private CityService cityService;

    private void getAllCityForBloomFilter() {
        List<City> list = cityService.list();
        list.parallelStream().forEach(city -> bloomFilter.put("city_info::" + city.getId()));
    }

    private ScheduledFuture scheduleTask(Runnable task) {
        return scheduledExecutorService.scheduleAtFixedRate(task,0,1, TimeUnit.MINUTES);
    }

    @PostConstruct
    public ScheduledFuture scheduleChange() {
        return scheduleTask(this::getAllCityForBloomFilter);
    }
}

再编写一个布隆过滤器的AOP拦截,如果布隆过滤器中不存在该key,则不允许访问数据库,也不允许建立缓存。

@Aspect
@Component
public class BloomFilterAop {
    @Autowired
    private BloomFilter<String> bloomFilter;

    @Around(value = "@annotation(com.guanjian.rediscaching.annotation.Bloom)")
    public Object bloom(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Cacheable cacheableAnnotion = methodSignature.getMethod().getDeclaredAnnotation(Cacheable.class);
        String[] cacheNames = cacheableAnnotion.cacheNames();
        String idKey = cacheableAnnotion.key();
        String[] paramNames = methodSignature.getParameterNames();
        if (paramNames != null && paramNames.length > 0) {
            Object[] args = joinPoint.getArgs();
            Map<String, Object> params = new HashMap<>();
            for (int i = 0; i < paramNames.length; i++) {
                params.put(paramNames[i], args[i]);
            }
            idKey = idKey.substring(1);
            String key = cacheNames[0] + "::" + params.get(idKey).toString();
            if (!bloomFilter.mightContain(key)) {
                throw new RuntimeException("系统不存在该key");
            }else {
                return joinPoint.proceed();
            }
        }
        throw new IllegalArgumentException("参数错误");
    }
}

最后是Controller,打上该标签。这里需要注意的是,当我们查询出来的对象为null的时候抛出异常,这样可以避免在Redis中建立缓存。这里保存对象的时候会把该对象的id写入布隆过滤器中,但由于可能存在不同的集群节点,所以会出现集群各节点的布隆过滤器数据不一致的问题,但每一分钟都会去检索数据库,所以每分钟之后,各个节点的布隆过滤器的数据会再次同步,当然我们会考虑更好的数据一致性处理方式。

@RestController
public class CityController {
    @Autowired
    private CityService cityService;
    @Autowired
    private BloomFilter<String> bloomFilter;

    @GetMapping("/findbyid")
    @Cacheable(cacheNames = "city_info",key = "#id")
    @Lock
    @Bloom
    public City findCityById(@RequestParam("id") int id) {
        City city = cityService.getById(id);
        if (city != null) {
            return city;
        }
        throw new IllegalArgumentException("id不存在");
    }

    @PostMapping("/save")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City saveCity(@RequestBody City city) {
        if (cityService.save(city)) {
            bloomFilter.put("city_info::" + city.getId());
            return city;
        }
        throw new IllegalArgumentException("保存失败");
    }

    @GetMapping("/deletebyid")
    @CacheEvict(cacheNames = "city_info",key = "#id")
    public boolean deleteCityById(@RequestParam("id") int id) {
        return cityService.removeById(id);
    }

    @PostMapping("/update")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City updateCity(@RequestBody City city) {
        City cityQuery = new City();
        cityQuery.setId(city.getId());
        QueryWrapper<City> wrapper = new QueryWrapper<>(cityQuery);
        cityService.update(city,wrapper);
        return city;
    }
}

添加布隆过滤器的分布式节点的同步模式

增加Hazelcast的配置,有关Hazelcast的内容,请参考JVM内存级分布式缓存Hazelcast

@Configuration
public class HazelcastConfiguration {
    @Bean
    public Config hazelCastConfig() {
        Config config = new Config();
        config.setInstanceName("hazelcast-instance").addMapConfig(
                new MapConfig().setName("configuration").setMaxSizeConfig(new MaxSizeConfig(200,
                        MaxSizeConfig.MaxSizePolicy.FREE_HEAP_SIZE)).setEvictionPolicy(EvictionPolicy.LFU)
                        .setTimeToLiveSeconds(-1));
        return config;
    }

    @Bean
    public HazelcastInstance instance() {
        return Hazelcast.newHazelcastInstance();
    }

    @Bean
    public Map<Integer,BloomFilter<String>> bloomFilters() {
        Map<Integer,BloomFilter<String>> blooms = instance().getMap("bloom");
        return blooms;
    }
}

修改布隆过滤器AOP

@Aspect
@Component
public class BloomFilterAop {
    @Autowired
    private Map<Integer,BloomFilter<String>> bloomFilters;

    @Around(value = "@annotation(com.guanjian.rediscaching.annotation.Bloom)")
    public Object bloom(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Cacheable cacheableAnnotion = methodSignature.getMethod().getDeclaredAnnotation(Cacheable.class);
        String[] cacheNames = cacheableAnnotion.cacheNames();
        String idKey = cacheableAnnotion.key();
        String[] paramNames = methodSignature.getParameterNames();
        if (paramNames != null && paramNames.length > 0) {
            Object[] args = joinPoint.getArgs();
            Map<String, Object> params = new HashMap<>();
            for (int i = 0; i < paramNames.length; i++) {
                params.put(paramNames[i], args[i]);
            }
            idKey = idKey.substring(1);
            String key = cacheNames[0] + "::" + params.get(idKey).toString();
            if (!bloomFilters.get(1).mightContain(key)) {
                throw new RuntimeException("系统不存在该key");
            }else {
                return joinPoint.proceed();
            }
        }
        throw new IllegalArgumentException("参数错误");
    }
}

调度器改为只运行一次

@Component
public class BloomFilterScheduler {
    @Autowired
    private BloomFilter<String> bloomFilter;
    @Autowired
    private CityService cityService;
    @Autowired
    private Map<Integer,BloomFilter<String>> bloomFilters;

    @PostConstruct
    public void getAllCityForBloomFilter() {
        List<City> list = cityService.list();
        list.parallelStream().forEach(city -> bloomFilter.put("city_info::" + city.getId()));
        bloomFilters.put(1,bloomFilter);
    }
}

最后是Controller

@RestController
public class CityController {
    @Autowired
    private CityService cityService;
    @Autowired
    private BloomFilter<String> bloomFilter;
    @Autowired
    private Map<Integer,BloomFilter<String>> bloomFilters;

    @GetMapping("/findbyid")
    @Cacheable(cacheNames = "city_info",key = "#id")
    @Lock
    @Bloom
    public City findCityById(@RequestParam("id") int id) {
        City city = cityService.getById(id);
        if (city != null) {
            return city;
        }
        throw new IllegalArgumentException("id不存在");
    }

    @PostMapping("/save")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City saveCity(@RequestBody City city) {
        if (cityService.save(city)) {
            CompletableFuture.runAsync(() -> {
                bloomFilter.put("city_info::" + city.getId());
                bloomFilters.put(1,bloomFilter);
            });
            return city;
        }
        throw new IllegalArgumentException("保存失败");
    }

    @GetMapping("/deletebyid")
    @CacheEvict(cacheNames = "city_info",key = "#id")
    public boolean deleteCityById(@RequestParam("id") int id) {
        return cityService.removeById(id);
    }

    @PostMapping("/update")
    @CachePut(cacheNames = "city_info",key = "#city.id")
    public City updateCity(@RequestBody City city) {
        City cityQuery = new City();
        cityQuery.setId(city.getId());
        QueryWrapper<City> wrapper = new QueryWrapper<>(cityQuery);
        cityService.update(city,wrapper);
        return city;
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档