精华内容
下载资源
问答
  • java 数据加载到内存jvm中

    千次阅读 2019-06-19 11:42:46
    为什么需要将java 数据加载到内存? 1 将数据加载到jvm运行内存中,会占用运行...每次使用的查询为null 就走数据查询刷新到内存。 3 刷到内存也是可以通过定时任务去刷新内存的值,既使用jvm 内存 又是变动的值 ...

    为什么需要将java 数据加载到内存?
    1 将数据加载到jvm运行内存中,会占用运行内存,一些对象,初始化数据,枚举等
    缺点:如果值有修改,需要重新部署项目才能生效。
    2 一些不想放到redis 缓存的数据可以放到jvm中
    使用方式:
    在这里插入图片描述
    每次使用的查询为null 就走数据查询刷新到内存。

    3 刷到内存也是可以通过定时任务去刷新内存的值,既使用jvm 内存 又是变动的值

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    风控预加载各种规则引擎使用ConcurrentHashMap
    可以根据map替换原来规则,新增规则维护。

    展开全文
  • JAVA数据缓存之内存缓存 简要介绍 java内存缓存其实就是本地缓存 , 实现方法一般是从数据库查询出需要缓存的数据然后放map里 ,根据需求可分为增量缓存和全量缓存,核心思想就是将想要查询数据库的用户拦住,从...

    JAVA数据缓存之内存缓存

    简要介绍

    java内存缓存其实就是本地缓存 , 实现方法一般是从数据库查询出需要缓存的数据然后放到map里 ,根据需求可分为增量缓存和全量缓存,核心思想就是将想要查询数据库的用户拦住,从内存缓存中拿到想要的数据返回给用户。

    使用场景

    1.前台查询用户量比较大, 一直去查数据库会对数据库造成压力。
    2.数据量比较大 从大数据量里边拿一小部分数据,一般是一个人的相关数据。
    3.非集群环境 集群环境使用内存缓存会使内存的使用量变化过大,集群环境数据量不是特别大推荐
    redis数据缓存 ,数据量过大使用mogoDB缓存。
    4.数据量特别大不建议使用,过度使用内存存储会对服务器造成压力。
    5.缓存之前一定得试一下你那个查询到底多少秒能查出来,如果查询都很慢,然后你又要求数据缓存做成实时的(比如一秒一刷)这样会造成线程队列堆积,这样会把服务直接刷死。

    特性

    1.直接使用JAVA内存缓存查询速度是比redis快的。
    2.不能持久化(mogoDB和redis可以持久化)。

    数据缓存前期准备

    数据缓存最不可或缺的步骤为定时任务,这里我们使用的是spring提供的定时任务 ,配合cron表达式实现定时刷新缓存, 真正的线上环境绝对不是只缓存一种数据,所以定时任务一定得是多线程的,不能为串行执行,所以需要进行如下配置。
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.SchedulingConfigurer;
    import org.springframework.scheduling.config.ScheduledTaskRegistrar;
    
    import java.util.concurrent.Executor;
    import java.util.concurrent.Executors;
    
    /**
     * 没有此配置,则schedule为单线程串行执行
     */
    @Configuration
    @EnableScheduling
    public class ScheduleConfig implements SchedulingConfigurer {
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.setScheduler(taskExecutor());
        }
    
        //配置线程池---触发器和任务共用的
        @Bean(destroyMethod="shutdown")
        public Executor taskExecutor() {
            return Executors.newScheduledThreadPool(10);
        }
    }
    

    初始化数据

    使用数据缓存之前首先需要初始化好装数据的map和记录增量缓存的缓存时刻(写增量缓存的话是需要记录上一次缓存的时间,在这个时间之后的数据需要进行缓存),这两种数据都写成全局变量也就是定义在类里边方法上边。
    	//因为要使用增量缓存  首先初始化用到的时间
        private Timestamp firmDataTime = null;
        //初始化hashMap用来存储缓存值
        public static ConcurrentHashMap<Integer , Firm> firmData = new ConcurrentHashMap<>();
    

    全量缓存使用场景

    数据在一定时间内是不变的,或者在特定的时间进行改变然后一定时间内不改变(比如市场结算时候的分成数据,市场会在每天闭市后进行结算分成,意思是这个数据会在每天闭市后生成,并在下一次闭市前保持不变)

    全量缓存代码块简要解释

    从1号开始每天进行一次全量缓存 首先清理缓存,然后查数据库,然后把数据库的数据装到map里边,逻辑是比较简单的。

    全量缓存代码展示

    //全量缓存
        @Scheduled(cron = "0 0 0 1/1 * ? ")
        public void cacheAllFirm(){
            firmData = null;
            List<Firm> firms = firmRepo.findAll();
            ConcurrentHashMap<Integer , Firm> allFirmMap= new ConcurrentHashMap<>();
            for(Firm firm : firms){
                allFirmMap.put(firm.getFirmId() , firm);
            }
            firmData = allFirmMap;
        }
    

    增量缓存使用场景

    一般其实做数据缓存的话是使用增量缓存,核心思想就是只缓存修改或新增的数据,缓存的数据少了,自然速度就快了。

    增量缓存代码块简要解释

    这里写的是一个每三秒进行一次增量缓存,首先记录一下当前时间,然后减去一个缓冲时间(意思就是每次增量缓存需要多缓存一些,防止因为任务的执行或者数据的变动少缓存数据,这个缓冲时间一般设置成2秒3秒左右,不用担心会多缓存数据,因为map 的存储特性key是唯一的,不会重复存储)如果map是空的首先就得进行一次全量缓存,不为空才增量 只查询记录缓存时间之后的数据,并put到map里,每次装完数据记录一个时间。

    增量缓存代码展示

    @Scheduled(cron = "*/3 * * * * ? ")
        public void cacheFirm(){
            log.info("每三秒增量缓存一次firm");
            Long time = new Timestamp(System.currentTimeMillis()).getTime() - 3000;
            if(firmData.isEmpty()){
                List<Firm> v = firmRepo.findAll();
                //第一次缓存先全量缓存一次
                log.info("全量缓存开始");
                addDateFirm(v , time);
            }
            else{
                List<Firm> v = firmRepo.findAllByUpdateTimeAfterOrCreateTimeAfter(firmDataTime , firmDataTime);
                //进行增量缓存
                log.info("增量缓存开始");
                addDateFirm(v , time);
            }
        }
    
        private void addDateFirm(List<Firm> firms  , Long time){
            if(firms != null && firms.size() > 0 ){
                for(Firm firm : firms){
                    firmData.put(firm.getFirmId() , firm);
                }
            }
            firmDataTime = new Timestamp(time);
        }
    

    以下是内存缓存的全部测试代码(包括以上举的例子)

    controller

    import com.tjdami.zs.test.jpa.model.Firm;
    import com.tjdami.zs.test.service.TestService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestHeader;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @Auther: Zyy
     * @Date: 2020/12/7 10:34
     * @Description:
     */
    @Api(value = "测试服务", description = "通用测试服务")
    @RequestMapping("/test")
    @RestController
    @Slf4j
    public class TestController {
    
        @Autowired
        private TestService testService;
    
        @RequestMapping(value = "/cacheFirm")
        @ApiOperation(value = "查看客户信息", httpMethod = "POST", notes = "查看客户信息")
        public Firm distribute(@RequestHeader("firmId") String firmId) throws Exception {
            return testService.getFirmCache(Integer.valueOf(firmId));
        }
    
    
    
    
    
    
    }
    

    service

    import com.tjdami.zs.test.cache.TextCache;
    import com.tjdami.zs.test.jpa.model.Firm;
    import org.springframework.stereotype.Service;
    
    /**
     * @Auther: Zyy
     * @Date: 2020/12/7 11:02
     * @Description:
     */
    @Service
    public class TestService {
    
        public Firm getFirmCache(Integer firmId) throws Exception {
            Firm firm = TextCache.firmData.get(firmId);
            return firm;
        }
    }
    

    缓存核心

    import com.tjdami.zs.test.jpa.model.Firm;
    import com.tjdami.zs.test.jpa.repo.FirmRepo;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    import java.sql.Timestamp;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * @Auther: Zyy
     * @Date: 2020/12/7 11:17
     * @Description:
     */
    @Component
    @Slf4j
    public class TextCache {
        @Autowired
        private FirmRepo firmRepo;
    
        //因为要使用增量缓存  首先初始化用到的时间
        private Timestamp firmDataTime = null;
        private Timestamp firmDataListTime = null;
        //初始化hashMap用来存储缓存值
        public static ConcurrentHashMap<Integer , Firm> firmData = new ConcurrentHashMap<>();
        //初始化一个存储firm集合的map集合
        public static ConcurrentHashMap<Integer , List<Firm>> firmDataList = new ConcurrentHashMap<>();
    
    
        @Scheduled(cron = "*/3 * * * * ? ")
        public void cacheFirm(){
            log.info("每三秒增量缓存一次firm");
            Long time = new Timestamp(System.currentTimeMillis()).getTime() - 3000;
            if(firmData.isEmpty()){
                List<Firm> v = firmRepo.findAll();
                //第一次缓存先全量缓存一次
                log.info("全量缓存开始");
                addDateFirm(v , time);
            }
            else{
                List<Firm> v = firmRepo.findAllByUpdateTimeAfterOrCreateTimeAfter(firmDataTime , firmDataTime);
                //进行增量缓存
                log.info("增量缓存开始");
                addDateFirm(v , time);
            }
        }
    
        private void addDateFirm(List<Firm> firms  , Long time){
            if(firms != null && firms.size() > 0 ){
                for(Firm firm : firms){
                    firmData.put(firm.getFirmId() , firm);
                }
            }
            firmDataTime = new Timestamp(time);
        }
    
    
        //全量缓存
        @Scheduled(cron = "0 0 0 1/1 * ? ")
        public void cacheAllFirm(){
            firmData = null;
            List<Firm> firms = firmRepo.findAll();
            ConcurrentHashMap<Integer , Firm> allFirmMap= new ConcurrentHashMap<>();
            for(Firm firm : firms){
                allFirmMap.put(firm.getFirmId() , firm);
            }
            firmData = allFirmMap;
        }
    }
    

    定时任务多线程配置

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.SchedulingConfigurer;
    import org.springframework.scheduling.config.ScheduledTaskRegistrar;
    
    import java.util.concurrent.Executor;
    import java.util.concurrent.Executors;
    
    /**
     * 没有此配置,则schedule为单线程串行执行
     */
    @Configuration
    @EnableScheduling
    public class ScheduleConfig implements SchedulingConfigurer {
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.setScheduler(taskExecutor());
        }
    
        //配置线程池---触发器和任务共用的
        @Bean(destroyMethod="shutdown")
        public Executor taskExecutor() {
            return Executors.newScheduledThreadPool(10);
        }
    }
    

    实体类

    import lombok.Data;
    import lombok.experimental.Accessors;
    
    import javax.persistence.*;
    import java.io.Serializable;
    
    @Data
    @Entity
    @Table(name = "firm")
    public class Firm implements Serializable {
    
    	@Id
    	@GeneratedValue
    
    	@Column(name = "firm_id")
    	private Integer firmId;
    
    	
    	@Column(name = "firm_name")
    	private String firmName;
    
    
    	@Column(name = "create_time", insertable = false, updatable = false)
    	private java.sql.Timestamp createTime;
    
    	@Column(name = "update_time", insertable = false, updatable = false)
    	private java.sql.Timestamp updateTime;
    
    }
    

    JPA

    import com.tjdami.zs.test.jpa.model.Firm;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
    import org.springframework.data.jpa.repository.Modifying;
    import org.springframework.data.jpa.repository.Query;
    
    import java.sql.Timestamp;
    import java.util.List;
    import java.util.Map;
    
    public interface FirmRepo extends JpaRepository<Firm, Integer>, JpaSpecificationExecutor<Firm> {
     
        List<Firm> findAllByUpdateTimeAfterOrCreateTimeAfter(Timestamp timea , Timestamp timeb);
        
    }
    

    以上就是本期的全部内容了,操作数据库的技术为JPA加哈伯奈特,换成mybatis或者jdbc也是一样的简单哦,如果大家觉得有帮助一定记得动动小手点个赞。

    展开全文
  • 查询数据的时候出现了jvm内存溢出,这里记录一下 原先的代码,这代码一看就有问题,按时间查询,如果时间短,数据少那当然没啥问题,但是如果我一下子要迁移2年的数据,那数据量就非常大,就有问题了。 ...

    最近要将一个老系统的数据迁移到新系统来,当然是将老数据库中的数据放到新数据库中来。
    当查询数据的时候出现了jvm内存溢出,这里记录一下

    原先的代码,这代码一看就有问题,按时间查询,查询出的东西是放在jvm内存里的,如果时间短,数据少那当然没啥问题,但是如果我一下子要迁移2年的数据,那数据量就非常大,内存就可能溢出了。

     ResultSet select = JdbcHelper.select(con, "SELECT * FROM jc_content_ext cex \n" +
                                    "\t\tLEFT JOIN jc_content jcon ON jcon.`content_id` = cex.`content_id`\n" +
                                    "                LEFT JOIN jc_content_txt ctx ON cex.`content_id` = ctx.`content_id` \n" +
                                    "                LEFT JOIN jc_content_channel jcc ON cex.`content_id` = jcc.`content_id` \n" +
                                    "                LEFT JOIN jc_channel jc ON jcc.`channel_id` = jc.`channel_id` \n" +
                                    "                LEFT JOIN jc_channel_ext jct ON jct.`channel_id` = jcc.`channel_id`\n" +
                                    "                LEFT JOIN jc_user_ext jue ON jue.`user_id` = jcon.`user_id`\n" +
                                    "                WHERE 1 = 1 AND  cex.`release_date` BETWEEN '"+TAG_LAST_TIME+"' AND '"+TAG_THIS_TIME+"' AND jcon.`status` = 2 LIMIT " + pageIndex +"," + pageSize
                            );
                            handleResult(select, columnMap, con);
    

    优化:还是分页查询,先查询出时间段内总共的记录数,根据记录数的范围,然后分页查询。
    优化后的代码。这里我按500/页来查询:
    注意1:这里查询数据总数的时候,还遇见了一些问题。
    如果直接count(*),ResultSet 是无法通过列名得到总的数据数。
    这时得随便找一个列名,然后起个别名,之后,取总记录数的时候,用该别名才能得到。原因嘛,我们不是用mybatis,也不是在mysql直接查,它是将结果集放在了ResultsSet里,所以我们直接是拿不到数据的,
    不起别名的话是这样在这里插入图片描述
    。这样就不好拿到数据了。这时的结果集,只有这个,如果起过别名在这里插入图片描述,这时我们就可以直接通过rs.getString(“a”)来得到列明为a的数据了。
    当然也可以一次查询出所有数据,然后循环结果集rs。但是如果数据太大,还是会导致内存溢出。
    注意2:
    在用COUNT(cex.content_id) as a取出数据的时候要,先 rs.next();,再rs.getString(“a”)。才能取到总的记录数。否则会异常。

    
                ResultSet rs = JdbcHelper.select(con, "SELECT COUNT(cex.content_id) as a FROM jc_content_ext cex \n" +
                        "\t\tLEFT JOIN jc_content jcon ON jcon.`content_id` = cex.`content_id`\n" +
                        "                LEFT JOIN jc_content_txt ctx ON cex.`content_id` = ctx.`content_id` \n" +
                        "                LEFT JOIN jc_content_channel jcc ON cex.`content_id` = jcc.`content_id` \n" +
                        "                LEFT JOIN jc_channel jc ON jcc.`channel_id` = jc.`channel_id` \n" +
                        "                LEFT JOIN jc_channel_ext jct ON jct.`channel_id` = jcc.`channel_id`\n" +
                        "                LEFT JOIN jc_user_ext jue ON jue.`user_id` = jcon.`user_id`\n" +
                        "                WHERE 1 = 1 AND  cex.`release_date` BETWEEN '"+TAG_LAST_TIME+"' AND '"+TAG_THIS_TIME+"' AND jcon.`status` = 2"
                );
    
                System.out.println("总条数++++++"+ rs.getRow());
                System.out.println("总条数++++++"+ rs);
    
                rs.next();
                System.out.println(rs.getString("a"));
                int count = Integer.parseInt(rs.getString("a"));
                System.out.println(count);
    
    
                if(count > 0 ){
                    if(count <= 500 ){
                        System.out.println("小于500 ================== " + count);
                        ResultSet select = JdbcHelper.select(con, "SELECT * FROM jc_content_ext cex \n" +
                                "\t\tLEFT JOIN jc_content jcon ON jcon.`content_id` = cex.`content_id`\n" +
                                "                LEFT JOIN jc_content_txt ctx ON cex.`content_id` = ctx.`content_id` \n" +
                                "                LEFT JOIN jc_content_channel jcc ON cex.`content_id` = jcc.`content_id` \n" +
                                "                LEFT JOIN jc_channel jc ON jcc.`channel_id` = jc.`channel_id` \n" +
                                "                LEFT JOIN jc_channel_ext jct ON jct.`channel_id` = jcc.`channel_id`\n" +
                                "                LEFT JOIN jc_user_ext jue ON jue.`user_id` = jcon.`user_id`\n" +
                                "                WHERE 1 = 1 AND  cex.`release_date` BETWEEN '"+TAG_LAST_TIME+"' AND '"+TAG_THIS_TIME+"' AND jcon.`status` = 2"
                        );
                        handleResult(select, columnMap, con);
    
                        con.close();
                    }else{
                   
                        int row = count;
                        int pageCount = row / 500 ;
    
                        for(int i =0; i<pageCount; i++){
                            int pageIndex = i * 500;
                            int pageSize  = 500 ;
                            ResultSet select = JdbcHelper.select(con, "SELECT * FROM jc_content_ext cex \n" +
                                    "\t\tLEFT JOIN jc_content jcon ON jcon.`content_id` = cex.`content_id`\n" +
                                    "                LEFT JOIN jc_content_txt ctx ON cex.`content_id` = ctx.`content_id` \n" +
                                    "                LEFT JOIN jc_content_channel jcc ON cex.`content_id` = jcc.`content_id` \n" +
                                    "                LEFT JOIN jc_channel jc ON jcc.`channel_id` = jc.`channel_id` \n" +
                                    "                LEFT JOIN jc_channel_ext jct ON jct.`channel_id` = jcc.`channel_id`\n" +
                                    "                LEFT JOIN jc_user_ext jue ON jue.`user_id` = jcon.`user_id`\n" +
                                    "                WHERE 1 = 1 AND  cex.`release_date` BETWEEN '"+TAG_LAST_TIME+"' AND '"+TAG_THIS_TIME+"' AND jcon.`status` = 2 LIMIT " + pageIndex +"," + pageSize
                            );
                            handleResult(select, columnMap, con);
                        }
                        con.close();
    
                    }
                }
    
    
    展开全文
  • :)当Java堆大小被限制为非常小(比如,16 MB)时,您可以创建一个内存中、堆外的数据存储,该存储可以容纳千兆字节的数据——甚至更多。过程是这样的:我使用Java的MappedByteBuffer类以及RandomAccessFile和Java NIO...

    32f3a2f2ebd4690cda9e91c393ff5736.gif

        本文将阐述如何在JAVA堆外的的内存中创建一个存储上GB甚至TB的数据库,当然,首先你得有这么大的物理内存。:)

    当Java堆大小被限制为非常小(比如,16 MB)时,您可以创建一个内存中、堆外的数据存储,该存储可以容纳千兆字节的数据——甚至更多。

    过程是这样的:

    我使用Java的MappedByteBuffer类以及RandomAccessFile和Java NIO(New Input/Output)FileChannel为一个项目开发了一个基本的存储解决方案。我的目标是在这个存储器中存储潜在的大量数据。

    优点:存储系统速度快;它不使用Java堆;使用方便;数据被持久化。然而,尽管数据是在堆外存储的,但我最初的实现使用Java HashMap作为数据的索引。该索引在完全加载时消耗了大量堆,这成为了一个问题。

    因此,我决定增强实现,对索引使用相同的方法,使用MappedByteBuffer而不是HashMap。我还增强了数据存储和索引,以使用基类ByteBuffer,它提供了在内存中创建数据存储或持久化数据存储的选项——都在Java堆之外。

    另一个优点是,虽然数据的读写速度很快,因为它们在内存中,但不会产生大型垃圾收集事件的开销,也不会受到非常大的Java堆的干扰。在维护非常小的Java堆的同时,您可以有效地存储tb级的内存数据(当然,如果您有足够的物理内存的话)。请继续阅读,了解如何做到这一点;您可以在这里下载所有代码:代码地址。

    ByteBuffer class

    java.nio.ByteBuffer继承于java.nio中的ByteBuffer类。用于将原始数据类型的值存储在内存中的字节数组中,并具有随机访问功能。您可以写入和读取除布尔型之外的所有基本类型,并且它会自动将要存储和检索的值转换为字节序列。除了基本类型之外,还可以写入和读取字节数组,这在存储字符串或支持序列化的Java对象时非常有用。

    为了确保在不影响Java堆的情况下获得最高的性能,可以使用allocateDirect方法为ByteBuffer分配内存。尽管这些缓冲区比非直接缓冲区有更高的分配成本,但使用它们读写数据更有效,因为它们使用本机操作系统的I/O操作,并且不会受到垃圾回收的影响。

    ByteBuffer允许相对引用和绝对引用,这意味着您可以一个接一个地写入一组值,并且类在每次调用后自动更新缓冲区内的活动位置(写入数据的位置)。或者您可以在每次调用中指定存储和检索值的位置。例如,下面代码中使用相对引用存储一系列值。

    Person person = new Person();//
    ByteBuffer bb = ByteBuffer.allocateDirect(size);
    bb.putInt(person.age);
    bb.putFloat(person.weeklySalary);
    bb.put(person.lastName.getBytes());
    bb.put(person.firstName.getBytes());
    bb.put((byte)(person.fullTime == true ? 1 : 0 ));

    每次写入时,指向下一步将数据写入缓冲区的位置的指针都会自动更新。注意,虽然不能直接存储布尔值,但可以将其编码为字节,如上面代码的最后一行所示。检索遵循相同的模式,如清下面代码所示。

    Person person = new Person();
    Person.age = bb.getInt();
    person.weeklySalary = bb.getFloat();//...

    要正确地读取一系列值,您需要知道(并设置)您的起始位置。在此之后,您就可以按顺序进行读取,在读取时依赖ByteBuffer来保持缓冲区的位置。例如,如果你存储的值在ByteBuffer的开始,你需要设置位置回到开始通过以下调用:

    bb.position(0); // Set the buffer position to the beginning

    注意,读取存储的字符串值是有问题的。为了读取作为字符串构造函数使用的正确字节数,还需要存储字符串长度。尽管这并不是特别困难,因为您编写代码来处理字符串数据、布尔数据和跟踪值集(记录)存储的位置,但是您已经有了内存数据库的开端。这正是我创建NoHeapDB类的原因,我将在本文后面讨论这个类。

    在此之前,让我们研究一下MappedByteBuffer和持久性。

    使用MappedByteBuffer来持久化数据

    ByteBuffer允许您使用内存中的字节区域来存储和检索数据。它的子类MappedByteBuffer允许您将文件的区域作为ByteBuffer映射到内存中。

    因此,当您向MappedByteBuffer写入和读取值时,数据被存储到或从它映射到的磁盘文件中读取。

    这种持久性模型有一些优点:它与ByteBuffer工作方式相同(使用随机访问以及相对和绝对定位),但没有物理内存大小的限制。此外,MappedByteBuffer使用底层操作系统直接进行文件和内存映射,这意味着可以避免大堆,并利用操作系统高度调优的内部特性来获得最佳性能。

    要将文件映射到虚拟内存并创建MappedByteBuffer,需要调用FileChannel.map。您可以创建java.nio。FileChannel对象有三种方式(每一种都在下面第三段代码中显示):

    • 打开或创建RandomAccessFile,并调用其getChannel方法。

    • 打开或创建一个文件,并在其FileInputStream或FileOutputStream对象上调用getChannel。这将会各自产生一个只读(read only)或读/写(read/write)通道。

    • 显式创建FileChannel实例,设置读或写模式。

    // Method 1: Using RandomAccessFile
    RandomAccessFile raFile = new RandomAccessFile(path, "rw");
    raFile.setLength(initialSize);
    FileChannel fc = raFile.getChannel();
    MappedByteBuffer mbb =
    fc.map(FileChannel.MapMode.READ_WRITE, 0, // position
    fc.size()); // size// Method 2: Using File
    File file = new File(path);
    file.createNewFile();
    FileInputStream fis = new FileInputStream(file);
    FileChannel readChannel = fis.getChannel();
    MappedByteBuffer readMap =
    readChannel.map(FileChannel.MapMode.READ_ONLY, 0, // position
    readChannel.size()); // size// Method 3: Creating a FileChannel
    FileChannel fc =
    FileChannel.open( FileSystems.getDefault().getPath(path),
    StandardOpenOption.WRITE,
    StandardOpenOption.READ);
    MappedByteBuffer mbb =
    fc.map(FileChannel.MapMode.READ_WRITE, 0, // position
    fc.size()); // size

    然后,可以像对ByteBuffer那样对MappedByteBuffer进行读写。不同之处在于更改被持久化到最初映射的文件中。

    此外,使用duplicate方法,您可以为原始缓冲区的内容创建多个视图,但是使用单独的缓冲区位置、限制和标记值。使用这些视图,您可以安全地从多个线程并发地读写同一底层内容(本例中是基于文件的)中的不同位置。这允许非常高吞吐量的内存和文件操作。

    为了演示这个过程,下面第四段代码为每个线程复制了单个ByteBuffer(或MappedByteBuffer)。为每个写到缓冲区的不同区域的线程创建一组线程,还创建一组读取器线程。每个写线程在它写入缓冲区的值前面加上它的线程号,当读线程输出最新的值时,可以看到它的线程号。

    ByteBuffer bb = ByteBuffer.allocateDirect(10_000);
    Runnable r = new Runnable() {@Overridepublic void run() {
    ByteBuffer localBB = bb.duplicate();
    String name = Thread.currentThread().getName();
    Integer threadNum = Integer.valueOf(name);int start = threadNum * Long.BYTES;
    localBB.position(start);
    localBB.mark();while ( true ) {// Make sure the first digit is the thread number. This// way when it's read and printed out, it will prove // that each thread is writing to its own buffer position// with no interference from the other threads
    Long val = Long.valueOf(""+(threadNum+1)+System.nanoTime());
    localBB.putLong(val);
    localBB.reset();
    Thread.yield();
    }
    }
    };int maxThreads = 12;// Start the writer threads, where each writes to the bufferfor ( int t = 0; t < maxThreads; t++ ) {
    Thread thread = new Thread(r);
    thread.setName(""+t);
    thread.start();
    }// read the values from the different parts of the buffer and// output them over and overwhile ( true ) {for ( int t = 0; t < maxThreads; t++ ) {
    bb.position(t*Long.BYTES);
    Long val = bb.getLong();
    System.out.print(val+", ");
    }
    System.out.println();
    Thread.yield();
    }
    NoHeapDB的实现

    当您以字节数组存储数据时(映射到持久文件存储或不映射到持久文件存储),您需要管理数据以方便地定位它。可以将数据写成有序列表或相同大小的数据数组,然后简单地遍历。但通常,您需要一种更灵活、更高效的查找方法。NoHeapDBStore类(noheapdb是一个虚包类)通过存储可迭代的数据并利用散列表作为按键进行O(1)数据查找的索引来实现这一点。这个索引是在FixedHash类中实现的,我也将在本文中介绍。

    要使用NoHeapDBStore,需要创建一个“数据存储”,就像在SQL数据库中创建一个表一样。您需要提供名称、初始大小和存储类型(仅用于指示持久性或内存存储)。初始大小的默认值是100 MB,存储类型的默认值是所有数据的内存持久性。您可以选择为所有持久数据存储指定一个主目录。每个存储都表示为NoHeapDBStore类的一个实例,原始数据以ByteBuffer类型保存在成员变量buffer中。

    创建内存中的存储。通过createMessageJournalBB方法创建内存中的数据存储。内存是用ByteBuffer.allocateDirect方法分配的,它请求JVM使用本机内存(而不是Java堆)作为缓冲区的后备存储。您对这个缓冲区中的字节所做的操作不受垃圾收集的影响(尽管当没有对它的更多引用时,整个ByteBuffer将被垃圾收集,就像对任何Java对象一样)。

    为什么你不需要垃圾收集。一般来说,自动内存管理是没有问题的,最好称为垃圾收集(GC)。尽管随着JVM的每次发布,GC都得到了改进,但大型堆(很多gb甚至tb)上的收集事件发生时仍然会干扰应用程序线程的执行。对于某些类型的应用程序,比如有实时需求的应用程序,这可能会导致严重错误。将数据保存在内存中但不放在Java堆中,可以保证不受GC干扰的高速数据访问。

    创建持久存储。持久性存储是通过createMessageJournalMBB方法创建的,这里稍微有点复杂,需要使用RandomAccessFile、FileChannel和MappedByteBuffer创建持久存储,请查看下面第五段代码:

    protected final boolean createMessageJournalMBB(String journalPath) {try {// First create the directory and file within
    File filePath = new File(journalFolder);
    filePath.mkdir();
    File file = new File(journalPath);
    fileExists = file.exists();
    journal = new RandomAccessFile(journalPath, "rw");if ( fileExists ) {// Existing file; so use its existing length
    bufferSize = (int)journal.length();
    } else {// New file; set the starting length
    journal.setLength(bufferSize);
    }
    channel = journal.getChannel();
    buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, bufferSize);if ( ! fileExists) {// New journal file; write the file header data
    writeJournalHeader(journal);
    currentEnd = journal.getFilePointer();
    } else {// Iterate through the existing records and find its current end
    currentEnd = scanJournal();
    }
    }catch (Exception e) {
    logger.log(Level.SEVERE, "createMessageJournalMBB Exception: ", e);
    }return false;
    }

    在上面这段代码中,首先创建文件的基目录。接下来,使用存储名称创建文件本身,然后使用该文件创建RandomAccessFile读/写流(在代码中称为日志)。如果该文件存在,则检索该文件的长度;否则,文件大小将设置为默认的初始存储大小。最后,通过映射日志的FileChannel创建一个读/写MappedByteBuffer。

    为了将来的兼容性,一些文件头数据由日志的名称和文件格式的版本组成。扫描现有的数据存储日志文件(来自以前的执行)以获取活动记录,并为每个活动记录建立索引,以便通过scanJournal方法快速检索。

    创建数据存储索引。每个数据存储保存一组记录,其中每个记录被标记为活动或不活动。非活动记录是指已删除的记录;它们被简单地标记为这样,它们的位置可以在以后重用。但是,为了快速检索O(1)记录,会创建一个哈希表索引,允许您以name-value对的形式存储和检索数据。

    此实现的索引是一个固定大小的哈希表(与可扩展哈希相反)。然而,当固定大小的哈希表的加载因子达到一个阈值时,它实际上会增长。

    索引被创建为用于内存中存储的ByteBuffer,并从RandomAccessFile创建为用于持久存储的MappedByteBuffer。它的默认起始大小是数据存储大小的四分之一,或者最小为64 MB。考虑到索引只存储键的散列版本和记录本身在数据存储中的位置,这通常已经足够大了。

    但是,您有一个选择:您可以在索引中存储原始键,也可以在散列版本中存储原始键(因为它实际上是用来查找存储记录位置的bucket的)。由于它更小,存储哈希值(一个四字节的整数)比基于字符串的键更快、更紧凑。

    数据存储记录结构。数据存储缓冲区(在内存中或持久存储)的结构使得所有记录都是连续的。除了数据之外,每个记录还包含记录的大小(以字节为单位)、它的类型以及指示它是否处于活动状态(即未删除)的标志。因此,可以连续遍历记录。当一个记录被删除时,它被简单地标记为已删除。这意味着可以将新记录追加到记录列表的末尾,或者插入到非活动记录的位置(如果有非活动记录且恰好与之匹配)。如果新记录更小,不活动的记录被分割成足够大的空间,以容纳新记录和一个更小的不活动的记录(参见下图)。

    eaaeceb831b1d2267cd9c2041855bdd3.png

     在已删除记录的位置插入一个12字节的记录

    要回收的非活动记录需要足够大,既能容纳新记录(数据和记录头),又至少能容纳另一个头,以容纳剩余的非活动记录空间。稍后我将详细介绍这个过程。

    存储数据记录。类似于ByteBuffer基类,NoHeapDB通过一系列put方法支持数据存储:

    • putInteger

    • putShort

    • putLong

    • putFloat

    • putDouble

    • putString

    • putObject

    • putChar

    每个方法都接受作为字符串的键和值本身,并且每个方法都调用实际实现存在的私有putVal方法。

    首先,根据数据类型确定数据长度。接下来,确定新记录的位置。这个过程是在setNewRecordLocation方法中执行的,该方法与getStorageLocation(参见第六段代码)一起找到一个空闲的(不活动的)槽,以便使用新记录回收。如果没有找到,该方法将新记录追加到缓冲区的末尾。

    代码6:搜索要插入新记录的空闲槽

    // Is there an exact match?
    LinkedList records = emptyIdx.get(recordLength);if (records != null && !records.isEmpty()) {
    location.offset = records.remove();// No need to append an empty record; just return offset
    location.newEmptyRecordSize = -1;return location;
    }// Store empty record slots to be removed
    ArrayList toRemove = new ArrayList<>();// No exact size match, find one just large enoughfor (Integer size : this.emptyIdx.keySet()) {// Enough room for the new record and another empty // record with a header and at least one byte of data?if (size >= recordLength + Header.HEADER_SIZE + 1) {
    records = emptyIdx.get(size);if (records == null || records.size() == 0) { // This was the last empty record of this size// so delete the entry in the index and continue// searching for a larger empty region (if any)
    toRemove.add(size);continue;
    }
    location.offset = records.remove();// You need to append an empty record after the new record// taking the size of the header into account
    location.newEmptyRecordSize =
    size - recordLength - Header.HEADER_SIZE;int newOffset = (int)
    location.offset + recordLength + Header.HEADER_SIZE;// Store the new empty record's offset
    storeEmptyRecord( newOffset, location.newEmptyRecordSize );break;
    }
    }

    所有不活动的记录位置存储在一个按大小升序排序的ArrayList中。准确地说,每个ArrayList条目都是该大小的非活动记录位置的LinkedList。首先,检查新记录的确切大小,在这种情况下,可以批量使用它。由于ArrayList是按大小排序的,所以只需用新的记录大小调用get,该位置就会从“空”列表中删除并返回。

    如果没有找到精确的匹配,则遍历列表,直到找到一个足够大的记录,以及一个额外的空记录(用于维护连续的记录列表)。回收的非活动槽从空列表中删除,并根据其新大小将新的非活动槽(分割原始槽以供重用的结果)添加回该空列表中。

    从技术上讲,这种标记和重用已删除记录的行为是一种垃圾收集形式。此外,它的处理时间可以根据遍历的非活动记录的数量而变化。从这个意义上说,它也是不可预测的。然而,它的处理成本分散在存储新记录和(在较小程度上)删除现有记录的行为上。因此,影响是有限的,这是您作为开发人员可以控制的。例如,确保在应用程序的时间关键部分不以任何方式修改数据存储,还可以确保不会发生这种处理。

    最后,数据记录存储在缓冲区中(参见代码7)。记录头由表示记录是活动的(未删除)的字节、数据类型(一个字节)和数据长度(四个字节)组成。之后,将数据值写入缓冲区。

    // First write the record headerbuffer.put(ACTIVE_RECORD); // 1 bytebuffer.put(type);          // 1 bytebuffer.putInt(datalen);    // 4 bytes// Write record valueswitch ( type ) {case LONG_RECORD_TYPE:buffer.putLong( (Long)val );break;case INT_RECORD_TYPE:buffer.putInt( (Integer)val );break;case DOUBLE_RECORD_TYPE:buffer.putDouble( (Double)val );break;case FLOAT_RECORD_TYPE:buffer.putFloat( (Float)val );break;case SHORT_RECORD_TYPE:buffer.putShort( (Short)val );break;case CHAR_RECORD_TYPE:buffer.putChar( (char)val );break;case TEXT_RECORD_TYPE:buffer.put( ((String)val).getBytes() );break;case BYTEARRAY_RECORD_TYPE:buffer.put( (byte[])val );break;
    }

    代码7:在数据存储中存储数据记录

    最后一步是为新记录建立索引,以便快速检索。通过FixedHash.put方法获取数据存储中记录的键和偏移量,它首先使用该键找到一个哈希表bucket。然后,它将自己定位到哈希表中的那个位置(bucket),并检查它是否已经被占用。

    注意:代码通过调用ByteBuffer来搜索偏移量。位置,然后调用标记保存位置。接下来,调用ByteBuffer.get将文件位置向前推进一个字节并读取值:“已占用”指示器。如果bucket是空闲的,调用reset会将文件位置移动回bucket的起始位置。或者,使用位置作为参数来调用get(),在不移动文件位置的情况下读取值。但是,我发现顺序调用mark和reset方法总体上更快(请参见代码清单8)。

    如果bucket是空闲的,那么键长度、键哈希码和记录偏移量都被写入索引缓冲区。可选地,您也可以让索引存储原始键,但我发现这样做会降低速度,并在缓冲区中占用更多空间,而您真正需要的只是键的哈希码来定位键。如果您喜欢存储密钥,可以将KEY_SIZE值设置为大于0的值,这样它就知道要复制多少字节。

    代码清单8: 通过将键存储在哈希表中用于O(1)查找来索引数据记录

    offset = getHashBucket(key.hashCode() );
    indexBuffer.position(offset);
    indexBuffer.mark();byte occupied = indexBuffer.get();if ( occupied == 0 ) {// found a free slot; go back to the beginning of it
    indexBuffer.reset();
    }else {
    collisions++;
    offset = findBucket(key, offset, false);// found a free slot; seek to it
    indexBuffer.position(offset);
    }// Write the data//
    indexBuffer.put((byte)key.length() );
    indexBuffer.putInt(key.hashCode()); // for resolving collisions, hashCode is faster than comparing stringsif ( KEY_SIZE > 0 ) {byte[] fixedKeyBytes = new byte[KEY_SIZE];
    System.arraycopy(key.getBytes(), 0, fixedKeyBytes, 0, key.length());
    indexBuffer.put( fixedKeyBytes );
    }
    indexBuffer.putLong( value ); // indexed record location

    如果有冲突,则调用findBucket(参见代码清单9)来遍历表以查找下一个空哈希bucket。如果冲突是因为索引中的键已经存在,则重用该位置:添加一个具有相同键的新值将替换之前的值。否则,一个桶一个桶地迭代表,直到找到一个空闲的桶。

    代码清单9。方法findBucket在碰撞后查找下一个空桶。

    while ( occupied > 0 && ! found) {int keyHash = indexBuffer.getInt();if ( keyHash == key.hashCode() ) {if ( KEY_SIZE > 0 ) {
    indexBuffer.position(
    offset + 1 + Integer.BYTES + KEY_SIZE );
    }
    found = true;break;
    }else {// Check for rollover past the end of the table
    offset += INDEX_ENTRY_SIZE_BYTES;if ( offset >= (sizeInBytes - INDEX_ENTRY_SIZE_BYTES)) {// Wrap to the beginning, skipping the first bucket// since it's reserved for the first record pointer
    offset = INDEX_ENTRY_SIZE_BYTES;
    }// Skip to the next bucket
    indexBuffer.position(offset);
    occupied = indexBuffer.get();
    }
    }

    由于每个索引项的字节数相同,只需调用ByteBuffer即可。在缓冲区中前进那么多字节的位置。检查缓冲区的末尾是否已经到达,如果到达,代码将包装到开始处。

    检索数据记录。既然我已经解释了记录是如何存储在数据存储中并随后建立索引的,现在就可以深入研究数据检索了。就像每个原始数据类型都有一系列的putX方法一样,也有一系列的getX方法用于检索:

    • getInteger

    • getShort

    • getLong

    • getFloat

    • getDouble

    • getString

    • getObject

    • getChar

    每个方法都接受一个String类型的键,并返回适当的数据类型。每个方法在实际实现存在的地方调用私有getVal方法(参见清单10)。首先,调用使用索引检索给定键的记录位置。如果在索引中没有找到记录位置,则返回null。

    代码清单10。从数据存储中检索数据记录

    Long offset = index.get(key);//...// Jump to this record's offset within the journal file
    buffer.position(offset.intValue());// First, read in the headerbyte active = buffer.get();if (active != 1) {return null;
    }byte type = buffer.get();int dataLength = buffer.getInt();// Next, read in the databyte[] bytes;switch ( type ) {case LONG_RECORD_TYPE:
    val = buffer.getLong();break;case INT_RECORD_TYPE:
    val = buffer.getInt();break;case DOUBLE_RECORD_TYPE:
    val = buffer.getDouble();break;case FLOAT_RECORD_TYPE:
    val = buffer.getFloat();break;case SHORT_RECORD_TYPE:
    val = buffer.getShort();break;case CHAR_RECORD_TYPE:
    val = buffer.getChar();break;case BYTEARRAY_RECORD_TYPE:
    bytes = new byte[dataLength];
    buffer.get(bytes);
    val = bytes;break;case TEXT_RECORD_TYPE:
    bytes = new byte[dataLength];
    buffer.get(bytes);
    val = new String(bytes);break;
    }

    如果找到文件位置,则将其设置为返回的偏移量(来自索引)。接下来,读取活动记录指示符字节。如果记录是活动的,则记录的数据类型和长度分别作为字节和整数读取。最后,根据记录的类型读取记录的数据,并且仅在字符串或字节数组的情况下,读取其长度。让我们备份并检查索引查找是如何找到记录的位置的(参见代码清单11)。

    代码清单11:从索引中加载记录位置

    public Long get(String key) {int bucketOffset = getHashBucket( key.hashCode() );
    indexBuffer.position(bucketOffset);byte occupied = indexBuffer.get();if ( occupied > 0 ) {
    bucketOffset = findBucket(key, offset, true);
    }if ( bucketOffset == -1 ) {// Record not foundreturn -1L;
    }// Return the location of the data recordreturn indexBuffer.getLong();
    }

    首先,通过散列键来定位索引中的bucket。缓冲区定位到返回的偏移量,并读取一个字节(占位标志)。如果桶被标记为已占用,则调用findBucket(参见代码清单9)来确定这是否确实是记录的散列桶。它通过比较键并迭代,直到找到键或遇到空桶(表示该键不在索引中)为止。最后,如果找到了键,记录在数据存储中的位置将作为长值从索引中读取并返回。

    删除数据记录。从数据存储中删除记录很简单(参见清单10)。在索引中快速查找确定记录是否存在,如果存在,则返回其位置。

    代码清单10。从数据存储中删除一条记录

    // Locate the message in the journal
    offset = getRecordOffset(key);if (offset == -1) {return false;
    }// read the header (to get record length); then set it as inactive
    buffer.position(offset.intValue());
    buffer.put(INACTIVE_RECORD);
    buffer.put(EMPTY_RECORD_TYPE);
    datalength = buffer.getInt();// Store the empty record location and size for later reuse
    storeEmptyRecord(offset, datalength);// Remove from the journal index
    index.remove( key );

    缓冲区位置设置为记录的开始位置,第一个字节设置为指示该记录是不活动的,记录类型设置为指示槽为空。然后将记录在缓冲区中的位置及其长度添加到空记录列表中,以便在向数据存储中添加新记录时回收。

    最后,这个记录的索引项也被删除。这个过程也很简单:键的哈希桶被定位。如果桶被占用,则使用findBucket进行碰撞检查(再次参见清单9),就像获取记录一样。如果确实找到了这个键,那么桶的第一个字节(已占用标志)被设置为0,表示桶不再被占用。

    遍历记录。我已经介绍了按键检索记录,但是您还可以遍历数据存储中的所有记录。从缓冲区的开始(参见图2),读取每个记录的活动字节、数据类型和长度。如果记录是活动的,则返回它。如果它不是活动的,则跳过记录位置并检查下一个记录,依此类推。 

    8be12998b70f2216bc12dcefe2416f97.png

    图2:数据存储中的记录结构示例

    迭代首先在方法iterateStart中查找缓冲区的开始部分(如果它是持久的数据存储,则跳过文件头),然后通过调用getNextRecord搜索下一个活动记录(参见清单11)。开始搜索的位置作为参数传递。

    while ( !found && current < (bufferSize - Header.HEADER_SIZE)) {boolean active = true;if (buffer.get() == INACTIVE_RECORD) {
    active = false;
    }// Read record type
    type = buffer.get();if (type == EMPTY_RECORD_TYPE) {
    buffer.position((int)currentEnd);break; // end of data records in file
    }// Get the data lengthint datalen = buffer.getInt();
    recordSize = Header.HEADER_SIZE + datalen;if ( active) {// Found the next active record
    found = true;// Store the location of the start of the next record
    iterateNext = current + recordSize;
    }else {// skip past the data to the beginning of the next record
    current += recordSize;
    buffer.position( (int)current );
    }
    }if ( found ) {// Return the recordreturn getValue(current, type);
    }

    代码循环,直到找到活动记录或到达文件的末尾为止。首先,读取活动记录字节,然后是数据类型和数据长度。如果该记录处于非活动状态,则使用该记录的数据长度,缓冲区指针将被提升到下一个记录。如果该记录是活动的,则将标志设置为结束循环,并检索并返回该记录的值。存储下一条记录的位置,并在调用iterateNext方法时使用它。这样你可以反复调用iterateNext,直到返回一个空值,表示没有更多的记录,如下所示:

    Object recordObj = iterateStart();while ( recordObj != null ) {// Do something with the returned record
    ...// Get the next record in the data store
    recordObj = iterateNext();
    }

    指定日志和索引大小。您可以选择指定数据存储缓冲区的起始大小(这也决定了索引大小);否则,它默认为100 MB。缓冲区将自动扩展为100 MB增量,当它被填满时。但是,如果您知道需要多少存储空间,那么从一开始就相应地调整数据存储的大小会更有效。

    扩展数据存储的代码因内存或持久缓冲区的不同而不同。清单12显示了内存中的版本。

    ByteBuffer newBuffer = ByteBuffer.allocateDirect((int)newLength);
    if
    ( buffer.hasArray() ) {
    byte[] array = buffer.array();
    newBuffer.put( array );
    }
    else {
    buffer.position(0);
    newBuffer.put(buffer);
    }
    buffer = newBuffer;
    journalLen = buffer.capacity();

    索引会自动调整为数据存储初始大小的四分之一,并且当它通过一个加载阈值(缺省情况下为75%)时,还会自动扩展(并重新散列)。

    使用NoHeapDB实现

    要使用堆外数据存储(在这里下载所有代码),请创建NoHeapDB类的一个实例,它本质上是用于所有操作的API。由于Java的ByteBuffer类的限制,您不能创建一个比带符号的四字节整数最大值(2,147,483,647字节,或2 GB)更大的数据存储。为了克服这个限制,您可以创建多个数据存储并在它们之间细分数据。或者,您可以扩展NoHeapDBStore类的实现,以创建一个ByteBuffer对象数组,以便对类的用户进行聚合、封装和隐藏。

    NoHeapDB类包含DataStore API,它有两个构造函数。一个使用一个字符串来表示数据存储备份文件应该写入的位置。否则,默认为用户的主目录。在这两种情况下,都会创建一个名为JavaOffHeap的目录,该目录将为每个持久数据存储包含两个文件:一个用于数据,另一个用于索引。

    有三个createDataStore方法。第一个有名字;第二个接受一个名称和一个类型(IN_MEMORY或persistent);第三个接受名称、类型和以兆字节为单位的起始大小。默认的类型是IN_MEMORY,默认的起始大小是100mb,记住数据存储会根据需要以100mb的增量增长。

    NoHeapDB db = new NoHeapDB();
    db.createStore("MyTestDataStore",
    DataStore.Storage.IN_MEMORY, //or DataStore.Storage.PERSISTED256); // in MB

    要插入数据,请调用其中一个putX方法。您可以继续按名称引用数据存储。或者,您可以获得对内部数据存储实现的引用,并直接在其上调用方法。它的API也与ByteBuffer的API匹配。

    String key = ...;
    String value = ...;
    db.putString("MyTestDataStore", key, value);// or
    Integer intValue = ...;
    DataStore store = db.getStore("MyTestDataStore");
    store.putInteger(key, intValue);

    检索数据也同样容易。在适当的情况下调用一个getX方法,为查找提供一个键。如果没有找到该值,则返回null。

    Integer intValue = db.getInteger("MyTestDataStore", key);// or
    DataStore store = db.getStore("MyTestDataStore");
    intValue = store.getInteger(key);

    调用remove方法删除数据值条目,提供值的键值,例如:

    String key = ...;
    db.remove("MyTestDataStore", key);// or
    DataStore store = db.getStore("MyTestDataStore");
    store.remove(key);

    要在不使用键的情况下遍历数据存储中的条目,可以调用iterateStart开始并重复调用iterateNext,直到返回一个空值。如果数据存储中没有值,iterateStart将返回null。

    Object val = db.iterateStart("MyTestDataStore");while ( val != null ) {// do something with val// ...
    val = db.iterateNext("MyTestDataStore");
    }

    你可以通过调用deleteStore来删除整个数据存储(也就是说,删除它所有的条目和删除它作为持久化存储创建的备份数据文件),例如:

    db.deleteStore("MyTestDataStore");

    总结

    NoHeapDB实现的优势是显而易见的:

    • 基于内存的存储性能

    • 高速、高效、磁盘持久性

    • 没有垃圾收集损失,因为数据在Java堆之外

    • 易用性

    • 100%纯Java解决方案

    这个实现还可以更进一步。这里有一些潜在改进的例子:

    • 使用ByteBuffer对象的内部数组来超过2gb的最大大小限制。

    • 添加安全选项,例如数据加密和用户身份验证。

    • 扩展MappedByteBuffer的使用,使其仅在内存中缓存文件的一部分。这对于大于物理RAM的基于文件的数据集非常有用。

    • 提供云中的第二层存储。

    这些只是我正在进行的一些增强。您可以在这里下载NoHeapDB存储的当前实现。http://www.ericbruno.com/NoHeapDB-EricBruno.zip

    展开全文
  • 查询结果是上千条甚至是上百万条记录,要求查询问题描述:在通常的三层构架下,客户通过Browser请求Web服务器查询数据库,而查询结果是上千条甚至是上百万条记录,要求查询结果传送客户端浏览器并分页显示。...
  • <p>java中为了分析高效快速,把数据到内存中,使用什么方式查询速度会更快,内存占用最少</p>
  • 今天进行库存查询,select * from table 查询出来的数据量有60万条,报了内存溢出的错误. 解决方式一: 对于mysql,我们只需要在driverurl后面加上:useCursorFetch=true&defaultFetchSize=500,开启fetchsize...
  • 下面说说看到的工作项目中的代码,是这个样子的,事先查询一次数据库,将查询到的整张表的数据到内存,以后使用时不再查询数据库,而直接操作内存中的数据,这主要用于数据库中的数据比较稳定,不会轻易改变的情况...
  • 背景 由于在网关中需要对接入的下级平台进行识别校验,只有合法认证的下级平台才会接入网关中,为了提高数据查询效率,我们不能每次都需要查询数据库,所以我们在早期使用了redis作为下级平台缓存,每次需要查询的...
  • javalist进行内存分页显示数据到页面

    千次阅读 2019-04-19 13:02:28
    由于项目某个页面从多方来源展示,无法做到查询数据库并进行分页,且数据量不大,只好用内存分页,奈何天资愚钝,网上找了半天加以改良成适合我的,才总算做好了分页,但是目前的坎又变成了筛选,暂不加以记录 ...
  • 将SQLServer数据表驻留内存是SQLServer提供的一项功能,在一般小型系统的开发过程中数据库将SQLServer数据表驻留内存是SQLServer提供的一项功能,在一般小型系统的开发过程中估计很少会涉及。这里整理了相关文档...
  • 问题描述:在通常的三层构架下,客户通过Browser请求Web服务器查询数据库,而查询结果是上千条甚至是上百万条记录,要求查询结果传送客户端浏览器并分页显示。考虑因素:1. Web服务器的资源消耗,包括:内存(用来...
  • 本小节主要讨论hazelcast如何使数据在各个节点被共享,以及...首先在Node A 上Node A上的输出接着在Node B上Node B上的输出可以看到在Node B可以查询到Node A上所插入的数据.下面就来看hazelcast是如何实现的,先来看...
  • 我们在没有经过设计的时候是这样来处理的,先写一个集合,然后执行jdbc,将返回的结果赋值给list,然后再返回页面上,但是当数据量大的时候,就会出现数据无法返回,内存溢出的情况,于是我们在有限的时间和空间下...
  • 使用Decorator设计模式增强response对象response增强案例——缓存数据到内存对于页面中很少更新的数据,例如商品分类,为避免每次都要从数据库查询分类数据,因此可把分类数据缓存在内存或文件中,以此来减轻数据库...
  • Java直接内存详解

    千次阅读 2017-12-10 10:46:17
    由于对直接内存的概念不是很清晰,所以查询了相关资料,准备一篇博文总结一下什么是直接内存直接内存(Direct Memory)就是Java堆外内存直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的...
  • 内存数据库(in-memory database,IMDB)是指那些完全用主存作为...还清晰记得记得当时我们在一个项目中即想利用数据库的查询优化、但是数据量又小足以被内存容纳的情况下,满街找DB2中类似的开关……现在有不少比较...
  • 在面试过程中,关于Java内存模型(侧重于:Java的并发原理)和JVM的内存模型并不是同一个问题,所以自己在通过查询资料之后对相关的知识点进行总结,如果有写得错误的地方,欢迎指出,也可以一起探讨。基于计算机的...
  • 数据库优化是一个很广的...仅对数据库系统本身而言,影响到查询 性能的因素从理论上来讲,包括数据库参数设置(其实就是通过参数控制数据库系统的内存,i/o,缓存,备份等一些管理性的东西),索引,分区,sql语句.数据库...
  • 一个400m的csv文件需要写入一个hashmap中,然后根据键值进行查询。 因为文件比较大,所以会出现内存溢出的问题。 首先第一个问题,400m的文件不好一次性放入一个hashmap中,里面有将近1000w条数据,因此把这个文件...
  • 最近进行编程的时候项目上线了,可是查询不了几次就发现出现连接池获取不连接,我把连接池已经调1000了,可是还是出现这种问题。所以怀疑我的数据库连接写的时候有问题,还请各位大神帮忙看看是怎么回事。 ...
  • 逻辑分页从数据库中将所有记录查询出业,存储到内存中,要想展示当前页数据,直接从内存中获取。优点:效率高缺点:占用内存比较高在java开发领域,我们使用的比较多的是物理分页。物理分页的实现:1.直接使用jdbc完成...
  • 一、 引起内存异常的...一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此
  • 在开发某个要操作数据库的项目时候,我突然想到如何数据库的数据量太大,一般项目设计的时候,会一次读取所有的数据到内存中,这有可能造成内存的溢出,或者系统效率的降低。那么我们如何设计一个开发方案来解决这中...
  • 那天中午吃饭,一个同事说,那个项目组的人快气死我了,程序有问题,早晨在群里@了他们,中午才回消息,然后竟然还说他们的程序没有问题,是我们这边调用的太频繁了。...用户数据已经打通,其实很简单的需求,...
  • 在写oracle的存储过程时,都会遇到一些业务逻辑,需要写Function,来实现一些业务,然后返回一个结果,而在使用这个Function的时候又是通过一句SQL来直接调用,但是如果在Function中有查询数据表读取配置或者其他...
  • Java 获取内存使用情况

    千次阅读 2018-03-08 14:01:42
    公司要求将各个设备上传的数据分析并且导出到统一的一个表中,目前在准备,数据比较多,所以需要考虑到内存的使用情况,经网上查询使用,出现了以下问题。 第一种方法Runtime LogUtils.debug("TotalMemory&...
  • Java 内存泄漏问题

    2013-04-09 20:36:40
    因为系统有内存泄露问题,导致频繁的Full GC,用jmap将内存使用情况dump下来,然后通过mat分析了一下,发现是由于缓存map和更新该map的线程池导致的,截图见附件,下面这个是涉及的类,研究了好几天这个问题,现在...
  • 1. 应用背景程序在设计...里面有5000块钱,然后你到取款机取款,取出3000,当正在取的时候,取款机已经查询到你有5000块钱,然后正准备减去300块钱的时候2)、你的老婆拿着那张银行卡对应的存折到银行取钱,也要取30...
  • 在面试过程中,关于Java内存模型(侧重于:Java的并发原理)和JVM的内存模型并不是同一个问题,所以自己在通过查询资料之后对相关的知识点进行总结,如果有写得错误的地方,欢迎指出,也可以一起探讨。 基于计算机...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 797
精华内容 318
关键字:

java数据查询到内存

java 订阅