spring與redis整合

俺就不起網名發表於2018-09-17

目錄

 

一、jedis對5種java資料型別的儲存方式

二、關於redis的一點介紹

三、相關程式碼

四、總結


一、jedis對5種java資料型別的儲存方式

一個快取資訊包含三個,name快取名稱,key快取的key值,value快取的value值。jedis常用操作如下:

public class JedisTest {
    private static final Jedis jedis = new Jedis("127.0.0.1", 6379);

    @Test
    public void test_string() {
        jedis.set("name", "zhangsan");
    }

    @Test
    public void test_hash() {
        //存放的類似map型別的
        jedis.hset("url", "google", "www.google.com");
        jedis.hset("url", "taobao", "www.taobao.com");
        jedis.hset("url", "baidu", "www.baidu.com");

        Map<String, String> userInfo = new HashMap<>();
        userInfo.put("name", "zhangsan");
        userInfo.put("age", "35");
        userInfo.put("sex", "man");
        jedis.hmset("userInfo", userInfo);
        String name = jedis.hget("userInfo", "name");
        //取多個返回值
        List<String> urlList = jedis.hmget("url", "google", "taobao", "baidu");
        //取hash所有key值
        Map<String, String> userInfoMap = jedis.hgetAll("userInfo");
    }

    @Test
    public void test_list() {
        jedis.lpush("charList", "abc");//lpush,在list首部新增元素
        jedis.rpush("charList", "def");//rpush,在listw尾部新增元素
        //擷取list
        List<String> charList = jedis.lrange("charList", 0, 1);
        jedis.lpop("charList");//在list首部刪除元素
        jedis.rpop("charList");//在list尾部刪除元素
    }

    @Test
    public void test_set() {
        jedis.sadd("setMem", "s1");
        jedis.sadd("setMem", "s2");
        Set<String> sets = jedis.smembers("setMem");
    }

    @Test
    public void test_sort_set() {
        jedis.zadd("sortSetMem", 1, "s1");
        jedis.zadd("sortSetMem", 2, "s1");
        Set<String> sets = jedis.zrange("sortSetMem", 0, 1);
        Set<String> revesortSet = jedis.zrevrange("sortSetMem", 0, 1);//反向取
    }
}

二、關於redis的一點介紹

具體見redis簡介

三、相關程式碼

1、CacheDefinition

在@Cacheable中沒有屬性描述快取有效時間,以及當有個多個相關快取(比如產品的快取)的關聯性屬性,所以提供兩個屬性,duration和tag。tag屬性的作用是將多個快取關聯,可以一起刪除這些快取(利用redis的hash資料型別)。

public class CacheDefinition {
    private String name;//快取名稱

    private int duration;//快取有效時間

    private String tag;//快取標籤

    public String getName() {
        return name;
    }

    public CacheDefinition setName(String name) {
        this.name = name;
        return this;
    }

    public int getDuration() {
        return duration;
    }

    public CacheDefinition setDuration(int duration) {
        this.duration = duration;
        return this;
    }

    public String getTag() {
        return tag;
    }

    public CacheDefinition setTag(String tag) {
        this.tag = tag;
        return this;
    }
}

2、CacheMapping

定義該註解,用於表示快取時間和標籤。

 

