Storm入门之第6章一个实际的例子

本文翻译自《Getting Started With Storm》译者:吴京润    编辑:郭蕾 方腾飞

本章要阐述一个典型的网络分析解决方案,而这类问题通常利用Hadoop批处理作为解决方案。与Hadoop不同的是,基于Storm的方案会实时输出结果。

 

 

我们的这个例子有三个主要组件(见图6-1)

  • 一个基于Node.js的web应用,用于测试系统
  • 一个Redis服务器,用于持久化数据
  • 一个Storm拓扑,用于分布式实时处理数据

架构概览

图6-1  架构概览

NOTE:你如果想先把这个例子运行起来,请首先阅读附录C

基于Node.js的web应用

我们已经伪造了简单的电子商务网站。这个网站只有三个页面:一个主页、一个产品页和一个产品统计页面。这个应用基于Express和Socket.io两个框架实现了向浏览器推送内容更新。制作这个应用的目的是为了让你体验Storm集群功能并看到处理结果,但它不是本书的重点,所以我们不会对它的页面做更详细描述。

主页

这个页面提供了全部有效产品的链接。它从Redis服务器获取数据并在页面上把它们显示出来。这个页面的URL是http://localhost:3000/。(见图6-2,译者注,图6-2翻译如下,全是文字就不制图了)

有效产品:

DVD播放器(带环绕立体声系统)

全高清蓝光dvd播放器

媒体播放器(带USB 2.0接口)

全高清摄像机

防水高清摄像机

防震防水高清摄像机

反射式摄像机

双核安卓智能手机(带64GB SD卡)

普通移动电话

卫星电话

64GB SD卡

32GB SD卡

16GB SD卡

粉红色智能手机壳

黑色智能手机壳

小山羊皮智能手机壳

图6-2 首页

产品页

产品页用来显示指定产品的相关信息,例如,价格、名称、分类。这个页面的URL是:http://localhost:3000/product/:id。(见图6-3,译者注:全是文字不再制图,翻译如下)

产品页:32英寸液晶电视

分类:电视机

价格:400

相关分类

图6-3,产品页

产品统计页

这个页面显示通过收集用户浏览站点,用Storm集群计算的统计信息。可以显示为如下概要:浏览这个产品的用户,在那些分类下面浏览了n次产品。该页的URL是:http://localhost:3000/product/:id/stats。(见图6-4,译者注:全是文字,不再制图,翻译如下)

浏览了该产品的用户也浏览了以下分类的产品:

1.摄像机

2.播放器

3.手机壳

4.存储卡

图6-4. 产品统计视图

启动这个Node.js web应用

首先启动Redis服务器,然后执行如下命令启动web应用:

    node webapp/app.js

为了向你演示,这个应用会自动向Redis填充一些产品数据作为样本。

Storm拓扑

为这个系统搭建Storm拓扑的目标是改进产品统计的实时性。产品统计页显示了一个分类计数器列表,用来显示访问了其它同类产品的用户数。这样可以帮助卖家了解他们的用户需求。拓扑接收浏览日志,并更新产品统计结果(见图6-5)。

图6-5  Storm拓扑的输入与输出

我们的Storm拓扑有五个组件:一个spout向拓扑提供数据,四个bolt完成统计任务。

UsersNavigationSpout

从用户浏览数据队列读取数据发送给拓扑

GetCategoryBolt

从Redis服务器读取产品信息,向数据流添加产品分类

UserHistoryBolt

读取用户以前的产品浏览记录,向下一步分发Product:Category键值对,在下一步更新计数器

ProductCategoriesCounterBolt

追踪用户浏览特定分类下的商品次数

NewsNotifierBolt

通知web应用立即更新用户界面

下图展示了拓扑的工作方式(见图6-6)

