游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

基于Spring接口,集成CaffeineRedis两级缓存

  原创:微信公众号码农参上,欢迎分享,转载请保留出处。
  在上一篇文章RedisCaffeine两级缓存,让访问速度纵享丝滑中,我们介绍了3种整合Caffeine和Redis作为两级缓存使用的方法,虽然说能够实现功能,但实现手法还是太粗糙了,并且遗留了一些问题没有处理。本文将在上一篇的基础上,围绕两个方面进行进一步的改造:JSR107定义了缓存使用规范,spring中提供了基于这个规范的接口,所以我们可以直接使用spring中的接口进行Caffeine和Redis两级缓存的整合改造在分布式环境下,如果一台主机的本地缓存进行修改,需要通知其他主机修改本地缓存,解决分布式环境下本地缓存一致性问题
  好了,在明确了需要的改进问题后,下面我们开始正式修改。改造
  在上篇文章的v3版本中,我们使用自定义注解的方式实现了两级缓存通过一个注解管理的功能。本文我们换一种方式,直接通过扩展spring提供的接口来实现这个功能,在进行整合之前,我们需要简单了解一下JSR107缓存规范。JSR107规范
  在JSR107缓存规范中定义了5个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry,参考下面这张图,可以看到除了Entry和Expiry以外,从上到下都是一对多的包含关系。
  从上面这张图我们可以看出,一个应用可以创建并管理多个CachingProvider,同样一个CachingProvider也可以管理多个CacheManager,缓存管理器CacheManager中则维护了多个Cache。
  Cache是一个类似Map的数据结构,Entry就是其中存储的每一个keyvalue数据对,并且每个Entry都有一个过期时间Expiry。而我们在使用spring集成第三方的缓存时,只需要实现Cache和CacheManager这两个接口就可以了,下面分别具体来看一下。Cache
  spring中的Cache接口规范了缓存组件的定义,包含了缓存的各种操作,实现具体缓存操作的管理。例如我们熟悉的RedisCache、EhCacheCache等,都实现了这个接口。
  在Cache接口中,定义了get、put、evict、clear等方法,分别对应缓存的存入、取出、删除、清空操作。不过我们这里不直接使用Cache接口,上面这张图中的AbstractValueAdaptingCache是一个抽象类,它已经实现了Cache接口,是spring在Cache接口的基础上帮助我们进行了一层封装,所以我们直接继承这个类就可以。
  继承AbstractValueAdaptingCache抽象类后,除了创建Cache的构造方法外,还需要实现下面的几个方法:在缓存中实际执行查找的操作,父类的get()方法会调用这个方法protectedabstractObjectlookup(Objectkey);通过key获取缓存值,如果没有找到,会调用valueLoader的call()方法publicTTget(Objectkey,CallableTvalueLoader);将数据放入缓存中publicvoidput(Objectkey,Objectvalue);删除缓存publicvoidevict(Objectkey);清空缓存中所有数据publicvoidclear();获取缓存名称,一般在CacheManager创建时指定StringgetName();获取实际使用的缓存ObjectgetNativeCache();
  因为要整合RedisTemplate和Caffeine的Cache,所以这些都需要在缓存的构造方法中传入,除此之外构造方法中还需要再传出缓存名称cacheName,以及在配置文件中实际配置的一些缓存参数。先看一下构造方法的实现:publicclassDoubleCacheextendsAbstractValueAdaptingCache{privateStringcacheName;privateRedisTemplateObject,ObjectredisTemplate;privateCacheObject,ObjectcaffeineCache;privateDoubleCacheConfigdoubleCacheConfig;protectedDoubleCache(booleanallowNullValues){super(allowNullValues);}publicDoubleCache(StringcacheName,RedisTemplateObject,ObjectredisTemplate,CacheObject,ObjectcaffeineCache,DoubleCacheConfigdoubleCacheConfig){super(doubleCacheConfig。getAllowNull());this。cacheNamecacheName;this。redisTemplateredisTemplate;this。caffeineCachecaffeineCache;this。doubleCacheConfigdoubleCacheConfig;}。。。}
  抽象父类的构造方法中只有一个boolean类型的参数allowNullValues,表示是否允许缓存对象为null。除此之外,AbstractValueAdaptingCache中还定义了两个包装方法来配合这个参数进行使用,分别是toStoreValue和fromStoreValue,特殊用途是用于在缓存null对象时进行包装、以及在获取时进行解析并返回。
  我们之后会在CacheManager中调用后面这个自己实现的构造方法,来实例化Cache对象,参数中DoubleCacheConfig是使用ConfigurationProperties读取的yml配置文件封装的数据对象,会在后面使用。
  当一个方法添加了Cacheable注解时,执行时会先调用父类AbstractValueAdaptingCache中的get(key)方法,它会再调用我们自己实现的lookup方法。在实际执行查找操作的lookup方法中,我们的逻辑仍然是先查找Caffeine、没有找到时再查找Redis:OverrideprotectedObjectlookup(Objectkey){先从caffeine中查找ObjectobjcaffeineCache。getIfPresent(key);if(Objects。nonNull(obj)){log。info(getdatafromcaffeine);returnobj;}再从redis中查找StringredisKeythis。name:key;objredisTemplate。opsForValue()。get(redisKey);if(Objects。nonNull(obj)){log。info(getdatafromredis);caffeineCache。put(key,obj);}returnobj;}
  如果lookup方法的返回结果不为null,那么就会直接返回结果给调用方。如果返回为null时,就会执行原方法,执行完成后调用put方法,将数据放入缓存中。接下来我们实现put方法:Overridepublicvoidput(Objectkey,Objectvalue){if(!isAllowNullValues()Objects。isNull(value)){log。error(thevalueNULLwillnotbecached);return;}使用toStoreValue(value)包装,解决caffeine不能存null的问题caffeineCache。put(key,toStoreValue(value));null对象只存在caffeine中一份就够了,不用存redis了if(Objects。isNull(value))return;StringredisKeythis。cacheName:key;OptionalLongexpireOptOptional。ofNullable(doubleCacheConfig)。map(DoubleCacheConfig::getRedisExpire);if(expireOpt。isPresent()){redisTemplate。opsForValue()。set(redisKey,toStoreValue(value),expireOpt。get(),TimeUnit。SECONDS);}else{redisTemplate。opsForValue()。set(redisKey,toStoreValue(value));}}
  上面我们对于是否允许缓存空对象进行了判断,能够缓存空对象的好处之一就是可以避免缓存穿透。需要注意的是,Caffeine中是不能直接缓存null的,因此可以使用父类提供的toStoreValue()方法,将它包装成一个NullValue类型。在取出对象时,如果是NullValue,也不用我们自己再去调用fromStoreValue()将这个包装类型还原,父类的get方法中已经帮我们做好了。
  另外,上面在put方法中缓存空对象时,只在Caffeine缓存中一份即可,可以不用在Redis中再存一份。
  缓存的删除方法evict()和清空方法clear()的实现就比较简单了,直接删除一条或全部数据即可:Overridepublicvoidevict(Objectkey){redisTemplate。delete(this。cacheName:key);caffeineCache。invalidate(key);}Overridepublicvoidclear(){SetObjectkeysredisTemplate。keys(this。cacheName。concat(:));for(Objectkey:keys){redisTemplate。delete(String。valueOf(key));}caffeineCache。invalidateAll();}
  获取缓存cacheName和实际缓存的方法实现:OverridepublicStringgetName(){returnthis。cacheName;}OverridepublicObjectgetNativeCache(){returnthis;}
  最后,我们再来看一下带有两个参数的get方法,为什么把这个方法放到最后来说呢,因为如果我们只是使用注解来管理缓存的话,那么这个方法不会被调用到,简单看一下实现:OverridepublicTTget(Objectkey,CallableTvalueLoader){ReentrantLocklocknewReentrantLock();try{lock。lock();加锁Objectobjlookup(key);if(Objects。nonNull(obj)){return(T)obj;}没有找到objvalueLoader。call();put(key,obj);放入缓存return(T)obj;}catch(Exceptione){log。error(e。getMessage());}finally{lock。unlock();}returnnull;}
  方法的实现比较容易理解,还是先调用lookup方法寻找是否已经缓存了对象,如果没有找到那么就调用Callable中的call方法进行获取,并在获取完成后存入到缓存中去。至于这个方法如何使用,具体代码我们放在后面使用这一块再看。
  需要注意的是,这个方法的接口注释中强调了需要我们自己来保证方法同步,因此这里使用了ReentrantLock进行了加锁操作。到这里,Cache的实现就完成了,下面我们接着看另一个重要的接口CacheManager。CacheManager
  从名字就可以看出,CacheManager是一个缓存管理器,它可以被用来管理一组Cache。在上一篇文章的v2版本中,我们使用的CaffeineCacheManager就实现了这个接口,除此之外还有RedisCacheManager、EhCacheCacheManager等也都是通过这个接口实现。
  下面我们要自定义一个类实现CacheManager接口,管理上面实现的DoubleCache作为spring中的缓存使用。接口中需要实现的方法只有下面两个:根据cacheName获取Cache实例,不存在时进行创建CachegetCache(Stringname);返回管理的所有cacheNameCollectionStringgetCacheNames();
  在自定义的缓存管理器中,我们要使用ConcurrentHashMap维护一组不同的Cache,再定义一个构造方法,在参数中传入已经在spring中配置好的RedisTemplate,以及相关的缓存配置参数:publicclassDoubleCacheManagerimplementsCacheManager{MapString,CachecacheMapnewConcurrentHashMap();privateRedisTemplateObject,ObjectredisTemplate;privateDoubleCacheConfigdcConfig;publicDoubleCacheManager(RedisTemplateObject,ObjectredisTemplate,DoubleCacheConfigdoubleCacheConfig){this。redisTemplateredisTemplate;this。dcConfigdoubleCacheConfig;}。。。}
  然后实现getCache方法,逻辑很简单,先根据name从Map中查找对应的Cache,如果找到则直接返回,这个参数name就是上一篇文章中提到的cacheName,CacheManager根据它实现不同Cache的隔离。
  如果没有根据名称找到缓存的话,那么新建一个DoubleCache对象,并放入Map中。这里使用的ConcurrentHashMap的putIfAbsent()方法放入,避免重复创建Cache以及造成Cache内数据的丢失。具体代码如下:OverridepublicCachegetCache(Stringname){CachecachecacheMap。get(name);if(Objects。nonNull(cache)){returncache;}cachenewDoubleCache(name,redisTemplate,createCaffeineCache(),dcConfig);CacheoldCachecacheMap。putIfAbsent(name,cache);returnoldCachenull?cache:oldCache;}
  在上面创建DoubleCache对象的过程中,需要先创建一个Caffeine的Cache对象作为参数传入,这一过程主要是根据实际项目的配置文件中的具体参数进行初始化,代码如下:privatecom。github。benmanes。caffeine。cache。CachecreateCaffeineCache(){CaffeineObject,ObjectcaffeineBuilderCaffeine。newBuilder();OptionalDoubleCacheConfigdcConfigOptOptional。ofNullable(this。dcConfig);dcConfigOpt。map(DoubleCacheConfig::getInit)。ifPresent(initcaffeineBuilder。initialCapacity(init));dcConfigOpt。map(DoubleCacheConfig::getMax)。ifPresent(maxcaffeineBuilder。maximumSize(max));dcConfigOpt。map(DoubleCacheConfig::getExpireAfterWrite)。ifPresent(eawcaffeineBuilder。expireAfterWrite(eaw,TimeUnit。SECONDS));dcConfigOpt。map(DoubleCacheConfig::getExpireAfterAccess)。ifPresent(eaacaffeineBuilder。expireAfterAccess(eaa,TimeUnit。SECONDS));dcConfigOpt。map(DoubleCacheConfig::getRefreshAfterWrite)。ifPresent(rawcaffeineBuilder。refreshAfterWrite(raw,TimeUnit。SECONDS));returncaffeineBuilder。build();}
  getCacheNames方法很简单,直接返回Map的keySet就可以了,代码如下:OverridepublicCollectionStringgetCacheNames(){returncacheMap。keySet();}配置使用
  在application。yml文件中配置缓存的参数,代码中使用ConfigurationProperties接收到DoubleCacheConfig类中:doublecache:allowNull:trueinit:128max:1024expireAfterWrite:30Caffeine过期时间redisExpire:60Redis缓存过期时间
  配置自定义的DoubleCacheManager作为默认的缓存管理器:ConfigurationpublicclassCacheConfig{AutowiredDoubleCacheConfigdoubleCacheConfig;BeanpublicDoubleCacheManagercacheManager(RedisTemplateObject,ObjectredisTemplate,DoubleCacheConfigdoubleCacheConfig){returnnewDoubleCacheManager(redisTemplate,doubleCacheConfig);}}
  Service中的代码还是老样子,不需要在代码中手动操作缓存,只要直接在方法上使用Cache相关注解即可:ServiceSlf4jAllArgsConstructorpublicclassOrderServiceImplimplementsOrderService{privatefinalOrderMapperorderMapper;Cacheable(valueorder,keyid)publicOrdergetOrderById(Longid){OrdermyOrderorderMapper。selectOne(newLambdaQueryWrapperOrder()。eq(Order::getId,id));returnmyOrder;}CachePut(cacheNamesorder,keyorder。id)publicOrderupdateOrder(Orderorder){orderMapper。updateById(order);returnorder;}CacheEvict(cacheNamesorder,keyid)publicvoiddeleteOrder(Longid){orderMapper。deleteById(id);}没有注解,使用get(key,callable)方法publicOrdergetOrderById2(Longid){DoubleCacheManagercacheManagerSpringContextUtil。getBean(DoubleCacheManager。class);CachecachecacheManager。getCache(order);Orderorder(Order)cache。get(id,(CallableObject)(){log。info(getdatafromdatabase);OrdermyOrderorderMapper。selectOne(newLambdaQueryWrapperOrder()。eq(Order::getId,id));returnmyOrder;});returnorder;}}
  注意最后这个没有添加任何注解的方法,只有以这种方式调用时才会执行我们在DoubleCache中自己实现的get(key,callable)方法。到这里,基于JSR107规范和spring接口的两级缓存改造就完成了,下面我们看一下遗漏的第二个问题。分布式环境改造
  前面我们说了,在分布式环境下,可能会存在各个主机上一级缓存不一致的问题。当一台主机修改了本地缓存后,其他主机是没有感知的,仍然保持了之前的缓存,那么这种情况下就可能取到脏数据。既然我们在项目中已经使用了Redis,那么就可以使用它的发布订阅功能来使各个节点的缓存进行同步。定义消息体
  在使用Redis发送消息前,需要先定义一个消息对象。其中的数据包括消息要作用于的Cache名称、操作类型、数据以及发出消息的源主机标识:DataNoArgsConstructorAllArgsConstructorpublicclassCacheMassageimplementsSerializable{privatestaticfinallongserialVersionUID3574997636829868400L;privateStringcacheName;privateCacheMsgTypetype;标识更新或删除操作privateObjectkey;privateObjectvalue;privateStringmsgSource;源主机标识,用来避免重复操作}
  定义一个枚举来标识消息的类型,是要进行更新还是删除操作:publicenumCacheMsgType{UPDATE,DELETE;}
  消息体中的msgSource是添加的一个消息源主机的标识,添加这个是为了避免收到当前主机发送的消息后,再进行重复操作,也就是说收到本机发出的消息直接丢掉什么都不做就可以了。源主机标识这里使用的是主机ip加项目端口的方式,获取方法如下:publicstaticStringgetMsgSource()throwsUnknownHostException{StringhostInetAddress。getLocalHost()。getHostAddress();EnvironmentenvSpringContextUtil。getBean(Environment。class);Stringportenv。getProperty(server。port);returnhost:port;}
  这样消息体的定义就完成了,之后只要调用redisTemplate的convertAndSend方法就可以把这个对象发布到指定的主题上了。Redis消息配置
  要使用Redis的消息监听功能,需要配置两项内容:MessageListenerAdapter:消息监听适配器,可以在其中指定自定义的监听代理类,并且可以自定义使用哪个方法处理监听逻辑RedisMessageListenerContainer:一个可以为消息监听器提供异步行为的容器,并且提供消息转换和分派等底层功能ConfigurationpublicclassMessageConfig{publicstaticfinalStringTOPICcache。msg;BeanRedisMessageListenerContainercontainer(MessageListenerAdapterlistenerAdapter,RedisConnectionFactoryredisConnectionFactory){RedisMessageListenerContainercontainernewRedisMessageListenerContainer();container。setConnectionFactory(redisConnectionFactory);container。addMessageListener(listenerAdapter,newPatternTopic(TOPIC));returncontainer;}BeanMessageListenerAdapteradapter(RedisMessageReceiverreceiver){returnnewMessageListenerAdapter(receiver,receive);}}
  在上面的监听适配器MessageListenerAdapter中,我们传入了一个自定义的RedisMessageReceiver接收并处理消息,并指定使用它的receive方法来处理监听到的消息,下面我们就来看看它如何接收消息并消费。消息消费逻辑
  定义一个类RedisMessageReceiver来接收并消费消息,需要在它的方法中实现以下功能:反序列化接收到的消息,转换为前面定义的CacheMassage类型对象根据消息的主机标识判断这条消息是不是本机发出的,如果是那么直接丢弃,只有接收到其他主机发出的消息才进行处理使用cacheName得到具体使用的那一个DoubleCache实例根据消息的类型判断要执行的是更新还是删除操作,调用对应的方法Slf4jComponentAllArgsConstructorpublicclassRedisMessageReceiver{privatefinalRedisTemplateredisTemplate;privatefinalDoubleCacheManagermanager;接收通知,进行处理publicvoidreceive(Stringmessage)throwsUnknownHostException{CacheMassagemsg(CacheMassage)redisTemplate。getValueSerializer()。deserialize(message。getBytes());log。info(msg。toString());如果是本机发出的消息,那么不进行处理if(msg。getMsgSource()。equals(MessageSourceUtil。getMsgSource())){log。info(收到本机发出的消息,不做处理);return;}DoubleCachecache(DoubleCache)manager。getCache(msg。getCacheName());if(msg。getType()CacheMsgType。UPDATE){cache。updateL1Cache(msg。getKey(),msg。getValue());log。info(更新本地缓存);}if(msg。getType()CacheMsgType。DELETE){log。info(删除本地缓存);cache。evictL1Cache(msg。getKey());}}}
  在上面的代码中,调用了DoubleCache中更新一级缓存方法updateL1Cache、删除一级缓存方法evictL1Cache,我们会后面在DoubleCache中进行添加。修改DoubleCache
  在DoubleCache中先添加上面提到的两个方法,由CacheManager获取到具体缓存后调用,进行一级缓存的更新或删除操作:更新一级缓存publicvoidupdateL1Cache(Objectkey,Objectvalue){caffeineCache。put(key,value);}删除一级缓存publicvoidevictL1Cache(Objectkey){caffeineCache。invalidate(key);}
  好了,完事具备只欠东风,我们要在什么场合发送消息呢?答案是在DoubleCache中存入缓存的put方法和移除缓存的evict方法中。首先修改put方法,方法中前面的逻辑不变,在最后添加发送消息通知其他节点更新一级缓存的逻辑:publicvoidput(Objectkey,Objectvalue){省略前面的不变代码。。。发送信息通知其他节点更新一级缓存CacheMassagecacheMassagenewCacheMassage(this。cacheName,CacheMsgType。UPDATE,key,value,MessageSourceUtil。getMsgSource());redisTemplate。convertAndSend(MessageConfig。TOPIC,cacheMassage);}
  然后修改evict方法,同样保持前面的逻辑不变,在最后添加发送消息的代码:publicvoidevict(Objectkey){省略前面的不变代码。。。发送信息通知其他节点删除一级缓存CacheMassagecacheMassagenewCacheMassage(this。cacheName,CacheMsgType。DELETE,key,null,MessageSourceUtil。getMsgSource());redisTemplate。convertAndSend(MessageConfig。TOPIC,cacheMassage);}
  适配分布式环境的改造工作到此结束,下面进行一下简单的测试工作。测试
  我们可以用idea的Allowparallelrun功能同时启动两个一样的springboot项目,来模拟分布式环境下的两台主机,注意在启动参数中添加Dserver。port参数来启动到不同端口。
  首先测试更新操作,使用接口修改某一个主机的本地缓存,可以看到发出消息的主机在收到消息后,直接丢弃不做任何处理:
  查看另一台主机的日志,收到消息并更新了本地缓存:
  再看一下缓存的删除情况,同样本地删除后再收到消息不做处理:
  看另一台主机收到消息后,会删除本地的一级缓存:
  可以看到,分布式环境下本地缓存通过Redis消息的发布订阅机制保证了一级缓存的一致性。
  另外,如果更加严谨一些的话,其实还应该处理一下缓存更新失败的情况,这里留个坑以后再填。简单说一下思路,我们应该在代码中捕获缓存更新失败的异常,然后删除二级缓存、本机以及其他主机的一级缓存,再等待下一次访问时直接拉取最新的数据进行缓存。同样,要想实现缓存失效同时作用于所有单机节点的本地缓存这一功能,也可以使用上面的发布订阅来实现。总结
  好了,这次缝缝补补的填坑之旅到这里就要结束了。可以看到使用基于JSR107规范的spring接口进行修改后,代码看起来舒服了很多,并且支持直接使用spring的Cache相关注解。如果想在项目中使用的话,自己封装一个简单的starter就可以了,使用起来也非常简单。
  那么,这次的分享就到这里,我是Hydra,下篇文章再见。
  本文及上一篇文章的示例代码已合并上传到了Hydra的Github上,公众号【码农参上】后台回复缓存获取链接,本文代码在项目的v4module中,欢迎小伙伴们来给个star啊
  作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。个人微信DrHydra9,欢迎添加好友,进一步交流。

