mapreduce实现一天内用户使用总时长

本文介绍了一种使用MapReduce技术统计用户一天内累计在线总时长的方法。通过自定义Mapper和Reducer处理用户访问记录,实现了精确计算每位用户的在线时间,并提供了完整的代码示例。
根据公司需要使用mapreduce实现用户一天内使用总时长,代码如下:
package com.online.time;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;

public class UserOnlineTime {
	// mapper
	public static class OnlineMapper extends Mapper{
		private Text info = new Text();
		private Text otherInfo = new Text();
		// 数据输入格式:手机号(空格)数据访问时间(空格)动作(空格)访问的页面
		public void map(Object key, Text value, Context context) throws IOException, InterruptedException{
			String[] userInfo = value.toString().split(" ");
			String phone = userInfo[1];
			String other = timeToSecond(userInfo[0]) + "-" + userInfo[2] + "-" + userInfo[3];
			
			info.set(phone);
			otherInfo.set(other);
			// map的输出为:<手机号, 数据访问时间-动作标识(共有两个动作:In-进入页面,Out-退出页面)-访问的页面>
			context.write(info, otherInfo);
		}
	}

	// reduce
	public static class OnlineReduce extends Reducer{
		private Text result = new Text();
		// 获取昨天日期
		String yesterday = getYesterday();
		public void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException{
			List list = new ArrayList();
			for (Text val : values) {
				list.add(val.toString());
			}
			
			// 将集合转换为数组
			String[] valueArray = (String[]) list.toArray(new String[list.size()]);
			
			String[] resultInfo = sortReduceValue(valueArray);
			
			// 36666-In-HouseController
			int resultTime = 0;
			for(int i = 0; i < resultInfo.length; i++){
				String[] tls = resultInfo[i].split("-");
				
				if("In".equals(tls[1])){
					// 数据已按照时间升序排序
					int inTime = Integer.parseInt(tls[0]);
					String inLocal = tls[2];
					
					for(int j = i + 1; j < resultInfo.length; j++){
						String[] ols = resultInfo[j].split("-");
						String outLocal = ols[2];
						
						// 判断是否是同一页面
						if(inLocal.equals(outLocal)){
							if("Out".equals(ols[1])){
								int outTime = Integer.parseInt(ols[0]);
								
								// 在本页面停留的时间
								int stayTime = outTime - inTime;
								// 在本页面停留时间大于10分钟,则抛弃此数据
								if(stayTime < 600){
									resultTime = resultTime + stayTime;
									break;
								}else{
									break;
								}
							}else{
								break;
							}
						}
					}
				}
			}
			
			if(resultTime == 0){
				resultTime = 1;
			}
			
			// 数据分隔符为 Tab键,方便交给hive管理
			result.set(String.valueOf(resultTime) + "	" + yesterday);
			context.write(key, result);
		}
	}
	
	// 将时间转换为秒
	public static int timeToSecond(String time){
		int second = 0;
		if(time != null && !"".equals(time)){
			String[] times = time.split(":");
			int hourSecond = Integer.parseInt(times[1]) * 60 * 60;
			int minuteSecond = Integer.parseInt(times[2]) * 60;
			second = hourSecond + minuteSecond + Integer.parseInt(times[3]);
		}
		return second;
	}
	
	// 对reduce数据按照时间从小到大排序(冒泡排序)
	public static String[] sortReduceValue(String[] values){
        for (int i = 0; i < values.length -1; i++){  
            for(int j = 0 ;j < values.length - i - 1; j++){
                if(Integer.parseInt(values[j].split("-")[0]) > Integer.parseInt(values[j + 1].split("-")[0])){
                    String temp = values[j];
                    values[j] = values[j + 1];
                    values[j + 1] = temp;
                }
            }            
        }
        return values;
	}
	
	// 获取昨天的日期
	public static String getYesterday(){
		Calendar cal=Calendar.getInstance();
		cal.add(Calendar.DATE,-1);
		Date time=cal.getTime();
		String yesterday = new SimpleDateFormat("yyyyMMdd").format(time);
		return yesterday;
	}
	
