取整数位数数字_取一个数字

在许多地区,小商店都习惯通过按到达顺序来为其分配连续编号,从而跟踪排队的顾客。 这些数字通常印在纸条上,并由一个物理机械分配器分配。 如果多个客户同时到达,则礼节和普通礼节很容易破坏关系。

在软件系统中经常会出现类似的问题。 人们通常需要给事物分配数字,并确保这些数字是唯一的并遵循某种模式。 对于此问题有一些通用的解决方案,但是分布式系统使事情变得复杂。 您不太可能使用ECM系统为角落面包店的客户分配编号。 (但是,如果您对此感兴趣,我可以联系您愿意的销售人员!)但是,您可能需要分配案例编号,客户ID,零件编号或更简单的名称。 数据库供应商仅针对此类问题实现序列号列类型。 但是,P8不提供对数据库序列号类型的直接访问,因此您必须使用其他机制。

在本文中,我们将研究如何在P8环境中解决此问题。 让我们总结一下要求:

  1. 我们需要绝对保证唯一的编号分配。 两次分配相同的号码是完全不可接受的。
  2. 我们希望数字遵循某种模式。 我们不希望数字分配之间存在差距。 模式可能很多,但出于我们的目的,我们将仅使用简单的增量。 我们得到的下一个数字将比前一个数字大一个。
  3. 我们希望所有这些在具有多个线程,多个处理器,多个服务器,多个层和多个用户的P8环境中可靠地工作并具有良好的性能。
  4. 在此过程中,我们需要大约12块看起来像奶油奶酪糖霜的美味红色天鹅绒蛋糕!

在描述我们的首选实现之前,我们将首先介绍一些无效或效果不佳的技术。 即使您永远不需要实现此特定用例,本文中说明的大多数要点也可以应用于P8编程的许多领域。

Java或.NET同步

如果您是一般企业开发或分布式开发的新手,您首先想到的可能是使用单例对象,该对象具有对更新计数器的部分的某种同步访问。 在Java中,这将是synchronized方法或代码块。 在C#中,它将是标记为sync的方法或受lock()保护的代码块。 具有同步访问权限的代码区域有时称为关键部分。 清单1显示了实现此目标的许多方法之一。

清单1.同步代码块
/**
 * *** DON'T DO IT THIS WAY ***
 */
public class Dispenser
{
    /** static access only, so private constructor */
    private Dispenser() {}
    private static int counter = 0;
    public static final synchronized int getNextValue()
    {
        return ++counter;
    }
}

对于某些问题,使用同步代码是可以的,但是对于我们的用例来说,缺点非常明显。 因为计数器的值仅存在于正在运行的程序的内存中,所以如果重新启动程序,则这些值将重新开始。 您可以更改Dispenser类以将更新的计数器值保存到文件中,但这将导致新的问题,因为同步没有跨流程边界进行协调。 即使不同的独立运行的应用程序(或同一应用程序的副本)正在使用Dispenser类,也可能会对该文件进行交错的读取和写入。 更糟糕的是,他们可能正在不同的计算机上访问具有相同名称的文件。 这两种情况都会导致违反我们用例要求的条件。

这是由于读写交错导致的故障示例。 假定记录在文件中的当前值为7,然后发生以下情况:

  • A从文件中读取值7。
  • B从文件中读取值7。
  • A将值8写入文件。
  • C从文件中读取值8。
  • C将值9写入文件。
  • B将值8写入文件。

在这一点上,A和B拥有相同的数字,他们认为这是排他性的。 文件的下一个读取器将最终获得与C相同的值。 您可能对使用文件锁定或其他特定于操作系统的技巧来同步分布式环境中对文件的访问有一些想法。 但是,这种解决方案的困难已在整个计算机科学中广为人知,并且正确地解决它是非常棘手的,因此我们不想过多地考虑这种情况。 如果您需要进一步说服力,请使用您喜欢的搜索引擎来研究“ NFS锁定问题”。

P8分配器对象