package storm.analytics;
...
public class TopologyStarter {public static void main(String[] args) {Logger.getRootLogger().removeAllAppenders();TopologyBuilder builder = new TopologyBuilder();builder.setSpout("read-feed", new UsersNavigationSpout(),3);builder.setBolt("get-categ", new GetCategoryBolt(),3).shuffleGrouping("read-feed");builder.setBolt("user-history", new UserHistoryBolt(),5).fieldsGrouping("get-categ", new Fields("user"));builder.setBolt("product-categ-counter", new ProductCategoriesCounterBolt(),5).fieldsGrouping("user-history", new Fields("product"));builder.setBolt("news-notifier", new NewsNotifierBolt(),5).shuffleGrouping("product-categ-counter");Config conf = new Config();conf.setDebug(true);conf.put("redis-host",REDIS_HOST);conf.put("redis-port",REDIS_PORT);conf.put("webserver", WEBSERVER);LocalCluster cluster = new LocalCluster();cluster.submitTopology("analytics", conf, builder.createTopology());}
}

Storm topology
Figure 6-6 Storm拓扑

UsersNavigationSpout

UsersNavigationSpout负责向拓扑提供浏览数据。每条浏览数据都是一个用户浏览过的产品页的引用。它们都被web应用保存在Redis服务器。我们一会儿就要看到更多信息。

你可以使用https://github.com/xetorthio/jedis从Redis服务器读取数据,这是个极为轻巧简单的Java Redis客户端。

NOTE:下面的代码块就是相关代码。

package storm.analytics;
public class UsersNavigationSpout extends BaseRichSpout {Jedis jedis;...@Overridepublic void nextTuple() {String content = jedis.rpop("navigation");if(content==null || "nil".equals(content)){try { Thread.sleep(300); } catch (InterruptedException e) {}} else {JSONObject obj=(JSONObject)JSONValue.parse(content);String user = obj.get("user").toString();String product = obj.get("product").toString();String type = obj.get("type").toString();HashMap<String, String> map = new HashMap<String, String>();map.put("product", product);NavigationEntry entry = new NavigationEntry(user, type, map);collector.emit(new Values(user, entry));}}@Overridepublic void declareOutputFields(OutputFieldsDeclarer declarer) {declarer.declare(new Fields("user", "otherdata"));}
}

spout首先调用jedis.rpop(“navigation”)从Redis删除并返回”navigation”列表最右边的元素。如果列表已经是空的,就休眠0.3秒,以免使用忙等待循环阻塞服务器。如果得到一条数据(数据是JSON格式),就解析它,并创建一个包含该数据的NavigationEntry POJO:

  • 浏览页面的用户
  • 用户浏览的页面类型
  • 由页面类型决定的额外页面信息。“产品”页的额外信息就是用户浏览的产品ID。

spout调用collector.emit(new Values(user, entry))分发包含这些信息的元组。这个元组的内容是拓扑里下一个bolt的输入。

GetCategoryBolt

这个bolt非常简单。它只负责反序列化前面的spout分发的元组内容。如果这是产品页的数据,就通过ProductsReader类从Redis读取产品信息,然后基于输入的元组再分发一个新的包含具体产品信息的元组:

  • 用户
  • 产品
  • 产品类别
