工作中用到了springboot的缓存,使用起来挺方便的,直接引入redis或者ehcache这些缓存依赖包和相关缓存的starter依赖包,然后在启动类中加入@EnableCaching注解,然后在需要的地方就可以使用@Cacheable和@CacheEvict使用和删除缓存了。这个使用很简单,相信用过springboot缓存的都会玩,这里就不再多说了。美中不足的是,springboot使用了插件式的集成方式,虽然用起来很方便,但是当你集成ehcache的时候就是用ehcache,集成redis的时候就是用redis。如果想两者一起用,ehcache作为本地一级缓存,redis作为集成式的二级缓存,使用默认的方式据我所知是没法实现的(如果有高人可以实现,麻烦指点下我)。毕竟很多服务需要多点部署,如果单独选择ehcache可以很好地实现本地缓存,但是如果在多机之间共享缓存又需要比较费时的折腾,如果选用集中式的redis缓存,因为每次取数据都要走网络,总感觉性能不会太好。本话题主要就是讨论如何在springboot的基础上,无缝集成ehcache和redis作为一二级缓存,并且实现缓存同步。

  为了不要侵入springboot原本使用缓存的方式,这里自己定义了两个缓存相关的注解,如下

    @Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable { String value() default ""; String key() default ""; //泛型的Class类型
Class<?> type() default Exception.class; } @Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheEvict { String value() default ""; String key() default ""; }

  如上两个注解和spring中缓存的注解基本一致,只是去掉了一些不常用的属性。说到这里,不知道有没有朋友注意过,当你在springboot中单独使用redis缓存的时候,Cacheable和CacheEvict注解的value属性,实际上在redis中变成了一个zset类型的值的key,而且这个zset里面还是空的,比如@Cacheable(value="cache1",key="key1"),正常情况下redis中应该是出现cache1 -> map(key1,value1)这种形式,其中cache1作为缓存名称,map作为缓存的值,key作为map里的键,可以有效的隔离不同的缓存名称下的缓存。但是实际上redis里确是cache1 -> 空(zset)和key1 -> value1,两个独立的键值对,试验得知不同的缓存名称下的缓存完全是共用的,如果有感兴趣的朋友可以去试验下,也就是说这个value属性实际上是个摆设,键的唯一性只由key属性保证。我只能认为这是spring的缓存实现的bug,或者是特意这么设计的,(如果有知道啥原因的欢迎指点)。

  回到正题,有了注解还需要有个注解处理类,这里我使用aop的切面来进行拦截处理,原生的实现其实也大同小异。切面处理类如下:

    import com.xuanwu.apaas.core.multicache.annotation.CacheEvict;
