背景算法
框圖數據庫
上圖中,Role和被設置Permission的Resource都是能夠有任意層級繼承關係的。性能
舉例網站
舉一個網站的例子來講:orm
若是,User表示網站用戶;Role表示角色;Resource表示全部可訪問的URL;Permission是對每個URL的某一個權限(如:查看,修改等)。blog
Role能夠有任意層級繼承關係,如:用戶角色能夠分爲Normal User和Admin User,Admin User下又能夠分爲Super Admin、Content Admin、Support Admin等。繼承
URL這種Resource也能夠有任意層級繼承關係的,如:對http://abc.com/A/A1/A11/A111.aspx這樣一個連接,能夠認爲http://abc.com/A1是一個URL Resource,http://abc.com/A/A1/A11是他的一個子Resource,http://abc.com/A/A1/A11/A111.aspx又是http://abc.com/A/A1/A11的一個子Resource。遞歸
對某一個Role來講,他對某一個Resource – R1的具體的Permissions,等於關聯到這個Role的Resource - R1及其全部父級Resource的Permissions的並集。索引
對於某一個User來講,他對某一個Resource的Permissions,等於他所屬的全部Roles的Permissions的並集。get
問題
各元素之間的關係容易理解,關鍵的難點在於,由於Role和Resource均可以是有無限層級繼承關係的,如何保證權限信息驗證具備較高的性能呢?當繼承關係較複雜時,遞歸檢測的性能無疑是不可接受的。
數據庫表
User(ID,Name)
Role(ID,Name,ParentID,LeftIndex,RightIndex)
UsersInRoles(UserID,RoleID)
Resource(ID,Name,ParentID,LeftIndex,RightIndex)
PermissionsOfRole(PermissionsValue,ResourceID,RoleID)
這裏簡單起見,對於Permissions,使用一個二進制位表示一個具體的Permission。咱們須要事先定義一個PermissionsValue的每個二進制位表示的Permission。例如:若是PermissionsValue的二進制值爲10101010,表示從低位到高位第二、四、六、8位所表明的權限的並集。
使用二進制位表示一個具體的Permission的好處是,處理Permissions的並操做能夠轉換爲二進制的OR;缺點是,具體的Permission想不能特別多,由於多一個就意味着PermissionsValue的最大值大一個2的次方。8位二進制的最大值是2的8次方,這不算很大,可是,1000位二進制的最大值是2的1000次方,這就是個不可想象的巨大數字了。好在,通常來說,具體的Permission項目不會特別多的。
該方案的關鍵,就在於Role和Resource表的LeftIndex和RightIndex這兩個字段了,咱們將使用這兩個字段,在避免遞歸的狀況下,實現較高性能的取某個繼承節點的全部子元素或全部父元素的算法。
算法
咱們以Role爲例,首先Role表中有且只有一條記錄存放全部Roles的頂層父節點(1,「Root Role」,1,2)。當他沒有子節點時,其LeftIndex和RightIndex的值分別爲1和2。當對其插入子節點時,LeftIndex和RightIndex的值須要作相應的調整,調整的規則以下(括號中爲LeftIndex和RightIndex的值):
按逆時針方向,你們能看出規則嗎?
按照這個規則,咱們能夠以下獲取某一個節點的全部字節點或全部父結點(使用僞SQL代碼表示):
獲取ID爲3的Role節點的全部的子結點包括自己:
SELECT * FROM Role WHERE
LeftIndex >= (SELECT LeftIndex FROM Role WHERE ID = 3)
AND
RightIndex <= (SELECT RightIndex FROM Role WHERE ID = 3)
注:若是要不包括ID=3的節點自己,只須要用>和<代替>=和<=。
獲取ID爲5的Role節點的全部父節點包括自己:
SELECT * FROM Role WHERE
LeftIndex <= (SELECT LeftIndex FROM Role WHERE ID = 3)
AND
RightIndex >= (SELECT RightIndex FROM Role WHERE ID = 3)
注:若是要不包括ID=5的節點自己,只須要用>和<代替>=和<=。
你們能夠根據上面的圖驗證一下算法的效果。徹底不須要遞歸,只須要簡單的判斷LeftIndex和RightIndex就行,性能天然是很是好的。
咱們甚至能夠以很是簡單的SQL語句得到某一個ID爲2的User對ID爲6的Resource的PermissionsValue:
DECLARE @PermissionsValue int;
SELECT @PermissionsValue = @PermissionsValue | PermissionsValue
FROM PermissionsOfRole WHERE
RoleID IN
(
SELECT ID FROM Role WHERE
LeftIndex >= (SELECT LeftIndex FROM Role WHERE ID IN
(SELECT RoleID FROM UsersInRoles WHERE UserID = 2))
AND
RightIndex <= (SELECT RightIndex FROM Role WHERE ID IN
(SELECT RoleID FROM UsersInRoles WHERE UserID = 2))
)
AND
ResourceID IN
(
SELECT ID FROM Resource WHERE
LeftIndex <= (SELECT LeftIndex FROM Resource WHERE ID = 6)
AND
RightIndex >= (SELECT RightIndex FROM Resource WHERE ID = 6)
);
SELECT @PermissionsValue;
上面的SQL雖然有很多嵌套的SELECT,可是,由於子查詢基本上都是對主鍵字段的條件判斷,LeftIndex和RightIndex咱們也會加上索引,所以,實際上不會對性能形成太大影響。
OK,查詢性能很好,不過這是以新建或修改Role和Resource的層級關係時的必定的性能損失爲代價的。每次新增或修改Role或Resource的層級關係時,必須按照前面所述的規則重置全部節點的LeftIndex和RightIndex值。不過,通常狀況下,因爲Role和Resource的維護操做佔系統總體操做的比例很小,幾乎能夠忽略,所以其性能損失也不是什麼大問題。具體的重置全部節點LeftIndex和RightIndex值的僞代碼我就不貼出來了,你們稍微花費幾個腦細胞就能想出來了^-^
//結束