package storm.analytics;public class GetCategoryBolt extends BaseBasicBolt {private ProductReader reader;...@Overridepublic void execute(Tuple input, BasicOutputCollector collector) {NavigationEntry entry = (NavigationEntry)input.getValue(1);if("PRODUCT".equals(entry.getPageType())){try {String product = (String)entry.getOtherData().get("product");//调用产品条目API,得到产品信息Product itm = reader.readItem(product);if(itm == null) {return;}String categ = itm.getCategory();collector.emit(new Values(entry.getUserId(), product, categ));} catch (Exception ex) {System.err.println("Error processing PRODUCT tuple"+ ex);ex.printStackTrace();}}}...
}

正如前面所提到的, 使用ProductsReader类读取产品具体信息。

package storm.analytics.utilities;
...
public class ProductReader {...public Product readItem(String id) throws Exception{String content = jedis.get(id);if(content == null || ("nil".equals(content))){return null;}Object obj = JSONValue.parse(content);JSONObjectproduct = (JSONObject)obj;Product i = new Product((Long)product.get("id"),(String)product.get("title"),(Long)product.get("price"),(String)product.get("category"));return i;}...
}

UserHistoryBolt

UserHistoryBolt是整个应用的核心。它负责持续追踪每个用户浏览过的产品,并决定应当增加计数的键值对。

我们使用Redis保存用户的产品浏览历史,同时基于性能方面的考虑,还应该保留一份本地副本。我们把数据访问细节隐藏在方法getUserNavigationHistory(user)addProductToHistory(user,prodKey)里,分别用来读/写访问。它们的实现如下

package storm.analytics;
...
public class UserHistoryBolt extends BaseRichBolt{@Overridepublic void execute(Tuple input) {String user = input.getString(0);String prod1 = input.getString(1);String cat1 = input.getString(2);//产品键嵌入了产品类别信息String prodKey = prod1+":"+cat1;Set productsNavigated = getUserNavigationHistory(user);//如果用户以前浏览过->忽略它if(!productsNavigated.contains(prodKey)) {//否则更新相关条目for (String other : productsNavigated) {String[] ot = other.split(":");String prod2 = ot[0];String cat2 = ot[1];collector.emit(new Values(prod1, cat2));collector.emit(new Values(prod2, cat1));}addProductToHistory(user, prodKey);}}
}

需要注意的是,这个bolt的输出是那些类别计数应当获得增长的产品。

看一看代码。这个bolt维护着一组被每个用户浏览过的产品。值得注意的是,这个集合包含产品:类别键值对,而不是只有产品。这是因为你会在接下来的调用中用到类别信息,而且这样也比每次从数据库获取更高效。这样做的原因是基于以下考虑,产品可能只有一个类别,而且它在整个产品的生命周期当中不会改变。

读取了用户以前浏览过的产品集合之后(以及它们的类别),检查当前产品以前有没有被浏览过。如果浏览过,这条浏览数据就被忽略了。如果这是首次浏览,遍历用户浏览历史,并执行collector.emit(new Values(prod1,cat2))分发一个元组,这个元组包含当前产品和所有浏览历史类别。第二个元组包含所有浏览历史产品和当前产品类别,由collectior.emit(new Values(prod2,cat1))。最后,将当前产品和它的类别添加到集合。

比如,假设用户John有以下浏览历史:

下面是将要处理的浏览数据

该用户没有浏览过产品8,因此你需要处理它。

因此要分发以下元组:

注意,左边的产品和右边的类别之间的关系应当作为一个整体递增。

现在,让我们看看这个Bolt用到的持久化实现。

public class UserHistoryBolt extends BaseRichBolt{...private Set getUserNavigationHistory(String user) {Set userHistory = usersNavigatedItems.get(user);if(userHistory == null) {userHistory = jedis.smembers(buildKey(user));if(userHistory == null)userHistory = new HashSet();usersNavigatedItems.put(user, userHistory);}return userHistory;}private void addProductToHistory(String user, String product) {Set userHistory = getUserNavigationHistory(user);userHistory.add(product);jedis.sadd(buildKey(user), product);}...
}

getUserNavigationHistory方法返回用户浏览过的产品集。首先,通过usersNavigatedItems.get(user)方法试图从本地内存得到用户浏览历史,否则,使用jedis.smembers(buildKey(user))从Redis服务器获取,并把数据添加到本地数据结构usersNavigatedItems

当用户浏览一个新产品时,调用addProductToHistory,通过userHistory.add(product)jedis.sadd(buildKey(user),product)同时更新内存数据结构和Redis服务器。

需要注意的是,当你需要做并行化处理时,只要bolt在内存中维护着用户数据,你就得首先通过用户做域数据流分组(译者注:原文是fieldsGrouping,详细情况请见第三章的域数据流组),这是一件很重要的事情,否则集群内将会有用户浏览历史的多个不同步的副本。

ProductCategoriesCounterBolt

该类持续追踪所有的产品-类别关系。它通过由UsersHistoryBolt分发的产品-类别数据对更新计数。

每个数据对的出现次数保存在Redis服务器。基于性能方面的考虑,要使用一个本地读写缓存,通过一个后台线程向Redis发送数据。

Bolt会向拓扑的下一个Bolt——NewsNotifierBolt——发送包含最新记数的元组,这也是最后一个Bolt,它会向最终用户广播实时更新的数据。

public class ProductCategoriesCounterBolt extends BaseRichBolt {...@Overridepublic void execute(){String product = input.getString(0);String categ = input.getString(1);int total = count(product, categ);collector.emit(new Values(product, categ, total));}...private int count(String product, String categ) {int count = getProductCategoryCount(categ, product);count++;storeProductCategoryCount(categ, product, count);return count;}...
}

这个bolt的持久化工作隐藏在getProductCategoryCountstoreProductCategoryCount两个方法中。它们的具体实现如下:

package storm.analytics;
...
public class ProductCategoriesCounterBolt extends BaseRichBolt {// 条目:分类 -> 计数HashMap<String,Integer> counter = new HashMap<String, Integer>();//条目:分类 -> 计数HashMap<String,Integer> pendingToSave = new HashMap<String,Integer>();...public int getProductCategoryCount(String categ, String product) {Integer count = counter.get(buildLocalKey(categ, product));if(count == null) {String sCount = jedis.hget(buildRedisKey(product), categ);if(sCount == null || "nil".equals(sCount)) {count = 0;} else {count = Integer.valueOf(sCount);}}return count;}...private void storeProductCategoryCount(String categ, String product, int count) {String key = buildLocalKey(categ, product);counter.put(key, count);synchronized (pendingToSave) {pendingToSave.put(key, count);}}...
}

方法getProductCategoryCount首先检查内存缓存计数器。如果没有有效令牌,就从Redis服务器取得数据。

方法storeProductCategoryCount更新计数器缓存和pendingToSae缓冲。缓冲数据由下述后台线程持久化。

package storm.analytics;public class ProductCategoriesCounterBolt extends BaseRichBolt {
...private void startDownloaderThread() {TimerTask t = startDownloaderThread() {@Overridepublic void run () {HashMap<String, Integer> pendings;synchronized (pendingToSave) {pendings = pendingToSave;pendingToSave = new HashMap<String,Integer>();}for (String key : pendings.keySet) {String[] keys = key.split(":");String product = keys[0];String categ = keys[1];Integer count = pendings.get(key);jedis.hset(buildRedisKey(product), categ, count.toString());}}};timer = new Timer("Item categories downloader");timer.scheduleAtFixedRate(t, downloadTime, downloadTime);}...
}

下载线程锁定pendingToSave, 向Redis发送数据时会为其它线程创建一个新的缓冲。这段代码每隔downloadTime毫秒运行一次,这个值可由拓扑配置参数download-time配置。download-time值越大,写入Redis的次数就越少,因为一对数据的连续计数只会向Redis写一次。

NewsNotifierBolt

为了让用户能够实时查看统计结果,由NewsNotifierBolt负责向web应用通知统计结果的变化。通知机制由Apache HttpClient通过HTTP POST访问由拓扑配置参数指定的URL。POST消息体是JSON格式。

测试时把这个bolt从拜年中删除。

01 package storm.analytics;
02...
03 public class NewsNotifierBolt extends BaseRichBolt {
04...
05@Override
06 public void execute(Tuple input) {
07 String product = input.getString(0);
08 String categ = input.getString(1);
09 int visits = input.getInteger(2);</code>
10 
11 String content = "{\"product\":\"+product+"\",\"categ\":\""+categ+"\",\"visits\":"+visits+"}";
12 HttpPost post = new HttpPost(webserver);
13 try {
14 post.setEntity(new StringEntity(content));
15HttpResponse response = client.execute(post);
16org.apache.http.util.EntityUtils.consume(response.getEntity());
17 catch (Exception e) {
18e.printStackTrace();
19reconnect();
20}
21}
22...
23}

Redis服务器

Redis是一种先进的、基于内存的、支持持久化的键值存储(见http://redis.io)。本例使用它存储以下信息:

  • 产品信息,用来为web站点服务
  • 用户浏览队列,用来为Storm拓扑提供数据
  • Storm拓扑的中间数据,用于拓扑发生故障时恢复数据
  • Storm拓扑的处理结果,也就是我们期望得到的结果。

产品信息

Redis服务器以产品ID作为键,以JSON字符串作为值保存着产品信息。

1redis-cli
2 redis 127.0.0.1:6379&gt; get 15
3 "{\"title\":\"Kids smartphone cover\",\"category\":\"Covers\",\"price\":30,\"id\":
415}"

用户浏览队列

用户浏览队列保存在Redis中一个键为navigation的先进先出队列中。用户浏览一个产品页时,服务器从队列左侧添加用户浏览数据。Storm集群不断的从队列右侧获取并移除数据。

01 redis 127.0.0.1:6379&gt; llen navigation
02(integer) 5
03 redis 127.0.0.1:6379&gt; lrange navigation 0 4
04 1) "{\"user\":\"59c34159-0ecb-4ef3-a56b-99150346f8d5\",\"product\":\"1\",\"type\":
05 \"PRODUCT\"}"
06 2) "{\"user\":\"59c34159-0ecb-4ef3-a56b-99150346f8d5\",\"product\":\"1\",\"type\":
07 \"PRODUCT\"}"
08 3) "{\"user\":\"59c34159-0ecb-4ef3-a56b-99150346f8d5\",\"product\":\"2\",\"type\":
09 \"PRODUCT\"}"
10 4) "{\"user\":\"59c34159-0ecb-4ef3-a56b-99150346f8d5\",\"product\":\"3\",\"type\":
11 \"PRODUCT\"}"
12 5) "{\"user\":\"59c34159-0ecb-4ef3-a56b-99150346f8d5\",\"product\":\"5\",\"type\":
13 \"PRODUCT\"}"

