Gavin要加油

Gavin要加油 查看完整档案

北京编辑西北大学  |  物联网工程 编辑北京京东世纪贸易有限公司  |  数据挖掘工程师 编辑 blog.gavinzh.com 编辑
编辑

test

个人动态

Gavin要加油 评论了文章 · 2018-09-04

实现生产者消费者模式的四种方式(Synchronized、Lock、Semaphore、BlockingQueue)

所谓生产者消费者模式,即N个线程进行生产,同时N个线程进行消费,两种角色通过内存缓冲区进行通信
生产着消费者图解
图片来源https://www.cnblogs.com/chent...

下面我们通过四种方式,来实现生产者消费者模式。

首先是最原始的synchronized方式

定义库存类(即图中缓存区)

class Stock {
    private String name;
    // 标记库存是否有内容
    private boolean hasComputer = false;

    public synchronized void putOne(String name) {
        // 若库存中已有内容,则生产线程阻塞等待
        while (hasComputer) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name = name;
        System.out.println("生产者...生产了 " + name);
        // 更新标记
        this.hasComputer = true;
        // 这里用notify的话,假设p0执行完毕,此时c0,c1都在wait, 同时唤醒另一个provider:p1,
        // p1判断标记后休眠,造成所有线程都wait的局面,即死锁;
        // 因此使用notifyAll解决死锁问题
        this.notifyAll();
    }

    public synchronized void takeOne() {
        // 若库存中没有内容,则消费线程阻塞等待生产完毕后继续
        while (!hasComputer) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("消费者...消费了 " + name);
        this.hasComputer = false;
        this.notifyAll();
    }
}

定义生产者和消费者(为了节省空间和方便阅读,这里将生产者和消费者定义成了匿名内部类)

public static void main(String[] args) {
    // 用于通信的库存类
    Stock computer = new Stock();
    // 定义两个生产者和两个消费者
    Thread p1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.putOne("Dell");
            }
        }
    });
    Thread p2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.putOne("Mac");
            }
        }
    });
    
    Thread c1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.takeOne();
            }
        }
    });
    Thread c2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.takeOne();
            }
        }
    });
    p1.start();
    p2.start();
    c1.start();
    c2.start();
}

运行结果图
synchronized方式运行结果图


第二种方式:Lock

Jdk1.5之后加入了Lock接口,一个lock对象可以有多个Condition类,Condition类负责对lock对象进行wait,notify,notifyall操作

定义库存类

class LockStock {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    // 加入库存概念,可批量生产和消费
    // 定义最大库存为10
    final String[] stock = new String[10];
    // 写入标记、读取标记、已有商品数量
    int putptr, takeptr, count;

    public void put(String computer) {
        // lock代替synchronized
        lock.lock();
        try {
            // 若库存已满则生产者线程阻塞
            while (count == stock.length)
                notFull.await();
            // 库存中加入商品
            stock[putptr] = computer;
            // 库存已满,指针置零,方便下次重新写入
            if (++putptr == stock.length) putptr = 0;
            ++count;
            System.out.println(computer + " 正在生产数据: -- 库存剩余:" + count);
            notEmpty.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String take(String consumerName) {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            // 从库存中获取商品
            String computer = stock[takeptr];
            if (++takeptr == stock.length) takeptr = 0;
            --count;
            System.out.println(consumerName + " 正在消费数据:" + computer + " -- 库存剩余:" + count);
            notFull.signal();
            return computer;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        // 无逻辑作用,放慢速度
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "";
    }
}

以上部分代码摘自java7 API中Condition接口的官方示例

接着还是定义生产者和消费者

public static void main(String[] args) {
    LockStock computer = new LockStock();
    Thread p1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.put("Dell");
            }
        }
    });
    Thread p2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.put("Mac");
            }
        }
    });

    Thread c1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.take("zhangsan");
            }
        }
    });
    Thread c2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                computer.take("李四");
            }
        }
    });
    // 两个生产者两个消费者同时运行
    p1.start();
    p2.start();
    c1.start();
    c2.start();
}

运行结果图:

Lock方式运行结果图


第三种方式:Semaphore
首先依旧是库存类:

class Stock {
    List<String> stock = new LinkedList();
    // 互斥量,控制共享数据的互斥访问
    private Semaphore mutex = new Semaphore(1);

    // canProduceCount可以生产的总数量。 通过生产者调用acquire,减少permit数目
    private Semaphore canProduceCount = new Semaphore(10);

    // canConsumerCount可以消费的数量。通过生产者调用release,增加permit数目
    private Semaphore canConsumerCount = new Semaphore(0);