这些问题的一种流行解决方案是使用数据库来保存分配器。 企业级关系数据库具有固有的分布式资源,具有可靠的锁定语义。 在P8体系结构中,应用程序无法直接访问后备数据库。 换句话说,必须为此类应用程序分别安排数据库访问,无论是通过J2EE数据源,直接JDBC连接还是其他方式。 在某些情况下,这可能会很好地工作,但通常仅访问分配器数据会很麻烦。

我们知道的一件事是,所有基于P8的应用程序都可以访问P8 ObjectStore和对象,但需要执行P8强制的访问检查。 因此,我们可以将分配器建模为P8对象。 具体来说,我们可以创建一个子类的实例CustomObject称为WjcDispenser一个名为整数自定义属性WjcCounter 。 (为名称提供了任意前缀“ Wjc”,以避免与其他类和属性名称冲突。)图1显示了此简单子类的UML图。

图1. WjcDispenser UML图
UML图显示WjcDispenser作为CustomObject的子类

我们假设可以方便地为需要使用该对象的所有应用程序的所有用户安排对该对象的安全访问。 现在,我们仅假设任何人都可以连接到ObjectStore并更新分配器对象。 有关解决这种安全情况的有趣方法,请参见使用J2EE servlet侧栏。

此外,我们将介绍分配器对象的初始创建。 一种好的技术是让随附的Java和.NET类检测是否缺少分配器对象,并根据需要或在该类的静态初始化器中创建它。 定位分配器对象的两种流行技术是使用预定义的ID值或将对象存储在ObjectStore中的预定义路径中。 也可以使用查询来查找WjcDispenser类的所有实例。 对于以下示例,我们假设为应用程序配置了ObjectStore标识和分配器对象的特定位置。

FileNet Content Engine(CE)协作锁

使用基于P8的分配器对象时,获取序列号的概念相同:读取旧值,更新并存储新值,然后将新值返回给调用方。 显然,物流的具体实施发生了变化。 Java或.NET同步的所有缺点继续存在。

CE服务器和API实现了称为协作锁定的功能。 最初实现此功能是为了提供与RFC-2518(WebDAV)兼容的协作锁定语义。 FolderDocumentCustomObject API类具有用于锁定和解锁这些对象的方法。 因为这是服务器中实现的内置P8功能,所以您可以开发类似于清单2所示的实现。此示例显示了一种内部方法,并假定其他一些代码已标识了分配器对象。

清单2. P8协作锁定
private static final String COUNTER_PROPERTY_NAME = "WjcCounter";
/**
 * *** DON'T DO IT THIS WAY ***
 */
private static int getNextValue(CustomObject dispenser)
{
    final Properties dispenserProperties = dispenser.getProperties();
    // Object might be locked by someone else, so try a few times
    for (int attemptNumber=0; attemptNumber<10; ++attemptNumber)
    {
        dispenser.lock(15, null);  // LOCK the object for 15 seconds
        try
        {
            // Because we use a refreshing save, the counter property
            // value will be returned.
            dispenser.save(RefreshMode.REFRESH);  // R/T
            break;
        }
        catch (EngineRuntimeException ere)
        {
            ExceptionCode ec = ere.getExceptionCode();
            if (ec != ExceptionCode.E_OBJECT_LOCKED)
            {
                // If we get an exception for any reason other than
                // the object already being locked, rethrow it.
                throw ere;
            }
            // already locked; try again after a little sleep
            try
            {
                Thread.sleep(100); // milliseconds
            }
            catch (InterruptedException e) { /* don't worry about this rarity */ }
            continue;  
        }
    }
    int oldValue = dispenserProperties.getInteger32Value(COUNTER_PROPERTY_NAME);
    int newValue = oldValue + 1;
    dispenserProperties.putValue(COUNTER_PROPERTY_NAME, newValue);
    dispenser.unlock();  // UNLOCK the object
    dispenser.save(RefreshMode.NO_REFRESH);  // R/T
    return newValue;
}

假设分配器对象被无实例化地实例化(即,通过Factory.CustomObject.getInstance()方法),则此技术需要花费一次往返CE服务器的时间来应用锁并获取当前属性值。 如果对象已经被锁定,我们将无法获得当前值,因此我们要迭代几次以锁定分配器。 存储新的计数器值需要花费另一个往返CE服务器的费用。 为此,这是合理的性能成本,并且锁定/解锁功能的整体使用也是合理的。

