簡介 處理併發問題的重點不在於你的設計是怎樣的,而在於你要評估你的併發,併在併發範圍內處理。你預估你的併發是多少,然後測試r+m是否支持。緩存的目的是為了應對普通對象資料庫的讀寫限制,依托與nosql的優勢進行高速讀寫。 redis本身也有併發瓶頸。所以你要把讀寫和併發區分開來處理。只讀業務是不是可 ...
-
簡介
處理併發問題的重點不在於你的設計是怎樣的,而在於你要評估你的併發,併在併發範圍內處理。
你預估你的併發是多少,然後測試r+m是否支持。緩存的目的是為了應對普通對象資料庫的讀寫限制,依托與nosql的優勢進行高速讀寫。redis本身也有併發瓶頸。所以你要把讀寫和併發區分開來處理。只讀業務是不是可以用mysql分佈做只讀庫和只讀表,進行讀寫分離+庫分佈,
拆庫拆表不能搞定再考慮上多級緩存
任何設計,你外面套一層,就多一倍的維護成本,緩存不是萬金油。這裡多級緩存主要指的是二級緩存技術,也就是依托nosql的高速讀取優勢。
-
mybatis
如果底層ORM框架用的是mybatis框架,就可以用mybatis自帶的緩存機制,mybatis自帶一級緩存和二級緩存。
一級緩存指的是sqlsession緩存,是session級別緩存
二級緩存指的是mapper級別的緩存,同一個namespace公用這一個緩存,所以對SqlSession是共用的,這裡的namespace指的是xml里的命名空間
mybatis預設開啟一級緩存,二級緩存預設不開啟,需要手動開啟。
一級緩存
mybatis的一級緩存是SqlSession級別的緩存,在操作資料庫的時候需要先創建SqlSession會話對象,在對象中有一個HashMap用於存儲緩存數據,
此HashMap是當前會話對象私有的,別的SqlSession會話對象無法訪問。
具體流程:1.第一次執行select完畢會將查到的數據寫入SqlSession內的HashMap中緩存起來
2.第二次執行select會從緩存中查數據,如果select相同切傳參數一樣,那麼就能從緩存中返回數據,不用去資料庫了,從而提高了效率
註意事項:
1.如果SqlSession執行了DML操作(insert、update、delete),並commit了,那麼mybatis就會清空當前SqlSession緩存中的所有緩存數據,
這樣可以保證緩存中的存的數據永遠和資料庫中一致,避免出現臟讀2.當一個SqlSession結束後那麼他裡面的一級緩存也就不存在了,mybatis預設是開啟一級緩存,不需要配置
3.mybatis的緩存是基於[namespace:sql語句:參數]來進行緩存的,意思就是,SqlSession的HashMap存儲緩存數據時,
二級緩存
是使用[namespace:sql:參數]作為key,查詢返回的語句作為value保存的。
例如:-1242243203:1146242777:winclpt.bean.userMapper.getUser:0:2147483647:select * from user where id=?:19
二級緩存是mapper級別的緩存,也就是同一個namespace的mappe.xml,當多個SqlSession使用同一個Mapper操作資料庫的時候,得到的數據會緩存在同一個二級緩存區域二級緩存預設是沒有開啟的,需要在xml配置setting全局參數中配置開啟二級緩存
<settings> <setting name="cacheEnabled" value="true"/><!--開啟mybatis緩存 預設是false:關閉二級緩存--><settings>
然後再具體的xml里配置
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>當前mapper下所有語句開啟二級緩存
這裡配置了一個LRU緩存,並每隔60秒刷新,最大存儲512個對象,而卻返回的對象是只讀的
若想禁用當前select語句的二級緩存,添加useCache="false"修改如下:
<select id="getCountByName" parameterType="java.util.Map" resultType="INTEGER" statementType="CALLABLE" useCache="false">
具體流程:
1.當一個sqlseesion執行了一次select後,在關閉此session的時候,會將查詢結果緩存到二級緩存
2.當另一個sqlsession執行select時,首先會在他自己的一級緩存中找,如果沒找到,就回去二級緩存中找,
找到了就返回,就不用去資料庫了,從而減少了資料庫壓力提高了性能註意事項:
1.如果SqlSession執行了DML操作(insert、update、delete),並commit了,那麼mybatis就會清空當前mapper緩存中的所有緩存數據,這樣可以保證緩存中的存的數據永遠和資料庫中一致,避免出現臟讀
2.mybatis的緩存是基於[namespace:sql語句:參數]來進行緩存的,意思就是,SqlSession的HashMap存儲緩存數據時,是使用[namespace:sql:參數]作為key,查詢返回的語句作為value保存的。例如:-1242243203:1146242777:winclpt.bean.userMapper.getUser:0:2147483647:select * from user where id=?:19
-
redis
這裡我實現了spring集成Jedis,並通過解析key實現自定義緩存失效時間。官網可以下載linux下的redis,沒有windows下的,但micro開發小組在github上維護了windows環境下的redis,
https://github.com/MSOpenTech/redis/releases
pom.xml引入依賴
<!--redis start--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.6.0.RELEASE</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.3</version> </dependency> <!--redis end-->
在xml配置文件里配置各種bean
<!--JedisPool線程池--> <!--redis是單線程的,為了滿足java多線程需求,jedis引入線程池的概念--> <bean id="jedisPool" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="100"/> <property name="maxTotal" value="700"/> <property name="maxWaitMillis" value="1000"/> <property name="testOnBorrow" value="false"/> </bean> <!--創建redis連接工廠--> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="${redis.hostname}"/> <property name="port" value="${redis.port}"/> <property name="password" value="${redis.password}"/> <property name="database" value="${redis.database}"/> <property name="poolConfig" ref="jedisPool"/> </bean> <!--配置redistempLate --> <bean id="redistempLate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"/> <!--<property name="defaultSerializer">--> <!--<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>--> <!--</property>--> </bean> <!--配置緩存配置信息--> <!--這是用的spring的cacheManager,不支持緩存有效期動態配置--> <!--<bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">--> <!--<!–配置redis模板–>--> <!--<constructor-arg name="redisOperations" ref="redistempLate"/>--> <!--<!–預設緩存失效時間–>--> <!--<property name="defaultExpiration" value="120"/>--> <!--<!–是否使用首碼–>--> <!--<property name="usePrefix" value="false"/>--> <!--</bean>--> <!--手動實現的redisCacheManager,支持緩存有效期動態配置,可在@Cacheable中的value屬性添加有效時間--> <bean id="myRedisCacheManager" class="com.djkj.demo.common.MyRedisCacheManager"> <!--配置redis模板--> <constructor-arg name="redisOperations" ref="redistempLate"/> <!--預設緩存失效時間--> <property name="defaultExpiration" value="120"/> <!--是否使用首碼--> <property name="usePrefix" value="false"/> <!--緩存名字和有效期的分隔符--> <property name="separator" value="#"/> <!-- 多個緩存有效期,一般的單個工程可以省略此項 --> <!--<property name="expires">--> <!--<map>--> <!--<entry key="caiya_a" value="1800"/>--> <!--</map>--> <!--</property>--> </bean> <!--配置RedisCacheConfig,redis緩存啟動類--> <bean id="redisCacheConfig" class="com.djkj.demo.common.RedisCacheConfig"> <constructor-arg ref="jedisConnectionFactory"/> <constructor-arg ref="redistempLate"/> <constructor-arg ref="myRedisCacheManager"/> </bean>
這裡jedisPool的數據源地址根據實際情況設置,reis預設port為6379,如果你的redis設置了校驗密碼則這裡需要否則不用,database設置的0,redis預設生成15個資料庫,0表示採用的是第一個。
最主要的是redisCacheConfig緩存啟動類和myRedisCacheManager緩存管理類,啟動類里設置了緩存的key的生成策略,管理類主要實現了自定義有效期
redisCacheConfigpackage com.djkj.demo.common; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import java.lang.reflect.Method; @Configuration @EnableCaching public class RedisCacheConfig extends CachingConfigurerSupport { private volatile JedisConnectionFactory jedisConnectionFactory; private volatile RedisTemplate<String,String> redisTemplate; private volatile RedisCacheManager redisCacheManager; public RedisCacheConfig(){ super(); } public RedisCacheConfig(JedisConnectionFactory jedisConnectionFactory ,RedisTemplate<String,String> redisTemplate, RedisCacheManager redisCacheManager){ this.jedisConnectionFactory = jedisConnectionFactory; this.redisTemplate = redisTemplate; this.redisCacheManager = redisCacheManager; } public JedisConnectionFactory getJedisConnectionFactory() { return jedisConnectionFactory; } public RedisTemplate<String, String> getRedisTemplate() { return redisTemplate; } public RedisCacheManager getRedisCacheManager() { return redisCacheManager; }
//主鍵生成策略 @Bean public KeyGenerator customKeyGenerator(){ return new KeyGenerator() { @Override public Object generate(Object o, Method method, Object... objects) { StringBuilder stringBuilder=new StringBuilder(); stringBuilder.append(o.getClass().getName()); stringBuilder.append(method.getName()); for (Object obj : objects) { stringBuilder.append(obj.toString().hashCode()); } System.out.println(stringBuilder.toString()); return stringBuilder.toString(); } }; } }myRedisCacheManager
package com.djkj.demo.common; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.springframework.cache.Cache; import org.springframework.data.redis.cache.RedisCache; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCachePrefix; import org.springframework.data.redis.core.RedisOperations; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.util.Collection; import java.util.Collections; import java.util.regex.Pattern; public class MyRedisCacheManager extends RedisCacheManager { private static final Logger logger = Logger.getLogger(MyRedisCacheManager.class); private static final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("JavaScript"); private static final Pattern pattern = Pattern.compile("[+\\-*/%]"); private String defaultCacheName; private String separator = "#"; public MyRedisCacheManager(RedisOperations redisOperations) { this(redisOperations, Collections.<String>emptyList()); } public MyRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) { super(redisOperations, cacheNames); } @Override public Cache getCache(String name) { String cacheName=""; String expirationStr=""; Long expiration=0L; String[] params = name.split(getSeparator()); if(params.length>=1){ cacheName = params[0]; } if(params.length>=2){ expirationStr = params[1]; } if(StringUtils.isBlank(cacheName)){ cacheName = defaultCacheName; } Cache cache = (RedisCache) super.getCache(cacheName); if (cache == null) { return null; } if(StringUtils.isNotEmpty(expirationStr)){ try { expiration = Double.valueOf(expirationStr).longValue(); }catch (Exception e){ logger.error("expiration exchange failed!"); } } if (expiration==null || expiration == 0L) { logger.warn("Default expiration time will be used for cache '{}' because cannot parse '{}', cacheName : " + cacheName + ", name : " + name); return cache; } return new RedisCache(cacheName, (isUsePrefix() ? getCachePrefix().prefix(cacheName) : null), getRedisOperations(), expiration); } public String getSeparator() { return separator; } public void setSeparator(String separator) { this.separator = separator; } private Long getExpiration(final String name, final int separatorIndex) { Long expiration = null; String expirationAsString = name.substring(separatorIndex + 1); try { // calculate expiration, support arithmetic expressions. if(pattern.matcher(expirationAsString).find()){ expiration = (long) Double.parseDouble(scriptEngine.eval(expirationAsString).toString()); }else{ expiration = Long.parseLong(expirationAsString); } } catch (NumberFormatException ex) { logger.error(String.format("Cannnot separate expiration time from cache: '%s'", name), ex); } catch (ScriptException e) { logger.error(String.format("Cannnot separate expiration time from cache: '%s'", name), e); } return expiration; } @Override public void setUsePrefix(boolean usePrefix) { super.setUsePrefix(usePrefix); } @Override public void setCachePrefix(RedisCachePrefix cachePrefix) { super.setCachePrefix(cachePrefix); } public void setDefaultCacheName(String defaultCacheName) { this.defaultCacheName = defaultCacheName; } }
根據符號 # 將緩存名切割,前面的作為緩存名,後面的作為有效期
-
具體應用方法
在service層通過@Cacheable註解使用緩存
//設置緩存有效時間和刷新時間
@Cacheable(value="testPojoCache" ,keyGenerator = "customKeyGenerator")
@Override
public List<TestPojo> query(TestPojo bean) {
List<TestPojo> testList = testPojoMapper.query(bean);
if(testList.size()>0){
for(TestPojo pojo:testList){
pojo.setTime(sdf.format(new Date()));
pojo.setAttr1(bean.getAttr1());
}
}
return testList;
}testPojoCache表示緩存name,30表示有效期,keyGenerator表示key的生成策略,用的是在配置類里通過@Bean配置的bean。
-
進階說明
spring通過集成jedis的方式有別於直接通過獲取Jedis對象的set方法,通過jedis.set(key,value)只會在redis伺服器產生一條數據,而用註解會產生兩條數據,以上面的例子為例,首先會生成一條key為testPojoCache的緩存數據,value內容是一個集合,放的是通過生存策略生成的值,再以這生成的值為key生成一條緩存數據,value為這個service方法返回的對象數據。當你根據不同的條件參數調用service的方法,都會在testPojoCache里存放鍵值,然後再創建緩存。那怎麼調用緩存呢?在調用service方法前,redis會根據生成策略生成的值到testPojoCache里去找,看有沒有。若果有,根據鍵值獲取緩存;如果沒有就查詢資料庫。