    public void put(String computer) {
        try {
            // 可生产数量 -1
            canProduceCount.acquire();
            mutex.acquire();
            // 生产一台电脑
            stock.add(computer);
            System.out.println(computer + " 正在生产数据" + " -- 库存剩余:" + stock.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放互斥锁
            mutex.release();
            // 释放canConsumerCount,增加可以消费的数量
            canConsumerCount.release();
        }
        // 无逻辑作用,放慢速度
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void get(String consumerName) {
        try {
            // 可消费数量 -1
            canConsumerCount.acquire();
            mutex.acquire();
            // 从库存消费一台电脑
            String removedVal = stock.remove(0);
            System.out.println(consumerName + " 正在消费数据:" + removedVal + " -- 库存剩余:" + stock.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mutex.release();
            // 消费后释放canProduceCount,增加可以生产的数量
            canProduceCount.release();
        }
    }
}

还是生产消费者:

public class SemaphoreTest {
    public static void main(String[] args) {
        // 用于多线程操作的库存变量
        final Stock stock = new Stock();
        // 定义两个生产者和两个消费者
        Thread dellProducer = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    stock.put("Del");
                }
            }
        });
        Thread macProducer = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    stock.put("Mac");
                }
            }
        });
        Thread consumer1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    stock.get("zhangsan");
                }
            }
        });
        Thread consumer2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    stock.get("李四");
                }
            }
        });
        dellProducer.start();
        macProducer.start();
        consumer1.start();
        consumer2.start();
    }
}

运行结果图:

Semaphore运行结果图


第四种方式:BlockingQueue
BlockingQueue的put和take底层实现其实也是使用了第二种方式中的ReentrantLock+Condition,并且帮我们实现了库存队列,方便简洁
1、定义生产者

class Producer implements Runnable {
    // 库存队列
    private BlockingQueue<String> stock;
    // 生产/消费延迟
    private int timeOut;
    private String name;

    public Producer(BlockingQueue<String> stock, int timeout, String name) {
        this.stock = stock;
        this.timeOut = timeout;
        this.name = name;
    }