中间数据

集群需要分开保存每个用户的历史数据。为了实现这一点,它在Redis服务器上保存着一个包含所有用户浏览过的产品和它们的分类的集合。

1 redis 127.0.0.1:6379&gt; smembers history:59c34159-0ecb-4ef3-a56b-99150346f8d5
2 1) "1:Players"
3 2) "5:Cameras"
4 3) "2:Players"
5 4) "3:Cameras"

结果

Storm集群生成关于用户浏览的有用数据,并把它们的产品ID保存在一个名为“prodcnt”的Redis hash中。

1 redis 127.0.0.1:6379&gt; hgetall prodcnt:2
2 1) "Players"
3 2) "1"
4 3) "Cameras"
5 4) "2"

测试拓扑

使用LocalCluster和一个本地Redis服务器执行测试(见图6-7)。向Redis填充产品数据,伪造访问日志。我们的断言会在读取拓扑向Redis输出的数据时执行。测试用户用java和groovy完成。
测试架构
图6-7. 测试架构

初始化测试

初始化由以下三步组成:
启动LocalCluster并提交拓扑。初始化在AbstractAnalyticsTest实现,所有测试用例都继承该类。当初始化多个AbstractAnalyticsTest子类对象时,由一个名为topologyStarted的静态标志属性确定初始化工作只会进行一次。

