diff --git a/README.md b/README.md index 98302c2a2a71e8c3933998627ef7e4bf90cb8895..267cedd503613f12f7282624ae08c472072cd8fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ![J2Cache](docs/J2Cache.png) + # J2Cache —— 基于内存和 Redis 的两级 Java 缓存框架 专用QQ群: `379110351` @@ -187,6 +188,7 @@ channel.close(); * https://gitee.com/tywo45/t-io * https://gitee.com/nutz/nutzboot * https://gitee.com/qingfengtaizi/wxmp +* https://gitee.com/xchao/j-im 更多项目收集中,如果你的项目用了,请告诉我 diff --git a/modules/spring-boot-starter/readme.md b/modules/spring-boot-starter/readme.md index 74cc2688e3a98e81391d3f1f1c1798d98e2b3fcd..1dbe5981d2a4354be34d23a8f4b4754fb9653050 100644 --- a/modules/spring-boot-starter/readme.md +++ b/modules/spring-boot-starter/readme.md @@ -10,9 +10,15 @@ j2cache.config-location=/j2cache-${spring.profiles.active}.properties 如下两项配置在application.properties,可以开启对spring cahce的支持 ``` j2cache.open-spring-cache=true +spring.cache.type=GENERIC ``` +如下配置在application.properties,可以选择缓存清除的模式 +* 缓存清除模式 +* active:主动清除,二级缓存过期主动通知各节点清除,优点在于所有节点可以同时收到缓存清除 +* passive:被动清除,一级缓存过期进行通知各节点清除一二级缓存 +* blend:两种模式一起运作,对于各个节点缓存准确性以及及时性要求高的可以使用(推荐使用前面两种模式中一种) ``` -spring.cache.type=none +j2cache.cache-clean-mode=passive ``` 在j2cache.properties中配置,可以使用springRedis进行广播通知缓失效 ``` @@ -21,5 +27,7 @@ j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPol 在j2cache.properties中配置,使用springRedis替换二级缓存 ``` j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider +j2cache.L2.config_section = redis ``` + diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java b/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java index a65b01387a15f7179fcb4b697ab4d3f9f786b3c7..a2ffd850b068fa20e8e4fc45495f93ae0c42c092 100644 --- a/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java @@ -29,11 +29,16 @@ public class J2CacheAutoConfiguration { } @Bean - @DependsOn("springUtil") - public CacheChannel cacheChannel() throws IOException { + public net.oschina.j2cache.J2CacheConfig j2CacheConfig() throws IOException{ net.oschina.j2cache.J2CacheConfig cacheConfig = new net.oschina.j2cache.J2CacheConfig(); cacheConfig = net.oschina.j2cache.J2CacheConfig.initFromConfig(j2CacheConfig.getConfigLocation()); - J2CacheBuilder builder = J2CacheBuilder.init(cacheConfig); + return cacheConfig; + } + + @Bean + @DependsOn({"springUtil","j2CacheConfig"}) + public CacheChannel cacheChannel(net.oschina.j2cache.J2CacheConfig j2CacheConfig) throws IOException { + J2CacheBuilder builder = J2CacheBuilder.init(j2CacheConfig); return builder.getChannel(); } diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java b/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java index 3c770684d9d1c5c6eb2b9edc8920428b64e3f3dd..a3a9d14b0f1ff7c97c46c6a26269066040b48cac 100644 --- a/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java @@ -11,9 +11,17 @@ public class J2CacheConfig { private String configLocation = "/j2cache.properties"; /** - * 是否开启spring cache缓存,注意:开启后需要添加spring.cache.type=none,将缓存类型设置为none + * 是否开启spring cache缓存,注意:开启后需要添加spring.cache.type=GENERIC,将缓存类型设置为GENERIC */ private Boolean openSpringCache = false; + + /** + * 缓存清除模式, + * active:主动清除,二级缓存过期主动通知各节点清除,优点在于所有节点可以同时收到缓存清除 + * passive:被动清除,一级缓存过期进行通知各节点清除一二级缓存, + * blend:两种模式一起运作,对于各个节点缓存准确以及及时性要求高的可以使用,正常用前两种模式中一个就可 + */ + private String cacheCleanMode = "passive"; public String getConfigLocation() { return configLocation; @@ -30,4 +38,12 @@ public class J2CacheConfig { public void setOpenSpringCache(Boolean openSpringCache) { this.openSpringCache = openSpringCache; } + + public String getCacheCleanMode() { + return cacheCleanMode; + } + + public void setCacheCleanMode(String cacheCleanMode) { + this.cacheCleanMode = cacheCleanMode; + } } diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java b/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java index 22fec12a1ecb88a4249137261e38ce7eefb14814..99c535676efed427c866df2e0d6ca396100c8caf 100644 --- a/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java @@ -1,45 +1,138 @@ package net.oschina.j2cache.autoconfigure; import java.io.Serializable; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.serializer.StringRedisSerializer; import net.oschina.j2cache.cache.support.util.J2CacheSerializer; +import net.oschina.j2cache.redis.RedisUtils; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisShardInfo; +import redis.clients.jedis.exceptions.JedisConnectionException; /** * 对spring redis支持的配置入口 + * * @author zhangsaizz * */ @Configuration -@AutoConfigureAfter({RedisAutoConfiguration.class}) -@AutoConfigureBefore({J2CacheAutoConfiguration.class}) +@AutoConfigureAfter({ RedisAutoConfiguration.class }) +@AutoConfigureBefore({ J2CacheAutoConfiguration.class }) public class J2CacheSpringRedisAutoConfiguration { + private final static int MAX_ATTEMPTS = 3; + + private final static int CONNECT_TIMEOUT = 5000; + + private static final Logger log = LoggerFactory.getLogger(J2CacheSpringRedisAutoConfiguration.class); + + @Bean("j2CahceRedisConnectionFactory") + @ConditionalOnMissingBean(name = "j2CahceRedisConnectionFactory") + public JedisConnectionFactory j2CahceRedisConnectionFactory(net.oschina.j2cache.J2CacheConfig j2CacheConfig) { + Properties l2CacheProperties = j2CacheConfig.getL2CacheProperties(); + String hosts = l2CacheProperties.getProperty("hosts"); + String mode = l2CacheProperties.getProperty("mode"); + String clusterName = l2CacheProperties.getProperty("cluster_name"); + String password = l2CacheProperties.getProperty("password"); + int database = Integer.parseInt(l2CacheProperties.getProperty("database")); + JedisConnectionFactory connectionFactory = null; + JedisPoolConfig config = RedisUtils.newPoolConfig(l2CacheProperties, null); + List nodes = new ArrayList<>(); + for (String node : hosts.split(",")) { + String[] s = node.split(":"); + String host = s[0]; + int port = (s.length > 1) ? Integer.parseInt(s[1]) : 6379; + RedisNode n = new RedisNode(host, port); + nodes.add(n); + } + + switch (mode) { + case "sentinel": + RedisSentinelConfiguration sentinel = new RedisSentinelConfiguration(); + sentinel.setMaster(clusterName); + sentinel.setSentinels(nodes); + connectionFactory = new JedisConnectionFactory(sentinel, config); + connectionFactory.setPassword(password); + connectionFactory.setDatabase(database); + break; + case "cluster": + RedisClusterConfiguration cluster = new RedisClusterConfiguration(); + cluster.setClusterNodes(nodes); + cluster.setMaxRedirects(MAX_ATTEMPTS); + connectionFactory = new JedisConnectionFactory(cluster, config); + connectionFactory.setPassword(password); + connectionFactory.setDatabase(database); + break; + case "sharded": + try { + for (String node : hosts.split(",")) { + connectionFactory = new JedisConnectionFactory(new JedisShardInfo(new URI(node))); + connectionFactory.setDatabase(database); + connectionFactory.setPoolConfig(config); + break; + } + } catch (URISyntaxException e) { + throw new JedisConnectionException(e); + } + break; + default: + for (RedisNode node : nodes) { + String host = node.getHost(); + int port = node.getPort(); + connectionFactory = new JedisConnectionFactory(config); + connectionFactory.setPassword(password); + connectionFactory.setHostName(host); + connectionFactory.setPort(port); + connectionFactory.setDatabase(database); + break; + } + if (!"single".equalsIgnoreCase(mode)) + log.warn("Redis mode [" + mode + "] not defined. Using 'single'."); + break; + } + return connectionFactory; + + } + @Bean("j2CacheRedisTemplate") - @ConditionalOnBean(RedisConnectionFactory.class) - public RedisTemplate j2CacheRedisTemplate(RedisConnectionFactory connectionFactory) { + @ConditionalOnBean(name = "j2CahceRedisConnectionFactory") + public RedisTemplate j2CacheRedisTemplate( + JedisConnectionFactory j2CahceRedisConnectionFactory) { RedisTemplate template = new RedisTemplate(); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setDefaultSerializer(new J2CacheSerializer()); - template.setConnectionFactory(connectionFactory); + template.setConnectionFactory(j2CahceRedisConnectionFactory); return template; } - @Bean("j2CacheRedisMessageListenerContainer") - RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){ - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(connectionFactory); - return container; - } + @Bean("j2CacheRedisMessageListenerContainer") + @ConditionalOnBean(name = "j2CahceRedisConnectionFactory") + RedisMessageListenerContainer container(JedisConnectionFactory j2CahceRedisConnectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(j2CahceRedisConnectionFactory); + return container; + } + } diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/ConfigureNotifyKeyspaceEventsAction.java b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/ConfigureNotifyKeyspaceEventsAction.java new file mode 100644 index 0000000000000000000000000000000000000000..bfb7b8054e164a4140191ddf2211682fbd45a04b --- /dev/null +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/ConfigureNotifyKeyspaceEventsAction.java @@ -0,0 +1,49 @@ +package net.oschina.j2cache.cache.support.redis; + +import java.util.List; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.RedisConnection; + +/** + * 设置redis键值回调 + * @param connection + */ +public class ConfigureNotifyKeyspaceEventsAction { + + static final String CONFIG_NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events"; + + + public void config(RedisConnection connection) { + String notifyOptions = getNotifyOptions(connection); + String customizedNotifyOptions = notifyOptions; + if (!customizedNotifyOptions.contains("E")) { + customizedNotifyOptions += "E"; + } + boolean A = customizedNotifyOptions.contains("A"); + if (!(A || customizedNotifyOptions.contains("g"))) { + customizedNotifyOptions += "g"; + } + if (!(A || customizedNotifyOptions.contains("x"))) { + customizedNotifyOptions += "x"; + } + if (!notifyOptions.equals(customizedNotifyOptions)) { + connection.setConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS, customizedNotifyOptions); + } + } + + private String getNotifyOptions(RedisConnection connection) { + try { + List config = connection.getConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS); + if (config.size() < 2) { + return ""; + } + return config.get(1); + } + catch (InvalidDataAccessApiUsageException e) { + throw new IllegalStateException( + "Unable to configure Redis to keyspace notifications. See http://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent", + e); + } + } +} diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisActiveMessageListener.java b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisActiveMessageListener.java new file mode 100644 index 0000000000000000000000000000000000000000..1faf99d00812acaf063f0f3a66f9fc31f7cbcdc0 --- /dev/null +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisActiveMessageListener.java @@ -0,0 +1,45 @@ +package net.oschina.j2cache.cache.support.redis; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import net.oschina.j2cache.ClusterPolicy; + +/** + * 监听二缓key失效,主动清除本地缓存 + * + * @author zhangsaizz + * + */ +public class SpringRedisActiveMessageListener implements MessageListener { + + private static Logger logger = LoggerFactory.getLogger(SpringRedisActiveMessageListener.class); + + private ClusterPolicy clusterPolicy; + + private String namespace; + + SpringRedisActiveMessageListener(ClusterPolicy clusterPolicy, String namespace) { + this.clusterPolicy = clusterPolicy; + this.namespace = namespace; + } + + @Override + public void onMessage(Message message, byte[] pattern) { + String key = message.toString(); + if (key == null) { + return; + } + if (key.startsWith(namespace + ":")) { + String[] k = key.replaceFirst(namespace + ":", "").split(":", 2); + if(k.length != 2) { + return; + } + clusterPolicy.evict(k[0], k[1]); + } + + } + +} diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java index 415b121ece82a23772f34544b35497b0a05d753e..b91d217afdfc4d162ae5ce0794ffe2ea52f25708 100644 --- a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java @@ -9,7 +9,6 @@ import java.util.Set; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.util.StringUtils; import net.oschina.j2cache.Level2Cache; @@ -21,7 +20,7 @@ import net.oschina.j2cache.Level2Cache; */ public class SpringRedisCache implements Level2Cache { - private String namespace = "j2cache~key"; + private String namespace; private String region; @@ -31,9 +30,7 @@ public class SpringRedisCache implements Level2Cache { if (region == null || region.isEmpty()) { region = "_"; // 缺省region } - if(!StringUtils.isEmpty(namespace)) { - this.namespace = namespace; - } + this.namespace = namespace; this.redisTemplate = redisTemplate; this.region = getRegionName(region); } @@ -49,11 +46,6 @@ public class SpringRedisCache implements Level2Cache { redisTemplate.opsForHash().delete(region); } - @Override - public Object get(String key) { - return redisTemplate.boundHashOps(region).get(key); - } - @Override public boolean exists(String key) { return redisTemplate.opsForHash().hasKey(region, key); @@ -63,7 +55,7 @@ public class SpringRedisCache implements Level2Cache { public void evict(String... keys) { for (String k : keys) { if (!k.equals("null")) { - redisTemplate.opsForHash().delete(region, k); + redisTemplate.opsForHash().delete(region, k); } else { redisTemplate.delete(region); } @@ -82,7 +74,7 @@ public class SpringRedisCache implements Level2Cache { @Override public byte[] getBytes(String key) { - return redisTemplate.opsForHash().getOperations().execute((RedisCallback) redis -> redis.hGet(region.getBytes(), key.getBytes())); + return redisTemplate.opsForHash().getOperations().execute((RedisCallback) redis -> redis.hGet(region.getBytes(), key.getBytes())); } @Override @@ -111,14 +103,22 @@ public class SpringRedisCache implements Level2Cache { @Override public void setBytes(String key, byte[] bytes) { - // TODO Auto-generated method stub - + redisTemplate.opsForHash().getOperations().execute((RedisCallback>) redis -> { + redis.set(_key(key).getBytes(), bytes); + redis.hSet(region.getBytes(), key.getBytes(), bytes); + return null; + }); } @Override public void setBytes(Map bytes) { - // TODO Auto-generated method stub - + bytes.forEach((k, v) -> { + setBytes(k, v); + }); + } + + private String _key(String key) { + return this.region + ":" + key; } } diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisGenericCache.java b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisGenericCache.java new file mode 100644 index 0000000000000000000000000000000000000000..043a62a001edfb0684372e31939ba29973a02b47 --- /dev/null +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisGenericCache.java @@ -0,0 +1,137 @@ +package net.oschina.j2cache.cache.support.redis; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; + +import net.oschina.j2cache.Level2Cache; + +public class SpringRedisGenericCache implements Level2Cache { + + private final static Logger log = LoggerFactory.getLogger(SpringRedisGenericCache.class); + + private String namespace; + + private String region; + + private RedisTemplate redisTemplate; + + public SpringRedisGenericCache(String namespace, String region, RedisTemplate redisTemplate) { + if (region == null || region.isEmpty()) { + region = "_"; // 缺省region + } + this.namespace = namespace; + this.redisTemplate = redisTemplate; + this.region = getRegionName(region); + } + + private String getRegionName(String region) { + if (namespace != null && !namespace.isEmpty()) + region = namespace + ":" + region; + return region; + } + + @Override + public void clear() { + Collection keys = keys(); + keys.stream().forEach(k -> { + redisTemplate.delete(k); + }); + } + + @Override + public boolean exists(String key) { + return redisTemplate.hasKey(_key(key)); + } + + @Override + public void evict(String... keys) { + for (String k : keys) { + redisTemplate.delete(_key(k)); + } + } + + @Override + public Collection keys() { + Set list = redisTemplate.keys(this.region + ":*"); + List keys = new ArrayList<>(list.size()); + for (String s : list) { + keys.add(s); + } + return keys; + } + + @Override + public byte[] getBytes(String key) { + return redisTemplate.opsForValue().getOperations().execute((RedisCallback) redis -> { + try { + return redis.get(_key(key).getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return redis.get(_key(key).getBytes()); + } + }); + } + + @Override + public List getBytes(Collection keys) { + return redisTemplate.opsForValue().getOperations().execute((RedisCallback>) redis -> { + byte[][] bytes = keys.stream().map(k -> _key(k)).toArray(byte[][]::new); + return redis.mGet(bytes); + }); + } + + @Override + public void setBytes(String key, byte[] bytes, long timeToLiveInSeconds) { + if (timeToLiveInSeconds <= 0) { + log.debug(String.format("Invalid timeToLiveInSeconds value : %d , skipped it.", timeToLiveInSeconds)); + setBytes(key, bytes); + } else { + redisTemplate.opsForValue().getOperations().execute((RedisCallback>) redis -> { + try { + redis.setEx(_key(key).getBytes("utf-8"), (int) timeToLiveInSeconds, bytes); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + redis.setEx(_key(key).getBytes(), (int) timeToLiveInSeconds, bytes); + } + return null; + }); + } + } + + @Override + public void setBytes(Map bytes, long timeToLiveInSeconds) { + bytes.forEach((k, v) -> setBytes(k, v, timeToLiveInSeconds)); + } + + @Override + public void setBytes(String key, byte[] bytes) { + redisTemplate.opsForValue().getOperations().execute((RedisCallback) redis -> { + try { + redis.set(_key(key).getBytes("utf-8"), bytes); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + redis.set(_key(key).getBytes(), bytes); + } + return null; + }); + } + + @Override + public void setBytes(Map bytes) { + bytes.forEach((k,v) -> setBytes(k, v)); + } + + private String _key(String key) { + return this.region + ":" + key; + } +} diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java index 6acecc253cf8ea732d405925f885c225a5e38262..3a6f9ec91141df6845d278557325fabd13acd07e 100644 --- a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java @@ -24,9 +24,11 @@ public class SpringRedisProvider implements CacheProvider { private RedisTemplate redisTemplate; - private String namespace = "j2cache"; + private String namespace; - protected ConcurrentHashMap caches = new ConcurrentHashMap<>(); + private String storage; + + protected ConcurrentHashMap caches = new ConcurrentHashMap<>(); @Override public String name() { @@ -45,12 +47,16 @@ public class SpringRedisProvider implements CacheProvider { @Override public Cache buildCache(String region, CacheExpiredListener listener) { - SpringRedisCache cache = caches.get(region); + Cache cache = caches.get(region); if (cache == null) { synchronized (SpringRedisProvider.class) { cache = caches.get(region); if (cache == null) { - cache = new SpringRedisCache(this.namespace, region, redisTemplate); + if("hash".equalsIgnoreCase(this.storage)) + cache = new SpringRedisCache(this.namespace, region, redisTemplate); + else { + cache = new SpringRedisGenericCache(this.namespace, region, redisTemplate); + } caches.put(region, cache); } } @@ -67,6 +73,7 @@ public class SpringRedisProvider implements CacheProvider { @Override public void start(Properties props) { this.namespace = props.getProperty("namespace"); + this.storage = props.getProperty("storage"); this.redisTemplate = SpringUtil.getBean("j2CacheRedisTemplate", RedisTemplate.class); } diff --git a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java index 6d1c53d062e957c437722ab8a9571bf79d7d3d1d..254bca439efdc5cc39740c6e338cbfbe2aab044a 100644 --- a/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java +++ b/modules/spring-boot-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java @@ -1,6 +1,8 @@ package net.oschina.j2cache.cache.support.redis; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Properties; import org.springframework.data.redis.core.RedisTemplate; @@ -9,6 +11,7 @@ import org.springframework.data.redis.listener.RedisMessageListenerContainer; import net.oschina.j2cache.ClusterPolicy; import net.oschina.j2cache.Command; +import net.oschina.j2cache.J2CacheConfig; import net.oschina.j2cache.cache.support.util.SpringUtil; /** @@ -20,30 +23,63 @@ public class SpringRedisPubSubPolicy implements ClusterPolicy{ private RedisTemplate redisTemplate; + private net.oschina.j2cache.autoconfigure.J2CacheConfig config; + + /** + * 是否是主动模式 + */ + private static boolean isActive = false; + private String channel = "j2cache_channel"; - + @SuppressWarnings("unchecked") @Override public void connect(Properties props) { - String channel_name = props.getProperty("jgroups.channel.name"); + J2CacheConfig j2config = SpringUtil.getBean(J2CacheConfig.class); + this.config = SpringUtil.getBean(net.oschina.j2cache.autoconfigure.J2CacheConfig.class); + this.redisTemplate = SpringUtil.getBean("j2CacheRedisTemplate", RedisTemplate.class); + if("active".equals(config.getCacheCleanMode())) { + isActive = true; + } + String channel_name = j2config.getL2CacheProperties().getProperty("channel"); if(channel_name != null && !channel_name.isEmpty()) { this.channel = channel_name; } - this.redisTemplate = SpringUtil.getBean("j2CacheRedisTemplate", RedisTemplate.class); RedisMessageListenerContainer listenerContainer = SpringUtil.getBean("j2CacheRedisMessageListenerContainer", RedisMessageListenerContainer.class); + listenerContainer.addMessageListener(new SpringRedisMessageListener(this, this.channel), new PatternTopic(this.channel)); + if(isActive) { + //设置键值回调 + ConfigureNotifyKeyspaceEventsAction action = new ConfigureNotifyKeyspaceEventsAction(); + action.config(listenerContainer.getConnectionFactory().getConnection()); + + String namespace = j2config.getL2CacheProperties().getProperty("namespace"); + String database = j2config.getL2CacheProperties().getProperty("database"); + String expired = "__keyevent@" + (database == null || "".equals(database) ? "0" : database) + "__:expired"; + String del = "__keyevent@" + (database == null || "".equals(database) ? "0" : database) + "__:del"; + List topics = new ArrayList<>(); + topics.add(new PatternTopic(expired)); + topics.add(new PatternTopic(del)); + listenerContainer.addMessageListener(new SpringRedisActiveMessageListener(this, namespace), topics); + } + } @Override public void sendEvictCmd(String region, String... keys) { - String com = new Command(Command.OPT_EVICT_KEY, region, keys).json(); - redisTemplate.convertAndSend(this.channel, com); + if(!isActive || "blend".equals(config.getCacheCleanMode())) { + String com = new Command(Command.OPT_EVICT_KEY, region, keys).json(); + redisTemplate.convertAndSend(this.channel, com); + } + } @Override public void sendClearCmd(String region) { - String com = new Command(Command.OPT_CLEAR_KEY, region, "").json(); - redisTemplate.convertAndSend(this.channel, com); + if(!isActive || "blend".equals(config.getCacheCleanMode())) { + String com = new Command(Command.OPT_CLEAR_KEY, region, "").json(); + redisTemplate.convertAndSend(this.channel, com); + } } @Override diff --git a/modules/spring-boot2-starter/pom.xml b/modules/spring-boot2-starter/pom.xml index c4c7bddaf7c35afb07d623593209d3968805ee0f..ed93161acd5b106a47990dfa5d68eaf9067b9492 100644 --- a/modules/spring-boot2-starter/pom.xml +++ b/modules/spring-boot2-starter/pom.xml @@ -35,9 +35,9 @@ compile - org.springframework.data - spring-data-redis - compile + org.springframework.boot + spring-boot-starter-data-redis + compile diff --git a/modules/spring-boot2-starter/readme.md b/modules/spring-boot2-starter/readme.md index af2b4e3b21a5531e72527a2d5ad92785b822cbd4..2d685346f47506dabb8968d8cf5cd9ce104ff75a 100644 --- a/modules/spring-boot2-starter/readme.md +++ b/modules/spring-boot2-starter/readme.md @@ -11,9 +11,15 @@ j2cache.config-location=/j2cache-${spring.profiles.active}.properties 如下两项配置在application.properties,可以开启对spring cahce的支持 ``` j2cache.open-spring-cache=true +spring.cache.type=GENERIC ``` +如下配置在application.properties,可以选择缓存清除的模式 +* 缓存清除模式 +* active:主动清除,二级缓存过期主动通知各节点清除,优点在于所有节点可以同时收到缓存清除 +* passive:被动清除,一级缓存过期进行通知各节点清除一二级缓存 +* blend:两种模式一起运作,对于各个节点缓存准确性以及及时性要求高的可以使用(推荐使用前面两种模式中一种) ``` -spring.cache.type=none +j2cache.cache-clean-mode=passive ``` 在j2cache.properties中配置,可以使用springRedis进行广播通知缓失效 ``` @@ -22,5 +28,9 @@ j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPol 在j2cache.properties中配置,使用springRedis替换二级缓存 ``` j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider +j2cache.L2.config_section = redis ``` + + + diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java index a65b01387a15f7179fcb4b697ab4d3f9f786b3c7..a2ffd850b068fa20e8e4fc45495f93ae0c42c092 100644 --- a/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheAutoConfiguration.java @@ -29,11 +29,16 @@ public class J2CacheAutoConfiguration { } @Bean - @DependsOn("springUtil") - public CacheChannel cacheChannel() throws IOException { + public net.oschina.j2cache.J2CacheConfig j2CacheConfig() throws IOException{ net.oschina.j2cache.J2CacheConfig cacheConfig = new net.oschina.j2cache.J2CacheConfig(); cacheConfig = net.oschina.j2cache.J2CacheConfig.initFromConfig(j2CacheConfig.getConfigLocation()); - J2CacheBuilder builder = J2CacheBuilder.init(cacheConfig); + return cacheConfig; + } + + @Bean + @DependsOn({"springUtil","j2CacheConfig"}) + public CacheChannel cacheChannel(net.oschina.j2cache.J2CacheConfig j2CacheConfig) throws IOException { + J2CacheBuilder builder = J2CacheBuilder.init(j2CacheConfig); return builder.getChannel(); } diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java index 3c770684d9d1c5c6eb2b9edc8920428b64e3f3dd..a3a9d14b0f1ff7c97c46c6a26269066040b48cac 100644 --- a/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheConfig.java @@ -11,9 +11,17 @@ public class J2CacheConfig { private String configLocation = "/j2cache.properties"; /** - * 是否开启spring cache缓存,注意:开启后需要添加spring.cache.type=none,将缓存类型设置为none + * 是否开启spring cache缓存,注意:开启后需要添加spring.cache.type=GENERIC,将缓存类型设置为GENERIC */ private Boolean openSpringCache = false; + + /** + * 缓存清除模式, + * active:主动清除,二级缓存过期主动通知各节点清除,优点在于所有节点可以同时收到缓存清除 + * passive:被动清除,一级缓存过期进行通知各节点清除一二级缓存, + * blend:两种模式一起运作,对于各个节点缓存准确以及及时性要求高的可以使用,正常用前两种模式中一个就可 + */ + private String cacheCleanMode = "passive"; public String getConfigLocation() { return configLocation; @@ -30,4 +38,12 @@ public class J2CacheConfig { public void setOpenSpringCache(Boolean openSpringCache) { this.openSpringCache = openSpringCache; } + + public String getCacheCleanMode() { + return cacheCleanMode; + } + + public void setCacheCleanMode(String cacheCleanMode) { + this.cacheCleanMode = cacheCleanMode; + } } diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java index 22fec12a1ecb88a4249137261e38ce7eefb14814..9b8a3163a1ceb1ceb853183f5f12f295e98af24a 100644 --- a/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/autoconfigure/J2CacheSpringRedisAutoConfiguration.java @@ -1,45 +1,147 @@ package net.oschina.j2cache.autoconfigure; import java.io.Serializable; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.util.StringUtils; import net.oschina.j2cache.cache.support.util.J2CacheSerializer; +import net.oschina.j2cache.redis.RedisUtils; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisShardInfo; +import redis.clients.jedis.exceptions.JedisConnectionException; /** * 对spring redis支持的配置入口 + * * @author zhangsaizz * */ @Configuration -@AutoConfigureAfter({RedisAutoConfiguration.class}) -@AutoConfigureBefore({J2CacheAutoConfiguration.class}) +@AutoConfigureAfter({ RedisAutoConfiguration.class }) +@AutoConfigureBefore({ J2CacheAutoConfiguration.class }) public class J2CacheSpringRedisAutoConfiguration { + private final static int MAX_ATTEMPTS = 3; + + private final static int CONNECT_TIMEOUT = 5000; + + private static final Logger log = LoggerFactory.getLogger(J2CacheSpringRedisAutoConfiguration.class); + + @SuppressWarnings("deprecation") + @Bean("j2CahceRedisConnectionFactory") + @ConditionalOnMissingBean(name = "j2CahceRedisConnectionFactory") + public JedisConnectionFactory j2CahceRedisConnectionFactory(net.oschina.j2cache.J2CacheConfig j2CacheConfig) { + Properties l2CacheProperties = j2CacheConfig.getL2CacheProperties(); + String hosts = l2CacheProperties.getProperty("hosts"); + String mode = l2CacheProperties.getProperty("mode"); + String clusterName = l2CacheProperties.getProperty("cluster_name"); + String password = l2CacheProperties.getProperty("password"); + int database = Integer.parseInt(l2CacheProperties.getProperty("database")); + JedisConnectionFactory connectionFactory = null; + JedisPoolConfig config = RedisUtils.newPoolConfig(l2CacheProperties, null); + List nodes = new ArrayList<>(); + for (String node : hosts.split(",")) { + String[] s = node.split(":"); + String host = s[0]; + int port = (s.length > 1) ? Integer.parseInt(s[1]) : 6379; + RedisNode n = new RedisNode(host, port); + nodes.add(n); + } + RedisPassword paw = RedisPassword.none(); + if (!StringUtils.isEmpty(password)) { + paw = RedisPassword.of(password); + } + + switch (mode) { + case "sentinel": + RedisSentinelConfiguration sentinel = new RedisSentinelConfiguration(); + sentinel.setDatabase(database); + sentinel.setPassword(paw); + sentinel.setMaster(clusterName); + sentinel.setSentinels(nodes); + connectionFactory = new JedisConnectionFactory(sentinel, config); + break; + case "cluster": + RedisClusterConfiguration cluster = new RedisClusterConfiguration(); + cluster.setClusterNodes(nodes); + cluster.setMaxRedirects(MAX_ATTEMPTS); + cluster.setPassword(paw); + connectionFactory = new JedisConnectionFactory(cluster, config); + break; + case "sharded": + try { + for (String node : hosts.split(",")) { + connectionFactory = new JedisConnectionFactory(new JedisShardInfo(new URI(node))); + break; + } + } catch (URISyntaxException e) { + throw new JedisConnectionException(e); + } + break; + default: + for (RedisNode node : nodes) { + String host = node.getHost(); + int port = node.getPort(); + RedisStandaloneConfiguration single = new RedisStandaloneConfiguration(host, port); + single.setDatabase(database); + JedisClientConfigurationBuilder clientConfiguration = JedisClientConfiguration.builder(); + clientConfiguration.usePooling().poolConfig(config); + clientConfiguration.connectTimeout(Duration.ofMillis(CONNECT_TIMEOUT)); + connectionFactory = new JedisConnectionFactory(single, clientConfiguration.build()); + break; + } + if (!"single".equalsIgnoreCase(mode)) + log.warn("Redis mode [" + mode + "] not defined. Using 'single'."); + break; + } + return connectionFactory; + + } + @Bean("j2CacheRedisTemplate") - @ConditionalOnBean(RedisConnectionFactory.class) - public RedisTemplate j2CacheRedisTemplate(RedisConnectionFactory connectionFactory) { + @ConditionalOnBean(name = "j2CahceRedisConnectionFactory") + public RedisTemplate j2CacheRedisTemplate( + JedisConnectionFactory j2CahceRedisConnectionFactory) { RedisTemplate template = new RedisTemplate(); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setDefaultSerializer(new J2CacheSerializer()); - template.setConnectionFactory(connectionFactory); + template.setConnectionFactory(j2CahceRedisConnectionFactory); return template; } - @Bean("j2CacheRedisMessageListenerContainer") - RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){ - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(connectionFactory); - return container; - } + @Bean("j2CacheRedisMessageListenerContainer") + @ConditionalOnBean(name = "j2CahceRedisConnectionFactory") + RedisMessageListenerContainer container(JedisConnectionFactory j2CahceRedisConnectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(j2CahceRedisConnectionFactory); + return container; + } + } diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/ConfigureNotifyKeyspaceEventsAction.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/ConfigureNotifyKeyspaceEventsAction.java new file mode 100644 index 0000000000000000000000000000000000000000..2d4134f9cb934cb875f88a351f9e91d55416b198 --- /dev/null +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/ConfigureNotifyKeyspaceEventsAction.java @@ -0,0 +1,49 @@ +package net.oschina.j2cache.cache.support.redis; + +import java.util.Properties; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.RedisConnection; + +/** + * 设置redis键值回调 + * @param connection + */ +public class ConfigureNotifyKeyspaceEventsAction { + + static final String CONFIG_NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events"; + + + public void config(RedisConnection connection) { + String notifyOptions = getNotifyOptions(connection); + String customizedNotifyOptions = notifyOptions; + if (!customizedNotifyOptions.contains("E")) { + customizedNotifyOptions += "E"; + } + boolean A = customizedNotifyOptions.contains("A"); + if (!(A || customizedNotifyOptions.contains("g"))) { + customizedNotifyOptions += "g"; + } + if (!(A || customizedNotifyOptions.contains("x"))) { + customizedNotifyOptions += "x"; + } + if (!notifyOptions.equals(customizedNotifyOptions)) { + connection.setConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS, customizedNotifyOptions); + } + } + + private String getNotifyOptions(RedisConnection connection) { + try { + Properties config = connection.getConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS); + if (config.isEmpty()) { + return ""; + } + return config.getProperty(config.stringPropertyNames().iterator().next()); + } + catch (InvalidDataAccessApiUsageException e) { + throw new IllegalStateException( + "Unable to configure Redis to keyspace notifications. See http://docs.spring.io/spring-session/docs/current/reference/html5/#api-redisoperationssessionrepository-sessiondestroyedevent", + e); + } + } +} diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisActiveMessageListener.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisActiveMessageListener.java new file mode 100644 index 0000000000000000000000000000000000000000..1faf99d00812acaf063f0f3a66f9fc31f7cbcdc0 --- /dev/null +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisActiveMessageListener.java @@ -0,0 +1,45 @@ +package net.oschina.j2cache.cache.support.redis; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import net.oschina.j2cache.ClusterPolicy; + +/** + * 监听二缓key失效,主动清除本地缓存 + * + * @author zhangsaizz + * + */ +public class SpringRedisActiveMessageListener implements MessageListener { + + private static Logger logger = LoggerFactory.getLogger(SpringRedisActiveMessageListener.class); + + private ClusterPolicy clusterPolicy; + + private String namespace; + + SpringRedisActiveMessageListener(ClusterPolicy clusterPolicy, String namespace) { + this.clusterPolicy = clusterPolicy; + this.namespace = namespace; + } + + @Override + public void onMessage(Message message, byte[] pattern) { + String key = message.toString(); + if (key == null) { + return; + } + if (key.startsWith(namespace + ":")) { + String[] k = key.replaceFirst(namespace + ":", "").split(":", 2); + if(k.length != 2) { + return; + } + clusterPolicy.evict(k[0], k[1]); + } + + } + +} diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java index f26bb6b5410bdebe393212303c419258d48f2060..b91d217afdfc4d162ae5ce0794ffe2ea52f25708 100644 --- a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisCache.java @@ -7,11 +7,8 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.springframework.dao.DataAccessException; -import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.util.StringUtils; import net.oschina.j2cache.Level2Cache; @@ -23,7 +20,7 @@ import net.oschina.j2cache.Level2Cache; */ public class SpringRedisCache implements Level2Cache { - private String namespace = "j2cache~key"; + private String namespace; private String region; @@ -33,9 +30,7 @@ public class SpringRedisCache implements Level2Cache { if (region == null || region.isEmpty()) { region = "_"; // 缺省region } - if(!StringUtils.isEmpty(namespace)) { - this.namespace = namespace; - } + this.namespace = namespace; this.redisTemplate = redisTemplate; this.region = getRegionName(region); } @@ -51,11 +46,6 @@ public class SpringRedisCache implements Level2Cache { redisTemplate.opsForHash().delete(region); } - @Override - public Object get(String key) { - return redisTemplate.boundHashOps(region).get(key); - } - @Override public boolean exists(String key) { return redisTemplate.opsForHash().hasKey(region, key); @@ -65,7 +55,7 @@ public class SpringRedisCache implements Level2Cache { public void evict(String... keys) { for (String k : keys) { if (!k.equals("null")) { - redisTemplate.opsForHash().delete(region, k); + redisTemplate.opsForHash().delete(region, k); } else { redisTemplate.delete(region); } @@ -84,21 +74,14 @@ public class SpringRedisCache implements Level2Cache { @Override public byte[] getBytes(String key) { - return redisTemplate.opsForHash().getOperations().execute(new RedisCallback() { - public byte[] doInRedis(RedisConnection redis) { - return redis.hGet(region.getBytes(), key.getBytes()); - } - }); + return redisTemplate.opsForHash().getOperations().execute((RedisCallback) redis -> redis.hGet(region.getBytes(), key.getBytes())); } @Override public List getBytes(Collection keys) { - return redisTemplate.opsForHash().getOperations().execute(new RedisCallback>() { - @Override - public List doInRedis(RedisConnection redis) throws DataAccessException { - byte[][] bytes = keys.stream().map(k -> k.getBytes()).toArray(byte[][]::new); - return redis.hMGet(region.getBytes(), bytes); - } + return redisTemplate.opsForHash().getOperations().execute((RedisCallback>) redis -> { + byte[][] bytes = keys.stream().map(k -> k.getBytes()).toArray(byte[][]::new); + return redis.hMGet(region.getBytes(), bytes); }); } @@ -120,14 +103,22 @@ public class SpringRedisCache implements Level2Cache { @Override public void setBytes(String key, byte[] bytes) { - // TODO Auto-generated method stub - + redisTemplate.opsForHash().getOperations().execute((RedisCallback>) redis -> { + redis.set(_key(key).getBytes(), bytes); + redis.hSet(region.getBytes(), key.getBytes(), bytes); + return null; + }); } @Override public void setBytes(Map bytes) { - // TODO Auto-generated method stub - + bytes.forEach((k, v) -> { + setBytes(k, v); + }); + } + + private String _key(String key) { + return this.region + ":" + key; } } diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisGenericCache.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisGenericCache.java new file mode 100644 index 0000000000000000000000000000000000000000..043a62a001edfb0684372e31939ba29973a02b47 --- /dev/null +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisGenericCache.java @@ -0,0 +1,137 @@ +package net.oschina.j2cache.cache.support.redis; + +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; + +import net.oschina.j2cache.Level2Cache; + +public class SpringRedisGenericCache implements Level2Cache { + + private final static Logger log = LoggerFactory.getLogger(SpringRedisGenericCache.class); + + private String namespace; + + private String region; + + private RedisTemplate redisTemplate; + + public SpringRedisGenericCache(String namespace, String region, RedisTemplate redisTemplate) { + if (region == null || region.isEmpty()) { + region = "_"; // 缺省region + } + this.namespace = namespace; + this.redisTemplate = redisTemplate; + this.region = getRegionName(region); + } + + private String getRegionName(String region) { + if (namespace != null && !namespace.isEmpty()) + region = namespace + ":" + region; + return region; + } + + @Override + public void clear() { + Collection keys = keys(); + keys.stream().forEach(k -> { + redisTemplate.delete(k); + }); + } + + @Override + public boolean exists(String key) { + return redisTemplate.hasKey(_key(key)); + } + + @Override + public void evict(String... keys) { + for (String k : keys) { + redisTemplate.delete(_key(k)); + } + } + + @Override + public Collection keys() { + Set list = redisTemplate.keys(this.region + ":*"); + List keys = new ArrayList<>(list.size()); + for (String s : list) { + keys.add(s); + } + return keys; + } + + @Override + public byte[] getBytes(String key) { + return redisTemplate.opsForValue().getOperations().execute((RedisCallback) redis -> { + try { + return redis.get(_key(key).getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return redis.get(_key(key).getBytes()); + } + }); + } + + @Override + public List getBytes(Collection keys) { + return redisTemplate.opsForValue().getOperations().execute((RedisCallback>) redis -> { + byte[][] bytes = keys.stream().map(k -> _key(k)).toArray(byte[][]::new); + return redis.mGet(bytes); + }); + } + + @Override + public void setBytes(String key, byte[] bytes, long timeToLiveInSeconds) { + if (timeToLiveInSeconds <= 0) { + log.debug(String.format("Invalid timeToLiveInSeconds value : %d , skipped it.", timeToLiveInSeconds)); + setBytes(key, bytes); + } else { + redisTemplate.opsForValue().getOperations().execute((RedisCallback>) redis -> { + try { + redis.setEx(_key(key).getBytes("utf-8"), (int) timeToLiveInSeconds, bytes); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + redis.setEx(_key(key).getBytes(), (int) timeToLiveInSeconds, bytes); + } + return null; + }); + } + } + + @Override + public void setBytes(Map bytes, long timeToLiveInSeconds) { + bytes.forEach((k, v) -> setBytes(k, v, timeToLiveInSeconds)); + } + + @Override + public void setBytes(String key, byte[] bytes) { + redisTemplate.opsForValue().getOperations().execute((RedisCallback) redis -> { + try { + redis.set(_key(key).getBytes("utf-8"), bytes); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + redis.set(_key(key).getBytes(), bytes); + } + return null; + }); + } + + @Override + public void setBytes(Map bytes) { + bytes.forEach((k,v) -> setBytes(k, v)); + } + + private String _key(String key) { + return this.region + ":" + key; + } +} diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java index 6acecc253cf8ea732d405925f885c225a5e38262..3a6f9ec91141df6845d278557325fabd13acd07e 100644 --- a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisProvider.java @@ -24,9 +24,11 @@ public class SpringRedisProvider implements CacheProvider { private RedisTemplate redisTemplate; - private String namespace = "j2cache"; + private String namespace; - protected ConcurrentHashMap caches = new ConcurrentHashMap<>(); + private String storage; + + protected ConcurrentHashMap caches = new ConcurrentHashMap<>(); @Override public String name() { @@ -45,12 +47,16 @@ public class SpringRedisProvider implements CacheProvider { @Override public Cache buildCache(String region, CacheExpiredListener listener) { - SpringRedisCache cache = caches.get(region); + Cache cache = caches.get(region); if (cache == null) { synchronized (SpringRedisProvider.class) { cache = caches.get(region); if (cache == null) { - cache = new SpringRedisCache(this.namespace, region, redisTemplate); + if("hash".equalsIgnoreCase(this.storage)) + cache = new SpringRedisCache(this.namespace, region, redisTemplate); + else { + cache = new SpringRedisGenericCache(this.namespace, region, redisTemplate); + } caches.put(region, cache); } } @@ -67,6 +73,7 @@ public class SpringRedisProvider implements CacheProvider { @Override public void start(Properties props) { this.namespace = props.getProperty("namespace"); + this.storage = props.getProperty("storage"); this.redisTemplate = SpringUtil.getBean("j2CacheRedisTemplate", RedisTemplate.class); } diff --git a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java index 6d1c53d062e957c437722ab8a9571bf79d7d3d1d..ff6b9c0fae2c4b9c260ca892767e5f68c07a8b4b 100644 --- a/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java +++ b/modules/spring-boot2-starter/src/net/oschina/j2cache/cache/support/redis/SpringRedisPubSubPolicy.java @@ -1,6 +1,8 @@ package net.oschina.j2cache.cache.support.redis; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Properties; import org.springframework.data.redis.core.RedisTemplate; @@ -9,6 +11,7 @@ import org.springframework.data.redis.listener.RedisMessageListenerContainer; import net.oschina.j2cache.ClusterPolicy; import net.oschina.j2cache.Command; +import net.oschina.j2cache.J2CacheConfig; import net.oschina.j2cache.cache.support.util.SpringUtil; /** @@ -20,30 +23,63 @@ public class SpringRedisPubSubPolicy implements ClusterPolicy{ private RedisTemplate redisTemplate; + private net.oschina.j2cache.autoconfigure.J2CacheConfig config; + + /** + * 是否是主动模式 + */ + private static boolean isActive = false; + private String channel = "j2cache_channel"; - + @SuppressWarnings("unchecked") @Override public void connect(Properties props) { - String channel_name = props.getProperty("jgroups.channel.name"); + J2CacheConfig j2config = SpringUtil.getBean(J2CacheConfig.class); + this.config = SpringUtil.getBean(net.oschina.j2cache.autoconfigure.J2CacheConfig.class); + this.redisTemplate = SpringUtil.getBean("j2CacheRedisTemplate", RedisTemplate.class); + if("active".equals(config.getCacheCleanMode())) { + isActive = true; + } + String channel_name = j2config.getL2CacheProperties().getProperty("channel"); if(channel_name != null && !channel_name.isEmpty()) { this.channel = channel_name; } - this.redisTemplate = SpringUtil.getBean("j2CacheRedisTemplate", RedisTemplate.class); RedisMessageListenerContainer listenerContainer = SpringUtil.getBean("j2CacheRedisMessageListenerContainer", RedisMessageListenerContainer.class); + listenerContainer.addMessageListener(new SpringRedisMessageListener(this, this.channel), new PatternTopic(this.channel)); + if(isActive || "blend".equals(config.getCacheCleanMode())) { + //设置键值回调 + ConfigureNotifyKeyspaceEventsAction action = new ConfigureNotifyKeyspaceEventsAction(); + action.config(listenerContainer.getConnectionFactory().getConnection()); + + String namespace = j2config.getL2CacheProperties().getProperty("namespace"); + String database = j2config.getL2CacheProperties().getProperty("database"); + String expired = "__keyevent@" + (database == null || "".equals(database) ? "0" : database) + "__:expired"; + String del = "__keyevent@" + (database == null || "".equals(database) ? "0" : database) + "__:del"; + List topics = new ArrayList<>(); + topics.add(new PatternTopic(expired)); + topics.add(new PatternTopic(del)); + listenerContainer.addMessageListener(new SpringRedisActiveMessageListener(this, namespace), topics); + } + } @Override public void sendEvictCmd(String region, String... keys) { - String com = new Command(Command.OPT_EVICT_KEY, region, keys).json(); - redisTemplate.convertAndSend(this.channel, com); + if(!isActive || "blend".equals(config.getCacheCleanMode())) { + String com = new Command(Command.OPT_EVICT_KEY, region, keys).json(); + redisTemplate.convertAndSend(this.channel, com); + } + } @Override public void sendClearCmd(String region) { - String com = new Command(Command.OPT_CLEAR_KEY, region, "").json(); - redisTemplate.convertAndSend(this.channel, com); + if(!isActive || "blend".equals(config.getCacheCleanMode())) { + String com = new Command(Command.OPT_CLEAR_KEY, region, "").json(); + redisTemplate.convertAndSend(this.channel, com); + } } @Override