import com.xuanwu.apaas.core.multicache.annotation.Cacheable;
import com.xuanwu.apaas.core.utils.JsonUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component; import java.lang.reflect.Method; /**
* 多级缓存切面
* @author rongdi
*/
@Aspect
@Component
public class MultiCacheAspect { private static final Logger logger = LoggerFactory.getLogger(MultiCacheAspect.class); @Autowired
private CacheFactory cacheFactory; //这里通过一个容器初始化监听器,根据外部配置的@EnableCaching注解控制缓存开关
private boolean cacheEnable; @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)")
public void cacheableAspect() {
} @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)")
public void cacheEvict() {
} @Around("cacheableAspect()")
public Object cache(ProceedingJoinPoint joinPoint) { //得到被切面修饰的方法的参数列表
Object[] args = joinPoint.getArgs();
// result是方法的最终返回结果
Object result = null;
//如果没有开启缓存,直接调用处理方法返回
if(!cacheEnable){
try {
result = joinPoint.proceed(args);
} catch (Throwable e) {
logger.error("",e);
}
return result;
} // 得到被代理方法的返回值类型
Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
// 得到被代理的方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 得到被代理的方法上的注解
Cacheable ca = method.getAnnotation(Cacheable.class);
//获得经过el解析后的key值
String key = parseKey(ca.key(),method,args);
Class<?> elementClass = ca.type();
//从注解中获取缓存名称
String name = ca.value(); try {
//先从ehcache中取数据
String cacheValue = cacheFactory.ehGet(name,key);
if(StringUtils.isEmpty(cacheValue)) {
//如果ehcache中没数据,从redis中取数据
cacheValue = cacheFactory.redisGet(name,key);
if(StringUtils.isEmpty(cacheValue)) {
//如果redis中没有数据
// 调用业务方法得到结果
result = joinPoint.proceed(args);
//将结果序列化后放入redis
cacheFactory.redisPut(name,key,serialize(result));
} else {
//如果redis中可以取到数据
//将缓存中获取到的数据反序列化后返回
if(elementClass == Exception.class) {
result = deserialize(cacheValue, returnType);
} else {
result = deserialize(cacheValue, returnType,elementClass);
}
}
//将结果序列化后放入ehcache
cacheFactory.ehPut(name,key,serialize(result));
} else {
//将缓存中获取到的数据反序列化后返回
if(elementClass == Exception.class) {
result = deserialize(cacheValue, returnType);
} else {
result = deserialize(cacheValue, returnType,elementClass);
}
} } catch (Throwable throwable) {
logger.error("",throwable);
} return result;
} /**
* 在方法调用前清除缓存,然后调用业务方法
* @param joinPoint
* @return
* @throws Throwable
*
*/
@Around("cacheEvict()")
public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable {
// 得到被代理的方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//得到被切面修饰的方法的参数列表
Object[] args = joinPoint.getArgs();
// 得到被代理的方法上的注解
CacheEvict ce = method.getAnnotation(CacheEvict.class);
//获得经过el解析后的key值
String key = parseKey(ce.key(),method,args);
//从注解中获取缓存名称
String name = ce.value();
// 清除对应缓存
cacheFactory.cacheDel(name,key);
return joinPoint.proceed(args);
} /**
* 获取缓存的key
* key 定义在注解上,支持SPEL表达式
* @return
*/
private String parseKey(String key,Method method,Object [] args){ if(StringUtils.isEmpty(key)) return null; //获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method); //使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SPEL上下文中
for(int i=0;i<paraNameArr.length;i++){
context.setVariable(paraNameArr[i], args[i]);
}
return parser.parseExpression(key).getValue(context,String.class);
} //序列化
private String serialize(Object obj) { String result = null;
try {
result = JsonUtil.serialize(obj);
} catch(Exception e) {
result = obj.toString();
}
return result; } //反序列化
private Object deserialize(String str,Class clazz) { Object result = null;
try {
if(clazz == JSONObject.class) {
result = new JSONObject(str);
} else if(clazz == JSONArray.class) {
result = new JSONArray(str);
} else {
result = JsonUtil.deserialize(str,clazz);
}
} catch(Exception e) {
}
return result; } //反序列化,支持List<xxx>
private Object deserialize(String str,Class clazz,Class elementClass) { Object result = null;
try {
if(clazz == JSONObject.class) {
result = new JSONObject(str);
} else if(clazz == JSONArray.class) {
result = new JSONArray(str);
} else {
result = JsonUtil.deserialize(str,clazz,elementClass);
}
} catch(Exception e) {
}
return result; } public void setCacheEnable(boolean cacheEnable) {
this.cacheEnable = cacheEnable;
} }

  上面这个界面使用了一个cacheEnable变量控制是否使用缓存,为了实现无缝的接入springboot,必然需要受到原生@EnableCaching注解的控制,这里我使用一个spring容器加载完成的监听器,然后在监听器里找到是否有被@EnableCaching注解修饰的类,如果有就从spring容器拿到MultiCacheAspect对象,然后将cacheEnable设置成true。这样就可以实现无缝接入springboot,不知道朋友们还有没有更加优雅的方法呢?欢迎交流!监听器类如下

    import com.xuanwu.apaas.core.multicache.CacheFactory;