	// main函数
	@SuppressWarnings("deprecation")
	public static void main(String[] args) throws Exception {
		Configuration conf = new Configuration();
		String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
		
		if(otherArgs.length != 2){
			System.err.println("Usage: wordcount  ");
			System.exit(2);
		}
		
		Job job = new Job(conf, "User Online Job");
		job.setNumReduceTasks(1);
		job.setJarByClass(UserOnlineTime.class);
		job.setMapperClass(OnlineMapper.class);
		job.setReducerClass(OnlineReduce.class);
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(Text.class);
		FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
		FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
		System.exit(job.waitForCompletion(true) ? 0 : 1);
	}
}
<think>好的,我需要解决用户的问题,即如何根据表中的id、online_status和time字段计算离线时长用户特别提到需要按时间倒序排序,找到最后一条离线状态的时间,以及最后一次在线状态后紧接着的离线时间,然后计算两者的时间差。 首先,回顾之前的解决方案,用户可能已经尝试过通过分别获取最后在线和离线时间来计算差值。但用户现在提到需要“最后一条离线状态的时间”和“最后一次在线状态后紧接着的离线时间”,这说明之前的方案可能没有正确捕捉到用户的需。之前的方案是取每个用户的最大离线时间,但用户现在需要的是在最后一次在线之后出现的第一个离线时间,这可能与最大离线时间不同。例如,用户可能在最后一次在线后又多次离线,而用户需要的是最后一次在线后的第一次离线时间,而不是最大的离线时间。这时候,之前的方案可能无法正确计算,因为它只是取了最大的离线时间,而不管是否在最后一次在线之后。 接下来,我需要考虑如何正确获取最后一次在线时间之后的最早的离线时间。这可能需要使用窗口函数来按用户分区,按时间排序,并找到在线状态后的第一个离线记录。例如,对于每个用户,按时间倒序排序,当online_status从1变为0时,记录这个时间点作为离线开始时间,然后计算与最后一次在线时间的差值。 这里需要注意的是,按时间倒序排序后,最后一次在线时间应该是最后一个online_status=1的记录的时间。然后,之后的第一个offline_status=0的时间即为所需的离线开始时间。因此,可能需要使用LAG或LEAD函数来比较前后记录的状态变化。 此外,用户提到需要处理每个id的数据,所以需要按id分组处理。可能还需要处理存在多个离线记录的情况,只取最后一次在线之后的最早离线时间。例如,用户可能在线后多次离线,但只需要第一次离线的记录。 另一个考虑是时间戳的处理,确保时间差计算正确。如果时间存储为Unix时间戳,则直接相减即可得到秒数;如果是日期时间格式,可能需要使用DATEDIFF等函数。 还需要考虑性能优化,尤其是当数据量较大时。使用窗口函数可能会增加计算开销,但通过适当的索引(如在id和time字段上建立索引)可以提高查询效率。同时,如果数据分区合理,比如按时间分区,可以加快查询速度。 现在,需要构建一个SQL查询,按id分组,按时间倒序排序,找到最后一次在线时间,然后找到之后第一个离线时间,计算时间差。这可能需要使用子查询或公共表表达式(CTE)来分步骤处理。 可能的步骤如下: 1. 对每个id的所有记录按时间倒序排序。 2. 标记每条记录是否为在线或离线。 3. 找到最后一次在线的时间。 4. 在最后一次在线时间之后,找到第一个离线时间。 5. 计算两者的差值。 例如,使用窗口函数LAST_VALUE来获取最后一次在线时间,然后使用LEAD或LAG函数找到后续的离线时间。或者,使用ROW_NUMBER来给每个用户的在线和离线记录排序,然后找到对应的时间。 此外,需要处理没有后续离线记录的情况,此时时间差可能为NULL,或者需要忽略这些用户。 还需要测试不同的情况,例如用户最后一次状态是在线,之后没有离线记录;或者用户多次在线和离线交替出现,需要正确捕获最后一次在线后的第一个离线时间。 总结来说,正确的步骤应该是: - 按用户id分组,按时间倒序排序。 - 确定最后一次在线的时间点。 - 在该时间点之后,找到第一个离线的记录的时间。 - 计算两者的时间差。 这可能需要使用窗口函数和条件过滤来实现。</think>### 优化后的解决方案 根据需需要精准定位**最后一次在线时间**及其后**首次离线时间**,需采用窗口函数和状态变化检测。以下是分步实现: --- #### 1. 按用户分组并标记状态变化 使用窗口函数按时间倒序排列,识别在线到离线的状态切换点: ```sql WITH status_sequence AS ( SELECT id, time, online_status, -- 获取上一条记录的状态 LAG(online_status) OVER (PARTITION BY id ORDER BY time DESC) AS prev_status FROM status_table ) SELECT * FROM status_sequence WHERE online_status = 0 AND prev_status = 1 -- 仅保留从在线变为离线的记录 ``` --- #### 2. 提取最后一次在线时间及首次离线时间 结合状态变化点获取关键时间: ```sql WITH last_online AS ( SELECT id, MAX(time) AS last_online_time FROM status_table WHERE online_status = 1 GROUP BY id ), first_offline_after_online AS ( SELECT id, MIN(time) AS first_offline_time -- 取首次出现的离线时间 FROM status_sequence WHERE online_status = 0 AND prev_status = 1 GROUP BY id ) ``` --- #### 3. 关联计算离线时长 通过用户ID关联两个结果集: ```sql SELECT lo.id, lo.last_online_time, fo.first_offline_time, -- 计算精确时间差(单位:秒) fo.first_offline_time - lo.last_online_time AS offline_duration FROM last_online lo LEFT JOIN first_offline_after_online fo ON lo.id = fo.id WHERE fo.first_offline_time > lo.last_online_time -- 排除无效记录 ``` --- ### 完整SQL示例 ```sql WITH status_sequence AS ( SELECT id, time, online_status, LAG(online_status) OVER (PARTITION BY id ORDER BY time DESC) AS prev_status FROM status_table ), last_online AS ( SELECT id, MAX(time) AS last_online_time FROM status_table WHERE online_status = 1 GROUP BY id ), first_offline_after_online AS ( SELECT id, MIN(time) AS first_offline_time FROM status_sequence WHERE online_status = 0 AND prev_status = 1 GROUP BY id ) SELECT lo.id, lo.last_online_time, fo.first_offline_time, fo.first_offline_time - lo.last_online_time AS offline_duration FROM last_online lo LEFT JOIN first_offline_after_online fo ON lo.id = fo.id WHERE fo.first_offline_time > lo.last_online_time; ``` --- ### 关键改进说明 1. **状态切换检测** 通过`LAG(online_status)`识别从在线(1)到离线(0)的跳变点,精准定位有效离线起始时间[^1]。 2. **时间窗口优化** 使用`MIN(time)`确保获取最后一次在线后的**首次**离线时间,避免历史离线记录干扰。 3. **空值过滤** `WHERE fo.first_offline_time > lo.last_online_time`排除未发生状态切换的用户。 --- ### 性能优化建议 1. **索引策略** 对`(id, time DESC)`建立联合索引,加速窗口函数的排序操作[^2]。 2. **数据分区** 若时间字段为日期类型,按分区可减少单次查询的数据扫描量(参考`datestr`字段设计)[^3]。 3. **计算资源控制** 若单任务运行时间过短(如30-40秒),适当合并任务以减少调度开销(参考引用[1]的MapReduce优化建议)[^4]。 --- ### 相关问题 1. 如何统计用户所有在线到离线的切换次数? 2. 如何计算用户历史累计离线总时长? 3. 如果存在时区不一致问题,如何校准时间戳?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值