需要注意的是,sleep语句是为了确保在试图获取结果之前LocalCluster已经正确启动了。

01 public abstract class AbstractAnalyticsTest extends Assert {
02  def jedis
03  static topologyStarted = false
04  static sync= new Object()
05  private void reconnect() {
06  jedis = new Jedis(TopologyStarter.REDIS_HOST, TopologyStarter.REDIS_PORT)
07  }
08  @Before
09  public void startTopology(){
10  synchronized(sync){
11  reconnect()
12  if(!topologyStarted){
13  jedis.flushAll()
14  populateProducts()
15  TopologyStarter.testing = true
16  TopologyStarter.main(null)
17  topologyStarted = true
18  sleep 1000
19  }
20  }
21  }
22  ...
23  public void populateProducts() {
24  def testProducts = [
25  [id: 0, title:"Dvd player with surround sound system",
26  category:"Players", price: 100],
27  [id: 1, title:"Full HD Bluray and DVD player",
28  category:"Players", price:130],
29  [id: 2, title:"Media player with USB 2.0 input",
30  category:"Players", price:70],
31  ...
32  [id: 21, title:"TV Wall mount bracket 50-55 Inches",
33  category:"Mounts", price:80]
34  ]
35  testProducts.each() { product -&gt;
36  def val =
37  "{ \"title\": \"${product.title}\" , \"category\": \"${product.category}\"," +
38  " \"price\": ${product.price}, \"id\": ${product.id} }"
39  println val
40  jedis.set(product.id.toString(), val.toString())
41  }
42  }
43  ...
44}

在AbstractAnalyticsTest中实现一个名为navigate的方法。为了测试不同的场景,我们要模拟用户浏览站点的行为,这一步向Redis的浏览队列(译者注:就是前文提到的键是navigation的队列)插入浏览数据。

01 public abstract class AbstractAnalyticsTest extends Assert {
02  ...
03 public void navigate(user, product) {
04  String nav =
05  "{\"user\": \"${user}\", \"product\": \"${product}\", \"type\": \"PRODUCT\"}".toString()
06  println "Pushing navigation: ${nav}"
07  jedis.lpush('navigation', nav)
08  }
09  ...
10}

实现一个名为getProductCategoryStats的方法,用来读取指定产品与分类的数据。不同的测试同样需要断言统计结果,以便检查拓扑是否按照期望的那样执行了。

01 public abstract class AbstractAnalyticsTest extends Assert {
02  ...
03  public int getProductCategoryStats(String product, String categ) {
04  String count = jedis.hget("prodcnt:${product}", categ)
05  if(count == null || "nil".equals(count))
06  return 0
07  return Integer.valueOf(count)
08  }
09  ...
10}

一个测试用例

下一步,为用户“1”模拟一些浏览记录,并检查结果。注意执行断言之前要给系统留出两秒钟处理数据。(记住ProductCategoryCounterBolt维护着一份计数的本地副本,它是在后台异步保存到Redis的。)