雷达电蚊香孕妇能用吗?雷达电蚊香可以说是一款非常方便的产品,很多家庭都把传统蚊香淘汰了用起了新产品,下面5号网的小编为你们介绍雷达电蚊香孕妇能用吗?雷达电蚊香孕妇能用吗电蚊香同蚊香一样,都含有……摩罗丹有几种包装?买的时候看仔细现在市面上的同一种药品牌子还是挺多的,一不小心就会买错牌子,下面5号网的小编为你们介绍摩罗丹有几种包装?买的时候看仔细。摩罗丹有几种包装摩罗丹具有和胃降逆,健脾消食……生脉饮副作用一般无副作用很多人都把生脉饮当成一个长期的保健药品来吃,那么这个药是否有副作用呢,下面5号网的小编为你们介绍生脉饮副作用一般无副作用。生脉饮副作用生脉饮由党参、麦冬、五味子等成分组成……三九胃泰能吃多久?好了就停药任何药都是不建议长期吃的,三九胃泰是一款效果很好的胃药,下面5号网的小编为你们介绍三九胃泰能吃多久?好了就停药。三九胃泰能吃多久三九胃泰颗粒的主要成份是三叉苦、九里香、两……近视会遗传给孩子吗?近视遗传怎么办自从电脑手机的普及,现在越来越多的人近视,很多人担心把近视遗传给孩子,那么近视是否会遗传呢?下面我们来一起介绍下吧!近视会遗传给孩子吗现在的年轻父母很多都是近视,因……骨折能吃韭菜吗?骨折能吃萝卜吗?韭菜和萝卜都是常吃的蔬菜,那么骨折病人能不能吃这两样呢,下面5号网的小编为你们介绍骨折能吃韭菜吗?骨折能吃萝卜吗?骨折能吃韭菜吗可以。骨折关键的治疗是复位、固定和康……雨夜之杂念印象当中深秋下这么大的雨似乎没有经历过,喧嚣的城市进入梦乡烦躁的人们终于安静了下来。二零二二年的国庆之夜就是这样伴随着电闪雷鸣不约而至,脑海中似乎想了很多却又好像没有什么……黄连上清片能治痘痘吗?黄连上清片能治牙疼吗?有的人说黄连上清片除了能清火还有祛痘的功效,这种说法是否正确呢,下面5号网的小编为你们介绍黄连上清片能治痘痘吗?黄连上清片能治牙疼吗?黄连上清片能治痘痘吗黄连上清片对痤疮……维生素e是早上吃还是晚上吃?维生素e是酸性还是碱性?关于维生素e应该怎么吃,很多人都是一知半解,下面5号网的小编为你们介绍维生素e是早上吃还是晚上吃?维生素e是酸性还是碱性?维生素e是早上吃还是晚上吃维生素E是脂溶性的,为……失眠多梦胸闷打嗝口苦,一个方子补足肝肾,助你夜夜有好眠今天和大家讲讲失眠的事:其实简单点来说失眠就是四个字:阳不交阴阴阳不交:阳不入阴心阳不能入肾,导致心肾不交,导致彻夜难眠。今天花几分钟带大家看看肝血不足,阳不……蒲地蓝消炎片能和阿莫西林一起吃吗能和罗红霉素一起吃吗?蒲地蓝消炎片是一款消炎功效的药品,很多人吃这个的时候不知道这个药不能和什么一起吃,下面5号网的小编为你们介绍蒲地蓝消炎片能和阿莫西林一起吃吗能和罗红霉素一起吃吗?蒲地蓝消炎片能……养血清脑颗粒几盒一个疗程?其实一般的药品服用都是讲究疗程的,按疗程服用效果更好,下面5号网的小编为你们介绍养血清脑颗粒几盒一个疗程?养血清脑颗粒几盒一个疗程养血清脑颗粒没有具体的疗程,有需要者应在……
长津湖一战,毛岸英牺牲在鸭绿江边,多年后彭德怀的电报揭开细节文朝经史记编辑朝经史记在战火连天的朝鲜战场上,仅入朝34天,毛岸英便不幸牺牲,死在了异国他乡。一开始他作为主席儿子的身份军中大多数人并不知情,所以对于他的过往大家也……我在回忆往事中沉沦我们之间还记得吗,不记得是不是因为忘记了曾经还是那么美好,都只是在回忆的时候想起,仿佛回到当年我们曾经相恋过的女孩,她还会突然出现在我眼前。你的名字叫兰,我的梦。那……电动牙刷和普通牙刷有什么区别电动牙刷的好处现在随着时代的发展出现了很多高科技的产品,但是这些高科技的产品和普通的有什么区别呢?今天小夏就带你了解一下吧,电动牙刷和普通牙刷的区别吧,以及电动牙刷的好处哦。电动牙刷和普通牙……如何挑选牙刷呢牙刷使用要注意什么呢大家在生活中应该每天都使用牙刷吧,但是你了解牙刷吗?今天小编就和大家一起来了解一下吧,究竟如何挑选牙刷呢,以及牙刷使用要注意什么呢?跟着小编我们一起来学习吧。如何挑选牙刷呢……抖音(朋友圈)优秀文案,建议收藏一、人是无法做到换位思考的,因为思想、经历、感官全都不一样,就像我说大海很漂亮,而你却说淹死过很多人。二、你害怕发生的事情,其实根本不用担心,因为它一定会如期而至,也一定……澳洲开关在即,一波签证优惠政策来袭,含技术旅游等多种签证如何恢复经济,是自疫情以来澳大利亚政府一直在考虑的问题。随着在下个月即将迎来的澳大利亚边境逐步开放,如何吸引和留住有助于澳洲经济恢复的人才,也是澳洲眼下比较关注的。25日……刘国梁当选副主席,国际乒联宫斗终于宣告结束北京时间11月25日凌晨,国际乒联代表大会在美国休斯敦举行,选举产生了新一届国际乒联的主席及副主席。结果,来自瑞典的佩特拉瑟林在没有竞争对手的情况下成功当选主席,中国乒协主席、……世界粮食日是每年的几月几日世界粮食日的意义时间粮食日是我们大家很多人都清楚的一个节日,主要是为了提醒我们节约粮食,同时为了纪念粮食的发展,那么我们便了解一下世界粮食日是每年的几月几日?世界粮食日的意义?世界粮食日是每年……洪灾会造成什么为什么洪涝灾害后会有瘟疫洪灾是我们大家都很熟悉的一种自然灾害,同时我们也都知道洪灾会对日常生活造成非常大的影响,那么我们便要了解一下洪灾会造成什么?为什么洪涝灾害后会有瘟疫?洪灾会造成什么严重的……海底捞黑海会员要消费多少钱海底捞会员等级区别海底捞是我们大家都很熟悉的一个火锅品牌,因为其味道非常好并且服务态度非常好,所以现在是非常受欢迎的,那么海底捞黑海会员要消费多少钱?海底捞会员等级区别?海底捞黑海会员要消费多少……不要觉得做个双眼皮就一步登天了!小心变美不成变毁容从美学角度来看,具有双眼皮的眼睛会使人的眼神更显明媚、灵活、容貌更俊俏,清秀。而单眼皮往往给人以一种单调、臃肿、缺乏生机的感觉,为了改变这种欠缺,诞生了手术形成重睑的方法,也就……原来吃红枣不能补血?真正补血的这4种食物,却很少人知道有的人持续贫血没有引起重视,在血液中红细胞,血红蛋白数量减少时会有不良症状,如果出现的是缺铁性贫血,应该通过合理补充铁元素来缓解,否则会容易乏力、胃口下降、脸色苍白。当然,在补……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网