怎样在 Akka Persistence 中实现分页查询
在 Akka Persistence 中,数据都缓存在服务内存(状态),后端存储的都是一些持久化的事件日志,没法使用类似 SQL 一样的 DSL 来进行分页查询。利用 Akka Streams 和 Actor 我们可以通过编码的方式来实现分页查询的效果,而且这个分页查询还是分步式并行的……
EventSourcedBehavior
Akka Persistence的EventSourcedBehavior里实现了CQRS模型,通过commandHandler与eventHandler解耦了命令处理与事件处理。commandHandler处理传入的命令并返回一个事件,并可选择将这个事件持久化;若事件需要持久化,则事件将被传给eventHandler处理,eventHandler处理完事件后将返回一个“新的”状态(也可以不更新,直接返回原状态)。
def apply[Command, Event, State](
persistenceId: PersistenceId,
emptyState: State,
commandHandler: (State, Command) => Effect[Event, State],
eventHandler: (State, Event) => State): EventSourcedBehavior[Command, Event, State]
建模
以我们习惯的数据库表建模来说,我们会有以下一张表:
create table t_config
(
data_id varchar(64),
namespace varchar(64) not null,
config_type varchar(32) not null,
content text not null,
constraint t_config_pk primary key (namespace, data_id)
);
create index t_config_idx_data_id on t_config (data_id);
ConfigManager actor 可以看作 t_config 表,它的 entityId 就是 namespace, State 里保存了所有记录的主键值(ConfigManagerState),这就相当于 t_config 表的 t_config_idx_data_id 索引。
而 ConfigEntity actor 可看作 t_config 表里存储的记录,每个 actor 实例就是一行记录。它的 entityId 由 namespace + data_id 组成,这就相当于 t_config 表的 t_config_pk 复合主键。 这里我们定义两个 EventSourcedBehavior:
ConfigManager:拥有所有配置ID列表,并作为 State 保存在 EventSourcedBehavior
ConfigEntity: 拥有每个配置数据,并作为 State 保存在 EventSourcedBehavior
实现
这里先贴出 ConfigManager 和 ConfigEntity 的部分代码,接下来再详解怎样实现分页查询。
ConfigManager
object ConfigManager {
sealed trait Command extends CborSerializable
sealed trait Event extends CborSerializable
sealed trait Response extends CborSerializable
final case class Query(dataId: Option[String], configType: Option[String], page: Int, size: Int) extends Command
final case class ReplyCommand(in: AnyRef, replyTo: ActorRef[Response]) extends Command
private final case class InternalResponse(replyTo: ActorRef[Response], response: Response) extends Command
case class ConfigResponse(status: Int, message: String = "", data: Option[AnyRef] = None) extends Response
final case class ConfigManagerState(dataIds: Vector[String] = Vector()) extends CborSerializable
val TypeKey: EntityTypeKey[Command] = EntityTypeKey("ConfigManager")
}
import ConfigManager._
class ConfigManager private (namespace: String, context: ActorContext[Command]) {
private implicit val system =www.shentuylzc.cn context.system
private implicit val timeout: Timeout = 5.seconds
import context.executionContext
private val configEntity = ConfigEntity.init(context.system)
def eventSourcedBehavior(www.tengyao3zc.cn): EventSourcedBehavior[Command, Event, ConfigManagerState] =
EventSourcedBehavior(
PersistenceId.of(TypeKey.name, namespace),
ConfigManagerState(), {
case (state, ReplyCommand(in, replyTo)) =>
replyCommandHandler(state, replyTo, in)
case (_, InternalResponse(replyTo,www.xinyueylzc.cn response)) =>
Effect.reply(replyTo)(response)
eventHandler)
private def processPageQuery(
state: ConfigManagerState,
replyTo: ActorRef[Response],
in: Query): Effect[Event, ConfigManagerState] = {
val offset = if (in.page > 0) (in.page - 1) * in.size else 0
val responseF = if (offset < state.dataIds.size) {
Source(state.dataIds)
.filter(dataId => in.dataId.forall(www.huizhonggjpt.cn=> v.contains(dataId)))
.mapAsync(20) { dataId =>
configEntity.ask[Option[ConfigState]](replyTo =>
ShardingEnvelope(dataId, ConfigEntity.Query(in.configType, replyTo)))
.collect { case Some(value) => value }
.drop(offset)
.take(in.size)
.runWith(Sink.seq)
.map(items => ConfigResponse(IntStatus.OK, data www.lafei6d.cn = Some(items)))
} else {
Future.successful(ConfigResponse(IntStatus.NOT_FOUND))
context.pipeToSelf(responseF) {
case Success(value) => InternalResponse(replyTo, value)
case Failure(e) => InternalResponse(replyTo, ConfigResponse(IntStatus.INTERNAL_ERROR, e.getLocalizedMessage))
ConfigEntity
object ConfigEntity {
case class ConfigState(namespace: String, dataId: String, configType: String, content: String)
sealed trait Command extends CborSerializable
sealed trait Event extends CborSerializable
final case class Query(configType: Option[String], replyTo: ActorRef[Option[ConfigState]]) extends Command
final case class ConfigEntityState(config: Option[ConfigState] = None) extends CborSerializable
val TypeKey: EntityTypeKey[Command] = EntityTypeKey("ConfigEntity"
import ConfigEntity._
class ConfigEntity private (namespace: String, dataId: String, context: ActorContext[Command]) {
def eventSourcedBehavior(): EventSourcedBehavior[Command, Event, ConfigEntityState] =
EventSourcedBehavior(PersistenceId.of(TypeKey.name, dataId), ConfigEntityState(), commandHandler, eventHandler)
def commandHandler(state: ConfigEntityState, command: Command): Effect[Event, ConfigEntityState] = command match {
case Query(configType, replyTo) www.jiuyueguojizc.cn=>
state.config match {
case None =>
Effect.reply(replyTo)(None)
case Some(config) =>
val resp = if (configType.forall(v => config.configType.contains(v))) Some(config) else None
Effect.reply(replyTo)(resp)
ConfigManager#processPageQuery 函数实现了大部分的分页查询逻辑(有部分逻辑需要由 ConfigEntity 处理)。
val offset = if (in.page > 0) (in.page - 1) * in.size else 0
val responseF = if (offset <www.baihuaylezc.cn state.dataIds.size) {
// process paging
} else {
Future.successful(ConfigResponse(IntStatus.OK, data = Some(Nil)))
这里首先获取实际的分页数据偏移量 offset ,再于 ConfigManager 状态里保存的 dataIds 的大小进行判断,若 offset < state.dataIds.size 则我们进行分页逻辑,否则直接返回一个空列表给前端。
Source(state.dataIds)
.filter(dataId => in.dataId.forall(v => v.contains(dataId)))
.mapAsync(20) { dataId =>
configEntity.ask[Option[ConfigState]](replyTo =>
ShardingEnvelope(s"$namespace@$dataId", ConfigEntity.Query(in.configType, replyTo)))
}
.collect { case Some(value) www.jujinyule.com=> value }
.drop(offset)
.take(in.size)
.runWith(Sink.seq)
.map(items => ConfigResponse(IntStatus.OK, www.letianhuanchao.cn data = Some(items)))
这个 Akka Streams 流即是分页处理的主要实现,若是SQL的话,它类似:
select * from t_config where data_id like '%"in.dataId"%' offset "offset" limit "in.size"
.mapAsync 在流执行流程中起了20个并发的异步操作,将委托每个匹配的 ConfigEntity (由s"$namespace@$dataId"生成entityId)执行 config_type 字段的查询。这样,完整的SQL语句类似:
select * from t_config where data_id like '%"in.dataId"%' and change_type = "in.changeType" offset "offset" limit "in.size"
ConfigEntity 对 change_type 部分的查询逻辑实现如下:
case Query(configType, replyTo) =>
state.config match {
case None =>
Effect.reply(replyTo)(None)
case Some(config) www.huizhongdl.cn=>
val resp = if (configType.forall(v => config.configType.contains(v))) Some(config) else None
Effect.reply(replyTo)(resp)
}
若in.configType为空,既不需要判断 change_type 这个字段,直接返回 Some(config) 即可,而这时的SQL语句类似:
select * from t_config where data_id like '%"in.dataId"%' and true offset "offset" limit "in.size"
Tip这里有个小技巧,对于 Option[T] 字段的判断,直接使用了 .forall 方法,它等价于:
option match {
case Some(x) => p(x)
case None => true
怎样在 Akka Persistence 中实现分页查询的更多相关文章
- mongo中的分页查询
/** * @param $uid * @param $app_id * @param $start_time * @param $end_time * @param $start_page * @p ...
- ssh框架中的分页查询
ssh中的分页查询是比较常用的,接下来我用代码来介绍如何实现一个分页查询 首先建立一个Model用来储存查询分页的信息 package com.haiziwang.qrlogin.utils; imp ...
- java使用插件pagehelper在mybatis中实现分页查询
摘要: com.github.pagehelper.PageHelper是一款好用的开源免费的Mybatis第三方物理分页插件 PageHelper是国内牛人的一个开源项目,有兴趣的可以去看源码,都有 ...
- JDBC在Java Web中的应用——分页查询
分页查询 通过JDBC实现分页查询的方法有很多种,而且不同的数据库机制也提供了不同的分页方式,在这里介绍两种非常典型的分页方法. 通过ResultSet的光标实现分页 通过ResultSet的光标实现 ...
- Akka系列(八):Akka persistence设计理念之CQRS
前言........ 这一篇文章主要是讲解Akka persistence的核心设计理念,也是CQRS(Command Query Responsibility Segregation)架构设计的典型 ...
- mysql分页查询详解
我们做的后端项目一般都会有admin管理端,当管理端将要展示数据的时候,就需要用到分页.所以分页的考查在面试中也相当多.在mysql中进行分页查询时,一般会使用limit查询,而且通常查询中都会使用o ...
- 用MySQL实现分页查询
MySQL中实现分页查询语句: //定义分页需要的变量 int pageNow=2;//当前页 int pageSize=3;//指定每页显示3条记录 int pageCount=1;//该值是计算出 ...
- [.NET] SQL数据分页查询
[.NET] SQL数据分页查询 程序下载 范例下载:点此下载 原始码下载:点此下载 NuGet封装:点此下载 数据查询 开发系统时,使用C#执行SQL查询指令,就可以从SQL数据库里查询所需数据. ...
- Oracle分页查询语句的写法(转)
Oracle分页查询语句的写法(转) 分页查询是我们在使用数据库系统时经常要使用到的,下文对Oracle数据库系统中的分页查询语句作了详细的介绍,供您参考. Oracle分页查询语句使我们最常用的 ...
随机推荐
- Jrebel 远程热部署[Idea]
服务器端 客户端-idea remote module 在服务器端,Tomcat的webapps下新建项目文件夹,如: demo/WEB-INF/classes/[jrebel.xml]
- Python 中 对logging 模块进行封装,记录bug日志、日志等级
是程序产生的日志 程序员自定义设置的 收集器和渠道级别那个高就以那个级别输出 日志和报告的作用: 报告的重点在于执行结果(执行成功失败,多少用例覆盖),返回结果 日志的重点在执行过程当中,异常点,哪里 ...
- jQuery原理系列-css选择器实现
jQuery最强大的功能在于它可以通过css选择器查找元素,它的源码中有一半是sizzle css选择器引擎的代码,在html5规范出来之后,增加了document.querySelector和doc ...
- 4. Retrieving a mapper(检索映射器)
Retrieving a mapper(检索映射器) 4.1. The Mappers factory(映射工厂) 可以通过 org.mapstruct.factory.Mappers 类检索映射器实 ...
- 实验吧-杂项-MD5之守株待兔(时间戳&python时间戳函数time.time())
其实也有点蒙圈,因为从没做过和时间戳有关的题. 打开网站,将系统密钥解密得到一串值,而自己的密钥解密是空的,既然说是要和系统匹配,就把解密得到的值以get方式送出去. 但是发现还是在自己的密钥也发生了 ...
- JAVA作用域和排序算法介绍
一.作用域 1.作用域的概念 所谓的作用域是指引用可以作用到的范围. 一个引用的作用域是从引用定义位置到包裹它的最近的大括号的结束位置.只有在作用域范围内才可以访问到引用,超出作用域无法访问引用. 定 ...
- eclipse的重要快捷键
1.快速修正:ctrl + 1 2.单词补全:alt + / 3.查看轮廓:ctrl + o 4.打开eclipse中工作区的资源: ctrl + shift + r 它可以打开当前eclipse的工 ...
- Eclipse反编译插件jad的安装
Eclipse反编译插件jad的安装 JadClipse是Jad的Eclipse插件,是一款非常实用而且方便的Java反编译插件.时间久了,可能忘记在哪里进行配置了,因此记录下以备后续使用(若需要). ...
- bzoj 4300绝世好题
呵呵呵呵 #include<bits/stdc++.h> #define INF 0x7fffffff #define LL long long #define N 100005 usin ...
- Spring耗时拦截器(url,restful)
import java.io.IOException; import java.util.Date; import javax.servlet.Filter; import javax.servlet ...