/**
 * 定義一個快取,支援類級別和方法級別,如果同時存在,類級別覆蓋方法級別配置
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CacheMapping {

    // 快取時間(秒)
    public int duration() default 60;

    // 快取標籤,用於清理快取
    public String tag() default "";
}

3、Storage

定義該介面,提供一些常用的快取方法。

public interface Storage {

    Object get(Object key);

    void put(Object key, Object value, int seconds);//seconds,過期時間,單位秒

    void evict(Object key);

    void replace(Object key, Object value, int seconds);

    void destroy();

    boolean available();
}

4、RedisStorage

定義了關於Cache的一些資訊,比如連線資訊(redisStorage)、快取名稱、tag、有效時間等資訊;
該類實現了上面的Storage介面,提供了基本的快取增刪改查操作。這裡注意可以key、value等值都進行序列化了,方便儲存和傳輸。然後對於redis的hash型別,採用hget/hset方法,第一個引數為快取名稱,第二個引數為快取key;
另外Jedis只提供了儲存string型別的方法,所以我們可以使用可以儲存位元組陣列的BinaryJedisCommands方法。

public class RedisStorage<ResourceType extends BinaryJedisCommands> implements Storage {

    //序列化預設為jdk序列化,也可以選擇為hessian2/kryo
    private Serializer<Object> serializer = new DefaultSerializer();
    private Deserializer<Object> deserializer = new DefaultDeserializer();
    private Logger log = Logger.getLogger(RedisStorage.class);

    private final Pool<ResourceType> pool;
    private final List<HostAndPort> hostAndPorts;

    public RedisStorage(Pool<ResourceType> pool, List<HostAndPort> hostAndPorts) {
        this.pool = checkNotNull(pool);
        this.hostAndPorts = checkNotNull(hostAndPorts);
    }

    //下面方法為Storage介面的方法實現
    @Override
    public Object get(Object key) {
        ResourceType resource = getResource();
        byte[] bytes = resource.get(key.toString().getBytes());
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        DeserializingConverter dc = new DeserializingConverter(deserializer);
        return dc.convert(bytes);//反序列化成物件
    }

    @Override
    public void put(Object key, Object value, int seconds) {
        ResourceType resource = getResource();
        SerializingConverter sc = new SerializingConverter(serializer);
        byte[] bytes = sc.convert(value);
        resource.setex(key.toString().getBytes(), seconds, bytes);
    }

    @Override
    public void evict(Object key) {
        ResourceType resource = getResource();
        if (resource.exists(key.toString().getBytes())) {
            resource.del(key.toString().getBytes());
        }
    }

    @Override
    public void replace(Object key, Object value, int seconds) {
        put(key, value, seconds);
    }

    @Override
    public void destroy() {
        if (!pool.isClosed()) {
            pool.close();
        }
    }

    //返回增加的條數(如果key存在update,update返回0)
    public Long hset(final Object key, final Object field, final Object value, int seconds) {
        ResourceType resource = getResource();
        SerializingConverter sc = new SerializingConverter(serializer);
        byte[] bytes = sc.convert(value);
        long result = resource.hset(key.toString().getBytes(), field.toString().getBytes(), bytes);
        resource.expire(key.toString().getBytes(), seconds);
        return result;
    }

    public Set<Object> hkeys(String tag) {
        ResourceType resource = getResource();
        Set<byte[]> bytes = resource.hkeys(tag.getBytes());
        Set<Object> stringSet = new HashSet<>();
        for (byte[] bytes1 : bytes) {
            stringSet.add(new String(bytes1));
        }
        return stringSet;
    }

    public Object hget(Object tag, Object key) {
        ResourceType resource = getResource();
        byte[] bytes = resource.hget(tag.toString().getBytes(), key.toString().getBytes());
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        DeserializingConverter dc = new DeserializingConverter(deserializer);
        return dc.convert(bytes);//反序列化成物件
    }

    //標籤類快取,利用hash資料型別(類似map),可以儲存某一類的快取
    //hash型別: key(field,value)
    public void putCacheToTag(Object tag, Object key, Object value, int duration) {
        if (StringUtils.isEmpty(tag)) {
            return;
        }
        Long resultCode = this.hset(tag, key, value, duration);
        if (null == resultCode) {
            throw new IllegalArgumentException("Fail to put cache to tag");
        }
    }

    //根據標籤刪除相關快取
    //如果可以通過key刪除,該方法可以不要
    public void evictTag(String tag) {
        if (StringUtils.isEmpty(tag)) {
            return;
        }
        this.evict(tag);
        Set<Object> keys = this.hkeys(tag);
        log.info("Removed cache tag " + tag);
        for (Object k : keys) {
            log.info("Removed cache,tag is " + tag + ",key is " + k.toString());
        }
    }


    //檢測是否可用
    @Override
    public boolean available() {
        ResourceType resource = getResource();
        byte[] bytes = resource.echo("test".getBytes());
        if (bytes == null || bytes.length == 0) {
            return false;
        }
        return Arrays.equals(bytes, "test".getBytes());
    }

    private static <T> T checkNotNull(T reference) {
        if (reference == null) {
            throw new NullPointerException();
        }
        return reference;
    }

    public Pool<ResourceType> getPool() {
        return pool;
    }

    private ResourceType getResource() {
        return pool.getResource();
    }
}

5、RedisStorageFactory

建立redisStoage,redis叢集資訊可以使用類JedisShardInfo。

/**
 * 構建jedis連線
 * 單個連線 JedisPool
 * 叢集連線 ShardedJedisPool
 */
public class RedisStorageFactory {
    public RedisStorage<? extends BinaryJedisCommands> newStorage(JedisPoolConfig poolConfig, List<JedisShardInfo> shardInfos) {
        List<HostAndPort> hostAndPortList = getHostAndPortByShardInfos(shardInfos);
        if (shardInfos.size() == 1) {
            return new RedisStorage<>(new JedisPool(poolConfig, shardInfos.get(0).getHost(), shardInfos.get(0).getPort(), shardInfos.get(0).getSoTimeout(), shardInfos.get(0).getPassword()), hostAndPortList);
        }
        ShardedJedisPool shardedJedisPool = new ShardedJedisPool(poolConfig, shardInfos);
        return new RedisStorage<>(shardedJedisPool, hostAndPortList);
    }

    private List<HostAndPort> getHostAndPortByShardInfos(List<JedisShardInfo> shardInfos) {
        List<HostAndPort> hostAndPorts = new ArrayList<>();
        for (JedisShardInfo shardInfo : shardInfos) {
            hostAndPorts.add(new HostAndPort(shardInfo.getHost(), shardInfo.getPort()));
        }
        return hostAndPorts;
    }

}

6、RedisCache

spring Cache介面的實現類,裡面的實現方法可以呼叫storage的方法。

public class RedisCache<ResourceType extends BinaryJedisCommands> implements Cache {

    private String name;
    private String tag;
    private int duration;
    private TimeUnit timeUnit;
    private RedisStorage<ResourceType> redisStorage;

    public RedisCache() {
    }

    public RedisCache(RedisStorage<ResourceType> redisStorage, String name, int duration, TimeUnit timeUnit) {
        this.redisStorage = redisStorage;
        this.name = name;
        this.duration = duration;
        this.timeUnit = timeUnit;
    }

    public RedisCache(RedisStorage<ResourceType> redisStorage, String name, String tag, int duration, TimeUnit timeUnit) {
        this.redisStorage = redisStorage;
        this.name = name;
        this.duration = duration;
        this.timeUnit = timeUnit;
        this.tag = tag;
    }


    @Override
    public String getName() {
        return name;
    }

    @Override
    public Object getNativeCache() {
        return redisStorage.getPool();
    }

    @Override
    public ValueWrapper get(Object key) {
        Object actObject = null;
        if (StringUtils.isEmpty(tag)) {
            actObject = redisStorage.get(key);
        } else {
            actObject = redisStorage.hget(tag, key);
        }
        return actObject == null ? null : new SimpleValueWrapper(actObject);
    }


    @Override
    public <T> T get(Object key, Class<T> aClass) {
        ValueWrapper valueWrapper = this.get(key);
        if (valueWrapper == null) {
            return null;
        }
        return aClass.cast(valueWrapper.get());
    }

    @Override
    public void put(Object key, Object value) {
        if (StringUtils.isEmpty(tag)) {
            redisStorage.put(key, value, (int) timeUnit.toSeconds(duration));
            return;
        }
        redisStorage.putCacheToTag(tag, key, value, duration);
    }

    @Override
    public void evict(Object key) {
        redisStorage.evict(key);//如果該方法不行,試試evictTag,key為tag
    }

    @Override
    public void clear() {
        redisStorage.destroy();
    }
}

7、RedisServersManager

該類主要是對redis的服務進行管理。

public class RedisServersManager {

    private Logger log = Logger.getLogger(RedisServersManager.class);
    //jedis連線分片資訊
    private List<JedisShardInfo> shards = new ArrayList<JedisShardInfo>();

    protected volatile RedisStorage<? extends BinaryJedisCommands> redisStorage;
    protected final static Object REDIS_STORAGE_LOCK = new Object();
    protected RedisStorageFactory factory = new RedisStorageFactory();
    protected JedisPoolConfig config = new JedisPoolConfig();

    /**
     * 根據servers生成jedis分片連線池
     */
    public void setServers(String servers) {
        String[] serversArrays = servers.trim().split(",");
        for (String server : serversArrays) {
            JedisShardInfo jedisShardInfo = shardInfo(server);
            this.shards.add(jedisShardInfo);
            log.info("Add a node to redis shard info,node info:" + jedisShardInfo);
        }
    }

    private JedisShardInfo shardInfo(String server) {
        String[] texts = server.split("\\^");
        JedisShardInfo shard = new JedisShardInfo(texts[0], Integer.parseInt(texts[1]), 1000);
        if (texts.length == 3) {
            shard.setPassword(texts[2]);
        }
        return shard;
    }

    public RedisStorage<? extends BinaryJedisCommands> newStorage() {
        if (null == redisStorage) {
            synchronized (REDIS_STORAGE_LOCK) {
                if (null == redisStorage) {
                    redisStorage = factory.newStorage(config, shards);
                    log.info("Successfully created a redis storage.");
                }
            }
        }
        return redisStorage;
    }

}

8、SpringDataCacheManager

實現CacheManager介面,對cache進行管理。通過ApplicationContextAware介面獲取到applicationContext,通過InitializingBean介面在初始化該類的時候把所有有快取註解資訊的放入到集合中。

public class SpringDataCacheManager extends RedisServersManager implements ApplicationContextAware, InitializingBean, CacheManager {

    private ApplicationContext applicationContext;
    private final Map<String, CacheDefinition> caches = new ConcurrentReferenceHashMap<>();
    private int defaultDuration = 60;//預設快取60秒
    private String servers = "127.0.0.1^7100^password,127.0.0.1^6379^password";

