摘要
Cassandra是一种适用于写多读少场景的分布式数据库。当我们需要将cassandra中的数据进行大量读取操作的时候,我们不可以将大量的读取操作直接打到cassandra数据库,而是需要将cassandra中的数据同步到redis,然后从redis读取。这样可以大大提高系统的效率。本文提供一种在cassandra和redis之间进行数据同步的实战操作经验,希望对读者有所帮助。
场景
在介绍解决方案的时候,不说明需求的场景就是耍流氓。本案的场景是这样的:
需要在接近亿级别的数据中进行最短路径搜索(只能说到这里了,总之就是最短路径搜索)。
- 数据总量较大:数千万至1亿条记录
- 读写比极大:数据全量更新频率为每周一次。数据读取频率方面,因为说的是最短路径搜索算法,所以每一次搜索都需要对数据库中的数据进行大量的遍历和搜索操作。每一次搜索操作都会化成无数次数据读取的流量。
架构
如上图所示,搜索的数据源来自于A数据库。A数据保存在业务数据库中,我们平台用的是Cassandra来保存这个业务数据。A数据的更新频率比较固定,为每周一次全量更新。
搜索数据源是以A数据为维度,对A数据进行一定的处理(主要是增加一些辅助性的列),再输出到搜索数据源。在线搜索模块需要读取搜索数据源,在读取到的数据中执行搜索算法。
难点
- A数据库是千万级别的数据量。
- 在线搜索模块每执行一次搜索任务都需要读取上千次搜索数据。要保证搜模块可以在规定的时间范围内得到结果,数千次数据库读取产生的延迟和数据库压力是不可接受的。
解决方案
本案采用全量同步的方式将数据从Cassandra加载到redis。
redis中key的设置:
A数据在redis上通过集合(Set)的形式保存。其中key是搜索算法中每次遍历的最小集合的公共字段的拼接。例如:搜索算法在执行过程中,必须不停地获取A数据中x,y,z三个字段相同地数据集合,那么key就可以设置为:${x}-${y}-${z}。这样做的好处是:redis上smembers操作的时间复杂度是O(1),因此不会随着redis中数据量的增加而增加从reids获取数据的时间。
注意:绝对不可以通过keys操作在redis上扫描数据,因为:1)redis上的数据量越大,该操作越耗时;2)jedisCluster不支持keys操作,要进行特殊处理。总之不要这样用!!!
redis中值的设置:
set中的值就是数据的摘要信息(注意必须保证是该条数据记录的幂等key)。这里特别说明了redis上尽量不要把整个数据对象Json序列化之后放上去,因为这样的话,如果字段特别长,会增加数据读写时间和解析时间,而且可能会失去幂等性。利用set数据不可重复的特性,将数据记录的幂等key作为member设置到redis中,除了可满足正常搜索的作用,而且当同步大量数据时,中途失败是经常发生的,使用这种记录的保存方式,可以保证在重新执行flink批量任务,不会导致数据重复的问题。
代码
读取Cassandra数据:
全量同步采用flink的DataSet操作,主要针对A数据。通过CassandraPojoInputFormat,实现对Cassandra数据的读取和解析。
读取Cassandra数据的核心代码如下:
String host = parameters.get("DataSourceHost", CassandraConfiguration.DefaultCassandraHost);
String cql = "select * from "+ CassandraConfiguration.DataKeySpace +"." + table + " where date = '" + date + "'";
DataSet<AData> aDataSet = env.createInput(QueryUtils.executeQuery(cql, host, AData.class),TypeInformation.of(new TypeHint<AData>() {}))
.name("GETTING A DATA for date:" + date);
其中QueryUtils.executQuery的代码如下:
public static CassandraInputFormatBase executeQuery(String cql, String clusters, Class clazz){
ClusterBuilder builder = new ClusterBuilder() {
private static final long serialVersionUID = 1;
@Override
protected Cluster buildCluster(com.datastax.driver.core.Cluster.Builder builder) {
return builder.addContactPoints(clusters)
// .withPoolingOptions(poolingOptions) 在flink中使用连接池会报序列化错误
.build();
}
};
return new CassandraPojoInputFormat(cql, builder, clazz);
}
获取数据后对数据进行各种join,然后通过RedisOutputFormat(扩展自RichOutputFormat)输出到redis。
outputDataSet.output(new RedisOutputFormat<AData>(new BatchADataRedisMapper(redisPrefix, true))).name("OUTPUT Data TO REDIS:" + date);
写入redis操作:
RedisOutputFormat.java代码:
@Log
public class RedisOutputFormat<T> extends RichOutputFormat<T> {
public interface RedisMapper<T> {
byte[] getKey(T data);
byte[] getField(T data);
byte[] getValue(T data);
default int getExpireSeconds(){
return 0;
};
default void writeRecord(JedisCluster cluster, T data){
byte[] key = this.getKey(data);
byte[] field = this.getField(data);
byte[] value = this.getValue(data);
cluster.hset(key, field, value);
log.info("writing data to redis:"+key);
}
default void close(JedisCluster cluster){
return;
}
default void open(JedisCluster cluster){
return;
}
}
private JedisCluster jedisCluster;
private RedisMapper mapper;
public
RedisOutputFormat(RedisMapper<T> mapper){
this.mapper = mapper;
}
@Override
public void configure(Configuration configuration) {
}
/**
* Connects to the target database and initializes the prepared statement.
*
* @param taskNumber The number of the parallel instance.
* @throws IOException Thrown, if the output could not be opened due to an
* I/O problem.
*/
@Override
public void open(int taskNumber, int numTasks) throws IOException {
ParameterTool parameters = (ParameterTool) getRuntimeContext().getExecutionConfig().getGlobalJobParameters();
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(parameters.getInt("redisPoolMaxSize",16));
poolConfig.setMaxIdle(parameters.getInt("redisPoolMaxIdle",3));
poolConfig.setMinIdle(parameters.getInt("redisPoolMinIdle",3));
poolConfig.setMaxWaitMillis(parameters.getInt("redisPoolMaxWaitMillis",3000));
Set<HostAndPort> hostSet = new HashSet<HostAndPort>();
String hostPortArrayString = parameters.get("redisHost",RedisConfiguration.host);
String[] hostPortArray = hostPortArrayString.split(",");
for (String hostPortString : hostPortArray) {
String[] hostAndPort = hostPortString.split(":");
String host = hostAndPort[0];
int port = 6379;
if (hostAndPort.length > 1){
port = Integer.parseInt(hostAndPort[1]);
}
hostSet.add(new HostAndPort(host, port));
}
jedisCluster = new JedisCluster(hostSet, 6000, 10, poolConfig);
this.mapper.open(jedisCluster);
}
@Override
public void writeRecord(T row) throws IOException {
this.mapper.writeRecord(jedisCluster, row);
if(this.mapper.getExpireSeconds() > 0){
jedisCluster.expire(this.mapper.getKey(row), this.mapper.getExpireSeconds());
}
}
/**
* Executes closes all resources of this instance.
*
* @throws IOException Thrown, if the input could not be closed properly.
*/
@Override
public void close() throws IOException {
if (null != jedisCluster) {
jedisCluster.close();
this.mapper.close(jedisCluster);
jedisCluster = null;
}
}
}
其中自定义的RedisMapper代码如下:
public class ADataRedisMapper implements Serializable,RedisOutputFormat.RedisMapper<AData> {
public ADataRedisMapper(String keyPrefix){
super(keyPrefix);
}
@Override
public void writeRecord(JedisCluster cluster, AData data){
byte[] key = this.getKey(data);
byte[] value = this.getValue(data);
cluster.sadd(key, value);
}
@Override
public byte[] getKey(AData data) {
String keyString = "";
keyString = keyPrefix + data.getX() + "-" + data.getY();
try {
byte[] keyByteArray = keyString.getBytes("utf-8");
return keyByteArray;
} catch (UnsupportedEncodingException e) {
byte[] keyByteArray = keyString.getBytes();
return keyByteArray;
}
}
@Override
public byte[] getField(AData data) {
return "no_field_expected".getBytes();
}
@Override
public byte[] getValue(AData data) {
byte[] retByteArray = null;
String retString = this.getADataAbstraction(data);
try {
retByteArray = retString.getBytes("utf-8");
} catch (UnsupportedEncodingException e) {
retByteArray = retString.getBytes();
}
return retByteArray;
}
private String getADataAbstraction(AData data){
Object[] array = new Object[]{
data.getX(),
data.getY(),
data.getZ(),
};
String abstraction = StringUtils.join(array, "$");
return abstraction;
}
}
通过Flink实现Cassandra数据同步到redis缓存实战系列文章电梯:
一:数据同步任务的实现
二:Flink任务的定时提交
三:异常处理
四:幂等性!幂等性!幂等性!Redis缓存数据幂等性设计