先放几个必要的依赖吧
代码语言:javascript复制<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>
配置文件
代码语言:javascript复制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+8
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'
配置类
代码语言:javascript复制@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);
}
}
代码语言:javascript复制@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
@Primary
public DataSource dataSource() {
return new DruidDataSource();
}
}
实体类
代码语言:javascript复制@Data
@TableName("city")
public class City implements Serializable{
@TableId
private Integer id;
private String name;
}
dao
代码语言:javascript复制@Mapper
public interface CityDao extends BaseMapper<City> {
}
service
代码语言:javascript复制public interface CityService extends IService<City> {
}
代码语言:javascript复制@Service
public class CityServiceImpl extends ServiceImpl<CityDao,City> implements CityService{
}
controller
代码语言:javascript复制@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接口
后端日志为
代码语言:javascript复制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语句,之前的日志如下
代码语言:javascript复制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
现在我们来给缓存设置过期时间
代码语言:javascript复制@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改掉
代码语言:javascript复制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
现在我们要通过标签来完成这个功能,新增一个标签
代码语言:javascript复制@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {
}
新增一个Redis工具类,包含了分布式锁的实现
代码语言:javascript复制@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,用于拦截缓存过期高并发
代码语言:javascript复制/**
* 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("参数错误");
}
}
最后将标签添加到查询方法上面
代码语言:javascript复制@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
代码实现(请注意,该实现依然存在漏洞,但是可以杜绝大部分的恶意访问)
先写一个标签
代码语言:javascript复制@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Bloom {
}
在RedisConfig中添加一个布隆过滤器的Bean
代码语言:javascript复制@Bean
public BloomFilter<String> bloomFilter() {
return BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000000,0.0003);
}
建立一个任务调度器,每一分钟获取一次数据库中的id值写入布隆过滤器中
代码语言:javascript复制@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,则不允许访问数据库,也不允许建立缓存。
代码语言:javascript复制@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写入布隆过滤器中,但由于可能存在不同的集群节点,所以会出现集群各节点的布隆过滤器数据不一致的问题,但每一分钟都会去检索数据库,所以每分钟之后,各个节点的布隆过滤器的数据会再次同步,当然我们会考虑更好的数据一致性处理方式。
代码语言:javascript复制@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
代码语言:javascript复制@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
代码语言:javascript复制@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("参数错误");
}
}
调度器改为只运行一次
代码语言:javascript复制@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
代码语言:javascript复制@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;
}
}