包含的表主要有
列車(列車編號,車種,始發站,始發站,終到站,發時,到時,里程)
車站內(車站id,車站名)
經過(列車編號,車站名,站次,里程,到時,發時)
車票(車票編號,車次,發站,容到站,發站次,到站次,發時,到時,座位類型,座位號,車票日期,票價)
主要是這四個表,查詢插入操作有些復雜不過都能完成
詳細看列車票務系統資料庫課程設計說明書,文庫里有
② 那個我也想要一個資料庫設計事例,就是火車售票管理SQL
目錄
概述: 5
1. 需求分析 5
1.1 用戶需求: 5
1.2 業務流程分析: 6
1.3 信息需求分析 6
1.4 功能需求分析: 7
2. (資料庫)概念(模型)設計 8
2.1構思ERD的四條原則及根據這些原則相應得出的實體、聯系及其屬性: 9
2.2、系統具體E-R圖: 9
3. (資料庫)邏輯(模型)設計 10
3.1 一般邏輯模型設計: 10
3.2 具體邏輯模型設計: 11
4. 資料庫物理設計與資料庫保護設計 12
5. 處理功能設計 12
5.1 主控模塊設計: 12
5.2 子模塊設計: 13
6. 資料庫應用系統的實現 14
6.1 資料庫及其表結構的建立: 14
6.2 創建表的相關視圖: 16
6.3 各表關系圖, 16
6.4 數據輸入:利用系統錄入數據,如下圖為各表內容: 17
6.5 模塊實現: 18
7. 資料庫應用系統運行 26
7.1 寫出系統操作使用的簡要說明。 26
7.2 按使用說明運行系統並列印出運行結果。 26
7.3 系統評價: 27
報告內容
概述:
隨著國民經濟快速發展, 人們出行、交通越來越頻繁, 對服務的快捷、便利性要求也越來越高。從而對客運行業的建設與管理提出了更高的要求。為適應和推動客運行業的發展, 各種交通公司和部門開始廣泛採用使用日趨成熟的計算機技術和資料庫技術來實現票務信息的現代化管理,具有手工管理所無法比擬的優點,如:檢索迅速,查找方便,可靠性高,存儲量大,保密性好,壽命長,成本底等。這些優點能夠極大地提高信息管理和業務管理的效率。
本學生火車訂票系統正是通過資料庫存儲信息實現高效率管理。該實驗設計首先進行需求分析,然後在需求文檔的指導下實現系統的功能,如操作員的信息管理功能及普通學生的火車信息查詢、訂票、退票等功能,最終實現的是學生購得一張自己滿意的票券,同時力求通過資料庫系統及計算機在其中的運用達到提高工作效率,節約人力資源的效果。
1. 需求分析
1.1 用戶需求:
(一)、問題描述:
學生火車票定票系統
(1)背景:一年兩次的火車票訂票管理
(2)主要實現以下功能:
1)學生基本信息的管理,尤其是所在地
2)學生購票的基本信息,尤其是價錢和車票目的地
3)購票以後的分發管理
4)退票的管理
5)信息的統計和查詢
6)操作員管理
(二)、目的及現狀:
1)、實驗目的:
資料庫設計就是要使學生採用本課程中學習的資料庫設計方法,運用其基本思路與主要圖表工具完成一個自己所了解的業務的資料庫應用系統信息需求分析與資料庫的概念設計、邏輯設計、物理設計以及處理功能設計,用自己熟悉的資料庫管理系統、程序設計語言及其相關開發工具實現該系統,並運行、評價、改進之;在此基礎上嚴格按本大綱所附報告提綱撰寫課程設計報告。通過本設計進一步弄懂資料庫系統及其相關的基本概念,理解資料庫系統的系統結構、主要特點,掌握資料庫設計的原理、方法及其基本過程,初步具備資料庫應用設計的能力,初步形成運用資料庫應用系統解決管理決策中的實際問題的基本素質。
2)、現狀和系統要求:
在傳統模式下利用人工進行火車訂票業務,存在著較多的缺點,如:效率底,保密性差,時間一長將產生大量的文件和數據,更不便於查找,更新,維護等。諸如這些情況,給各相關部門工作人員帶來了很大困難,嚴重影響了他們的工作效率。運用計算機技術和資料庫技術來實現票務信息的現代化管理,具有手工管理所無法比擬的優點,如:檢索迅速,查找方便,可靠性高,存儲量大,保密性好,壽命長,成本底等。這些優點能夠極大地提高信息管理和業務管理的效率。
在本系統中,系統用戶共有兩種, 並根據用戶許可權的不同而實現不同的功能,如操作員
擁有添加、修改、刪除某火車相關信息及修改自己的個人信息的功能。學生有對車票信息、、哪一車次哪天還剩餘多少張票和自己所訂票券的查詢功能,訂票功能及退票功能。系統利用計算機和資料庫的高效率大大減輕了學校票點工作人員的勞動強度, 提高了各部門的工作效率。
1.2 業務流程分析:
(一)、描述系統的業務流程:
本系統共有兩種用戶, 根據用戶許可權的不同而實現不同的功能。
操作員的許可權最大,他進入系統必須先登錄。操作員可以添加、修改、刪除某車票的相關信息,可以修改自己的個人信息;查詢、刪除學生的訂票情況,確認學生是否已付款取票等。
學生可以按目的地的車次對車票信息進行查詢,可以訂票,訂票時須錄入自己的信息及所選擇的車次,系統將檢查該車次票數是否已訂完或不足,若已訂完或不足則提示錯誤信息並返回到訂票界面,訂票成功後將生成訂票單。學生還可以對自己所訂的票券即訂票單進行查詢,以及查詢哪一車次哪天還剩餘多少張票。用戶付款和取票可在學校票點完成。
(二)、初步業務流程圖:
1.3 信息需求分析
1.3.1 資料收集
1.3.2 事項分析:
在本火車票訂票系統中,各資料的基本數據項列舉如下:
學生資料:學號,姓名,密碼,所在學院,專業,班級,電話,目的城市
車票基本信息:車次號,出發站,開車時間,到達站,到達時間,車票種類,余票數
車次詳細信息:ID號,車次號,途徑站,票價,
訂票單信息:訂單號,訂票人學號,訂單時間,付款取票與否
訂票具體信息:ID號,訂單號,所得車次號,目的城市,訂票數,總票價,取票時間
訂票點信息:票點號,票點主任的員工號,聯系電話,所在校區
操作員資料:員工號,票點號,密碼,姓名,性別,電話
1.4 功能需求分析:
(一)、完善業務流程圖:
(二)、功能層次圖:
本系統共有兩種用戶, 根據用戶許可權的不同而實現不同的功能,如操作員查詢、添加、修改、刪除某火車相關信息及查詢、修改自己的個人信息的功能。學生對車票信息、、哪一車次哪天還剩餘多少張票和訂票功能以及對自己所訂票券的查詢功能、退票功能。
2. (資料庫)概念(模型)設計
2.1構思ERD的四條原則及根據這些原則相應得出的實體、聯系及其屬性:
① 原則1 (確定實體):能獨立存在的事物,例如人、物、事、地、團體、機構、活動、事項等等,在其有多個由基本項描述的特性需要關注時,就應把它作為實體。
在本系統中,實體主要有學生、操作員、車票信息、車票詳細信息表、訂票單,訂票詳細信息表,訂票點。
②原則2 (確定聯系):兩個或多個實體間的關聯與結合,如主管,從屬,組成,佔有,作用,配合,協同等等,當需要予以關注時,應作為聯系。實體間的聯系可分為一對一、一對多、多對多等三類,在確定聯系時還要確定其類型。
在本系統中,學生、車票信息、訂票單和訂票詳細信息表之間存在「訂購」的聯系,一個車票信息可以被多個學生購買,而一個學生只可以購買多個車次所屬的一到兩張車票,它們之間的聯系是一對多的「購買」聯系,同時一次登錄無論訂多少車次只生成一張訂票單。訂票點和操作員之間存在「隸屬」的聯系,它們之間的聯系是一對多的「隸屬」聯系;車票信息和車票詳細信息表之間存在「包含」與被包含的關系;訂票單和訂票詳細信息表之間也存在「包含」與被包含的關系。
③原則3 (確定屬性):實體的屬性是實體的本質特徵。實體應有標識屬性(能把不同個體區分開來的屬性組),並指定其中一個作為主標識。聯系的屬性是聯系的結果或狀態。
從這條原則可得到實體和聯系的屬性如下:
學生(學號,姓名,密碼,所在學院,專業,班級,電話,目的城市)
車票基本信息(車次號,出發站,開車時間,到達站,到達時間,車票種類,余票數)
車次詳細信息(ID號,車次號,途徑站,票價)
訂票單信息(訂單號,訂票人學號,訂單時間,付款取票與否)
訂票具體信息(ID號,訂單號,所得車次號,目的城市,訂票數,總票價,取票時間)
訂票點(票點號,票點主任的員工號,所在校區,電話)
操作員(員工號,票點號,密碼,姓名,性別,電話)
④原則4(一事一地):信息分析中得到的基本項要在且僅在實體聯系圖中的一個地方作為屬性出現。
根據以上的分析,可以畫出本系統的原始ERD的基本結構。如
2.2、系統具體E-R圖:
3. (資料庫)邏輯(模型)設計
3.1 一般邏輯模型設計:
(一)、由ERD導出一般關系模型的四條原則:
原則1(實體轉換為關系模式):ERD中每個獨立的實體轉換為一個關系模式,實體的屬性組成關系的屬性,實體的主標識轉換成關系的主碼。
原則2(從實體及其主從聯系轉換為關系模式):ERD中一個從實體及其主從聯系轉換為一個關系,從實體的屬性及其主實體關系的屬性組成的屬性,其主實體關系的主碼,在主從關系聯系為一對多聯系時還要加上可把同一主實體個體所對應的從實體個體區分開來的,從實體的一組屬性,作為該關系的主碼。對子類實體可作類似一對一聯系的從實體的轉換。
原則3(一對多聯系在關系模式中的表示):ERD中的一個一對多聯系通過在其「多」實體關繫上增加「1」實體關系的主碼(作為外碼)和聯系本身的屬性來表示。
原則4(多對多聯系轉換為關系):ERD中的一個多對多聯系轉換為一個關系,其被聯系實體關系的主碼和該聯系本身的屬性一起組成的屬性,被聯系關系的主碼組成該關系的復合主碼。
(二)、資料庫初步構思的關系框架:
通過ERD轉換為一般關系模型四條原則分析,得到須在資料庫中進行存儲的一般關系模型如下(帶下劃線的為主碼,帶#的為外鍵):
學生(學號,姓名,密碼,所在學院,專業,班級,電話,目的城市)
車票基本信息(車次號,出發站,開車時間,到達站,到達時間,車票種類,余票數)
車次詳細信息(ID號,車次號#,途徑站,票價)
訂票單信息(訂單號,訂票人學號#,訂單時間,付款取票與否)
訂票具體信息(ID號,訂單號#,所得車次號#,目的城市,訂票數,總票價,取票時間)
訂票點(票點號,票點主任的員工號#,所在校區)
操作員(員工號,密碼,姓名,性別,電話)
3.2 具體邏輯模型設計:
(1)、Student表:用來保存學生信息:
項名 類型 長度 小數位 值域 主鍵 外鍵 空鍵
學號 字元 12 是 非空
姓名 字元 20 非空
密碼 字元 12 非空
學院 字元 40 非空
專業 字元 30 非空
班級 數據 4 0 >0 非空
電話 字元 12 非空
目的城市 字元 30 非空
(2)、Ticket表:用來保存車票信息表
項名 類型 長度 小數位 值域 主鍵 外鍵 空鍵
車次號 字元 20 是 非空
出發站 字元 30 非空
開車時間 日期 8 非空
到達站 字元 30 非空
到達時間 日期 8 非空
車票種類 字元 20 非空
余票數 數據 8 0 >0 非空
(3)、Citysite表:用來保存車票詳細信息表:
項名 類型 長度 小數位 值域 主鍵 外鍵 空鍵
ID號 整數,自動編號 8 是 非空
車次號 字元 20 是 非空
途徑城市 字元 30 非空
票價 decimal 非空
(4)、Ticketsite表:用來保存訂票點表:
項名 類型 長度 小數位 值域 主鍵 外鍵 空鍵
票點號 整數,自動編號 8 0 是 非空
票點主任員工號 整數,自動編號 8 0 是 非空
聯系電話 字元 12 非空
所在校區 字元 40 非空
(5)、Admin表:用來保存操作員表:
項名 類型 長度 小數位 值域 主鍵 外鍵 空鍵
員工號 整數,自動編號 8 0 是 非空
用戶名 字元 20 非空
密碼 字元 12 非空
姓名 字元 20 非空
性別 字元 4 非空
電話 字元 12 非空
(6)、Book表:用來保存訂單表:
項名 類型 長度 小數位 值域 主鍵 外鍵 空鍵
訂單號 整數,自動編號 8 0 是 非空
學號 字元 是 非空
訂單時間 字元 非空
付款取票與否 整數 2 0 非空
(7)、Ticket表:用來保存取票單表:
項名 類型 長度 小數位 值域 主鍵 外鍵 空鍵
ID號 整數,自動編號 8 0 是 非空
取票號 字元 8 0 是 非空
車次號 字元 是 非空
目的城市 字元 非空
訂票數 數據 8 0 >0 非空
總票價 Decimal 非空
取票時間 日期 8 非空
4. 資料庫物理設計與資料庫保護設計
根據表的結構關系,本系統採用SQL Server 2000資料庫。SQL Server 2000是微軟公司關系型資料庫產品,它是在由MS SQL Server 7.0建立的堅固基礎之上產生的。客戶的需求極大的推動了該產的革新,SQL Server2000在易用性、可縮放性和可靠性,以及數據倉庫等諸多方面有了很大的增強。這使得SQL Server 2000在很多資料庫產品發展最快的應用領域(如電子商務、移動計算、分支自動化、商業級應用和數據交換中心等)中成為領先者
在資料庫中創建表的同時須創建相關的索引。索引就是加快檢索表中數據的方法。資料庫的索引類似於書籍的索引。在書籍中,索引允許用戶不必翻閱完整個書就能迅速地找到所需要的信息。在資料庫中,索引也允許資料庫程序迅速地找到表中的數據,而不必掃描整個資料庫。本資料庫表較為簡單,且每個表中定義主鍵約束或者唯一性鍵約束,已經間接創建了索引,故無需再創建索引。
5. 處理功能設計
5.1 主控模塊設計:
(1)、登錄系統模塊:
在登錄窗體界面中,你可以輸入代碼和密碼,選擇你的身份(操作員或系統管理員),確認後就可進入主界面窗體。如果你是學生,且還沒注冊,可以在此窗體界面上點擊「注冊」,在注冊界面輸入所要求的項,確定後重新返回登錄界面,用你剛注冊的賬號登錄進入主界面。
在主界面中包含上述模塊圖的幾部分,根據身份,即是用戶或者管理員,對應許可權不同,分為不同的主界面,即用戶主界面和管理員主界面。
(2)、系統設置模塊圖:
用戶:在此模塊用戶可以查詢、修改自身注冊信息以及退出登錄,退出系統後將返回登錄界面。
管理員模塊:在此模塊管理員除了擁有用戶的各項設置外還可以注冊新的管理員,但為了安全起見,此功能只有當登錄人是票點主任時才可執行。
5.2 子模塊設計:
5.2.1、管理員模塊
1、車票信息管理模塊:在此模塊,管理員可以執行車票信息錄入、查詢、刪除和修改功能,不過刪除和修改功能須當該車次沒有被預訂的情況下才可執行。
2、訂票管理模塊:在此模塊,管理員可以查詢所有訂單情況、已付款取票和未付款取票的訂單情況,還可以按學號查詢某學生的訂票情況以及各種統計信息,並在學生來付款取票時執行「付款」操作。另外還可以在訂單已付款或學生取消訂單時刪除訂單。
5.2.2、用戶模塊:
1、訂票管理模塊:在此模塊,用戶可以查詢車票信息和自身訂票情況。查詢車票情況分為按車次、按目的地、按起始站—目的站查詢,當查詢到自身所想要的車票時即可訂票,但注意預定票的目的地須與學生家鄉所在城市相符,否則系統不允許訂票;查詢自身訂票情況包括訂單信息及金額統計,還須注意訂票信息上要求的取票期限,訂票人須在規定期限內去所在校區的票點付款取票,逾期票點工作人員將不予處理。
2、用戶小貼士模塊:在這里,用戶將了解本訂票系統的訂票流程及相關規定及用戶訂票後付款取票地點的信息等。
6. 資料庫應用系統的實現
6.1 資料庫及其表結構的建立:
利用SQLServer企業管理器創建資料庫Tickets,然後創建表:
1、學生表Book
create table Student
(Sno varchar(12) primary key, Sname varchar(20) not null,
Ssex varchar(4) not null, Spw varchar(12) not null, //密碼
Sadm varchar(40) not null, //學院
Sdept varchar(30) not null, Sclass numeric(4) not null,
Stel varchar(12) not null, //電話
Semail varchar(50) not null, Shcity varchar(30) not null //所在地
);
2、車票信息表Ticket
create table Ticket
(Tno varchar(20) primary key, //車次號
Startcity varchar(30) not null, Starttime varchar(20) not null,
Endcity varchar(100) not null, Endtime varchar(20) not null,
Ttype varchar(20) CHECK (Ttype IN('硬座','軟座','硬卧','軟卧')),
Tickets int not null //票數
);
3、車次站次信息表Citysite
create table Citysite
(Cityno numeric(8) identity(1,1), //票點號
Tno varchar(20), City varchar(100),
Tprice decimal not null, //票價
primary key (Cityno,Tno),
foreign key (Tno) references Ticket(Tno) on delete cascade
)
4、訂票單表Book
create table Book
(Bno numeric(8) identity(1,1) primary key, Sno varchar(12),
Maketime datetime not null, judge int,
foreign key (Sno) references Student(Sno),);
5、訂票詳細信息表Bookno
create table Bookno
(ID numeric(8) identity(1,1) primary key,
Bno numeric(8), Tno varchar(20), City varchar(100),
Booktime varchar(20) not null, Ticketnums int not null,
Price decimal not null, //票價
Pickdate datetime not null, foreign key (Tno) references Ticket(Tno),
foreign key (Bno) references Book(Bno));
6、操作員表Operater
create table Operater
(Adname varchar(20) primary key, //登錄用戶名
Adpw varchar(12) not null, //登錄密碼
Opname varchar(20) not null, //員工真實姓名
Opsex varchar(4) not null, Optel varchar(12) not null,
Opemail varchar(50) not null);
7、訂票點表Ticketsite
create table Ticketsite
(Siteno numeric(8) primary key identity, //票點號
Adname varchar(20), // 票點主任登錄名
Sitetel varchar(12) not null, Siteaddr varchar(40),
foreign key (Adname) references Operater(Adname));
//為安全起見,學校各校區訂票點內容的添加要以直接輸入資料庫的方式進行
insert into Ticketsite(Adname,Sitetel,Siteaddr) values('linyp','870432','龍洞校區行政樓205')
insert into Ticketsite(Adname,Sitetel,Siteaddr) values('admin','87084432','大學城校區E區205')
6.2 創建表的相關視圖:
1、創建V_Book視圖,得到對於某種車票的訂票人數和訂票張數
create view V_Book(Tno,stu_no,sumticket)
as
select Tno,count(distinct Sno),sum(Ticketnums) from Bookno,Book
where Bookno.Bno=Book.Bno group by Tno
2、創建W_Book視圖,得到對於所有訂票學生的人數和訂票總張數
create view W_Book(stu_nums,sumtickets)
as select count(distinct Sno),sum(Ticketnums) from Bookno,Book
where Bookno.Bno=Book.Bno
3、創建P_Book視圖,得到對於已付款取票的人數和車票總張數
create view P_Book(cout_no,cout_nums)
as select count(distinct Sno),sum(Ticketnums) from Bookno,Book
where Bookno.Bno=Book.Bno and judge=1
4、創建M_Book視圖, 得到對於某一訂票的總金額
create view M_Book(Allprice)
as select sum(Tprice*Ticketnums) from Book,Bookno,Citysite
where Bookno.Bno=Book.Bno and Bookno.Tno=Citysite.Tno and Bookno.City=Citysite.City
6.3 各表關系圖,
易知表Ticket與表Citysite之間是父表與子表的關系:
表Book與表Bookno之間也是父表與子表的關系:
它們之間都是父表對子表的一對多的關系。
根據表之間的結構關系圖,可以得出各表的完整性約束條件如下圖:
表名 主鍵列名 外鍵
外鍵列名 參照表
Student Sno 無
Ticket Tno 無
Citysite Cityno Tno Ticket
Book Bno Sno Student
Bookno ID Bno,Tno Book,Ticket
Operater Adname 無
Ticketsite Siteno Adname Operater
6.4 數據輸入:利用系統錄入數據,如下圖為各表內容:
6.5 模塊實現:
本次系統設計我前台採用jsP技術,後台採用SQL Server 2000,操作系統採用Windows XP。
JSP(JavaServer Pages)是由Sun Microsystems公司倡導、許多公司參與一起建立的一種動態網頁技術標准,它是在傳統的網頁HTML文件(*.htm,*.html)中插入Java程序段(Scriptlet)和JSP標記(tag),從而形成JSP文件(*.jsp)。JSP具備了Java技術的簡單易用,完全的面向對象,具有平台無關性且安全可靠,主要面向網際網路的所有特點。
③ Power Designer設計影院售票系統
地址: http://web.52im.net/article.php?articleid=6747
Power Designer是Sybase公司的CASE工具集,使用它可以方便地對管理信息系統進行分析設計,它幾乎包括了資料庫模型設計的全過程。利用Power Designer可以製作數據流程圖、概念數據模型、物理數據模型,可以生成多種客戶端開發工具的應用程序,還可為數據倉庫製作結構模型,也能對團隊設計模型進行控制。Power Designer的4種模型:概念數據模型 (CDM)物理數據模型 (PDM) 面向對象模型 (OOM) 業務程序模型 (BPM) 我主要介紹一下PDM:PDM 敘述資料庫的物理實現,幫助你考慮真實的物理實現的細節。你能通過修正PDM來適合你的表現或物理約束。主要目的是把CDM中建立的現實世界模型生成特定的DBMS腳本,產生資料庫中保存信息的儲存結構,保證數據在資料庫中的完整性和一致性。PDM是適合於系統設計階段的工具。簡單說:就是PDM可以自動生成諸如'create table'之類的sql腳本.在數據建模過程中,我們建立概念數據模型,通過正向工程生成物理數據模型,生成資料庫建庫腳本,最後將物理數據模型生成關系資料庫。系統資料庫設計人員希望能夠將資料庫設計和關系資料庫生成無縫地集成起來,如何保證物理數據模型與其對應資料庫之間的雙向同步成為數據建模非常關鍵的一點。Powerdesigner作為強大的Case工具,為我們提供了方便的逆向工程特性。可以將目前所有流行的後端資料庫(包括Sybase、DB2、Oracle等)的結構信息通過逆向工程加入到PowerDesigner的物理數據模型和概念數據模型中,包括表、索引、觸發器、視圖等。下面說一下如何用PowerDesigner進行逆向工程.1.我用的資料庫是oracle9i,我為了訪問oracle資料庫,在我的機器上安裝了oracle客戶端(提供了oracle客戶端的驅動程序,而精簡客戶端則不可以),配置一個名稱為mylcl的服務:MYLCL = (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.3.106)(PORT = 1521)) ) (CONNECT_DATA = (SID = pwsc) ) )用戶名為:testuser,密碼為test. 2.在pd中,新建一個pdm,選擇資料庫為oracle9i3.選擇Database->configure connections,轉到system dsn標簽,點擊"添加",選擇驅動程序,由於我的資料庫是oracle,所以我選擇"oracle in oraclient10g_home1"(安裝了oracle客戶端才有這個驅動,而精簡客戶端沒有此驅動)4.在data source name 中,可以隨便命名一個"ora-test",在tns-server name中選擇第一步中的服務名稱:mylcl.點擊"test connection",輸入用戶名密碼,connection ok!5.點擊database->reverse engineer database ,選擇odbc datasource:ora-test.然後點擊確定哈哈,看見什麼了.我就不說了吧pdm的圖表如何?其實pd還可以通過sql來進行反向工程,我就不說了特此存檔.同時也希望對大家有幫助.
④ 不就是一個訂票網站嗎,12306的核心模型設計思路究竟復雜在哪兒
12306這個系統,核心要解決的問題是網上售票。涉及到個角色使用該系統:用戶、鐵道部。用戶的核心訴求是查詢余票、購票;鐵道部的核心訴求是 售票。購票和售票其實是一個場景,對用戶來說是購票,對鐵道部來說是售票。因此,我們要設計一個在線的網站系統,解決用戶的查詢余票、購票,以及鐵道部的 售票這3個核心訴求。看起來,這3個場景都是圍繞火車票展開的。
查詢余票:用戶輸入出發地、目的地、出發日三個條件,查詢可能存在的車次,用戶可以看到每個車次經過的站點名稱,以及每種座位的余票數量。
購票:購票分為訂票和付款兩個階段,本文重點分析訂票的模型設計和實現思路。
其實還有很多其他的需求,比如給不同的車次設定銷售座位數配額,以及不同的區段設置不同的限額。但相比前面兩個需求來說,我覺得這個需求相對次要一些。
需求分析
確實,12306也是一個電商系統,而且看起來商品就是票了。因為如果把一張票看成是一個商品,那購票就類似於購買商品,然後每張票都有庫存,商品 也有庫存的概念。但是如果我們仔細想想,會發現12306要復雜很多,因為我們無法預先確定好所有的票,如果非要確定,那隻能通過窮舉法了。
我們以北京西到深圳北的G71車次高鐵為例(這里只考慮南下的方向,不考慮深圳北到北京西的,那是另外一個車次,叫G72),它有17個站(北京西 是01號站,深圳北是17號站),3種座位(商務、一等、二等)。表面看起來,這不就是3個商品嗎?G71商務座、G71一等座、G71二等座。大部分輕 易噴12306的技術人員(包括某些中等規模公司的專家、CTO)就是在這里栽第一個跟頭的。實際上,G71有136*3=408種商品(408個 SKU),怎麼算來的?如下:
如果賣北京西始發的,有16種賣法(因為後面有16個站),北京西到:保定、石家莊、鄭州、武漢、長沙、廣州、虎門、深圳。。。。都是一個獨立的商 品,同理,石家莊上車的,有15種下車的可能,以此類推,單以上下車的站來計算,有136種票:16+15+14….+2+1=136。每種票都有3種座 位,一共是408個商品。
為了方便後面的討論,我們先明確一下票是什麼?
一張票的核心信息包括:出發時間、出發地、目的地、車次、座位號。持有票的人就擁有了一個憑證,該憑證表示持有它的人可以坐某個車次的某個座位號, 從某地到某地。所以,一張票,對用戶來說是一個憑證,對鐵道部來說是一個承諾;那對系統來說是什麼呢?不知道。這就是我們要分析業務,領域建模的原因,我 們再繼續思考吧。
明白了票的核心信息後,我們再看看G71這個車次的高鐵,可以賣多少張票?
討論前先說明一下,一輛火車的物理座位數(站票也可以看成是一種座位,因為站票也有數量配額)不等於可用的最大配合。所有的物理座位不可能都通過 12306網站來銷售,而是只會銷售一部分,比如40%。其餘的還是會通過線下的方式銷售。不僅如此,可能有些站點上車的人會比較多,有些比較少,所以我 們還會給不同的區間配置不同的限額。比如D31 北京南至上海共有765張,北京南有260張,楊柳青有80張,泰安有76張。如果楊柳青的80張票售完就會顯示無票,就算其他站有票也會顯示無票的。每 個車次肯定會有各種座位的配額和限額的配置的,這種配置我目前無法預料,但我已經把這些規則都封裝近車次聚合根里了,所有的配置策略都是基於座位類型、站 點、區間配置的。關於票的配置抽象出來,我覺得主要有3種:1)某個區段最多允許出多少張;2)某個區段最少允許出多少張;3)某個站點上車的最多多少 張;當用戶訂票時,把用戶指定的區段和這3種配置條件進行比較,3個條件都滿足,則可以出票。不滿足,則認為無票了。下面舉個例子:
ABCDEFG,這是所有站點。座位總配額是100,假設B站點上車,E站下車的人比較少,那我們就可以設定BE這個區段最多隻能出10張票。所 以,只要是用戶的訂票是在這個區段內的,就最多出10張。再比如,一列車次,總共100個座位配額,希望全程票最少滿足80張,那我們只要給AG這個區段 設定最少80張。那任何訂票請求,如果是子區間的,就不能超過100-80,即20張。這兩種條件必須同時滿足,才允許出票。
但是,不管如何做配額和限額,我們總是針對某個車次進行配置,這些配置只是車次內部售票時的一些額外的判斷條件(業務規則),不影響車次模型的核心 地位和對外暴露的功能。所以,為了本文討論的清楚起見,我後續的討論都不涉及配額和限額的問題,而是認為任何區段都可以享受火車最大的物理座位數。
並且,為了討論問題方便,我們減少一些站點來討論。假設某個車次有A,B,C,D四個站點。那001這個人購買了A,B這個區間,系統會分配給 001一個座位x;但是因為001坐到B站點後會下車,所以相當於x這個座位又空出來了,也就是說,從B站點開始,系統又可以認為x這個座位是可用的。所 以,我們得出結論:同一個座位,其實可以同時出售AB,BC這兩張票。通過這個簡單的分析,我們知道,一列火車雖然只有有限的座位數,比如1000個座 位。但可以賣出的票遠遠不止1000個。還是以A,B,C,D四個站點為例,假如火車總共有1000個座位,那AB可以賣1000張,BC也可以賣 1000張,同樣,CD也可以賣1000張。也就是說,理論上最多可以賣出3000張票。但是如果換一種賣法,所有人都是買ABCD的票,也就是說所有的 票都是經過所有站點的,那就是最多隻能賣出1000張票了。而實際的場景,一定是介於1000到3000之間。然後實際的G71這個車次,有17個站,那 到底可以賣出多少個票,大家應該可以算了吧。理論上這17個站中的任意兩個站點之間所形成的線段,都可以出售為一張票。我數學不好,算不太清楚,麻煩有數 學好的人幫我算算,呵呵。
通過上面的分析,我們知道一張票的本質是某個車次的某一段區間(一條線段),這個區間包含了若干個站點。然後我們還發現,只要區間不重疊,那座位就不會發生競爭,可以被回收利用,也就是說,可以同時預先出售。
另外,經過更深入的分析,我們還發現區間有4種關系:1)不重疊;2)部分重疊;3)完全重疊;4)覆蓋;不重疊的情況我們已經討論過了,而覆蓋也 是重疊的一種。所以我們發現如果重疊,比如有兩個區間發生重疊,那重疊部分的區間(可能誇一個或多個站點)是在爭搶座位的。因為假設一列火車有100個座 位,那每個原子區間(兩個相鄰站點的連線),最多允許重疊99次。
所以,經過上面的分析,我們知道了一個車次能夠出售一張車票的核心業務規則是什麼?就是:這張車票所包含的每個原子區間的重疊次數加1都不能超過車次的總座位數,實際上重疊次數+1也可以理解為線段的厚度。
模型設計
上面我分析了一下票的本質是什麼。那接下來我們再來看看怎麼設計模型,來快速實現購票的需求,重點是怎麼設計商品聚合以及減庫存的邏輯。
傳統電商的思路
如果按照普通電商的思路,把票(站點區間)設計為商品(聚合根),然後為票設計庫存數量。我個人覺得是很糟糕的。因為一方面這種聚合根非常多(上面 的G71就有408個);另一方面,即便枚舉出來了,一次購票也一定會影響非常多其他聚合根的庫存數量(只要被部分或全部重疊的區間都受影響)。這樣的一 次訂單處理的復雜度是難以評估的。而且這么多聚合根的更新要在一個事務里,這不是為難資料庫嗎?而且,這種設計必然帶來大量的事務的並發沖突,很可能導致 資料庫死鎖。總之,我認為這種是典型的由於領域模型的設計錯誤,導致並發沖突高、數據持久化落地困難。或者如果要解決並發問題,只能排隊單線程處理,但是 仍然解決不了要在一個事務里修改大量聚合根的尷尬局面。聽說12306是採用了Pivotal Gemfire這種高大上的內存資料庫,我對這個不太了解。我不可想像要是不使用內存資料庫,他們要怎麼實現車次內的票之間的數據強一致性(就是保證所有 出售的票都是符合上面討論的業務規則的)?所以,這種設計,我個人認為是思維定勢了,把火車票看成是普通電商的商品來看待。所以,我們有時做設計又要依賴 於經驗,又要不能被以往經驗所束縛,真的不容易,關鍵還是要根據具體的業務場景多多深入分析,盡量分析抽象出問題的本質出來,這樣才能對症下葯。那是否有 其他的設計思路呢?
我的思路
聚合設計
通過上面的分析我們知道,其實任何一次購票都是針對某個車次的,我認為車次是負責處理訂票的聚合根。我們看看一 個車次包含了哪些信息?一個車次包括了:1)車次名稱,如G71;2)座位數,實際座位數會分類型,比如商務座20個,一等座200個;二等座500個; 我們這里為了簡化問題,可以暫時忽略類型,我認為這個類型不影響核心的模型的設計決策。需要格外注意的是:這里的座位數不要理解為真實的物理座位數,很有 可能比真實的座位數要少。因為我們不可能把一個車次的所有座位都在網上通過12306來出售,而是只出售一部分,具體出售多少,要由工作人員人工指定。 3)經過的站點信息(包括站點的ID、站點名稱等),注意:車次還會記錄這些站點之間的順序關系;4)出發時間;看過GRASP九大模式中的信息專家模式的同學應該知道,將職責分配給擁有執行該職責所需信息的類。我們這個場景,車次具有一次出票的所有信息,所以我們應該把出票的職責交給車次。另外學過DDD的同學應該知道,聚合設計有一個原則,就是:聚合內強一致性,聚合之間最終一致性。經 過上面的分析,我們知道要產生一張票,其實要影響很多和這個票對應的線段相交的其他票的可用數量。因為所有的站點信息都在車次聚合內部,所以車次聚合內部 自然可以維護所有的原子區間,以及每個原子區間的可用票數(相當於是庫存數)。當一個原子區間的可用票數為0的時候,意味著火車針對這個區間的票已經賣完 了。所以,我們完全可以讓車次這個聚合根來保證出票時對所有原子區間的可用票數的更新的強一致性。對於車次聚合根來說,這很簡單,因為只是幾次簡單的內存 操作而已,耗時可以忽略。一列火車假如有ABCD四個站點,那原子區間就是3個。對於G71,則是16個。
怎麼判斷是否能出票
基於上面的聚合設計,出票時扣減庫存的邏輯是:
根據訂單信息,拿到出發地和目的地,然後獲取這段區間里的所有的原子區間。然後嘗試將每個原子區間的可用票數減1,如果所有的原子區間都夠減,則購 票成功;否則購票失敗,提示用戶該票已經賣完了。是不是很簡單呢?知道了出票的邏輯,那退票的邏輯也就很簡單了,就是把這個票的所有原子區間的可用票數加 1就OK了。如果我們從線段的厚度的角度去考慮,那出票時,每個原子區間的厚度就是+1,退票時就是減一。就是相反的操作,但本質是一樣的。
所以,通過這樣的思路,我們將一次訂票的處理控制在了一個聚合根里,用聚合根內的強一致性的特性保證了訂票處理的強一致性,同時也保證了性能,免去 了並發沖突的可能性。傳統電商那種把票單做類似商品的核心聚合根的設計,我當時第一眼看到就覺得不妥。因為這違背了DDD強調的強一致性應該由聚合根來保 證、聚合根之間的最終一致性通過Saga來保證的原則。
還有一個很重要的概念我想說一下我的看法,就是座位和區間的關系。因為有些朋友和我講,考慮座位號的問題,雖然都能減1,座位號也必須是同一個。我 覺得座位是全局共享的,和區段無關(也許我的理解完全有誤,請大家指正)。座位是一個物理概念,一個用戶成功購買了一張票後,座位就會少一個,一張票唯一 對應一個座位,但是一個座位有可能會對應多張票;而區間是一個邏輯上的概念,區間的作用有兩個:1)表示票的出發地和目的地;2)記錄票的可用數額。如果 區間能連通(即該區間內的每個原子區間的可用數額都大於0),則表示允許擁有一個座位。所以,我覺得座位和票(區間)是兩個維度的概念。
如何為票分配座位
我覺得車次聚合根內部應該維護所有該車次已經售出的票,已經出售的票的的本質是區間和座位的對應關系。系統處理訂票時,用戶提交過來的是一段區間。所以,系統應該做兩個事情:
先根據區間去判斷是否有可用的座位;
如果有可用座位,則再通過演算法去選擇一個可用的座位;
當得到一個可用座位後,就可以生成一張票了,然後保存這個票到車次聚合根內部即可。下面舉個例子:
假設現在的情況是座位有3個,站點有4個
座位:1,2,3
站點:abcd
票的賣法1:
票1:ab,1
票2:bc,2
票3:cd,3
票4:ac,3
票5:bd,1
這種選座位的方式應該比較高效,因為總是優先從座位池裡去拿座位,只有在萬不得已的時候才會去回收可重復利用的票。
上面的4,5兩個票,就是考慮回收利用的結果。
票的賣法2:
票1:ab,1
票2:bc,1
票3:cd,1
票4:ac,2
票5:bd,3
這種選座位的方式應該相對低效,因為總是優先會去掃描是否有可回收的座位,而掃描相對直接從座位池裡去拿票總是成本相對要高的。
上面的2,3兩個票,就是考慮回收利用的結果。
但是,優先從座位池裡拿票的演算法有缺陷,就是會出現雖然第一步判斷認為有可用的座位,但是這個座位可能不是全程都是同一個座位。舉例:
假設現在的情況是座位有3個,站點有4個
座位:1,2,3
站點:abcd
票的賣法3:
票1:ab,1
票2:bc,2
票3:cd,3
現在如果有人要買ad的票,那可用的座位有2,或者3。但是無論是2還是3,都要這個乘客中途換車位。比如賣給他座位2,那他ab是坐的座位2,但是bc的時候要坐座位1的。否則拿票2的那個人上車時,發現座位2已經有人了。而通過優先回收利用的演算法,是沒這個問題的。
所以,從上面的分析我們也知道選座位的演算法該怎麼寫了,就是採用優先回收利用座位的演算法。我認為不管我們這里怎麼設計演算法,都不影響大局,因為這一切都只發生在車次聚合根內部,這就是預先設計好聚合根,明確出票職責在哪個對象上的好處。
模型分析總結
我認為票不是核心聚合根,票只是一次出票的結果,一個憑證而已。
12306真正的核心聚合根應該是車次,車次具有出票的職責,一次出票具體做的事情有:
判斷是否可出票;
選擇可用的座位;
更新一次出票時所有原子區間的可用票數,用於判斷下次是否能出票;
維護所有已售出的票,用於為選擇可用座位提供依據;
通過這樣的模型設計,我們可以確保一次出票處理只會在一個車次聚合根內進行。這樣的好處是:
不需要依賴資料庫事務就能實現數據修改的強一致性,因為所有修改只在一個聚合根內發生;
在保證數據強一致性的同時還能提供很高的並發處理能力,具體設計見下面的架構設計;
架構設計(非本文重點,沒興趣的朋友可以略過)
我覺得12306這樣的業務場景,非常適合使用CQRS架構;因為首先它是一個查多寫少、但是寫的業務邏輯非常復雜的系統。所以,非常適合做架構層面的讀寫分離,即採用CQRS架構。而且應該使用數據存儲也分離的CQRS。這樣CQ兩端才可以完全不需要顧及對方的問題,各自優化自己的問題即可。我們可以在C端使用DDD領域模型的思路,用良好設計的領域模型實現復雜的業務規則和業務邏輯。而Q端則使用分布式緩存方案,實現可伸縮的查詢能力。
訂票的實現思路
同時藉助像ENode這樣的框架,我們可以實現in-memory + Event Sourcing的架構。Event Sourcing技術,可以讓領域模型的所有狀態修改的持久化統一起來,本來要用ORM的方式保存聚合根最新狀態的,現在只需要簡單的通用的方式保存一個 事件即可(一次訂票只涉及一個車次聚合根的修改,修改只產生一個事件,只需要持久化一個事件(一個JSON串)即可,保證了高性能,無須依賴事務,而且通 過ENode可以解決並發問題)。我們只要保存了聚合根每次變化的事件(事件的結構怎麼設計,本文不做多的介紹了,大家可以思考下),就相當於保存了聚合 根的最新狀態。而正是由於Event Sourcing技術的引入,讓我們的模型可以一直存活在內存中,即可以使用in-memory技術。不要小看in-memory技術,in- memory技術在某些方面對提高命令的處理性能非常有幫助。比如就以我們車次聚合根處理出票的邏輯,假設某個車次有大量的命令發送到分布式消息隊列,然 後有一台機器訂閱了這個隊列的消息,然後這台機器處理這個車次的訂票命令時,由於這個車次聚合根一直在內存,所以就省去了每次要去資料庫取出聚合根的步 驟,相當於少了一次資料庫IO。這樣的好處是,因為一個車次能夠真正出售的票是有限的,因為座位就那麼幾個,比如就1000個座位,估計一般正常情況也就 出個2000個左右的票吧(具體能出多少張票要取決於區間的相交程度,上面分析過)。也就是說,這個聚合根只會產生2000個事件,也就是說只會有 2000個訂票命令的處理是會產生事件,並持久化事件;而其餘的大量命令,因為車次在內存計算後發現沒有餘票了,就不會做任何修改,也不會產生領域事件, 這樣就可以直接處理下一個訂票命令了。這樣就可以大大提高處理訂票命令的性能。
另外一個問題我覺得還需要提一下,因為用戶訂票成功後,還需要付款。但用戶有可能不去付款或者沒有在規定的時間內完成付款。那這種情況下,系統會自 動釋放該用戶之前訂購的票。所以基於這樣的需求,我們在業務上需要支持業務級別的2pc。即先預扣庫存,也就是先佔住這張票一定時間(比如15分鍾),然 後付款成功後再真實給你這張票,系統做真正的庫存修改。通過這樣的預扣處理,可以保證不會出現超賣的情況。這個思路其實和傳統電商比如淘寶這樣的系統類 似,我就不多展開了,我之前寫的Conference案例也是這樣的思路,大家有興趣的可以去看一下我之前錄制的視頻。
查詢余票的實現思路
我覺得余票的查詢的實現相對簡單。雖然對於12306來說,查詢的請求佔了80%,提交訂單的請求只佔20%。但查詢由於對數據沒有修改,所以我們 完全可以使用分布式緩存來實現。我們只需要精心設計好緩存的key即可;緩存key的多少要看成本,如果所有可能的查詢都設計對應的key,那時間復雜度 為1,查詢性能自然高;但代價也大,因為key多了。如果想key少一點,那查詢的復雜度自然要上去一點。所以緩存設計無非就是空間換時間的思路。然後, 緩存的更新無非就是:自動失效、定時更新、主動通知3種。通過CQRS架構,由於CQ兩端是事件驅動的,當C端有任何狀態變化,都會產生對應的事件去通知 Q端,所以我們幾乎可以做到Q端的准實時更新。
同時由於CQ兩端的完全解耦,Q端我們可以設計多種存儲,如資料庫和緩存(Redis等);資料庫用於線下維護關系型數據,緩存用戶實時查詢。數據 庫和緩存的更新速度相互不受影響,因為是並行的。對同一個事件,可以10台機器負責更新緩存,100台機器負責更新資料庫。即便資料庫的更新很慢,也不會 影響緩存的更新進度。這就是CQRS架構的好處,CQ的架構完全不同,且我們隨時可以重建一種新的Q端存儲。不知道大家體會到了沒有?
關於緩存key的設計,我覺得主要從查詢余票時傳遞的信息來考慮。12306的關鍵查詢是:出發地、目的地、出發日期三個信息。我覺得有兩種key 的設計思路:1)直接設計了該查詢條件的key,然後快速拿到車次信息,直接返回;這種方式就是要求我們系統已經枚舉了所有車次的所有可能出現的票(區 間)的緩存key,相信你一定知道這樣的key是非常多的。2)不是枚舉所有區間,而是把每個車次的每個原子區間(相鄰的兩個站點所連成的直線)的可用票 數作為key。這樣,key就非常少了,因為車次假如有10000個,然後每個車次平均15個區間,那也就15W個key而已。當我們要查詢時,只需要把 用戶輸入的出發地和目的地之間的所有原子區間的可用票數都查出來,然後比較出最小可用票數的那個原子區間。則這個原子區間的可用票數就是用戶輸入的區間的 可用票數了。當然,到這里我提到考慮出發日期。我認為出發日期是用來決定具體是哪個車次聚合根的。同一個車次,不同的日期,對應的聚合根實例是不同的,即 便是同一天,也可能有多個車次聚合根,因為有些車次一天有幾班的,比如上午9點發車的一班,下午3點發車的一般。所以,我們也只要把日期也作為緩存key 的一部分即可。