在此用例中使用P8协同锁定的主要问题在于,它仅仅是协同锁定 。 CE服务器不会仅由于存在锁而禁止任何更改。 乐观地,您可能会假设所有应用程序都将遵循协作锁定顺序。 但是实际上,您仍然必须允许应用程序错误绕过锁定的可能性。 很难想象有人编写了使用分配器对象但忽略了锁定的独立代码。

触摸加事件处理程序

附带说明一下,但是如果您看一下上一节中有关协作锁定的讨论,您会发现至少要经过两次往返服务器才能可靠地获得序列号。 可以单程往返吗? 为使该想法可行,我们至少需要在CE服务器上完成部分计算。 CE机制是事件处理程序。 这是一个思想实验:

  • 无须获取实例化分配器对象(无往返)。
  • 我们需要对分配器对象进行某种更改,以便触发事件。 CE具有允许您定义自定义事件的功能。 实际上,该类称为CustomEvent 。 因此,作为所有这些一次性设置的一部分,请定义一个新的自定义事件,并将其持久保存在保存分配器对象的ObjectStore中。
  • 当有人调用可订阅对象上的raiseEvent()方法时,将触发自定义事件,而不是引发其他事件的副作用。 在服务器上,订阅和事件处理与系统定义的事件相同。 调用分配器对象上的raiseEvent() ,然后使用刷新调用save() (以获取WjcCounter的当前值)。
  • save()将触发CustomEvent
  • 作为一次性设置的一部分,提供一个事件处理程序,该事件处理程序已订阅WjcDispenser实例或类上的特定类型的CustomEvent 。 事件处理程序将计算并保存WjcCounter属性的新值。 由于无法在同步事件处理程序中更新属性,因此我们将使事件处理程序变为异步(在预订中指定了同步或异步)。
  • 客户端应用程序知道事件处理程序将如何更新WjcCounter ,因此它将执行相同的计算以预测WjcCounter的新值。

在这个思想实验中,您要考虑到,无论有多少个独立的客户端应用程序请求对分配器对象的一次更新。 CE服务器不会“优化”中间(冗余)更新。 您还记得异步事件处理程序一定会被执行。 实际上,您甚至可能还记得在某个地方听到异步事件处理程序是通过队列处理的(这是对的)。 所有这些似乎可以WjcCounter每次客户端到CE服务器的WjcCounter更新,可靠和可预测。

您可能已经从以上描述的语气中猜测,这里潜伏着一些问题。 实际上,有两个问题。 首先,在ObjectStore中的分配器对象的更新与异步事件处理程序的执行之间存在一小段时间。 如果有多个独立客户端同时发生更新,则它们都将看到相同的WjcCounter刷新值并计算相同的更新值。 即使您可以克服这一点,并且以某种方式将特定的客户端save()活动与事件处理程序的特定触发方式绑定在一起,也存在第二个问题。 第二个问题是,尽管确实通过队列处理了异步事件,但是队列有多个读取器。 因此,不能保证异步事件处理程序将以与触发更新相同的顺序执行。

第一作家获胜

CE服务器具有内置功能,用于可靠地检测交错更新。 行政长官实施一项称为“ 第一作家获胜”的政策。 这意味着,如果两个请求正在更新同一对象,则第一个将成功,第二个将失败。 对于失败的更新,服务器将引发EngineRuntimeException ,其ExceptionCodeE_OBJECT_MODIFIED 。 附带的异常消息是“对象...自检索以来已被修改。” 那是什么意思呢?

ObjectStore中的每个可独立持久存储的对象都标记有更新序列号(USN)。 这不是一般意义上的属性,但是其值通过方法IndependentlyPersistableObject.getUpdateSequenceNumber()公开。 每当在对象库中更新对象时,CE服务器都会自动增加USN。 当您从服务器获取对象时,也会提取USN并将其带到客户端。 这些API将USN作为对象save()一部分发送回服务器。 如果发送的USN值与存储库中当前保留的USN值不匹配,则CE服务器会知道(由于其他对象)已对对象进行了更改,因此进行了更改。 这表示数据库世界称为乐观锁定的简化形式。

