Mapper中获取分片信息

本文介绍在MapReduce中处理不同类型的输入文件,包括单文件输入、合并文件输入及使用MultipleInputs时如何正确获取输入路径。针对不同场景,文章详细解释了如何通过反射等技术手段解决特定类型输入分片的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

场景

输入路径中含有重要信息,需要在map的时候进行处理。

场合

只有一个Mapper,每个mapper只处理一个输入文件
protected void setup(Context context) throws IOException, InterruptedException {
    Path path = ((FileSplit) context.getInputSplit()).getPath();
}
使用似CombineTextInputFormat作为输入处理
protected void setup(Context context) throws IOException, InterruptedException {
    //只要众多合并文件中的第一个路径,通常会在combine的时候将处理的文件按某种逻辑合并作为一个分片
    Path firstPath = ((CombineFileSplit) context.getInputSplit()).getPath(0);
}
MultipleInputs

使用上面的方式会报错

Error: java.lang.ClassCastException: org.apache.hadoop.mapreduce.lib.input.TaggedInputSplit cannot be cast to org.apache.hadoop.mapreduce.lib.input.FileSplit
	at com.miaozhen.verify.admVerify.calGivt.GivtMapper.setup(GivtMapper.java:26)
    at org.apache.hadoop.mapreduce.Mapper.run(Mapper.java:143)
    at org.apache.hadoop.mapreduce.lib.input.DelegatingMapper.run(DelegatingMapper.java:55)
    at org.apache.hadoop.mapred.MapTask.runNewMapper(MapTask.java:799)
    at org.apache.hadoop.mapred.MapTask.run(MapTask.java:347)
    at org.apache.hadoop.mapred.YarnChild$2.run(YarnChild.java:174)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.security.auth.Subject.doAs(Subject.java:422)
    at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1688)
    at org.apache.hadoop.mapred.YarnChild.main(YarnChild.java:168)

因为TaggedInputSplit类不是public的,无法通过强制转换来解决,可以通过反射来获得TaggedInputSplit中的inputSplit,源自hadoop MultipleInputs fails with ClassCastException

protected void setup(Context context) throws IOException, InterruptedException {
    InputSplit split = context.getInputSplit();
    Class<? extends InputSplit> splitClass = split.getClass();

    FileSplit fileSplit = null;
    if (splitClass.equals(FileSplit.class)) {
        fileSplit = (FileSplit) split;
    } else if (splitClass.getName().equals(
        "org.apache.hadoop.mapreduce.lib.input.TaggedInputSplit")) {
        // begin reflection hackery...
        try {
            Method getInputSplitMethod = splitClass
                .getDeclaredMethod("getInputSplit");
            getInputSplitMethod.setAccessible(true);
            fileSplit = (FileSplit) getInputSplitMethod.invoke(split);
        } catch (Exception e) {
            // wrap and re-throw error
            throw new IOException(e);
        }
    }
    Path path = fileSplit.getPath();
}
<think>我们使用AOP来实现日志记录功能,并且将日志按月份分片存储到不同的数据库表中。方案如下: 1. **AOP切面设计**:创建一个切面,通过注解标记需要记录日志的方法,在方法执行后记录操作日志。 2. **日志分片存储**:根据当前时间生成对应的月份分片表名,将日志存储到对应月份的表中。 3. **动态表名处理**:在MyBatis等持久层框架中,可以使用动态表名的方式来实现按月分表。 具体步骤: ### 1. 定义日志注解 首先,定义一个注解`@LogRecord`,用于标记需要记录日志的方法,并可以设置日志内容。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogRecord { String content() default ""; // 操作内容 } ``` ### 2. 创建AOP切面 创建一个切面,拦截带有`@LogRecord`注解的方法,在方法执行成功后记录日志。 ```java @Aspect @Component public class LogAspect { @Autowired private LogService logService; // 日志服务 @AfterReturning(pointcut = "@annotation(logRecord)", returning = "result") public void doAfterReturning(JoinPoint joinPoint, LogRecord logRecord, Object result) { // 获取注解中的内容 String content = logRecord.content(); // 构建日志实体 LogEntity logEntity = new LogEntity(); logEntity.setContent(content); logEntity.setCreateTime(new Date()); // 其他字段,如操作人、IP等,可以从请求上下文或参数中获取 // 调用日志服务,保存日志(这里会处理分表) logService.save(logEntity); } } ``` ### 3. 日志实体设计 日志实体类,注意这里不需要指定表名,因为表名是动态的。 ```java public class LogEntity { private Long id; private String content; // 操作内容 private Date createTime; // 操作时间 // 其他字段... } ``` ### 4. 日志服务与分表实现 在日志服务中,根据当前时间生成表名,然后使用MyBatis的动态表名功能插入到对应月份的表中。 #### 动态表名处理 我们可以使用ThreadLocal来传递动态表名。首先,定义一个动态表名上下文: ```java public class DynamicTableNameHolder { private static final ThreadLocal<String> tableName = new ThreadLocal<>(); public static void setTableName(String name) { tableName.set(name); } public static String getTableName() { return tableName.get(); } public static void clear() { tableName.remove(); } } ``` 然后,在MyBatis的拦截器中,根据日志的创建时间计算表名,并设置到动态表名上下文中。但是,我们这里更直接的方式是在插入日志时动态计算表名,并修改SQL。 另一种方式:在Mapper XML中使用动态表名。但是MyBatis默认不支持表名作为参数传递,我们可以通过自定义拦截器来实现。 这里我们使用简单的方法:在日志服务中,直接拼接表名,然后调用不同的Mapper方法(每个表对应一个Mapper?显然不现实)。因此,我们采用自定义拦截器来动态修改表名。 #### 自定义MyBatis拦截器 创建一个拦截器,拦截插入操作,动态修改表名为按月分片的表。 ```java @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class DynamicTableNameInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); // 获取原始SQL String originalSql = (String) metaObject.getValue("delegate.boundSql.sql"); // 如果SQL中包含特定的表名(比如固定前缀),则进行替换 // 假设我们基础表名为log,分片表名为log_202201, log_202202等 if (originalSql.contains("log")) { // 从当前线程上下文中获取动态表名 String dynamicTableName = DynamicTableNameHolder.getTableName(); if (dynamicTableName != null) { // 替换表名 String newSql = originalSql.replaceAll("log", dynamicTableName); metaObject.setValue("delegate.boundSql.sql", newSql); } } return invocation.proceed(); } } ``` 在日志服务中,我们在调用Mapper的插入方法之前,先根据日志的创建时间设置动态表名。 ```java @Service public class LogServiceImpl implements LogService { @Autowired private LogMapper logMapper; @Override public void save(LogEntity log) { // 根据日志的创建时间生成表名后缀,如202201 String tableSuffix = new SimpleDateFormat("yyyyMM").format(log.getCreateTime()); String tableName = "log_" + tableSuffix; // 设置动态表名 DynamicTableNameHolder.setTableName(tableName); try { logMapper.insert(log); } finally { DynamicTableNameHolder.clear(); } } } ``` ### 5. 创建日志表 需要预先创建日志表,可以按月创建,比如: - log_202201 - log_202202 - ... 表结构相同。可以在程序启动时检查表是否存在,不存在则创建,或者使用定时任务提前创建。 ### 6. 配置MyBatis拦截器 在MyBatis配置中注册拦截器: ```java @Configuration public class MyBatisConfig { @Bean public DynamicTableNameInterceptor dynamicTableNameInterceptor() { return new DynamicTableNameInterceptor(); } } ``` ## 注意事项 1. **动态表名的线程安全**:我们使用了ThreadLocal来传递表名,确保每个线程使用自己的表名,不会相互干扰。 2. **表不存在的问题**:在插入之前,需要确保该月的表已经存在。可以在插入前检查,如果不存在则创建。或者使用一个定时任务,在每月初创建当月的日志表。 3. **性能**:动态修改SQL对性能影响很小,因为拦截器只对包含特定表名的SQL进行修改。 ## 分表创建策略 可以在日志服务中,插入前检查表是否存在,不存在则创建: ```java @Service public class LogServiceImpl implements LogService { @Autowired private LogMapper logMapper; @Autowired private JdbcTemplate jdbcTemplate; @Override public void save(LogEntity log) { String tableSuffix = new SimpleDateFormat("yyyyMM").format(log.getCreateTime()); String tableName = "log_" + tableSuffix; // 检查表是否存在,不存在则创建 createTableIfNotExists(tableName); DynamicTableNameHolder.setTableName(tableName); try { logMapper.insert(log); } finally { DynamicTableNameHolder.clear(); } } private void createTableIfNotExists(String tableName) { // 使用jdbcTemplate检查表是否存在,如果不存在则创建 String sql = "CREATE TABLE IF NOT EXISTS " + tableName + " LIKE log"; jdbcTemplate.execute(sql); } } ``` 这里假设已经有一张基础表`log`,我们使用`LIKE`语句创建一张相同结构的表。如果没有基础表,则需要提供完整的建表语句。 ## 总结 通过AOP切面拦截需要记录日志的方法,然后根据当前时间动态确定分片表名,利用MyBatis拦截器动态替换SQL中的表名,实现日志的按月分表存储。同时,在插入前确保分表存在,不存在则创建。 这个方案实现了日志记录与业务逻辑的解耦,并且支持日志的按月分表存储,便于后续的日志管理和查询。 [^1]: 引用自AOP日志示例 [^2]: 引用自AOP思想介绍 [^3]: 引用自分片规则示例 §§相关问题§§ 1. AOP切面中如何获取方法参数和返回值? 2. 动态表名拦截器会影响其他SQL吗? 3. 按月分表后,如何查询跨月的日志数据?</think>### 使用AOP实现日志记录并按月分表存储的方案 #### 1. **AOP切面设计** 通过自定义注解和切面实现日志记录,与业务逻辑解耦: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MonthlyLog { String value(); // 操作描述 } ``` ```java @Aspect @Component public class LogAspect { @Autowired private LogService logService; // 日志服务层 @AfterReturning(pointcut = "@annotation(monthlyLog)", returning = "result") public void logAfterReturning(JoinPoint joinPoint, MonthlyLog monthlyLog, Object result) { // 1. 构建日志实体 LogEntity log = new LogEntity(); log.setOperation(monthlyLog.value()); // 从注解获取操作描述 log.setCreateTime(new Date()); // 当前时间 // 2. 获取方法参数信息(可选) Object[] args = joinPoint.getArgs(); if (args.length > 0) { log.setParams(JsonUtils.toJson(args[0])); // 序列化参数 } // 3. 调用分表存储服务 logService.saveWithMonthlySharding(log); } } ``` #### 2. **动态表名分片策略** 使用月份后缀动态生成表名(如 `log_202301`): ```java @Service public class LogServiceImpl implements LogService { @Autowired private LogMapper logMapper; // MyBatis Mapper @Override public void saveWithMonthlySharding(LogEntity log) { // 1. 生成动态表名 String tableSuffix = new SimpleDateFormat("yyyyMM").format(log.getCreateTime()); String dynamicTable = "log_" + tableSuffix; // 如 log_202301 // 2. 检查表是否存在(不存在则创建) createTableIfAbsent(dynamicTable); // 3. 设置动态表名并插入 logMapper.insertLog(dynamicTable, log); } private void createTableIfAbsent(String tableName) { // 执行DDL语句(示例) String sql = "CREATE TABLE IF NOT EXISTS " + tableName + " LIKE log_template"; jdbcTemplate.execute(sql); // 使用JdbcTemplate执行 } } ``` #### 3. **MyBatis动态SQL处理** 在Mapper中通过参数传递表名: ```xml <!-- LogMapper.xml --> <insert id="insertLog"> INSERT INTO ${tableName} (operation, params, create_time) VALUES (#{log.operation}, #{log.params}, #{log.createTime}) </insert> ``` ```java // Mapper接口 public interface LogMapper { void insertLog(@Param("tableName") String tableName, @Param("log") LogEntity log); } ``` #### 4. **业务层使用示例** 在需要记录日志的方法上添加注解: ```java @Service public class UserService { @MonthlyLog("更新用户信息") public void updateUser(User user) { // 业务逻辑... } } ``` #### 5. **表结构设计建议** 基础表结构(每月分表继承此结构): ```sql CREATE TABLE log_template ( id BIGINT AUTO_INCREMENT PRIMARY KEY, operation VARCHAR(100) NOT NULL, -- 操作描述 params TEXT, -- 方法参数 create_time DATETIME NOT NULL -- 操作时间 ); ``` ### 关键实现细节 1. **动态表名生成** - 基于操作时间生成 `log_yyyyMM` 格式表名 - 首次访问时自动建表(`CREATE TABLE IF NOT EXISTS`) 2. **AOP与业务解耦** - 通过 `@MonthlyLog` 注解标记需要日志的方法 - 切面自动捕获操作参数和时间戳[^1] 3. **分表查询优化** - 跨月查询使用 `UNION ALL` 合并多表 ```sql SELECT * FROM log_202301 UNION ALL SELECT * FROM log_202302 WHERE operation = '用户更新' ``` 4. **事务管理** - 日志记录与业务操作使用独立事务,避免日志失败影响主业务 ### 注意事项 1. **表管理策略** - 添加定时任务提前创建下月表(避免首次访问延迟) - 归档历史数据(如将半年前数据迁移到备份库) 2. **性能优化** - 批量插入日志时使用异步处理(如 `@Async`) - 为 `create_time` 字段建立索引 3. **扩展性设计** - 可配置分片粒度(月/季度/年),通过配置文件切换 - 支持多数据源分片(不同表存储在不同数据库实例)[^3] [^1]: AOP通过拦截方法实现业务与日志解耦 [^3]: 分片策略可按时间维度动态路由表名
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值