欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

数据库缓存mybatis,redis

程序员文章站 2023-04-01 08:48:57
简介 处理并发问题的重点不在于你的设计是怎样的,而在于你要评估你的并发,并在并发范围内处理。你预估你的并发是多少,然后测试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">-->
            <!--&lt;!&ndash;配置redis模板&ndash;&gt;-->
            <!--<constructor-arg name="redisoperations" ref="redistemplate"/>-->
            <!--&lt;!&ndash;默认缓存失效时间&ndash;&gt;-->
            <!--<property name="defaultexpiration" value="120"/>-->
            <!--&lt;!&ndash;是否使用前缀&ndash;&gt;-->
            <!--<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的生成策略,管理类主要实现了自定义有效期
    rediscacheconfig
    package 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里去找,看有没有。若果有,根据键值获取缓存;如果没有就查询数据库。