    //InitializingBean的實現方法
    @Override
    public void afterPropertiesSet() throws Exception {
        setServers(servers);
        parseFromContext(applicationContext);
    }

    private void parseFromContext(final ApplicationContext context) {
        String[] beanNames = context.getBeanNamesForType(Object.class);
        for (final String beanName : beanNames) {
            final Class<?> classType = context.getType(beanName);
            //查詢該類是否有service和repository註解
            Service service = findAnnotation(classType, Service.class);
            Repository repository = findAnnotation(classType, Repository.class);
            if (service == null && repository == null) {
                continue;
            }
            ReflectionUtils.doWithMethods(classType, new ReflectionUtils.MethodCallback() {
                public void doWith(Method method) {
                    ReflectionUtils.makeAccessible(method);
                    CacheMapping cacheMapping = findCacheMapping(method, classType);//先找類級別,沒有再找當前方法級別
                    Cacheable cacheable = findAnnotation(method, Cacheable.class);
                    String[] cacheNames = getCacheNamesWithCacheable(cacheable);
                    for (String cacheName : cacheNames) {
                        CacheDefinition cacheDefinition = new CacheDefinition()
                                .setName(cacheName)
                                .setDuration(cacheMapping.duration())
                                .setTag(cacheMapping.tag());
                        caches.put(cacheName, cacheDefinition);
                    }
                }
            }, new ReflectionUtils.MethodFilter() {
                public boolean matches(Method method) {
                    return !method.isSynthetic() && findAnnotation(method, Cacheable.class) != null;
                }
            });
            if (context.getParent() != null) {
                parseFromContext(context.getParent());
            }
        }
    }

    private CacheMapping findCacheMapping(Method method, Class<?> handlerType) {
        CacheMapping cacheMapping = findAnnotation(handlerType, CacheMapping.class);
        if (cacheMapping == null) {//如果類註解沒有,再從方法註解查詢
            cacheMapping = findAnnotation(method, CacheMapping.class);
        }
        if (cacheMapping == null) {
            throw new IllegalStateException("No cache mapping (@CacheMapping) could be detected on '" +
                    method.toString() + "'. Make sure to set the value parameter on the annotation or " +
                    "declare a @CacheMapping at the class-level with the default cache cacheMapping to use.");
        }
        return cacheMapping;
    }

    private String[] getCacheNamesWithCacheable(Cacheable cacheable) {
        return cacheable.value();
    }

    //CacheManager的實現方法,該方法每次呼叫cache的get方法之前會呼叫一次
    @Override
    public Cache getCache(String name) {
        CacheDefinition cacheDefinition = caches.get(name);
        if (null != cacheDefinition) {
            return new RedisCache<>(newStorage(), name, cacheDefinition.getTag(), cacheDefinition.getDuration(), TimeUnit.SECONDS);
        }
        return new RedisCache<>(newStorage(), name, defaultDuration, TimeUnit.SECONDS);
    }

    @Override
    public Collection<String> getCacheNames() {
        return caches.keySet();
    }

    //ApplicationContextAware
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

9、Service類

@Service
public class PersonService2 {

    @Cacheable(value = "getPersonByName", key = "'PersonService.getPersonByName'+#name")
    @CacheMapping(duration = 20, tag = "PERSON")
    public Person getPersonByName(String name) {
        // 方法內部實現不考慮快取邏輯,直接實現業務
        System.out.println("呼叫了Service方法");
        return getFromDB(name);
    }

    private Person getFromDB(String name) {
        System.out.println("從資料庫查詢了");
        return new Person();
    }
}

10、測試類

public class PersonCacheTest {

    private PersonService2 personService2;

    @Before
    public void setUp() {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans/application_redis_cache.xml");
        personService2 = context.getBean("personService2", PersonService2.class);
    }

    @Test
    public void testGetPersonByName() {
        System.out.println("第一次查詢………………");
        personService2.getPersonByName("張三");
        System.out.println("第二次查詢………………");
        personService2.getPersonByName("李四");
        System.out.println("第三次查詢………………");
        personService2.getPersonByName("張三");
    }
}

輸出

第一次查詢………………
呼叫了Service方法
從資料庫查詢了
第二次查詢………………
呼叫了Service方法
從資料庫查詢了
第三次查詢………………

第三次沒有從資料庫查詢了。

 

四、總結

1、BinaryJedisCommands介面可以儲存位元組陣列,儲存的key/value建議先轉化成位元組陣列然後再序列化;

2、單服務jedis使用到連線池介面有JedisPoolConfig/JedisPool,叢集的對應有JedisPoolConfig/JedisShardInfo/ShardedJedisPool;

3、實現spring Cache需要實現Cache介面和CacheManage介面(cache介面提供增刪改查,cacheManage介面對cache進行管理)。

相關文章