在自己写的一个Demo中使用了数据库存储的操作,发现一些问题,场景大致是:spout从kafka中读取数据,发送给bolt1计算得到一个数据,bolt1发给bolt2存储到Redis,bolt2发送给bolt3,在bolt3中存储到Mysql。在前面两个bolt中都没啥问题,在bolt3中,当并发量非常大时,存在获取不到数据库连接的问题,笔者首先使用的是原声jdbc,然后换了C3P0连接池,都会在不同时机出现上述问题,于是去看了看Storm的官方文档,使用了官方提供的JDBC集成方式,发现自己遇到的问题很好的解决了。所以这里主要来说下Storm对JDBC的官方集成支持。
首先来看看官方文档的说明。
Storm提供的JDBC集成是针对单表来操作的,也就是说一个jdbc bolt不管是查询还是插入操作,针对的是单表。当然,我们自己可以改写,后面再说。这里需要用到两个很重要的组件,分别是ConnectionProvider和JdbcMapper,其中ConnectionProvider负责获得数据库连接,JdbcMapper负责将Tupe和数据表里的字段做映射。
storm-jdbc提供的官方API中包括了查询和插入操作,因为查询比较简单,这里是以插入数据的角度总结的,关于查询,只要理解了插入,查询比较简单,在最后附了一块代码,感兴趣的可以看看。
添加依赖,不需要的话跳过
org.apache.storm
storm-jdbc
1.0.1
1. ConnectionProvider和JdbcMapper组件介绍
(1)ConnectionProvider
根据文档说明,我们可以自己去实现一个ConnectionProvider,同时Storm也给我们提供了一个很好的实现org.apache.storm.jdbc.common.HikariCPConnectionProvider,查了下源码,这里使用的是HikariDataSource。HikariDataSource据说是“当前性能最好的数据源”,或许我上述问题的解决,关键问题就在于此~闲话不多说,通常来说,我们使用既定实现就够用了,使用方式如下:
Map hikariConfigMap = Maps.newHashMap();
hikariConfigMap.put("dataSourceClassName","com.mysql.jdbc.jdbc2.optional.MysqlDataSource");
hikariConfigMap.put("dataSource.url", "jdbc:mysql://localhost/test");
hikariConfigMap.put("dataSource.user","root");
hikariConfigMap.put("dataSource.password","password");
ConnectionProvider connectionProvider = new HikariCPConnectionProvider(hikariConfigMap);
(2)JdbcMapper:
如该组件的名字示意,JdbcMapper是个Mapper映射,将tupe和数据表的row做映射,也就是说,tupe中的field对应row中的column。同样的,我们也可以自己去实现JdbcMapper,或者使用Storm为我们提供的简单实现SimpleJdbcMapper。实现JdbcMapper接口只需要实现一个方法:
public interface JdbcMapper extends Serializable {
List getColumns(ITuple tuple);
}
Column是storm-jdbc定义的一个VO,对应于tupe中的field和表中的某一个字段。举个栗子,假如我们要插入的数据表包含三个字段col1,col2,col3,那么我们应该在实现该方法时,使得返回的List中包含三个elements,分别对应这三个字段。Column主要包含三个重要的属性,分别是Name/Val/Type,即列名/列值/列数据类型,在实现Mapper时,对于每个column分别需要指定这三个属性。在执行insert操作时,其中Name对应数据表中的字段名,Val一般而言便是从Tupe中获取而来(将要插入的值),Type通常是java.sql.Type的一种。
实现该Mapper的作用就是将数据表的字段和List里的Colum一一对应,因为storm-jdbc是通过这种映射来操作数据表的。在实现该方法时,需要尤其注意返回的List的顺序,即在往List中添加Column时,column的顺序应该和数据表中的字段顺序一一对应,storm-jdbc仅根据顺序来插入,而不会根据name来维护顺序,所以这里需要格外小心。
storm-jdbc为我们提供了简单的实现——SimpleJdbcMapper。如其名字所示,这是个简单的Mapper,它假设我们tupe中需要存储到数据库的field和数据库中的字段名一样,也就是说,如果将插入的表中有字段a1、a2,那么tupe中也有这样的a1、a2的field。SimpleJdbcMapper提供了两个构造器,分别是
public SimpleJdbcMapper(String tableName, ConnectionProvider connectionProvider) {
Validate.notEmpty(tableName);
Validate.notNull(connectionProvider);
int queryTimeoutSecs = 30;
connectionProvider.prepare();
JdbcClient client = new JdbcClient(connectionProvider, queryTimeoutSecs);
this.schemaColumns = client.getColumnSchema(tableName);
}
public SimpleJdbcMapper(List schemaColumns) {
Validate.notEmpty(schemaColumns);
this.schemaColumns = schemaColumns;
}
在SimpleJdbcMapper的源码中,他将根据schemaColumns来实现JdbcMapper接口中的List getColumns(ITuple tuple)方法,即从schemaColumns获取需要插入那几个字段,从tupe中获取需要插入的值,然后交给jdbcClient去执行插入操作。而这两个构造器的主要作用就是来初始化schemaColumns。如果我们使用第一个,那么它将通过connectionProvider和table自己去获取数据表的metadata,以初始化schemaColumns;如果我们使用第二个,我们需要自己提供一个组装好的schemaColumns。SimpleJdbcMapper的源码如下:
public class SimpleJdbcMapper implements JdbcMapper {
private List schemaColumns;
public SimpleJdbcMapper(String tableName, ConnectionProvider connectionProvider) {
Validate.notEmpty(tableName);
Validate.notNull(connectionProvider);
int queryTimeoutSecs = 30;
connectionProvider.prepare();
JdbcClient client = new JdbcClient(connectionProvider, queryTimeoutSecs);
this.schemaColumns = client.getColumnSchema(tableName);
}
public SimpleJdbcMapper(List schemaColumns) {
Validate.notEmpty(schemaColumns);
this.schemaColumns = schemaColumns;
}
@Override
public List getColumns(ITuple tuple) {
List columns = new ArrayList();
for(Column column : schemaColumns) {
String columnName = column.getColumnName();
Integer columnSqlType = column.getSqlType();
if(Util.getJavaType(columnSqlType).equals(String.class)) {
String value = tuple.getStringByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Short.class)) {
Short value = tuple.getShortByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Integer.class)) {
Integer value = tuple.getIntegerByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Long.class)) {
Long value = tuple.getLongByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Double.class)) {
Double value = tuple.getDoubleByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Float.class)) {
Float value = tuple.getFloatByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Boolean.class)) {
Boolean value = tuple.getBooleanByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(byte[].class)) {
byte[] value = tuple.getBinaryByField(columnName);
columns.add(new Column(columnName, value, columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Date.class)) {
Long value = tuple.getLongByField(columnName);
columns.add(new Column(columnName, new Date(value), columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Time.class)) {
Long value = tuple.getLongByField(columnName);
columns.add(new Column(columnName, new Time(value), columnSqlType));
} else if(Util.getJavaType(columnSqlType).equals(Timestamp.class)) {
Long value = tuple.getLongByField(columnName);
columns.add(new Column(columnName, new Timestamp(value), columnSqlType));
} else {
throw new RuntimeException("Unsupported java type in tuple " + Util.getJavaType(columnSqlType));
}
}
return columns;
}
}
有了以上ConnectionProvider和JdbcMapper之后,我们就可以去构建一个InsertBolt,storm-jdbc也给我们提供了API。该实现使用起来比较简单:
Map hikariConfigMap = Maps.newHashMap();
hikariConfigMap.put("dataSourceClassName","com.mysql.jdbc.jdbc2.optional.MysqlDataSource");
hikariConfigMap.put("dataSource.url", "jdbc:mysql://localhost/test");
hikariConfigMap.put("dataSource.user","root");
hikariConfigMap.put("dataSource.password","password");
ConnectionProvider connectionProvider = new HikariCPConnectionProvider(hikariConfigMap);
String tableName = "user_details";
JdbcMapper simpleJdbcMapper = new SimpleJdbcMapper(tableName, connectionProvider);
JdbcInsertBolt userPersistanceBolt = new JdbcInsertBolt(connectionProvider, simpleJdbcMapper)
.withTableName(tableName)
.withQueryTimeoutSecs(30);
上面的代码已经是一个完整的bolt,在使用SimpleJdbcMapper时,通过指定数据连接的方式初始化Mapper,也就是上面所说的第一种构造器。通过这种方式会比较受限制,因为这种机制要求我们,将要插入的数据表中的所有字段信息,都要在tupe中获取到对应的值,然而我们在应用中往往不会这么刚好符合上述的这种假定,我们可能只需要插入一个表中M个字段中的N个(N
相对而言,Storm集成redis比较简单,官方也提供了包装好的RedisStoreBolt和RedisLookupBolt,感兴趣的自己去apache官网查看,这里说说比较灵活的方式:继承AbstractRedisBolt。
首先贴上源码:
public abstract class AbstractRedisBolt extends BaseRichBolt {
protected OutputCollector collector;
private transient JedisCommandsInstanceContainer container;
private JedisPoolConfig jedisPoolConfig;
private JedisClusterConfig jedisClusterConfig;
/**
* Constructor for single Redis environment (JedisPool)
* @param config configuration for initializing JedisPool
*/
public AbstractRedisBolt(JedisPoolConfig config) {
this.jedisPoolConfig = config;
}
/**
* Constructor for Redis Cluster environment (JedisCluster)
* @param config configuration for initializing JedisCluster
*/
public AbstractRedisBolt(JedisClusterConfig config) {
this.jedisClusterConfig = config;
}
/**
* {@inheritDoc}
*/
@Override
public void prepare(Map map, TopologyContext topologyContext, OutputCollector collector) {
// FIXME: stores map (stormConf), topologyContext and expose these to derived classes
this.collector = collector;
if (jedisPoolConfig != null) {
this.container = JedisCommandsContainerBuilder.build(jedisPoolConfig);
} else if (jedisClusterConfig != null) {
this.container = JedisCommandsContainerBuilder.build(jedisClusterConfig);
} else {
throw new IllegalArgumentException("Jedis configuration not found");
}
}
/**
* Borrow JedisCommands instance from container.
* JedisCommands is an interface which contains single key operations.
* @return implementation of JedisCommands
* @see JedisCommandsInstanceContainer#getInstance()
*/
protected JedisCommands getInstance() {
return this.container.getInstance();
}
/**
* Return borrowed instance to container.
* @param instance borrowed object
*/
protected void returnInstance(JedisCommands instance) {
this.container.returnInstance(instance);
}
}
通过源码我们看到,AbstractRedisBolt提供了两个构造器,分别用来初始化JedisPool或者JedisCluster,笔者在这里用的是JedisPool的方式。使用JedisPool,则需要提供JedisPoolConfig实例,用来初始化pool。具体使用,我们贴上官网的例子,大家根据例子结合自己的需求修改,比较easy。
public static class LookupWordTotalCountBolt extends AbstractRedisBolt {
private static final Logger LOG = LoggerFactory.getLogger(LookupWordTotalCountBolt.class);
private static final Random RANDOM = new Random();
public LookupWordTotalCountBolt(JedisPoolConfig config) {
super(config);
}
public LookupWordTotalCountBolt(JedisClusterConfig config) {
super(config);
}
@Override
public void execute(Tuple input) {
JedisCommands jedisCommands = null;
try {
jedisCommands = getInstance();
String wordName = input.getStringByField("word");
String countStr = jedisCommands.get(wordName);
if (countStr != null) {
int count = Integer.parseInt(countStr);
this.collector.emit(new Values(wordName, count));
// print lookup result with low probability
if(RANDOM.nextInt(1000) > 995) {
LOG.info("Lookup result - word : " + wordName + " / count : " + count);
}
} else {
// skip
LOG.warn("Word not found in Redis - word : " + wordName);
}
} finally {
if (jedisCommands != null) {
returnInstance(jedisCommands);
}
this.collector.ack(input);
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// wordName, count
declarer.declare(new Fields("wordName", "count"));
}
}
不用理会代码中的Random相关,其目的只是为了打印日志而又不想全部打印,就以较小的概率打印日志。主要的代码是
jedisCommands = getInstance();
jedisCommands.get(wordName);
returnInstance(jedisCommands);
即拿到jedis实例、执行插入或者查找、放回jedis实例,具体操作自己实现,就不多说啦。