    @Override
    public void run() {
        while (true) {
            try {
                stock.put(name);
                System.out.println(name + " 正在生产数据" + " -- 库存剩余:" + stock.size());
                TimeUnit.MILLISECONDS.sleep(timeOut);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2、定义消费者

class Consumer implements Runnable {
    // 库存队列
    private BlockingQueue<String> stock;
    private String consumerName;

    public Consumer(BlockingQueue<String> stock, String name) {
        this.stock = stock;
        this.consumerName = name;
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 从库存消费一台电脑
                String takeName = stock.take();
                System.out.println(consumerName + " 正在消费数据:" + takeName + " -- 库存剩余:" + stock.size());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3、定义库存并运行

public static void main(String[] args) {
        // 定义最大库存为10
        BlockingQueue<String> stock = new ArrayBlockingQueue<>(10);
        Thread p1 = new Thread(new Producer(stock, 500, "Mac"));
        Thread p2 = new Thread(new Producer(stock, 500, "Dell"));
        Thread c1 = new Thread(new Consumer(stock,"zhangsan"));
        Thread c2 = new Thread(new Consumer(stock, "李四"));

        p1.start();
        p2.start();
        c1.start();
        c2.start();

    }

运行结果图:
BlockingQueue运行结果图.png

感谢阅读~欢迎指正和补充~~~

查看原文

Gavin要加油 评论了文章 · 2018-09-04

利用Redis锁解决高并发问题

这里我们主要利用Redissetnx的命令来处理高并发。

setnx 有两个参数。第一个参数表示键。第二个参数表示值。如果当前键不存在,那么会插入当前键,将第二个参数做为值。返回 1。如果当前键存在,那么会返回0

创建库存表

CREATE TABLE `storage` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `number` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

设置初始库存为10

创建订单表

CREATE TABLE `order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `number` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

测试不用锁的时候

$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'root');
$sql="select `number` from  storage where id=1 limit 1";
$res = $pdo->query($sql)->fetch();
$number = $res['number'];
if($number>0)
{
    $sql ="insert into `order`  VALUES (null,$number)";

    $order_id = $pdo->query($sql);
    if($order_id)
    {

        $sql="update storage set `number`=`number`-1 WHERE id=1";
        $pdo->query($sql);
    }
}

ab测试模拟并发,发现库存是正确的。

mysql> select * from storage;
+----+--------+
| id | number |
+----+--------+
|  1 |      0 |
+----+--------+
1 row in set (0.00 sec)

在来看订单表

mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
|  1 |     10 |
|  2 |     10 |
|  3 |      9 |
|  4 |      7 |
|  5 |      6 |
|  6 |      5 |
|  7 |      5 |
|  8 |      5 |
|  9 |      4 |
| 10 |      1 |
+----+--------+
10 rows in set (0.00 sec)

发现存在几个订单都是操作的同一个库存数据,这样就可能引起超卖的情况。

修改代码加入redis锁进行数据控制

<?php
/**
 * Created by PhpStorm.
 * User: daisc
 * Date: 2018/7/23
 * Time: 14:45
 */
class Lock
{
    private static $_instance ;
    private   $_redis;
    private function __construct()
    {
        $this->_redis =  new Redis();
        $this->_redis ->connect('127.0.0.1');
    }
    public static function getInstance()
    {
        if(self::$_instance instanceof self)
        {
            return self::$_instance;
        }
        return self::$_instance = new  self();
    }

    /**
     * @function 加锁
     * @param $key 锁名称
     * @param $expTime 过期时间
      */
    public function set($key,$expTime)
    {
        //初步加锁
        $isLock = $this->_redis->setnx($key,time()+$expTime);
        if($isLock)
        {
            return true;
        }
        else
        {
            //加锁失败的情况下。判断锁是否已经存在,如果锁存在切已经过期,那么删除锁。进行重新加锁
            $val = $this->_redis->get($key);
            if($val&&$val<time())
            {
                $this->del($key);
                return  $this->_redis->setnx($key,time()+$expTime);
            }
            return false;
        }
    }


    /**
     * @param $key 解锁
     */
    public function del($key)
    {
        $this->_redis->del($key);
    }

}



$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'root');
$lockObj = Lock::getInstance();
//判断是能加锁成功
if($lock = $lockObj->set('storage',10))
{
    $sql="select `number` from  storage where id=1 limit 1";
    $res = $pdo->query($sql)->fetch();
    $number = $res['number'];
    if($number>0)
    {
        $sql ="insert into `order`  VALUES (null,$number)";

        $order_id = $pdo->query($sql);
        if($order_id)
        {

            $sql="update storage set `number`=`number`-1 WHERE id=1";
            $pdo->query($sql);
        }
    }
    //解锁
    $lockObj->del('storage');

}
else
{
    //加锁不成功执行其他操作。
}

再次进行ab测试,查看测试结果

mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
|  1 |     10 |
|  2 |      9 |
|  3 |      8 |
|  4 |      7 |
|  5 |      6 |
|  6 |      5 |
|  7 |      4 |
|  8 |      3 |
|  9 |      2 |
| 10 |      1 |
+----+--------+
10 rows in set (0.00 sec)

发现订单表没有操作同一个库存数据的情况。所以利用redis锁是可以有效的处理高并发的。

这里在加锁的时候其实是可以不需要判断过期时间的,这里我们为了避免造成死锁,所以加一个过期时间的判断。当过期的时候主动删除该锁。

图片描述

查看原文

Gavin要加油 评论了文章 · 2018-09-04

「造个轮子」——cicada(轻量级 WEB 框架)

前言

俗话说 「不要重复造轮子」,关于是否有必要不再本次讨论范围。

创建这个项目的主要目的还是提升自己,看看和知名类开源项目的差距以及学习优秀的开源方式。

好了,现在着重来谈谈 cicada 这个项目的核心功能。

我把他定义为一个快速、轻量级 WEB 框架;没有过多的依赖,核心 jar 包仅 30KB。

也仅需要一行代码即可启动一个 HTTP 服务。


特性

现在来谈谈重要的几个特性。

当前版本主要实现了基本的请求、响应、自定义参数以及拦截器功能。

功能虽少,但五脏俱全。

在今后的迭代过程中会逐渐完善上图功能,有好的想法也欢迎提 https://github.com/crossoverJie/cicada/issues

快速启动

下面来看看如何快速启动一个 HTTP 服务。

只需要创建一个 Maven 项目,并引入核心包。

<dependency>
    <groupId>top.crossoverjie.opensource</groupId>
    <artifactId>cicada-core</artifactId>
    <version>1.0.0</version>
</dependency>

如上图所示,再配置一个启动类即可。

public class MainStart {

    public static void main(String[] args) throws InterruptedException {
        CicadaServer.start(MainStart.class,"/cicada-example") ;
    }
}

配置业务 Action

当然我们还需要一个实现业务逻辑的地方。cicada 提供了一个接口,只需要实现该接口即可实现具体逻辑。

创建业务 Action 实现 top.crossoverjie.cicada.server.action.WorkAction 接口。

@CicadaAction(value = "demoAction")
public class DemoAction implements WorkAction {


    private static final Logger LOGGER = LoggerBuilder.getLogger(DemoAction.class) ;

    private static AtomicLong index = new AtomicLong() ;

    @Override
    public WorkRes<DemoResVO> execute(Param paramMap) throws Exception {
        String name = paramMap.getString("name");
        Integer id = paramMap.getInteger("id");
        LOGGER.info("name=[{}],id=[{}]" , name,id);

        DemoResVO demoResVO = new DemoResVO() ;
        demoResVO.setIndex(index.incrementAndGet());
        WorkRes<DemoResVO> res = new WorkRes();
        res.setCode(StatusEnum.SUCCESS.getCode());
        res.setMessage(StatusEnum.SUCCESS.getMessage());
        res.setDataBody(demoResVO) ;
        return res;
    }

}

同时需要再自定义类中加上 @CicadaAction 注解,并需要指定一个 value,该 value 主要是为了在请求路由时能找到业务类。

这样启动应用并访问

http://127.0.0.1:7317/cicada-example/demoAction?name=12345&id=10

便能执行业务逻辑同时得到服务端的返回。

目前默认支持的是 json 响应,后期也会加上模板解析。

服务中也会打印相关日志。

灵活的参数配置

这里所有的请求参数都封装在 Param 中,可以利用其中的各种 API 获取请求数据。

之所以是灵活的:我们甚至可以这样请求:

http://127.0.0.1:7317/cicada-example/demoAction?jsonData="info": {
    "age": 22,
    "name": "zhangsan"
  }

这样就可以传递任意结构的数据,只要业务处理时进行解析即可。

自定义拦截器

拦截器是一个框架的基本功能,可以利用拦截器实现日志记录、事务提交等通用工作。

为此 cicada 提供一个接口: top.crossoverjie.cicada.server.intercept.CicadaInterceptor

我们只需要实现该接口即可编写拦截功能:

@Interceptor(value = "executeTimeInterceptor")
public class ExecuteTimeInterceptor implements CicadaInterceptor {

    private static final Logger LOGGER = LoggerBuilder.getLogger(ExecuteTimeInterceptor.class);

    private Long start;

    private Long end;

    @Override
    public void before(Param param) {
        start = System.currentTimeMillis();
    }

    @Override
    public void after(Param param) {
        end = System.currentTimeMillis();

        LOGGER.info("cast [{}] times", end - start);
    }
}

这里演示的是记录所有 action 的执行时间。

目前默认只实现了 action 的拦截,后期也会加入自定义拦截器。

拦截适配器

虽说在拦截器中提供了 before/after 两个方法,但也不是所有的方法都需要实现。

因此 cicada 提供了一个适配器:

top.crossoverjie.cicada.server.intercept.AbstractCicadaInterceptorAdapter

我们需要继承他便可按需实现其中的某个方法,如下所示:

@Interceptor(value = "loggerInterceptor")
public class LoggerInterceptorAbstract extends AbstractCicadaInterceptorAdapter {

    private static final Logger LOGGER = LoggerBuilder.getLogger(LoggerInterceptorAbstract.class) ;

    @Override
    public void before(Param param) {
        LOGGER.info("logger param=[{}]",param.toString());
    }

}

性能测试

既然是一个 HTTP 服务框架,那性能自然也得保证。

在测试条件为:300 并发连续压测两轮;1G 内存、单核 CPU、1Mbps。用 Jmeter 压测情况如下:

同样的服务器用 Tomcat 来压测看看结果。

Tomcat 的线程池配置:

<Executor name="tomcatThreadPool" namePrefix="consumer-exec-"
        maxThreads="510" minSpareThreads="10"/>

我这里请求的是 Tomcat 的一个 doc 目录,虽说结果看似 cicada 的性能比 Tomcat 还强。

但其实这个对比过程中的变量并没有完全控制好,Tomcat 所返回的是 HTML,而 cicada 仅仅返回了 json,当然问题也不止这些。

但还是能说明 cicada 目前的性能还是不错的。

总结

本文没有过多讨论 cicada 实现原理,感兴趣的可以看看源码,都比较简单。

在后续的更新中会仔细探讨这块内容。

同时不出意外 cicada 会持续更新,未来也会加入更多实用的功能。

甚至我会在适当的时机将它应用于我的生产项目,也希望更多朋友能参与进来一起把这个「轮子」做的更好。

项目地址:https://github.com/crossoverJie/cicada

你的点赞与转发是最大的支持。

查看原文

Gavin要加油 回答了问题 · 2016-11-07

spring+mybatis,tomcat启动异常

namespace没有指定?

<mapper namespace="xxxx.xxxxx">

这里的xxxx.xxxxx指的是你的xml对应的接口名称

关注 2 回答 1

Gavin要加油 关注了问题 · 2016-11-07

spring+mybatis,tomcat启动异常

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in ServletContext resource [/WEB-INF/applicationContext.xml]: Invocation of init method failed; nested exception is org.springframework.core.NestedIOException: Failed to parse config resource: ServletContext resource [/WEB-INF/mybatis-config.xml]; nested exception is org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: org.apache.ibatis.builder.BuilderException: Error parsing Mapper XML. Cause: java.lang.NullPointerException
是映射文件的问题,在mybatis配置文件中去掉映射文件就没有问题
mapper文件:
<mapper>
<resultMap type="../org.gocom.crm.bean.Operator" id="operators">
<result property="userId" column="userid"/>
<result property="userName" column="operatorname"/>
<result property="password" column="password"/>
</resultMap>
<select id="queryOperator" resultType="operators">
<![CDATA[
select userid,operatorname,password from ac_operator where rownum<=10
]]>
</select>
</mapper>

关注 2 回答 1

Gavin要加油 回答了问题 · 2016-10-27

屏蔽 框架的 log4j 输出

建议使用slf4j这个日志统一接口,即使现在用的log4j,以后想换成别的日志框架也是很方便的。

接下来,你需要配置log4j.properties这个文件

log4j.rootLogger=error,stdout


log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout

#你的项目xxx包
log4j.logger.xxx.*=DEBUG,stdout

以上日志配置意思是:
你的项目包下的日志都打印debug级别的信息,输出到控制台。
其他日志只显示error级别的。

推荐我的一篇文章:https://segmentfault.com/a/11...
logback是log4j的作者写的新的日志框架,效率很高。

关注 2 回答 1

Gavin要加油 关注了问题 · 2016-10-27

屏蔽 框架的 log4j 输出

最近尝试在ssh的java项目中使用日志框架log4j取代system.out,但是发现加入log4j后,整个后台都被ssh的调试信息输出占满,我的本意是只显示自己的调试信息,不想看框架的调试信息,该怎么设置?还有如果我的项目中大量的使用log4j来输出调试信息,打包发布的时候,调高输出等级的话,框架会把这些输出语句也编译到class里面吗?还是说能做到智能点,始终少一点语句运行效率更高。

关注 2 回答 1

Gavin要加油 关注了问题 · 2016-10-27

解决如何理解java的异常处理机制?

在学习java时,如何看待java的异常处理机制,应不应该把异常看作一个普通的对象?

应不应该把catch看作逻辑正确的完整的一部分?

如何对java的异常处理机制有更深刻的理解以便更加适合的使用它?

关注 3 回答 2

Gavin要加油 回答了问题 · 2016-10-27

解决如何理解java的异常处理机制?

首先要知道,java中一切皆是对象,异常当然是一个对象。
接下来要知道异常分为受检查异常运行时异常(感谢@泊浮目提醒)。
你所说的异常处理机制应该是受检查异常,受检查异常是可以被java的异常处理机制所处理的,因为他们都是实现了Throwable这个接口。

catch是逻辑正确完整的一部分
因为catch住的异常可能会影响你的方法下一步的动作。
例如打开一个文件,这时你得确定文件是打开了的这样才可以读写信息。
catch住的异常会让你确认如果文件没有打开,原因是什么,你接下来需要怎么做。是创建新文件?还是就此返回,不读写信息?

最后,想了解异常处理机制,还是需要看《java编程思想》这本神书的。

关注 3 回答 2

Gavin要加油 发布了文章 · 2016-10-27

Guava学习:Cache缓存入门

摘要: 学习Google内部使用的工具包Guava,在Java项目中轻松地增加缓存,提高程序获取数据的效率。

一、什么是缓存?

根据科普中国的定义,缓存就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多,故缓存的作用就是帮助硬件更快地运行。

在这里,我们借用了硬件缓存的概念,当在Java程序中计算或查询数据的代价很高,并且对同样的计算或查询条件需要不止一次获取数据的时候,就应当考虑使用缓存。换句话说,缓存就是以空间换时间,大部分应用在各种IO,数据库查询等耗时较长的应用当中。

二、缓存原理

当获取数据时,程序将先从一个存储在内存中的数据结构中获取数据。如果数据不存在,则在磁盘或者数据库中获取数据并存入到数据结构当中。之后程序需要再次获取数据时,则会先查询这个数据结构。从内存中获取数据时间明显小于通过IO获取数据,这个数据结构就是缓存的实现。

这里引入一个概念,缓存命中率:从缓存中获取到数据的次数/全部查询次数,命中率越高说明这个缓存的效率好。由于机器内存的限制,缓存一般只能占据有限的内存大小,缓存需要不定期的删除一部分数据,从而保证不会占据大量内存导致机器崩溃。

如何提高命中率呢?那就得从删除一部分数据着手了。目前有三种删除数据的方式,分别是:FIFO(先进先出)LFU(定期淘汰最少使用次数)LRU(淘汰最长时间未被使用)

三、GuavaCache工作方式

GuavaCache的工作流程获取数据->如果存在,返回数据->计算获取数据->存储返回。由于特定的工作流程,使用者必须在创建Cache或者获取数据时指定不存在数据时应当怎么获取数据。GuavaCache采用LRU的工作原理,使用者必须指定缓存数据的大小,当超过缓存大小时,必定引发数据删除。GuavaCache还可以让用户指定缓存数据的过期时间,刷新时间等等很多有用的功能。

四、GuavaCache使用Demo

4.1 简单使用

有人说我就想简简单单的使用cache,就像Map那样方便就行。接下来展示一段简单的使用方式。

首先定义一个需要存储的Bean,对象Man:

/**
 * @author jiangmitiao
 * @version V1.0
 * @Title: 标题
 * @Description: Bean
 * @date 2016/10/27 10:01
 */
public class Man {
    //身份证号
    private String id;
    //姓名
    private String name;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Man{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

接下来我们写一个Demo:

import com.google.common.cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;


/**
 * @author jiangmitiao
 * @version V1.0
 * @Description: Demo
 * @date 2016/10/27 10:00
 */
public class GuavaCachDemo {
    private LoadingCache<String,Man> loadingCache;

    //loadingCache
    public void InitLoadingCache() {
        //指定一个如果数据不存在获取数据的方法
        CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() {
            @Override
            public Man load(String key) throws Exception {
                //模拟mysql操作
                Logger logger = LoggerFactory.getLogger("LoadingCache");
                logger.info("LoadingCache测试 从mysql加载缓存ing...(2s)");
                Thread.sleep(2000);
                logger.info("LoadingCache测试 从mysql加载缓存成功");
                Man tmpman = new Man();
                tmpman.setId(key);
                tmpman.setName("其他人");
                if (key.equals("001")) {
                    tmpman.setName("张三");
                    return tmpman;
                }
                if (key.equals("002")) {
                    tmpman.setName("李四");
                    return tmpman;
                }
                return tmpman;
            }
        };
        //缓存数量为1,为了展示缓存删除效果
        loadingCache = CacheBuilder.newBuilder().maximumSize(1).build(cacheLoader);
    }
    //获取数据,如果不存在返回null
    public Man getIfPresentloadingCache(String key){
        return loadingCache.getIfPresent(key);
    }
    //获取数据,如果数据不存在则通过cacheLoader获取数据,缓存并返回
    public Man getCacheKeyloadingCache(String key){
        try {
            return loadingCache.get(key);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return null;
    }
    //直接向缓存put数据
    public void putloadingCache(String key,Man value){
        Logger logger = LoggerFactory.getLogger("LoadingCache");
        logger.info("put key :{} value : {}",key,value.getName());
        loadingCache.put(key,value);
    }
}

接下来,我们写一些测试方法,检测一下

public class Test {
    public static void main(String[] args){
        GuavaCachDemo cachDemo = new GuavaCachDemo()
        System.out.println("使用loadingCache");
        cachDemo.InitLoadingCache();

        System.out.println("使用loadingCache get方法  第一次加载");
        Man man = cachDemo.getCacheKeyloadingCache("001");
        System.out.println(man);

        System.out.println("\n使用loadingCache getIfPresent方法  第一次加载");
        man = cachDemo.getIfPresentloadingCache("002");
        System.out.println(man);

        System.out.println("\n使用loadingCache get方法  第一次加载");
        man = cachDemo.getCacheKeyloadingCache("002");
        System.out.println(man);

        System.out.println("\n使用loadingCache get方法  已加载过");
        man = cachDemo.getCacheKeyloadingCache("002");
        System.out.println(man);

        System.out.println("\n使用loadingCache get方法  已加载过,但是已经被剔除掉,验证重新加载");
        man = cachDemo.getCacheKeyloadingCache("001");
        System.out.println(man);

        System.out.println("\n使用loadingCache getIfPresent方法  已加载过");
        man = cachDemo.getIfPresentloadingCache("001");
        System.out.println(man);

        System.out.println("\n使用loadingCache put方法  再次get");
        Man newMan = new Man();
        newMan.setId("001");
        newMan.setName("额外添加");
        cachDemo.putloadingCache("001",newMan);
        man = cachDemo.getCacheKeyloadingCache("001");
        System.out.println(man);
    }
}

测试结果如下:

150850_81Jv_1983603.png

4.2 高级特性

由于目前使用有局限性,接下来只讲我用到的一些方法。

我来演示一下GuavaCache自带的两个Cache

GuavaCacheDemo.java

import com.google.common.cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;


/**
 * @author jiangmitiao
 * @version V1.0
 * @Description: Demo
 * @date 2016/10/27 10:00
 */
public class GuavaCachDemo {
    private Cache<String, Man> cache;
    private LoadingCache<String,Man> loadingCache;
    private RemovalListener<String, Man> removalListener;

    public void Init(){
        //移除key-value监听器
        removalListener = new RemovalListener<String, Man>(){
            public void onRemoval(RemovalNotification<String, Man> notification) {
                Logger logger = LoggerFactory.getLogger("RemovalListener");
                logger.info(notification.getKey()+"被移除");
                //可以在监听器中获取key,value,和删除原因
                notification.getValue();
                notification.getCause();//EXPLICIT、REPLACED、COLLECTED、EXPIRED、SIZE

            }};
        //可以使用RemovalListeners.asynchronous方法将移除监听器设为异步方法
        //removalListener = RemovalListeners.asynchronous(removalListener, new ThreadPoolExecutor(1,1,1000, TimeUnit.MINUTES,new ArrayBlockingQueue<Runnable>(1)));
    }

    //loadingCache
    public void InitLoadingCache() {
        //指定一个如果数据不存在获取数据的方法
        CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() {
            @Override
            public Man load(String key) throws Exception {
                //模拟mysql操作
                Logger logger = LoggerFactory.getLogger("LoadingCache");
                logger.info("LoadingCache测试 从mysql加载缓存ing...(2s)");
                Thread.sleep(2000);
                logger.info("LoadingCache测试 从mysql加载缓存成功");
                Man tmpman = new Man();
                tmpman.setId(key);
                tmpman.setName("其他人");
                if (key.equals("001")) {
                    tmpman.setName("张三");
                    return tmpman;
                }
                if (key.equals("002")) {
                    tmpman.setName("李四");
                    return tmpman;
                }
                return tmpman;
            }
        };
        //缓存数量为1,为了展示缓存删除效果
        loadingCache = CacheBuilder.newBuilder().
                //设置2分钟没有获取将会移除数据
                expireAfterAccess(2, TimeUnit.MINUTES).
                //设置2分钟没有更新数据则会移除数据
                expireAfterWrite(2, TimeUnit.MINUTES).
                //每1分钟刷新数据
                refreshAfterWrite(1,TimeUnit.MINUTES).
                //设置key为弱引用
                weakKeys().
//                weakValues().//设置存在时间和刷新时间后不能再次设置
//                softValues().//设置存在时间和刷新时间后不能再次设置
                maximumSize(1).
                removalListener(removalListener).
                build(cacheLoader);
    }

    //获取数据,如果不存在返回null
    public Man getIfPresentloadingCache(String key){
        return loadingCache.getIfPresent(key);
    }

    //获取数据,如果数据不存在则通过cacheLoader获取数据,缓存并返回
    public Man getCacheKeyloadingCache(String key){
        try {
            return loadingCache.get(key);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return null;
    }

    //直接向缓存put数据
    public void putloadingCache(String key,Man value){
        Logger logger = LoggerFactory.getLogger("LoadingCache");
        logger.info("put key :{} value : {}",key,value.getName());
        loadingCache.put(key,value);
    }

    public void InitDefault() {
        cache = CacheBuilder.newBuilder().
                expireAfterAccess(2, TimeUnit.MINUTES).
                expireAfterWrite(2, TimeUnit.MINUTES).
//                refreshAfterWrite(1,TimeUnit.MINUTES).//没有cacheLoader的cache不能设置刷新,因为没有指定获取数据的方式
                weakKeys().
//                weakValues().//设置存在时间和刷新时间后不能再次设置
//                softValues().//设置存在时间和刷新时间后不能再次设置
                maximumSize(1).
                removalListener(removalListener).
                build();
    }

    public Man getIfPresentCache(String key){
        return cache.getIfPresent(key);
    }
    public Man getCacheKeyCache(final String key) throws ExecutionException {
        return cache.get(key, new Callable<Man>() {
            public Man call() throws Exception {
                //模拟mysql操作
                Logger logger = LoggerFactory.getLogger("Cache");
                logger.info("Cache测试 从mysql加载缓存ing...(2s)");
                Thread.sleep(2000);
                logger.info("Cache测试 从mysql加载缓存成功");
                Man tmpman = new Man();
                tmpman.setId(key);
                tmpman.setName("其他人");
                if (key.equals("001")) {
                    tmpman.setName("张三");
                    return tmpman;
                }
                if (key.equals("002")) {
                    tmpman.setName("李四");
                    return tmpman;
                }
                return tmpman;
            }
        });
    }

    public void putCache(String key,Man value){
        Logger logger = LoggerFactory.getLogger("Cache");
        logger.info("put key :{} value : {}",key,value.getName());
        cache.put(key,value);
    }
}

在这个demo中,分别采用了Guava自带的两个Cache:LocalLoadingCache和LocalManualCache。并且添加了监听器,当数据被删除后会打印日志。

Main:

public static void main(String[] args){
    GuavaCachDemo cachDemo = new GuavaCachDemo();
    cachDemo.Init();

    System.out.println("使用loadingCache");
    cachDemo.InitLoadingCache();

    System.out.println("使用loadingCache get方法  第一次加载");
    Man man = cachDemo.getCacheKeyloadingCache("001");
    System.out.println(man);

    System.out.println("\n使用loadingCache getIfPresent方法  第一次加载");
    man = cachDemo.getIfPresentloadingCache("002");
    System.out.println(man);

    System.out.println("\n使用loadingCache get方法  第一次加载");
    man = cachDemo.getCacheKeyloadingCache("002");
    System.out.println(man);

    System.out.println("\n使用loadingCache get方法  已加载过");
    man = cachDemo.getCacheKeyloadingCache("002");
    System.out.println(man);

    System.out.println("\n使用loadingCache get方法  已加载过,但是已经被剔除掉,验证重新加载");
    man = cachDemo.getCacheKeyloadingCache("001");
    System.out.println(man);

    System.out.println("\n使用loadingCache getIfPresent方法  已加载过");
    man = cachDemo.getIfPresentloadingCache("001");
    System.out.println(man);

    System.out.println("\n使用loadingCache put方法  再次get");
    Man newMan = new Man();
    newMan.setId("001");
    newMan.setName("额外添加");
    cachDemo.putloadingCache("001",newMan);
    man = cachDemo.getCacheKeyloadingCache("001");
    System.out.println(man);

    ///////////////////////////////////
    System.out.println("\n\n使用Cache");
    cachDemo.InitDefault();

    System.out.println("使用Cache get方法  第一次加载");
    try {
        man = cachDemo.getCacheKeyCache("001");
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println(man);

    System.out.println("\n使用Cache getIfPresent方法  第一次加载");
    man = cachDemo.getIfPresentCache("002");
    System.out.println(man);

    System.out.println("\n使用Cache get方法  第一次加载");
    try {
        man = cachDemo.getCacheKeyCache("002");
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println(man);

    System.out.println("\n使用Cache get方法  已加载过");
    try {
        man = cachDemo.getCacheKeyCache("002");
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println(man);

    System.out.println("\n使用Cache get方法  已加载过,但是已经被剔除掉,验证重新加载");
    try {
        man = cachDemo.getCacheKeyCache("001");
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println(man);

    System.out.println("\n使用Cache getIfPresent方法  已加载过");
    man = cachDemo.getIfPresentCache("001");
    System.out.println(man);

    System.out.println("\n使用Cache put方法  再次get");
    Man newMan1 = new Man();
    newMan1.setId("001");
    newMan1.setName("额外添加");
    cachDemo.putloadingCache("001",newMan1);
    man = cachDemo.getCacheKeyloadingCache("001");
    System.out.println(man);
}

测试结果如下:
152412_Afd2_1983603.png

152425_uKCJ_1983603.png

由上述结果可以表明,GuavaCache可以在数据存储到达指定大小后删除数据结构中的数据。我们可以设置定期删除而达到定期从数据库、磁盘等其他地方更新数据等(再次访问时数据不存在重新获取)。也可以采用定时刷新的方式更新数据

还可以设置移除监听器对被删除的数据进行一些操作。通过RemovalListeners.asynchronous(RemovalListener,Executor)方法将监听器设为异步,笔者通过实验发现,异步监听不会在删除数据时立刻调用监听器方法。

五、GuavaCache结构初探

153356_Z1zV_1983603.png

类结构图

GuavaCache并不希望我们设置复杂的参数,而让我们采用建造者模式创建Cache。GuavaCache分为两种Cache:CacheLoadingCache。LoadingCache继承了Cache,他比Cache主要多了get和refresh方法。多这两个方法能干什么呢?

在第四节高级特性demo中,我们看到builder生成不带CacheLoader的Cache实例。在类结构图中其实是生成了LocalManualCache类实例。而带CacheLoader的Cache实例生成的是LocalLoadingCache。他可以定时刷新数据,因为获取数据的方法已经作为构造参数方法存入了Cache实例中。同样,在get时,不需要像LocalManualCache还需要传入一个Callable实例。

实际上,这两个Cache实现类都继承自LocalCache,大部分实现都是父类做的。

六、总结回顾

缓存加载:CacheLoader、Callable、显示插入(put)

缓存回收:LRU,定时(expireAfterAccessexpireAfterWrite),软弱引用,显示删除(Cache接口方法invalidateinvalidateAll)

监听器:CacheBuilder.removalListener(RemovalListener)

清理缓存时间:只有在获取数据时才或清理缓存LRU,使用者可以单起线程采用Cache.cleanUp()方法主动清理。

刷新:主动刷新方法LoadingCache.referesh(K)

信息统计:CacheBuilder.recordStats() 开启Guava Cache的统计功能。Cache.stats() 返回CacheStats对象。(其中包括命中率等相关信息)

获取当前缓存所有数据:cache.asMap(),cache.asMap().get(Object)会刷新数据的访问时间(影响的是:创建时设置的在多久没访问后删除数据)

LocalManualCache和LocalLoadingCache的选择

ManualCache可以在get时动态设置获取数据的方法,而LoadingCache可以定时刷新数据。如何取舍?我认为在缓存数据有很多种类的时候采用第一种cache。而数据单一,数据库数据会定时刷新时采用第二种cache。

具体工程中的情况也欢迎大家与我交流,互相学习。

参考资料:

http://www.cnblogs.com/peida/...

https://github.com/tiantianga...

http://www.blogjava.net/DLevi...

http://ifeve.com/google-guava...

更多文章:http://blog.gavinzh.com

查看原文

赞 4 收藏 12 评论 0

认证与成就

  • 获得 68 次点赞
  • 获得 11 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-10-24
个人主页被 1.7k 人浏览