import com.xuanwu.apaas.core.multicache.MultiCacheAspect;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component; import java.util.Map; /**
* 用于spring加载完成后,找到项目中是否有开启缓存的注解@EnableCaching
* @author rongdi
*/
@Component
public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> { @Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 判断根容器为Spring容器,防止出现调用两次的情况(mvc加载也会触发一次)
if(event.getApplicationContext().getParent()==null){
//得到所有被@EnableCaching注解修饰的类
Map<String,Object> beans = event.getApplicationContext().getBeansWithAnnotation(EnableCaching.class);
if(beans != null && !beans.isEmpty()) {
MultiCacheAspect multiCache = (MultiCacheAspect)event.getApplicationContext().getBean("multiCacheAspect");
multiCache.setCacheEnable(true);
} }
}
}

  实现了无缝接入,还需要考虑多点部署的时候,多点的ehcache怎么和redis缓存保持一致的问题。在正常应用中,一般redis适合长时间的集中式缓存,ehcache适合短时间的本地缓存,假设现在有A,B和C服务器,A和B部署了业务服务,C部署了redis服务。当请求进来,前端入口不管是用LVS或者nginx等负载软件,请求都会转发到某一个具体服务器,假设转发到了A服务器,修改了某个内容,而这个内容在redis和ehcache中都有,这时候,A服务器的ehcache缓存,和C服务器的redis不管控制缓存失效也好,删除也好,都比较容易,但是这时候B服务器的ehcache怎么控制失效或者删除呢?一般比较常用的方式就是使用发布订阅模式,当需要删除缓存的时候在一个固定的通道发布一个消息,然后每个业务服务器订阅这个通道,收到消息后删除或者过期本地的ehcache缓存(最好是使用过期,但是redis目前只支持对key的过期操作,没办法操作key下的map里的成员的过期,如果非要强求用过期,可以自己加时间戳自己实现,不过用删除出问题的几率也很小,毕竟加缓存的都是读多写少的应用,这里为了方便都是直接删除缓存)。总结起来流程就是更新某条数据,先删除redis中对应的缓存,然后发布一个缓存失效的消息在redis的某个通道中,本地的业务服务去订阅这个通道的消息,当业务服务收到这个消息后去删除本地对应的ehcache缓存,redis的各种配置如下

  

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuanwu.apaas.core.multicache.subscriber.MessageSubscriber;
import org.springframework.cache.CacheManager;
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.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import java.util.HashMap;
import java.util.Map; @Configuration
public class MultiCacheConfig { @Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
//设置缓存过期时间(秒)
Map<String, Long> expires = new HashMap<>();
expires.put("ExpOpState",0L);
expires.put("ImpOpState",0L);
rcm.setExpires(expires);
rcm.setDefaultExpiration(600);
return rcm;
} @Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
StringRedisSerializer redisSerializer = new StringRedisSerializer();
template.setValueSerializer(redisSerializer);
template.afterPropertiesSet();
return template;
} /**
* redis消息监听器容器
* 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
* 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//订阅了一个叫redis.uncache的通道
container.addMessageListener(listenerAdapter, new PatternTopic("redis.uncache"));
//这个container 可以添加多个 messageListener
return container;
} /**
* 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) {
//这个地方 是给messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“handle”
return new MessageListenerAdapter(receiver, "handle");
} }

消息发布类如下:

    import com.xuanwu.apaas.core.multicache.CacheFactory;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; @Component
public class MessageSubscriber { private static final Logger logger = LoggerFactory.getLogger(MessageSubscriber.class); @Autowired
private CacheFactory cacheFactory; /**
* 接收到redis订阅的消息后,将ehcache的缓存失效
* @param message 格式为name_key
*/
public void handle(String message){ logger.debug("redis.ehcache:"+message);
if(StringUtils.isEmpty(message)) {
return;
}
String[] strs = message.split("#");
String name = strs[0];
String key = null;
if(strs.length == 2) {
key = strs[1];
}
cacheFactory.ehDel(name,key); } }

  具体操作缓存的类如下:

import com.xuanwu.apaas.core.multicache.publisher.MessagePublisher;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import java.io.InputStream; /**
* 多级缓存切面
* @author rongdi
*/
@Component
public class CacheFactory { private static final Logger logger = LoggerFactory.getLogger(CacheFactory.class); @Autowired
private RedisTemplate redisTemplate; @Autowired
private MessagePublisher messagePublisher; private CacheManager cacheManager; public CacheFactory() {
InputStream is = this.getClass().getResourceAsStream("/ehcache.xml");
if(is != null) {
cacheManager = CacheManager.create(is);
}
} public void cacheDel(String name,String key) {
//删除redis对应的缓存
redisDel(name,key);
//删除本地的ehcache缓存,可以不需要,订阅器那里会删除
// ehDel(name,key);
if(cacheManager != null) {
//发布一个消息,告诉订阅的服务该缓存失效
messagePublisher.publish(name, key);
}
} public String ehGet(String name,String key) {
if(cacheManager == null) return null;
Cache cache=cacheManager.getCache(name);
if(cache == null) return null;
cache.acquireReadLockOnKey(key);
try {
Element ele = cache.get(key);
if(ele == null) return null;
return (String)ele.getObjectValue();
} finally {
cache.releaseReadLockOnKey(key);
} } public String redisGet(String name,String key) {
HashOperations<String,String,String> oper = redisTemplate.opsForHash();
try {
return oper.get(name, key);
} catch(RedisConnectionFailureException e) {
//连接失败,不抛错,直接不用redis缓存了
logger.error("connect redis error ",e);
return null;
}
} public void ehPut(String name,String key,String value) {
if(cacheManager == null) return;
if(!cacheManager.cacheExists(name)) {
cacheManager.addCache(name);
}
Cache cache=cacheManager.getCache(name);
//获得key上的写锁,不同key互相不影响,类似于synchronized(key.intern()){}
cache.acquireWriteLockOnKey(key);
try {
cache.put(new Element(key, value));
} finally {
//释放写锁
cache.releaseWriteLockOnKey(key);
}
} public void redisPut(String name,String key,String value) {
HashOperations<String,String,String> oper = redisTemplate.opsForHash();
try {
oper.put(name, key, value);
} catch (RedisConnectionFailureException e) {
//连接失败,不抛错,直接不用redis缓存了
logger.error("connect redis error ",e);
}
} public void ehDel(String name,String key) {
if(cacheManager == null) return;
Cache cache = cacheManager.getCache(name);
if(cache != null) {
//如果key为空,直接根据缓存名删除
if(StringUtils.isEmpty(key)) {
cacheManager.removeCache(name);
} else {
cache.remove(key);
}
}
} public void redisDel(String name,String key) {
HashOperations<String,String,String> oper = redisTemplate.opsForHash();
try {
//如果key为空,直接根据缓存名删除
if(StringUtils.isEmpty(key)) {
redisTemplate.delete(name);
} else {
oper.delete(name,key);
}
} catch (RedisConnectionFailureException e) {
//连接失败,不抛错,直接不用redis缓存了
logger.error("connect redis error ",e);
}
}
}

 工具类如下

    import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject; import java.util.*; public class JsonUtil { private static ObjectMapper mapper; static {
mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
} /**
* 将对象序列化成json
*
* @param obj 待序列化的对象
* @return
* @throws Exception
*/
public static String serialize(Object obj) throws Exception { if (obj == null) {
throw new IllegalArgumentException("obj should not be null");
}
return mapper.writeValueAsString(obj);
} /**
带泛型的反序列化,比如一个JSONArray反序列化成List<User>
*/
public static <T> T deserialize(String jsonStr, Class<?> collectionClass,
Class<?>... elementClasses) throws Exception {
JavaType javaType = mapper.getTypeFactory().constructParametrizedType(
collectionClass, collectionClass, elementClasses);
return mapper.readValue(jsonStr, javaType);
} /**
* 将json字符串反序列化成对象
* @param src 待反序列化的json字符串
* @param t 反序列化成为的对象的class类型
* @return
* @throws Exception
*/
public static <T> T deserialize(String src, Class<T> t) throws Exception {
if (src == null) {
throw new IllegalArgumentException("src should not be null");
}
if("{}".equals(src.trim())) {
return null;
}
return mapper.readValue(src, t);
} }

  具体使用缓存,和之前一样只需要关注@Cacheable和@CacheEvict注解,同样也支持spring的el表达式。而且这里的value属性表示的缓存名称也没有上面说的那个问题,完全可以用value隔离不同的缓存,例子如下
  @Cacheable(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
  @CacheEvict(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
  附上主要的依赖包
  "org.springframework.boot:spring-boot-starter-redis:1.4.2.RELEASE",
  'net.sf.ehcache:ehcache:2.10.4',
  "org.json:json:20160810"

springboot中使用自定义两级缓存的更多相关文章

  1. Redis+Caffeine两级缓存,让访问速度纵享丝滑

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 在高性能的服务架构设计中,缓存是一个不可或缺的环节.在实际的项目中,我们通常会将一些热点数据存储到Redis或MemCache这类缓存中间件中, ...

  2. 基于Spring接口,集成Caffeine+Redis两级缓存

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 在上一篇文章Redis+Caffeine两级缓存,让访问速度纵享丝滑中,我们介绍了3种整合Caffeine和Redis作为两级缓存使用的方法,虽 ...

  3. Spring+ehcache+redis两级缓存

    问题描述 场景:我们的应用系统是分布式集群的,可横向扩展的.应用中某个接口操作满足以下一个或多个条件: 1. 接口运行复杂代价大, 2. 接口返回数据量大, 3. 接口的数据基本不会更改, 4. 接口 ...

  4. 用guava快速打造两级缓存能力

    首先,咱们都有一共识,即可以使用缓存来提升系统的访问速度! 现如今,分布式缓存这么强大,所以,大部分时候,我们可能都不会去关注本地缓存了! 而在一起高并发的场景,如果我们一味使用nosql式的缓存,如 ...

  5. Springboot中IDE支持两种打包方式,即jar包和war包

    Springboot中IDE支持两种打包方式,即jar包和war包 打包之前修改pom.xml中的packaging节点,改为jar或者war    在项目的根目录执行maven 命令clean pa ...

  6. J2CACHE 两级缓存框架

    概述 缓存框架我们有ehcache 和 redis 分别是 本地内存缓存和 分布式缓存框架.在实际情况下如果单台机器 使用ehcache 就可以满足需求了,速度快效率高,有些数据如果需要多台机器共享这 ...

  7. SpringBoot学习笔记(10)-----SpringBoot中使用Redis/Mongodb和缓存Ehcache缓存和redis缓存

    1. 使用Redis 在使用redis之前,首先要保证安装或有redis的服务器,接下就是引入redis依赖. pom.xml文件如下 <dependency> <groupId&g ...

  8. mybatis两级缓存原理剖析

    https://blog.csdn.net/zhurhyme/article/details/81064108 对于mybatis的缓存认识一直有一个误区,所以今天写一篇文章帮自己订正一下.mybat ...

  9. SpringBoot中设置自定义拦截器

    SpringBoot中设置自动以拦截器需要写一个类继承HandlerInterceptorAdapter并重写preHandle方法 例子 public class AuthorityIntercep ...

随机推荐

  1. Kubernetes如何支持有状态服务的部署?

    作者:Jack47 转载请保留作者和原文出处 PS:如果喜欢我写的文章,欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. Kubernetes对无状态服务有完善的支持 ...

  2. JAVAEE——BOS物流项目12:角色、用户管理,使用ehcache缓存,系统菜单根据登录人展示

    1 学习计划 1.角色管理 n 添加角色功能 n 角色分页查询 2.用户管理 n 添加用户功能 n 用户分页查询 3.修改Realm中授权方法(查询数据库) 4.使用ehcache缓存权限数据 n 添 ...

  3. MySQL中的行级锁,表级锁,页级锁

    在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足. 在数据库的锁机制中介绍过,在DBMS中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引 ...

  4. OO开发思想:面向对象的开发方法(Object oriented,OO)

    面向对象的开发方法(Object oriented,OO)认为是好文章吧,拿来分享一下(转载) 面向对象的开发方法(Object oriented,OO) 从事软件开发的工程 师们常常有这样 的体会: ...

  5. uwsgi 服务 invalid request block size: 4161 (max 4096)...skip问题的解决

    问题报错: invalid request block size: 4161 (max 4096)...skip 问题原因:默认的uwsgi分配一个小的buffer(4k)来接收每个请求的头信息,如果 ...

  6. 与班尼特·胡迪一起攻破浮空城 (HZNU-2264)

    与班尼特·胡迪一起攻破浮空城 AC Time Limit:  1 s      Memory Limit:   256 MB Description 桐人为了拯救被困在浮空城堡最顶层的亚丝娜,决定从第 ...

  7. 利用redis自制幻灯片弹幕

    前段时间赶项目,忙结婚,各大技术平台都没时间上,不过还是抽出点时间为自己的婚礼做了一点小玩具,今天我就来给大家分享一下. 先来看一下效果 这个项目是基于微信个人订阅号的,订阅号的开发在此我就不再赘述了 ...

  8. SOFA 源码分析 — 自定义线程池原理

    前言 在 SOFA-RPC 的官方介绍里,介绍了自定义线程池,可以为指定服务设置一个独立的业务线程池,和 SOFARPC 自身的业务线程池是隔离的.多个服务可以共用一个独立的线程池. API使用方式如 ...

  9. 云计算一:VMware workstation的安装和使用教程

    VMware workstation的安装和使用教程 一.VMware 安装 1.从网上找到VMware的安装包以及要安装的映像文件,下载到本地,然后备份一份存储到百度云盘. 链接:http://pa ...

  10. 5月2日——iOS11定位失效问题

    所存在的问题: (1)定位不能正常使用 (2)首次安装APP 时 "是否允许使用定位信息"  系统提示框不显示 iOS定位失效原因: 因为苹果现在增加了一项新的隐私保护功能 NSL ...