关于guava:一文读懂Guava-EventBus订阅发布事件

作者:京东科技 刘子洋背景最近我的项目呈现同一音讯发送屡次的景象,对上游业务方造成困扰,通过排查发现应用EventBus形式不正确。也借此机会学习了下EventBus并进行分享。以下为分享内容,本文次要分为五个局部,篇幅较长,望大家急躁浏览。 1、简述:简略介绍EventBus及其组成部分。2、原理解析:次要对listener注册流程及Event公布流程进行解析。3、应用领导:EventBus简略的应用领导。4、注意事项:在应用EventBus中须要留神的一些暗藏逻辑。5、分享时发问的问题6、我的项目中遇到的问题:上述问题进行详细描述并复现场景。1、简述1.1、概念下文摘自EventBus源码正文,从正文中能够直观理解到他的性能、个性、注意事项。 【源码正文】 Dispatches events to listeners, and provides ways for listeners to register themselves. The EventBus allows publish-subscribe-style communication between components without requiring the components to explicitly register with one another (and thus be aware of each other). It is designed exclusively to replace traditional Java in-process event distribution using explicit registration. It is not a general-purpose publish-subscribe system, nor is it intended for interprocess communication. Receiving Events ...

February 15, 2023 · 5 min · jiezi

关于guava:Google-Guava

Guava工程蕴含了若干被Google的java我的项目宽泛使依赖的外围库,例如:汇合、缓存、原生类型反对、并发库、通用注解、字符串解决、I/O等。不可变的对象当对象被不可信的库调用时,不可变的模式是平安的。不可变对象被多个线程调用时,不存在动态条件问题。不可变对象不须要思考变动,因而能够节省时间和空间。不可变对象因为固定不变,能够作为常量来平安应用。

November 27, 2022 · 1 min · jiezi

Guava Cache本地缓存在 Spring Boot应用中的实践

概述在如今高并发的互联网应用中,缓存的地位举足轻重,对提升程序性能帮助不小。而 3.x开始的 Spring也引入了对 Cache的支持,那对于如今发展得如火如荼的 Spring Boot来说自然也是支持缓存特性的。当然 Spring Boot默认使用的是 SimpleCacheConfiguration,即使用 ConcurrentMapCacheManager 来实现的缓存。但本文将讲述如何将 Guava Cache缓存应用到 Spring Boot应用中。Guava Cache是一个全内存的本地缓存实现,而且提供了线程安全机制,所以特别适合于代码中已经预料到某些值会被多次调用的场景下文就上手来摸一摸它,结合对数据库的操作,我们让 Guava Cache作为本地缓存来看一下效果!准备工作准备好数据库和数据表并插入相应实验数据(MySQL)比如我这里准备了一张用户表,包含几条记录:我们将通过模拟数据库的存取操作来看看 Guava Cache缓存加入后的效果。搭建工程:Springboot + MyBatis + MySQL + Guava Cachepom.xml 中添加如下依赖: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!–for mybatis–> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!–for Mysql–> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!– Spring boot Cache–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!–for guava cache–> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency> </dependencies>建立 Guava Cache配置类引入 Guava Cache的配置文件 GuavaCacheConfig@Configuration@EnableCachingpublic class GuavaCacheConfig { @Bean public CacheManager cacheManager() { GuavaCacheManager cacheManager = new GuavaCacheManager(); cacheManager.setCacheBuilder( CacheBuilder.newBuilder(). expireAfterWrite(10, TimeUnit.SECONDS). maximumSize(1000)); return cacheManager; }}Guava Cache配置十分简洁,比如上面的代码配置缓存存活时间为 10 秒,缓存最大数目为 1000 个配置 application.propertiesserver.port=82# Mysql 数据源配置spring.datasource.url=jdbc:mysql://121.116.23.145:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=xxxxxxspring.datasource.driver-class-name=com.mysql.jdbc.Driver# mybatis配置mybatis.type-aliases-package=cn.codesheep.springbt_guava_cache.entitymybatis.mapper-locations=classpath:mapper/*.xmlmybatis.configuration.map-underscore-to-camel-case=true编写数据库操作和 Guava Cache缓存的业务代码编写 entitypublic class User { private Long userId; private String userName; private Integer userAge; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Integer getUserAge() { return userAge; } public void setUserAge(Integer userAge) { this.userAge = userAge; }}编写 mapperpublic interface UserMapper { List<User> getUsers(); int addUser(User user); List<User> getUsersByName( String userName );}编写 service@Servicepublic class UserService { @Autowired private UserMapper userMapper; public List<User> getUsers() { return userMapper.getUsers(); } public int addUser( User user ) { return userMapper.addUser(user); } @Cacheable(value = “user”, key = “#userName”) public List<User> getUsersByName( String userName ) { List<User> users = userMapper.getUsersByName( userName ); System.out.println( “从数据库读取,而非读取缓存!” ); return users; }}看得很明白了,我们在 getUsersByName接口上添加了注解:@Cacheable。这是 缓存的使用注解之一,除此之外常用的还有 @CachePut和 @CacheEvit,分别简单介绍一下:@Cacheable:配置在 getUsersByName方法上表示其返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问@CachePut:配置于方法上时,能够根据参数定义条件来进行缓存,其与 @Cacheable不同的是使用 @CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中,所以主要用于数据新增和修改操作上@CacheEvict:配置于方法上时,表示从缓存中移除相应数据。编写 controller@RestControllerpublic class UserController { @Autowired private UserService userService; @Autowired CacheManager cacheManager; @RequestMapping( value = “/getusersbyname”, method = RequestMethod.POST) public List<User> geUsersByName( @RequestBody User user ) { System.out.println( “——————————————-” ); System.out.println(“call /getusersbyname”); System.out.println(cacheManager.toString()); List<User> users = userService.getUsersByName( user.getUserName() ); return users; }}改造 Spring Boot应用主类主要是在启动类上通过 @EnableCaching注解来显式地开启缓存功能@SpringBootApplication@MapperScan(“cn.codesheep.springbt_guava_cache”)@EnableCachingpublic class SpringbtGuavaCacheApplication { public static void main(String[] args) { SpringApplication.run(SpringbtGuavaCacheApplication.class, args); }}最终完工的整个工程的结构如下:实际实验通过多次向接口 localhost:82/getusersbyname POST数据来观察效果:可以看到缓存的启用和失效时的效果如下所示(上文 Guava Cache的配置文件中设置了缓存 user的实效时间为 10s):怎么样,缓存的作用还是很明显的吧!后 记由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊程序羊的 2018年终总(gen)结(feng) ...

January 8, 2019 · 2 min · jiezi

如何判断一个元素在亿级数据中是否存在?

前言最近有朋友问我这么一个面试题目:现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。需求其实很清晰,只是要判断一个数据是否存在即可。但这里有一个比较重要的前提:非常庞大的数据。常规实现先不考虑这个条件,我们脑海中出现的第一种方案是什么?我想大多数想到的都是用 HashMap 来存放数据,因为它的写入查询的效率都比较高。写入和判断元素是否存在都有对应的 API,所以实现起来也比较简单。为此我写了一个单测,利用 HashSet 来存数据(底层也是 HashMap );同时为了后面的对比将堆内存写死:-Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError 为了方便调试加入了 GC 日志的打印,以及内存溢出后 Dump 内存。 @Test public void hashMapTest(){ long star = System.currentTimeMillis(); Set<Integer> hashset = new HashSet<>(100) ; for (int i = 0; i < 100; i++) { hashset.add(i) ; } Assert.assertTrue(hashset.contains(1)); Assert.assertTrue(hashset.contains(2)); Assert.assertTrue(hashset.contains(3)); long end = System.currentTimeMillis(); System.out.println(“执行时间:” + (end - star)); }当我只写入 100 条数据时自然是没有问题的。还是在这个基础上,写入 1000W 数据试试:执行后马上就内存溢出。可见在内存有限的情况下我们不能使用这种方式。实际情况也是如此;既然要判断一个数据是否存在于集合中,考虑的算法的效率以及准确性肯定是要把数据全部 load 到内存中的。Bloom Filter基于上面分析的条件,要实现这个需求最需要解决的是如何将庞大的数据 load 到内存中。而我们是否可以换种思路,因为只是需要判断数据是否存在,也不是需要把数据查询出来,所以完全没有必要将真正的数据存放进去。伟大的科学家们已经帮我们想到了这样的需求。Burton Howard Bloom 在 1970 年提出了一个叫做 Bloom Filter(中文翻译:布隆过滤)的算法。它主要就是用于解决判断一个元素是否在一个集合中,但它的优势是只需要占用很小的内存空间以及有着高效的查询效率。所以在这个场景下在合适不过了。Bloom Filter 原理下面来分析下它的实现原理。官方的说法是:它是一个保存了很长的二级制向量,同时结合 Hash 函数实现的。听起来比较绕,但是通过一个图就比较容易理解了。如图所示:首先需要初始化一个二进制的数组,长度设为 L(图中为 8),同时初始值全为 0 。当写入一个 A1=1000 的数据时,需要进行 H 次 hash 函数的运算(这里为 2 次);与 HashMap 有点类似,通过算出的 HashCode 与 L 取模后定位到 0、2 处,将该处的值设为 1。A2=2000 也是同理计算后将 4、7 位置设为 1。当有一个 B1=1000 需要判断是否存在时,也是做两次 Hash 运算,定位到 0、2 处,此时他们的值都为 1 ,所以认为 B1=1000 存在于集合中。当有一个 B2=3000 时,也是同理。第一次 Hash 定位到 index=4 时,数组中的值为 1,所以再进行第二次 Hash 运算,结果定位到 index=5 的值为 0,所以认为 B2=3000 不存在于集合中。整个的写入、查询的流程就是这样,汇总起来就是:对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1 。当有数据查询时也是同样的方式定位到数组中。一旦其中的有一位为 0 则认为数据肯定不存在于集合,否则数据可能存在于集合中。所以布隆过滤有以下几个特点:只要返回数据不存在,则肯定不存在。返回数据存在,但只能是大概率存在。同时不能清除其中的数据。第一点应该都能理解,重点解释下 2、3 点。为什么返回存在的数据却是可能存在呢,这其实也和 HashMap 类似。在有限的数组长度中存放大量的数据,即便是再完美的 Hash 算法也会有冲突,所以有可能两个完全不同的 A、B 两个数据最后定位到的位置是一模一样的。这时拿 B 进行查询时那自然就是误报了。删除数据也是同理,当我把 B 的数据删除时,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。基于以上的 Hash 冲突的前提,所以 Bloom Filter 有一定的误报率,这个误报率和 Hash 算法的次数 H,以及数组长度 L 都是有关的。自己实现一个布隆过滤算法其实很简单不难理解,于是利用 Java 实现了一个简单的雏形。public class BloomFilters { /** * 数组长度 / private int arraySize; /* * 数组 / private int[] array; public BloomFilters(int arraySize) { this.arraySize = arraySize; array = new int[arraySize]; } /* * 写入数据 * @param key / public void add(String key) { int first = hashcode_1(key); int second = hashcode_2(key); int third = hashcode_3(key); array[first % arraySize] = 1; array[second % arraySize] = 1; array[third % arraySize] = 1; } /* * 判断数据是否存在 * @param key * @return / public boolean check(String key) { int first = hashcode_1(key); int second = hashcode_2(key); int third = hashcode_3(key); int firstIndex = array[first % arraySize]; if (firstIndex == 0) { return false; } int secondIndex = array[second % arraySize]; if (secondIndex == 0) { return false; } int thirdIndex = array[third % arraySize]; if (thirdIndex == 0) { return false; } return true; } /* * hash 算法1 * @param key * @return / private int hashcode_1(String key) { int hash = 0; int i; for (i = 0; i < key.length(); ++i) { hash = 33 * hash + key.charAt(i); } return Math.abs(hash); } /* * hash 算法2 * @param data * @return / private int hashcode_2(String data) { final int p = 16777619; int hash = (int) 2166136261L; for (int i = 0; i < data.length(); i++) { hash = (hash ^ data.charAt(i)) * p; } hash += hash << 13; hash ^= hash >> 7; hash += hash << 3; hash ^= hash >> 17; hash += hash << 5; return Math.abs(hash); } /* * hash 算法3 * @param key * @return */ private int hashcode_3(String key) { int hash, i; for (hash = 0, i = 0; i < key.length(); ++i) { hash += key.charAt(i); hash += (hash << 10); hash ^= (hash >> 6); } hash += (hash << 3); hash ^= (hash >> 11); hash += (hash << 15); return Math.abs(hash); }}首先初始化了一个 int 数组。写入数据的时候进行三次 hash 运算,同时把对应的位置置为 1。查询时同样的三次 hash 运算,取到对应的值,一旦值为 0 ,则认为数据不存在。实现逻辑其实就和上文描述的一样。下面来测试一下,同样的参数:-Xms64m -Xmx64m -XX:+PrintHeapAtGC @Test public void bloomFilterTest(){ long star = System.currentTimeMillis(); BloomFilters bloomFilters = new BloomFilters(10000000) ; for (int i = 0; i < 10000000; i++) { bloomFilters.add(i + “”) ; } Assert.assertTrue(bloomFilters.check(1+"")); Assert.assertTrue(bloomFilters.check(2+"")); Assert.assertTrue(bloomFilters.check(3+"")); Assert.assertTrue(bloomFilters.check(999999+"")); Assert.assertFalse(bloomFilters.check(400230340+"")); long end = System.currentTimeMillis(); System.out.println(“执行时间:” + (end - star)); }执行结果如下:只花了 3 秒钟就写入了 1000W 的数据同时做出来准确的判断。当让我把数组长度缩小到了 100W 时就出现了一个误报,400230340 这个数明明没在集合里,却返回了存在。这也体现了 Bloom Filter 的误报率。我们提高数组长度以及 hash 计算次数可以降低误报率,但相应的 CPU、内存的消耗就会提高;这就需要根据业务需要自行权衡。Guava 实现刚才的方式虽然实现了功能,也满足了大量数据。但其实观察 GC 日志非常频繁,同时老年代也使用了 90%,接近崩溃的边缘。总的来说就是内存利用率做的不好。其实 Google Guava 库中也实现了该算法,下面来看看业界权威的实现。-Xms64m -Xmx64m -XX:+PrintHeapAtGC @Test public void guavaTest() { long star = System.currentTimeMillis(); BloomFilter<Integer> filter = BloomFilter.create( Funnels.integerFunnel(), 10000000, 0.01); for (int i = 0; i < 10000000; i++) { filter.put(i); } Assert.assertTrue(filter.mightContain(1)); Assert.assertTrue(filter.mightContain(2)); Assert.assertTrue(filter.mightContain(3)); Assert.assertFalse(filter.mightContain(10000000)); long end = System.currentTimeMillis(); System.out.println(“执行时间:” + (end - star)); }也是同样写入了 1000W 的数据,执行没有问题。观察 GC 日志会发现没有一次 fullGC,同时老年代的使用率很低。和刚才的一对比这里明显的要好上很多,也可以写入更多的数据。源码分析那就来看看 Guava 它是如何实现的。构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。我这里的测试 demo 分别是 1000W 以及 0.01。Guava 会通过你预计的数量以及误报率帮你计算出你应当会使用的数组大小 numBits 以及需要计算几次 Hash 函数 numHashFunctions 。这个算法计算规则可以参考维基百科。put 写入函数真正存放数据的 put 函数如下:根据 murmur3_128 方法的到一个 128 位长度的 byte[]。分别取高低 8 位的到两个 hash 值。再根据初始化时的到的执行 hash 的次数进行 hash 运算。bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);其实也是 hash取模拿到 index 后去赋值 1.重点是 bits.set() 方法。其实 set 方法是 BitArray 中的一个函数,BitArray 就是真正存放数据的底层数据结构。利用了一个 long[] data 来存放数据。所以 set() 时候也是对这个 data 做处理。在 set 之前先通过 get() 判断这个数据是否存在于集合中,如果已经存在则直接返回告知客户端写入失败。接下来就是通过位运算进行位或赋值。get() 方法的计算逻辑和 set 类似,只要判断为 0 就直接返回存在该值。mightContain 是否存在函数前面几步的逻辑都是类似的,只是调用了刚才的 get() 方法判断元素是否存在而已。总结布隆过滤的应用还是蛮多的,比如数据库、爬虫、防缓存击穿等。特别是需要精确知道某个数据不存在时做点什么事情就非常适合布隆过滤。这段时间的研究发现算法也挺有意思的,后续应该会继续分享一些类似的内容。如果对你有帮助那就分享一下吧。本问的示例代码参考这里:https://github.com/crossoverJie/JCSprout你的点赞与分享是对我最大的支持 ...

November 26, 2018 · 4 min · jiezi