假定CE服务器检测到这些交错的更改,则采用这种方式而不是纯粹自愿的协作锁定功能是合乎逻辑的。 通过CE API,您可以使用我们在其他地方提到的称为无获取实例化的方法来绕过服务器检查,但是在这种情况下,您必须竭尽全力做到这一点。 当您在API中本地实例化对象而不从服务器获取对象时,这就是无法获取的实例化。 在这种情况下,USN值具有一个特殊值,该值指示CE服务器跳过USN检查。 有时称为无保护更新。 如果以后从服务器获取或刷新任何属性,则也会获取当前的USN。

但是,在我们的用例中,对于某人先进行无访存实例化,然后进行无保护的更新是没有意义的。 若要获取计数器属性的当前值,必须从服务器获取一种或另一种方法。 有人可能会通过无保护的更新来恶意破坏计数器的属性,但是同一恶意方可以在正常更新周期内执行相同的操作。 因此,那里没有新的危险。 由于用例的语义,某人通过编码错误执行此操作的几率很低。

为了利用USN检查和CE服务器的先写赢者策略,您将尝试更新分配器对象中的计数器并检测针对交错更改而报告的错误。 清单3显示了如何执行此操作的示例。

清单3.第一作家获胜
private static final String COUNTER_PROPERTY_NAME = "WjcCounter";
/** 
 * This property filter is used to minimize data returned in fetches and refreshes.
 */
private static final PropertyFilter PF_COUNTER = new PropertyFilter();
static
{
    PF_COUNTER.addIncludeProperty(1, null, null, COUNTER_PROPERTY_NAME, null);
}

/**
 * Get the next value efficiently by exploiting First Writer Wins
 */
public int getNextValue(boolean feelingUnlucky)
{
    final Properties dispenserProperties = dispenser.getProperties();
    // Object might be updated by someone else, so try a few times
    for (int attemptNumber=0; attemptNumber<10; ++attemptNumber)
    {
        // If cached data invalid, fetch the current value
        // from the server.  This also covers the fetchless
        // instantiation case.
        if (feelingUnlucky
        ||  dispenser.getUpdateSequenceNumber() == null 
        ||  !dispenserProperties.isPropertyPresent(COUNTER_PROPERTY_NAME))
        {
            // fetchProperties will fail if the USN doesn't match, so null it out
            dispenser.setUpdateSequenceNumber(null);
            dispenser.refresh(PF_COUNTER);  // R/T
        }
        int oldValue = dispenserProperties.getInteger32Value(COUNTER_PROPERTY_NAME);
        int newValue = oldValue + 1;
        dispenserProperties.putValue(COUNTER_PROPERTY_NAME, newValue);
        try
        {
            // Because we use a refreshing save, the counter property's
            // new value will be returned from the server.
            dispenser.save(RefreshMode.REFRESH, PF_COUNTER);  // R/T
            return newValue;
        }
        catch (EngineRuntimeException ere)
        {
            ExceptionCode ec = ere.getExceptionCode();
            if (ec != ExceptionCode.E_OBJECT_MODIFIED)
            {
                // If we get an exception for any reason other than
                // the object being concurrently modified, rethrow it.
                throw ere;
            }
            // Someone else modified it.  Invalidate our cached data and try again.
            dispenser.setUpdateSequenceNumber(null);
            dispenserProperties.removeFromCache(COUNTER_PROPERTY_NAME);
            continue;  
        }
    }
    // too many iterations without success
    throw new RuntimeException("Oops");
}

/**
 * Set by constructor or some other means.
 * Fetchless instantiation is OK.
 */
private final CustomObject dispenser;

乍一看,这似乎与我们之前的协作锁定代码进行了两次服务器往返。 对于第一次计数器更新是正确的。 我们必须从服务器获取当前的计数器值,但是在成功更新之后会记住计数器的状态。 如果在此期间没有其他人更新分配器对象,则我们以后的更新将只花费一次往返。 另一方面,如果一对应用程序轮流更新分配器,也许是由于某种负载平衡,那记住的状态适得其反。 在这种情况下,通常需要花费三个往返行程来进行更新(最初的失败更新尝试,当前计数器值的获取以及最终成功的更新)。 即使竞争的应用程序之间进行更新的时间很长,也会产生额外的费用(例如,A在小时上获得计数器值,而B在半小时上获得计数器值)。 是否支付额外的费用取决于您拥有多少个独立阅读应用程序,它们如何重叠等等。 布尔参数feelingUnlucky控制该方法是支付两个往返更新序列的费用,还是一次或三个往返更新序列的赌博。

其他注意事项

这是实现和部署时需要考虑的一些其他事项。

USN代替财产

由于存储库中的每个独立可持久对象都已经具有单调递增的更新序列号,为什么不使用它代替计数器值的自定义属性呢? 如果您愿意接受一些折衷,则可以执行此操作,但是除了避免定义counter属性本身之外,您实际上不会节省太多。

  • USN的正常进程将增加一。 如果您需要一些其他的序列号递增模式,那么您就不走运了。
  • 实际上,仅模糊地指定了USN的CE升级模式。 您可以使用的实际官方用途是将其与null进行比较,或者比较两个USN小于,大于或等于的值。 尽管将来仍会单调增加行为,但在将来的CE版本中可能会改变逐个行为。
  • 严格来说,USN的增加不受您的控制。 每当更新持久对象时,CE服务器都会递增USN,而不仅仅是有人请求新序列号时。
  • 您仍将必须进行相同数量的服务器往返,以更新分配器对象或获取USN值。 没有性能优势。

许多掌柜

无论用于实现代码的技术是什么,如果数量很大,重复更新单个对象都会在存储库中引起性能热点。 高,按数据库标准表示高。 因此,即使每天进行数千次更新也不大可能成为问题。 在非常高的数量下,您可能希望通过具有多个分配器对象(每个对象负责一定范围的值)来使事情更具可伸缩性。 换句话说,由不同分配器实例给出的值仍然是全局唯一的。

除了3.x Java API

CE 3.x Java API没有公开上面使用的更新序列号。 实际上,对于大多数操作而言,该API都会扭转局面,并使用“最后写入者获胜”的策略。 这不是不可能的,但是编写一个在3.x Java API中既具有性能又在功能上正确的分配器是相当棘手的。 如果您的业务限制允许这样做,那么最好的办法就是将应用程序的那部分(或全部)迁移到CE 4.x API。

最后的想法

本文介绍了用于实现序列号分配器的多种技术。 像其他所有问题一样,您要去解决的麻烦部分取决于业务需求以及实施时间表上的其他问题。 如果有人要求我以完全镀金,无障碍的方式实施此操作,则外观如下所示:

  • 使用类似于清单3中所示的第一作者赢代码。
  • 将该代码放入一个简单的J2EE servlet中。 通过将其部署在J2EE Web容器中,我们将自动获得J2EE基础结构的扩展,故障转移,隔离和其他好处。
  • 限制对一个或多个分配器对象的P8写访问,以便普通用户无法绕过Servlet来更新值。
  • 使用具有CE权限的RunAs角色配置Servlet,以更新分配器对象。 (请参阅使用J2EE servlet侧栏。)
  • 如有必要,请实施某种方案以验证请求者是否有权向分配器询问号码。 仅当担心数字被浪费时,这才是有趣的。
  • 从乐观的假设开始servlet,单次往返是从分配器获取号码的正常费用。 换句话说,从feelingUnlucky是假开始。 跟踪实践中发现错误的频率。 当错误的发生率超过某个百分比时,请切换到始终支付两次往返费用的悲观视图。 定期切换回乐观视图,以查看情况是否已更改。
  • 根据需要提供客户端实用程序代码,用于调用servlet以获取序列号。

不用说,我还要求在纸杯蛋糕上付款。 好吃


翻译自: https://www.ibm.com/developerworks/data/library/techarticle/dm-0910filenetp8api/index.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值