01 package functional
02 class StatsTest extends AbstractAnalyticsTest {
03  @Test
04  public void testNoDuplication(){
05      navigate("1""0"// Players
06      navigate("1""1"// Players
07      navigate("1""2"// Players
08      navigate("1""3"// Cameras
09      Thread.sleep(2000// Give two seconds for the system to process the data.
10      assertEquals 1, getProductCategoryStats("0""Cameras")
11      assertEquals 1, getProductCategoryStats("1""Cameras")
12      assertEquals 1, getProductCategoryStats("2""Cameras")
13      assertEquals 2, getProductCategoryStats("0""Players")
14      assertEquals 3, getProductCategoryStats("3""Players")
15  }
16}

对可扩展性和可用性的提示

为了能在一章的篇幅中讲明白整个方案,它已经被简化了。正因如此,一些与可扩展性和可用性有关的必要复杂性也被去掉了。这方面主要有两个问题。
Redis服务器不只是一个故障的节点,还是性能瓶颈。你能接收的数据最多就是Redis能处理的那些。Redis可以通过分片增强扩展性,它的可用性可以通过主从配置得到改进。这都需要修改拓扑和web应用的代码实现。
另一个缺点就是web应用不能通过增加服务器成比例的扩展。这是因为当产品统计数据发生变化时,需要通知所有关注它的浏览器。这一“通知浏览器”的机制通过Socket.io实现,但是它要求监听器和通知器在同一主机上。这一点只有当GET /product/:id/statsPOST /news满足以下条件时才能实现,那就是这二者拥有相同的分片标准,确保引用相同产品的请求由相同的服务器处理。

文章转自 并发编程网-ifeve.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/352328.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

20222817 2022-2023-2《网络攻防实践》第十一次作业

目录 1.实践内容 2.实践过程 3 学习中遇到的问题及解决 4 实践总结 1.实践内容 1.Web浏览器渗透攻击实验 任务&#xff1a;使用攻击机和Windows靶机进行浏览器渗透攻击实验&#xff0c;体验网页木马构造及实施浏览器攻击的实际过程。 2.取证分析实践—网页木马攻击场景分析…

【Storm入门指南】第六章 真实示例

本章将演示一个典型的网页分析方案&#xff0c;通常使用 Hadoop 批量作业来解决的问题。不像 Hadoop 的实现方案&#xff0c;基于 Storm 的解决方案实时刷新并呈现结果。 示例有三个主要部分&#xff08;如图6.1所示&#xff09;&#xff1a; 一个 Node.js 的web应用&#xff0…

Storm 实时分析系统详解

一、Storm 概述 1、Storm 简介 Storm 是一个分布式的,可靠的,容错的数据流处理系统。它会把工作任务委托给不同类型的组件,每个组件负责处理一项简单特定的任务。Storm 集群的输入流由一个被称作 spout 的组件管理,spout 把数据传递给 bolt, bolt 要么把数据保存到某种存…

Storm 的一个案例

本章要阐述一个典型的网络分析解决方案&#xff0c;而这类问题通常利用Hadoop批处理作为解决方案。与Hadoop不同的是&#xff0c;基于Storm的方案会实时输出结果。 我们的这个例子有三个主要组件&#xff08;见图6-1&#xff09; 一个基于Node.js的web应用&#xff0c;用于测试…

getting start with storm 翻译 第六章 part-4

转载请注明出处&#xff1a;http://blog.csdn.net/lonelytrooper/article/details/9982967 Redis服务器 Redis是一套高级的用于持久化的内存KeyValue存储系统(见http://redis.io)。使用它来存储下述信息&#xff1a; ﹒产品信息&#xff0c;用于服务网站。 ﹒用户导航队列…

Phpstorm调试详解(包含命令行以及浏览器)

2019独角兽企业重金招聘Python工程师标准>>> 运行环境 PHP版本&#xff1a;5.5.12 Phpstorm : 9.0.2 xdebug : 2.2.5 下载地址&#xff0c;请下载与当前PHP版本对应的版本 系统平台&#xff1a;Windows 7 一 &#xff0c;要先给php安装xdebug扩展 1&#xff0c;…

windows核心编程之进程(3)

获取系统版本的函数: GetVersion返回一个DWORD类型的值&#xff0c;高子返回MS-DOS版本号&#xff0c;低字返回windows版本号 本来按照逻辑来说应该是低字的高字节返回windows的主版本号&#xff0c;低字节返回此版本号&#xff0c;但是编写该代码程序员犯了一个小错误&#…

2023CCPC河南省赛 VP记录

感觉现在的xcpc&#xff0c;风格越来越像CF&#xff0c;不是很喜欢&#xff0c;还是更喜欢多点算法题的比赛 VP银了&#xff0c;VP银也是银 感觉省赛都是思维题&#xff0c;几乎没有算法题&#xff0c;感觉像打了场大型的CF B题很简单没开出来&#xff0c;一直搞到最后&…

大学计算机专业 学习Python学习路线图(最新版)

这是我刚开始学习python时的一套学习路线&#xff0c;从入门到上手。&#xff08;不敢说精通&#xff0c;哈哈~&#xff09; 希望对大家有帮助哈~ 大家需要高清得完整python学习路线可以 一、Python入门、环境搭建、变量、数据类型 二、数据库编程 三、Linux系统 四、网页编…

哪些手机待办事项软件可以记录备忘并设置提醒?

当下手机软件的种类那么多&#xff0c;为什么有那么多人使用手机待办事项软件呢&#xff1f;原因很简单&#xff0c;因为这类软件可以记录备忘并设置提醒&#xff0c;非常适合哪些喜欢提前规划待办事项的人使用。但是&#xff0c;面对如此多的手机待办事项软件&#xff0c;具体…

【HTML】第 2 节 - HTML 标签

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、缘起 2、标题标签 3、段落标签 4、文本格式化标签 5、图像标签 5.1、基本作用 5.2、属性 6、超链接标签 7、音频标…

Vivado下时序逻辑模块的仿真

文章目录 D触发器两级D触发器带异步复位的D触发器带异步复位和同步置数的D触发器移位寄存器单口RAM伪双口RAM真双口RAM单口ROM 组合逻辑电路在逻辑功能上特点是任意时刻的输出仅仅取决于当前时刻的输入&#xff0c;与电路原来的状态无关。 时序逻辑在逻辑功能上的特点是任意时刻…

单元测试方法-cmockery实践

目录 单元测试概念 引子 定义 内容 方法 单元测试模型 测试模型构建 单元测试工具简介 Cmockery使用介绍 简介 使用 VPBX实践 UT框架搭建 目录 编译&#xff1a; 实例demo 例1&#xff1a; 例2&#xff1a; 例3&#xff1a; 例4&#xff1a; 例5&#xff…

从0开始搭建vue3+vite+ts+pinia项目

目录 项目搭建选项 项目搭建步骤 本地开发环境 Vite脚手架构建项目 关联Git仓库 开发工具 安装pinia 安装Sass 安装Vant-UI 安装postcss-pxtorem&#xff08;移动端项目&#xff09; 安装axios 本地调试 环境变量 本地代理 全局UI组件 路由中间件 项目部署 n…

layui(2)----页面元素

水平导航栏 依赖加载模块&#xff1a;element <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport"…

AcWing 回转游戏 dfs IDA* 剪枝 统一操作 java

&#x1f351; 算法题解专栏 &#x1f351; 回转游戏 如下图所示&#xff0c;有一个 # 形的棋盘&#xff0c;上面有 1 , 2 , 3 1,2,3 1,2,3 三种数字各 8 8 8 个。 给定 8 8 8 种操作&#xff0c;分别为图中的 A s i m H A \\sim H AsimH。 这些操作会按照图中字母和箭头…

MCGS昆仑通态触摸屏导入博途自定义数据类型和DB块变量的具体方法演示

MCGS昆仑通态触摸屏导入博途自定义数据类型和DB块变量的具体方法演示 如下图所示,在博途中新建项目后,添加自己所需的数据类型,然后选中该数据类型,右击选择“从块生成源“—”仅所选块“, 如下图所示,设置文件名后点击保存, 如下图所示,选中需要导出的DB块,右击选…

vue 自定义el-table穿梭框功能

一、需求描述&#xff1a;前段时间接到一个需求是点击做一个类似穿梭框的表格点击选中功能&#xff0c;弹框的左边是全部数据展示&#xff0c;点击表格行数据可以选中自动增加到右边的已选框&#xff0c;并且可以手动删除、重置选中数据。点击确定后到展示到主页面&#xff0c;…

Java核心知识点常考面试题(持续更新中)

Java核心知识点常考面试题&#xff08;持续更新中&#xff09; 线程与线程池Java锁机制轻量级锁重量级锁 线程与线程池 一、线程 1、线程的状态 2、线程的创建方式 &#xff08;1&#xff09;继承 Thread 类&#xff1b; &#xff08;2&#xff09;实现 Runnable 接口&#…

IDEA2023.1配置Git

一、前言 在使用新设备整理git项目时&#xff0c;报了以下问题&#xff0c;在整理项目的同时&#xff0c;做下记录。 二、下载git 2.1.官网下载git 官网下载地址&#xff1a; git官网&#xff0c;如下图所示&#xff1a; 如果选方法二&#xff0c; 接下来会让选择系统&#…