package com.jarvis.cache.redis; import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jarvis.cache.AbstractCacheManager; import com.jarvis.cache.exception.CacheCenterConnectionException; import com.jarvis.cache.script.AbstractScriptParser; import com.jarvis.cache.serializer.ISerializer; import com.jarvis.cache.serializer.StringSerializer; import com.jarvis.cache.to.AutoLoadConfig; import com.jarvis.cache.to.CacheKeyTO; import com.jarvis.cache.to.CacheWrapper; import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; import redis.clients.jedis.ShardedJedis; import redis.clients.jedis.ShardedJedisPool; /** * Redis缓存管理 * @author jiayu.qiu */ public class ShardedCachePointCut extends AbstractCacheManager { private static final Logger logger=LoggerFactory.getLogger(ShardedCachePointCut.class); private static final StringSerializer keySerializer=new StringSerializer(); private ShardedJedisPool shardedJedisPool; /** * Hash的缓存时长:等于0时永久缓存;大于0时,主要是为了防止一些已经不用的缓存占用内存;hashExpire小于0时,则使用@Cache中设置的expire值(默认值为-1)。 */ private int hashExpire=-1; /** * 是否通过脚本来设置 Hash的缓存时长 */ private boolean hashExpireByScript=false; public ShardedCachePointCut(AutoLoadConfig config, ISerializer<Object> serializer, AbstractScriptParser scriptParser) { super(config, serializer, scriptParser); } private void returnResource(ShardedJedis shardedJedis) { shardedJedis.close(); } @Override public void setCache(final CacheKeyTO cacheKeyTO, final CacheWrapper<Object> result, final Method method, final Object args[]) throws CacheCenterConnectionException { if(null == shardedJedisPool || null == cacheKeyTO) { return; } String cacheKey=cacheKeyTO.getCacheKey(); if(null == cacheKey || cacheKey.length() == 0) { return; } ShardedJedis shardedJedis=null; try { int expire=result.getExpire(); shardedJedis=shardedJedisPool.getResource(); Jedis jedis=shardedJedis.getShard(cacheKey); String hfield=cacheKeyTO.getHfield(); if(null == hfield || hfield.length() == 0) { if(expire == 0) { jedis.set(keySerializer.serialize(cacheKey), getSerializer().serialize(result)); } else if(expire > 0) { jedis.setex(keySerializer.serialize(cacheKey), expire, getSerializer().serialize(result)); } } else { hashSet(jedis, cacheKey, hfield, result); } } catch(Exception ex) { logger.error(ex.getMessage(), ex); } finally { returnResource(shardedJedis); } } private static byte[] hashSetScript; static { try { String tmpScript="redis.call('HSET', KEYS[1], KEYS[2], ARGV[1]);\nredis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]));"; hashSetScript=tmpScript.getBytes("UTF-8"); } catch(UnsupportedEncodingException ex) { logger.error(ex.getMessage(), ex); } } private static final Map<Jedis, byte[]> hashSetScriptSha=new ConcurrentHashMap<Jedis, byte[]>(); private void hashSet(Jedis jedis, String cacheKey, String hfield, CacheWrapper<Object> result) throws Exception { byte[] key=keySerializer.serialize(cacheKey); byte[] field=keySerializer.serialize(hfield); byte[] val=getSerializer().serialize(result); int hExpire; if(hashExpire < 0) { hExpire=result.getExpire(); } else { hExpire=hashExpire; } if(hExpire == 0) { jedis.hset(key, field, val); } else if(hExpire > 0) { if(hashExpireByScript) { byte[] sha=hashSetScriptSha.get(jedis); if(null == sha) { sha=jedis.scriptLoad(hashSetScript); hashSetScriptSha.put(jedis, sha); } List<byte[]> keys=new ArrayList<byte[]>(); keys.add(key); keys.add(field); List<byte[]> args=new ArrayList<byte[]>(); args.add(val); args.add(keySerializer.serialize(String.valueOf(hExpire))); try { jedis.evalsha(sha, keys, args); } catch(Exception ex) { logger.error(ex.getMessage(), ex); try { sha=jedis.scriptLoad(hashSetScript); hashSetScriptSha.put(jedis, sha); jedis.evalsha(sha, keys, args); } catch(Exception ex1) { logger.error(ex1.getMessage(), ex1); } } } else { Pipeline p=jedis.pipelined(); p.hset(key, field, val); p.expire(key, hExpire); p.sync(); } } } @SuppressWarnings("unchecked") @Override public CacheWrapper<Object> get(final CacheKeyTO cacheKeyTO, final Method method, final Object args[]) throws CacheCenterConnectionException { if(null == shardedJedisPool || null == cacheKeyTO) { return null; } String cacheKey=cacheKeyTO.getCacheKey(); if(null == cacheKey || cacheKey.length() == 0) { return null; } CacheWrapper<Object> res=null; ShardedJedis shardedJedis=null; try { shardedJedis=shardedJedisPool.getResource(); Jedis jedis=shardedJedis.getShard(cacheKey); byte bytes[]=null; String hfield=cacheKeyTO.getHfield(); if(null == hfield || hfield.length() == 0) { bytes=jedis.get(keySerializer.serialize(cacheKey)); } else { bytes=jedis.hget(keySerializer.serialize(cacheKey), keySerializer.serialize(hfield)); } Type returnType=method.getGenericReturnType(); res=(CacheWrapper<Object>)getSerializer().deserialize(bytes, returnType); } catch(Exception ex) { logger.error(ex.getMessage(), ex); } finally { returnResource(shardedJedis); } return res; } /** * 根据缓存Key删除缓存 * @param cacheKeyTO 缓存Key */ @Override public void delete(CacheKeyTO cacheKeyTO) throws CacheCenterConnectionException { if(null == shardedJedisPool || null == cacheKeyTO) { return; } String cacheKey=cacheKeyTO.getCacheKey(); if(null == cacheKey || cacheKey.length() == 0) { return; } logger.debug("delete cache:" + cacheKey); ShardedJedis shardedJedis=null; try { shardedJedis=shardedJedisPool.getResource(); if("*".equals(cacheKey)) { Collection<Jedis> list=shardedJedis.getAllShards(); for(Jedis jedis: list) { jedis.flushDB(); } } else if(cacheKey.indexOf("*") != -1) { // 如果传进来的值中 带有 * 或 ? 号,则会使用批量删除(遍历所有Redis服务器),性能非常差,建议使用这种方法。 // 建议使用 hash表方缓存需要批量删除的数据。 batchDel(shardedJedis, cacheKey); } else { Jedis jedis=shardedJedis.getShard(cacheKey); String hfield=cacheKeyTO.getHfield(); if(null == hfield || hfield.length() == 0) { jedis.del(keySerializer.serialize(cacheKey)); } else { jedis.hdel(keySerializer.serialize(cacheKey), keySerializer.serialize(hfield)); } this.getAutoLoadHandler().resetAutoLoadLastLoadTime(cacheKeyTO); } } catch(Exception ex) { logger.error(ex.getMessage(), ex); } finally { returnResource(shardedJedis); } } private static byte[] delScript; static { StringBuilder tmp=new StringBuilder(); tmp.append("local keys = redis.call('keys', KEYS[1]);\n"); tmp.append("if(not keys or #keys == 0) then \n return nil; \n end \n"); tmp.append("redis.call('del', unpack(keys)); \n return keys;"); try { delScript=tmp.toString().getBytes("UTF-8"); } catch(UnsupportedEncodingException ex) { logger.error(ex.getMessage(), ex); } } private static final Map<Jedis, byte[]> delScriptSha=new ConcurrentHashMap<Jedis, byte[]>(); private void batchDel(ShardedJedis shardedJedis, String cacheKey) throws Exception { Collection<Jedis> list=shardedJedis.getAllShards(); for(Jedis jedis: list) {// 如果是批量删除缓存,则要遍历所有redis,避免遗漏。 byte[] sha=delScriptSha.get(jedis); byte[] key=keySerializer.serialize(cacheKey); if(null == sha) { sha=jedis.scriptLoad(delScript); delScriptSha.put(jedis, sha); } try { @SuppressWarnings("unchecked") List<String> keys=(List<String>)jedis.evalsha(sha, 1, key); if(null != keys && keys.size() > 0) { /* * for(String tmpKey: keys) { autoLoadHandler.resetAutoLoadLastLoadTime(tmpKey); } */ } } catch(Exception ex) { logger.error(ex.getMessage(), ex); try { sha=jedis.scriptLoad(delScript); delScriptSha.put(jedis, sha); @SuppressWarnings("unchecked") List<String> keys=(List<String>)jedis.evalsha(sha, 1, key); if(null != keys && keys.size() > 0) { /* * for(String tmpKey: keys) { autoLoadHandler.resetAutoLoadLastLoadTime(tmpKey); } */ } } catch(Exception ex1) { logger.error(ex1.getMessage(), ex1); } } } } public ShardedJedisPool getShardedJedisPool() { return shardedJedisPool; } public void setShardedJedisPool(ShardedJedisPool shardedJedisPool) { this.shardedJedisPool=shardedJedisPool; } public int getHashExpire() { return hashExpire; } public void setHashExpire(int hashExpire) { if(hashExpire < 0) { return; } this.hashExpire=hashExpire; } public boolean isHashExpireByScript() { return hashExpireByScript; } public void setHashExpireByScript(boolean hashExpireByScript) { this.hashExpireByScript=hashExpireByScript; } }