13 changed files with 434 additions and 1 deletions
@ -0,0 +1,33 @@ |
|||||
|
HELP.md |
||||
|
target/ |
||||
|
.mvn/wrapper/maven-wrapper.jar |
||||
|
!**/src/main/**/target/ |
||||
|
!**/src/test/**/target/ |
||||
|
|
||||
|
### STS ### |
||||
|
.apt_generated |
||||
|
.classpath |
||||
|
.factorypath |
||||
|
.project |
||||
|
.settings |
||||
|
.springBeans |
||||
|
.sts4-cache |
||||
|
|
||||
|
### IntelliJ IDEA ### |
||||
|
.idea |
||||
|
*.iws |
||||
|
*.iml |
||||
|
*.ipr |
||||
|
|
||||
|
### NetBeans ### |
||||
|
/nbproject/private/ |
||||
|
/nbbuild/ |
||||
|
/dist/ |
||||
|
/nbdist/ |
||||
|
/.nb-gradle/ |
||||
|
build/ |
||||
|
!**/src/main/**/build/ |
||||
|
!**/src/test/**/build/ |
||||
|
|
||||
|
### VS Code ### |
||||
|
.vscode/ |
||||
@ -0,0 +1,8 @@ |
|||||
|
package com.aeon.tcp.common; |
||||
|
|
||||
|
|
||||
|
public class Constants { |
||||
|
|
||||
|
public static final String STREAM_KEY_PREFIX = "aeon_tcp_stream_"; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
package com.aeon.tcp.disruptor; |
||||
|
|
||||
|
import cn.hutool.json.JSONUtil; |
||||
|
import com.lmax.disruptor.EventHandler; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.concurrent.atomic.AtomicBoolean; |
||||
|
|
||||
|
/** |
||||
|
* 批量 Handler |
||||
|
* |
||||
|
* 特点: |
||||
|
* 1. 锁内仅 swap buffer |
||||
|
* 2. 支持 batchSize + 定时 flush |
||||
|
* 3. Disruptor 对象复用安全 |
||||
|
*/ |
||||
|
public abstract class BaseBatchEventHandler<T> implements EventHandler<T> { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(BaseBatchEventHandler.class); |
||||
|
|
||||
|
private final int batchSize; |
||||
|
|
||||
|
/** 当前写入 buffer(只在锁内访问) */ |
||||
|
private List<T> buffer = new ArrayList<>(); |
||||
|
|
||||
|
private final Object lock = new Object(); |
||||
|
private final AtomicBoolean running = new AtomicBoolean(true); |
||||
|
|
||||
|
public BaseBatchEventHandler(int batchSize) { |
||||
|
this.batchSize = batchSize; |
||||
|
} |
||||
|
|
||||
|
/** 子类实现处理 */ |
||||
|
protected abstract void flushToHandle(List<T> events); |
||||
|
|
||||
|
/** 子类实现深拷贝 */ |
||||
|
protected abstract T copyOf(T event); |
||||
|
|
||||
|
@Override |
||||
|
public void onEvent(T event, long sequence, boolean endOfBatch) { |
||||
|
List<T> toFlush = null; |
||||
|
|
||||
|
synchronized (lock) { |
||||
|
buffer.add(copyOf(event)); |
||||
|
|
||||
|
if (buffer.size() >= batchSize) { |
||||
|
toFlush = buffer; |
||||
|
buffer = new ArrayList<>(batchSize); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 在锁外
|
||||
|
if (toFlush != null) { |
||||
|
flushSafely(toFlush); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** 定时 flush 线程 */ |
||||
|
@Override |
||||
|
public void onStart() { |
||||
|
Thread flusher = new Thread(() -> { |
||||
|
while (running.get()) { |
||||
|
try { |
||||
|
Thread.sleep(1000); |
||||
|
timedFlush(); |
||||
|
} catch (InterruptedException e) { |
||||
|
Thread.currentThread().interrupt(); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("timer flush failed", e); |
||||
|
} |
||||
|
} |
||||
|
}, "batch-flusher"); |
||||
|
|
||||
|
flusher.setDaemon(true); |
||||
|
flusher.start(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onShutdown() { |
||||
|
running.set(false); |
||||
|
timedFlush(); |
||||
|
} |
||||
|
|
||||
|
/** 定时触发 flush */ |
||||
|
private void timedFlush() { |
||||
|
List<T> toFlush = null; |
||||
|
|
||||
|
synchronized (lock) { |
||||
|
if (!buffer.isEmpty()) { |
||||
|
toFlush = buffer; |
||||
|
buffer = new ArrayList<>(batchSize); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (toFlush != null) { |
||||
|
flushSafely(toFlush); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** flush 统一保护 */ |
||||
|
private void flushSafely(List<T> list) { |
||||
|
if (list.isEmpty()) return; |
||||
|
|
||||
|
try { |
||||
|
flushToHandle(list); |
||||
|
logger.info("batch flush success, list{}", JSONUtil.toJsonStr(list)); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("batch flush failed, size={}", list.size(), e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
package com.aeon.tcp.disruptor; |
||||
|
|
||||
|
import com.aeon.tcp.disruptor.redis.stream.RedisStreamEvent; |
||||
|
import com.aeon.tcp.disruptor.redis.stream.RedisStreamEventFactory; |
||||
|
import com.aeon.tcp.disruptor.redis.stream.RedisStreamEventHandler; |
||||
|
import com.aeon.tcp.disruptor.redis.stream.RedisStreamProducer; |
||||
|
import com.lmax.disruptor.RingBuffer; |
||||
|
import com.lmax.disruptor.SleepingWaitStrategy; |
||||
|
import com.lmax.disruptor.dsl.Disruptor; |
||||
|
import com.lmax.disruptor.dsl.ProducerType; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
|
||||
|
import java.util.concurrent.ThreadFactory; |
||||
|
|
||||
|
@Configuration |
||||
|
public class DisruptorConfig { |
||||
|
|
||||
|
@Value("${redis.stream.operation.batch-size:100}") |
||||
|
private int batchSize; |
||||
|
|
||||
|
/** 创建带线程名的 ThreadFactory **/ |
||||
|
private ThreadFactory createDisruptorThreadFactory(String namePrefix) { |
||||
|
return r -> { |
||||
|
Thread t = new Thread(r); |
||||
|
t.setName(namePrefix + "-" + t.getId()); |
||||
|
t.setDaemon(true); |
||||
|
return t; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
public Disruptor<RedisStreamEvent> redisStreamDisruptor( |
||||
|
RedisStreamProducer producer) { |
||||
|
|
||||
|
int ringSize = 32768; |
||||
|
|
||||
|
Disruptor<RedisStreamEvent> disruptor = new Disruptor<>( |
||||
|
new RedisStreamEventFactory(), |
||||
|
ringSize, |
||||
|
createDisruptorThreadFactory("redis-stream-disruptor"), |
||||
|
ProducerType.MULTI, |
||||
|
new SleepingWaitStrategy() |
||||
|
); |
||||
|
|
||||
|
disruptor.handleEventsWith( |
||||
|
new RedisStreamEventHandler(producer, batchSize) |
||||
|
); |
||||
|
|
||||
|
disruptor.start(); |
||||
|
|
||||
|
return disruptor; |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
public RingBuffer<RedisStreamEvent> redisStreamRingBuffer( |
||||
|
Disruptor<RedisStreamEvent> disruptor) { |
||||
|
|
||||
|
return disruptor.getRingBuffer(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
package com.aeon.tcp.disruptor.redis.stream; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class RedisStreamEvent { |
||||
|
|
||||
|
private String payload; |
||||
|
private String deviceId; |
||||
|
private long ts; |
||||
|
|
||||
|
public void clear() { |
||||
|
payload = null; |
||||
|
deviceId = null; |
||||
|
ts = 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
package com.aeon.tcp.disruptor.redis.stream; |
||||
|
|
||||
|
import com.lmax.disruptor.EventFactory; |
||||
|
|
||||
|
public class RedisStreamEventFactory implements EventFactory<RedisStreamEvent> { |
||||
|
|
||||
|
@Override |
||||
|
public RedisStreamEvent newInstance() { |
||||
|
return new RedisStreamEvent(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
package com.aeon.tcp.disruptor.redis.stream; |
||||
|
|
||||
|
import com.aeon.tcp.disruptor.BaseBatchEventHandler; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
public class RedisStreamEventHandler extends BaseBatchEventHandler<RedisStreamEvent> { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(RedisStreamEventHandler.class); |
||||
|
|
||||
|
private final RedisStreamProducer producer; |
||||
|
|
||||
|
public RedisStreamEventHandler(RedisStreamProducer producer, int batchSize) { |
||||
|
super(batchSize); |
||||
|
this.producer = producer; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected RedisStreamEvent copyOf(RedisStreamEvent e) { |
||||
|
|
||||
|
RedisStreamEvent copy = new RedisStreamEvent(); |
||||
|
|
||||
|
copy.setDeviceId(e.getDeviceId()); |
||||
|
copy.setPayload(e.getPayload()); |
||||
|
copy.setTs(e.getTs()); |
||||
|
|
||||
|
return copy; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void flushToHandle(java.util.List<RedisStreamEvent> list) { |
||||
|
try { |
||||
|
producer.batchHandle(list); |
||||
|
} catch (Exception e) { |
||||
|
logger.error("Redis Stream batch write error", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
package com.aeon.tcp.disruptor.redis.stream; |
||||
|
|
||||
|
import com.aeon.tcp.common.Constants; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.data.redis.core.StringRedisTemplate; |
||||
|
import org.springframework.data.redis.core.RedisCallback; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Component |
||||
|
@RequiredArgsConstructor |
||||
|
public class RedisStreamProducer { |
||||
|
|
||||
|
private final StringRedisTemplate redisTemplate; |
||||
|
|
||||
|
private static final int PARTITIONS = 16; |
||||
|
|
||||
|
public void batchHandle(List<RedisStreamEvent> list) { |
||||
|
|
||||
|
redisTemplate.executePipelined((RedisCallback<Object>) connection -> { |
||||
|
|
||||
|
for (RedisStreamEvent event : list) { |
||||
|
String deviceId = event.getDeviceId(); |
||||
|
String payload = event.getPayload(); |
||||
|
long ts = event.getTs(); |
||||
|
|
||||
|
int partition = Math.abs(deviceId.hashCode()) % PARTITIONS; |
||||
|
String streamKey = Constants.STREAM_KEY_PREFIX + partition; |
||||
|
|
||||
|
Map<byte[], byte[]> map = new HashMap<>(); |
||||
|
|
||||
|
map.put("deviceId".getBytes(), deviceId.getBytes()); |
||||
|
map.put("payload".getBytes(), payload.getBytes()); |
||||
|
map.put("ts".getBytes(), String.valueOf(ts).getBytes()); |
||||
|
|
||||
|
connection.streamCommands() |
||||
|
.xAdd(streamKey.getBytes(), map); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
package com.aeon.tcp.disruptor.redis.stream; |
||||
|
|
||||
|
import com.aeon.tcp.f10.entity.RedisStreamEntity; |
||||
|
import com.lmax.disruptor.RingBuffer; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class RedisStreamService { |
||||
|
|
||||
|
private static final Logger logger = LoggerFactory.getLogger(RedisStreamService.class); |
||||
|
|
||||
|
private final RingBuffer<RedisStreamEvent> ringBuffer; |
||||
|
|
||||
|
public void write(RedisStreamEntity redisStreamEntity) { |
||||
|
boolean ok = ringBuffer.tryPublishEvent((event, sequence) -> { |
||||
|
event.setDeviceId(redisStreamEntity.getDeviceId()); |
||||
|
event.setPayload(redisStreamEntity.getPayload()); |
||||
|
event.setTs(redisStreamEntity.getTs()); |
||||
|
}); |
||||
|
|
||||
|
if (!ok) { |
||||
|
logger.error("[RedisStreamService] RingBuffer FULL, message dropped"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
package com.aeon.tcp.f10.entity; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class RedisStreamEntity { |
||||
|
|
||||
|
private String deviceId; |
||||
|
private String payload; |
||||
|
private long ts; |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue