抽奖功能的设计与实现

需求点

近期开发抽奖功能,涉及到的问题以及相应的需求点列举如下:

  1. 能够根据奖品初始设置的抽中概率进行分配
  2. 防止前端并发操作产生的超中情况
  3. 避免频繁抽中单一奖品,导致抽中奖项种类不均匀
  4. 抽中的概率能够根据实时库存进行相应变化,便于灵活的增加减少奖品数量
  5. 奖品分为几等奖,每等奖中奖品数量不为一,并且同一等奖的抽中概率相同
  6. 保证每次抽奖均抽中

方案设计

将抽中奖品的逻辑实现放在后台,前端只负责相应的交互与结果展示,这样可以降低前端逻辑,更加可控

mock数据

首先模拟奖品数据,包括奖品id,奖品名称,奖品分类,奖品抽中概率,奖品库存

id 名称 分类 抽中概率(0-100) 库存
1 奖品1 一等奖 1 10
2 奖品2 二等奖 5 50
3 奖品3 三等奖 34 100
4 奖品4 三等奖 34 100
5 奖品5 三等奖 34 150
6 奖品6 四等奖 60 300
7 奖品7 四等奖 60 300
8 奖品8 四等奖 60 350
9 奖品9 四等奖 60 500

mock的数据满足以上列举的需求,即可以设置初始抽中概率值,库存可以灵活的去设置修改,每等奖中奖品数量不为一且相同等奖的抽中概率相同

实现设计

引入区间的概念,即每个奖品的区间距离为抽中概率乘以库存数量,并且单个奖品的区间左值为前面奖品区间距离之和,如上mock的数据为例:

id 名称 分类 抽中概率(0-100) 库存 区间
1 奖品1 一等奖 1 10 [0,10)
2 奖品2 二等奖 5 50 [10,260)
3 奖品3 三等奖 34 100 [260,3660)
4 奖品4 三等奖 34 100 [3660,7060)
5 奖品5 三等奖 34 150 [7060,12160)
6 奖品6 四等奖 60 300 [12160,30160)
7 奖品7 四等奖 60 300 [30160,48160)
8 奖品8 四等奖 60 350 [48160,69160)
9 奖品9 四等奖 60 500 [69160,99160)

附:若存在未中奖的情况,则需要添加未中奖区间,该区间可以为[区间最右值,区间最右值 乘n-区间最右值),n为抽几次中一次奖的数量

继续阅读全文 “抽奖功能的设计与实现”

Java NIO 核心组件学习笔记

背景知识

同步、异步、阻塞、非阻塞

首先,这几个概念非常容易搞混淆,但NIO中又有涉及,所以总结一下[1]。

  • 同步:API调用返回时调用者就知道操作的结果如何了(实际读取/写入了多少字节)。
  • 异步:相对于同步,API调用返回时调用者不知道操作的结果,后面才会回调通知结果。
  • 阻塞:当无数据可读,或者不能写入所有数据时,挂起当前线程等待。
  • 非阻塞:读取时,可以读多少数据就读多少然后返回,写入时,可以写入多少数据就写入多少然后返回。

对于I/O操作,根据Oracle官网的文档,同步异步的划分标准是“调用者是否需要等待I/O操作完成”,这个“等待I/O操作完成”的意思不是指一定要读取到数据或者说写入所有数据,而是指真正进行I/O操作时,比如数据在TCP/IP协议栈缓冲区和JVM缓冲区之间传输的这段时间,调用者是否要等待。

所以,我们常用的 read() 和 write() 方法都是同步I/O,同步I/O又分为阻塞和非阻塞两种模式,如果是非阻塞模式,检测到无数据可读时,直接就返回了,并没有真正执行I/O操作。

总结就是,Java中实际上只有 同步阻塞I/O、同步非阻塞I/O 与 异步I/O 三种机制,我们下文所说的是前两种,JDK 1.7才开始引入异步 I/O,那称之为NIO.2。

传统IO

我们知道,一个新技术的出现总是伴随着改进和提升,Java NIO的出现亦如此。

传统 I/O 是阻塞式I/O,主要问题是系统资源的浪费。比如我们为了读取一个TCP连接的数据,调用 InputStream 的 read() 方法,这会使当前线程被挂起,直到有数据到达才被唤醒,那该线程在数据到达这段时间内,占用着内存资源(存储线程栈)却无所作为,也就是俗话说的占着茅坑不拉屎,为了读取其他连接的数据,我们不得不启动另外的线程。在并发连接数量不多的时候,这可能没什么问题,然而当连接数量达到一定规模,内存资源会被大量线程消耗殆尽。另一方面,线程切换需要更改处理器的状态,比如程序计数器、寄存器的值,因此非常频繁的在大量线程之间切换,同样是一种资源浪费。

随着技术的发展,现代操作系统提供了新的I/O机制,可以避免这种资源浪费。基于此,诞生了Java NIO,NIO的代表性特征就是非阻塞I/O。紧接着我们发现,简单的使用非阻塞I/O并不能解决问题,因为在非阻塞模式下,read()方法在没有读取到数据时就会立即返回,不知道数据何时到达的我们,只能不停的调用read()方法进行重试,这显然太浪费CPU资源了,从下文可以知道,Selector组件正是为解决此问题而生。

Java NIO 核心组件

1.Channel

概念

Java NIO中的所有I/O操作都基于Channel对象,就像流操作都要基于Stream对象一样,因此很有必要先了解Channel是什么。以下内容摘自JDK 1.8的文档

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.

从上述内容可知,一个Channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。

通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具体的有FileChannel、SocketChannel等。

通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。

当然,也有区别,主要体现在如下两点:

  • 一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream)
  • 通道有非阻塞I/O模式

实现

Java NIO中最常用的通道实现是如下几个,可以看出跟传统的 I/O 操作类是一一对应的。

  • FileChannel:读写文件
  • DatagramChannel: UDP协议网络通信
  • SocketChannel:TCP协议网络通信
  • ServerSocketChannel:监听TCP连接

继续阅读全文 “Java NIO 核心组件学习笔记”