位圖法在mongodb中的應(yīng)用
JAVA前線
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
1 需求背景
假設(shè)系統(tǒng)用戶一共有三種角色:普通用戶、管理員、超級管理員,現(xiàn)在需要設(shè)計一張用戶角色表記錄這類信息。我們不難設(shè)計出如下方案:
| id | name | super | admin | normal |
|---|---|---|---|---|
| 101 | 用戶一 | 1 | 0 | 0 |
| 102 | 用戶二 | 0 | 1 | 0 |
| 103 | 用戶三 | 0 | 0 | 1 |
| 104 | 用戶四 | 1 | 1 | 1 |
用戶一具有超級管理員角色,用戶二具有管理員角色,用戶三具有普通用戶角色,用戶四同時具有三種角色。
2 發(fā)現(xiàn)問題
如果新增加一種角色呢?可以新增一個字段:
| id | name | super | admin | normal | new |
|---|---|---|---|---|---|
| 101 | 用戶一 | 1 | 0 | 0 | 0 |
| 102 | 用戶二 | 0 | 1 | 0 | 0 |
| 103 | 用戶三 | 0 | 0 | 1 | 0 |
| 104 | 用戶四 | 1 | 1 | 1 | 0 |
按照上述一個字段表示一種角色設(shè)計表,功能沒有問題,優(yōu)點是容易理解結(jié)構(gòu)清晰,但是我們想一想有沒有什么問題?筆者遇到過如下問題:
在復(fù)雜業(yè)務(wù)環(huán)境一份數(shù)據(jù)可能會使用在不同場景,例如上述數(shù)據(jù)存儲在MySQL數(shù)據(jù)庫,這一份數(shù)據(jù)還會被用在如下場景:
檢索數(shù)據(jù)需要同步一份到ES 使用此表通過Flink計算業(yè)務(wù)指標(biāo) 訂閱此表Binlog消息進(jìn)行業(yè)務(wù)處理
如果表結(jié)構(gòu)發(fā)生變化,數(shù)據(jù)源之間需要重新對接,業(yè)務(wù)方也要進(jìn)行代碼修改,這樣開發(fā)成本非常高。有沒有辦法避免此類問題?
3 解決方案
我們可以使用位圖法,同一個字段可以表示多個業(yè)務(wù)含義。首先設(shè)計如下數(shù)據(jù)表,userFlag字段暫時不填:
| id | name | user_flag |
|---|---|---|
| 101 | 用戶一 | 暫時不填 |
| 102 | 用戶二 | 暫時不填 |
| 103 | 用戶三 | 暫時不填 |
| 104 | 用戶四 | 暫時不填 |
位圖每一個bit表示一種角色:

使用位圖法表示如下數(shù)據(jù):
| id | name | super | admin | normal |
|---|---|---|---|---|
| 101 | 用戶一 | 1 | 0 | 0 |
| 102 | 用戶二 | 0 | 1 | 0 |
| 103 | 用戶三 | 0 | 0 | 1 |
| 104 | 用戶四 | 1 | 1 | 1 |
用戶一位圖如下,十進(jìn)制數(shù)值等于4:

用戶二位圖如下,十進(jìn)制數(shù)值等于2:

用戶三位圖如下,十進(jìn)制數(shù)值等于1:

用戶四位圖如下,十進(jìn)制數(shù)值等于7:

現(xiàn)在可以填寫數(shù)據(jù)表第三列:
| id | name | user_flag |
|---|---|---|
| 101 | 用戶一 | 4 |
| 102 | 用戶二 | 2 |
| 103 | 用戶三 | 1 |
| 104 | 用戶四 | 7 |
4 代碼實例
本文結(jié)合mongodb實現(xiàn)思路有兩種:
方案一:取出二進(jìn)制字段在應(yīng)用層運算 方案二:在數(shù)據(jù)層直接運算二進(jìn)制字段
4.1 用戶實體
用戶實體對應(yīng)數(shù)據(jù)表user:
@Document(collection = "user")
public class User {
@Id
@Field("_id")
private String id;
@Field("userId")
private String userId;
@Field("role")
private Long role;
}
4.2 用戶角色
定義枚舉時不要直接定義為1、2、4這類數(shù)字,應(yīng)該采用位移方式進(jìn)行定義,這樣使用者可以明白設(shè)計者的意圖。
public enum UserRoleEnum {
// 1 -> 00000001
NORMAL(1L << 0, "普通用戶"),
// 2 -> 00000010
MANAGER(1L << 1, "管理員"),
// 4 -> 00000100
SUPER(1L << 2, "超級管理員"),
;
private Long code;
private String description;
private UserRoleEnum(Long code, String description) {
this.code = code;
this.description = description;
}
// 新增角色 -> 位或操作
// oldRole -> 00000001 -> 普通用戶角色
// addRole -> 00000010 -> 新增管理員角色
// newRole -> 00000011 -> 具有普通用戶和管理員角色
public static Long addRole(Long oldRole, Long addRole) {
return oldRole | addRole;
}
// 刪除角色 -> 異或操作
// oldRole -> 00000011 -> 普通用戶和管理員角色
// delRole -> 00000010 -> 刪除管理員角色
// newRole -> 00000001 -> 普通用戶角色
public static Long removeRole(Long oldRole, Long delRole) {
return oldRole ^ delRole;
}
// 是否具有某種角色 -> 位與操作
// allRole -> 00000011 -> 普通用戶和管理員角色
// qryRole -> 00000001 -> 查詢是否具有管理員角色
// resRole -> 00000001 -> 具有管理員角色
public static boolean hasRole(Long role, Long queryRole) {
Long resRole = (role & queryRole);
return queryRole == resRole;
}
}
4.3 數(shù)據(jù)準(zhǔn)備
新增用戶一到用戶四:
db.user.insertMany([
{
"userId": "user1",
"role": NumberLong(4)
},
{
"userId": "user2",
"role": NumberLong(2)
},
{
"userId": "user3",
"role": NumberLong(1)
},
{
"userId": "user4",
"role": NumberLong(7)
}
])
4.4 應(yīng)用層運算
應(yīng)用層運算有三個關(guān)鍵步驟:
查詢用戶角色 內(nèi)存計算新角色 更新數(shù)據(jù)庫
@Service
public class UserBizService {
@Resource
private MongoTemplate mongoTemplate;
// 查詢用戶
public User getUser(String userId) {
Query query = new Query();
Criteria criteria = Criteria.where("userId").is(userId);
query.addCriteria(criteria);
User user = mongoTemplate.findOne(query, User.class);
return user;
}
// 新增角色
public boolean addRole(String userId, Long addRole) {
// 查詢用戶角色
User user = getUser(userId);
// 計算新角色
Long finalRole = UserRoleEnum.addRole(user.getRole(), addRole);
// 更新數(shù)據(jù)庫
Query query = new Query();
Criteria criteria = Criteria.where("userId").is(userId);
query.addCriteria(criteria);
Update update = new Update();
update.set("role", finalRole);
mongoTemplate.updateFirst(query, update, User.class);
return true;
}
// 刪除角色
public boolean removeRole(String userId, Long delRole) {
// 查詢用戶角色
User user = getUser(userId);
// 計算新角色
Long finalRole = UserRoleEnum.removeRole(user.getRole(), delRole);
// 更新數(shù)據(jù)庫
Query query = new Query();
Criteria criteria = Criteria.where("userId").is(userId);
query.addCriteria(criteria);
Update update = new Update();
update.set("role", finalRole);
mongoTemplate.updateFirst(query, update, User.class);
return true;
}
// 查詢用戶是否具有某種角色
public boolean queryRole(String userId, Long queryRole) {
// 查詢用戶角色
User user = getUser(userId);
// 計算是否具有某種角色
return UserRoleEnum.hasRole(user.getRole(), queryRole);
}
}
4.5 數(shù)據(jù)層運算
4.5.1 位運算操作符
(1) 查詢操作符
| 操作符 | 含義 |
|---|---|
| bitsAllClear | 指定二進(jìn)制位全為0 |
| bitsAllSet | 指定二進(jìn)制位全為1 |
| bitsAnyClear | 任意指定二進(jìn)制位為0 |
| bitsAnySet | 任意指定二進(jìn)制位為1 |
下列語句可以查出用戶四:

0-2三個位置全部等于1
db.user.find({
"role": {
$bitsAllSet: [0, 1, 2]
}
})
0-2任意一個位置等于1
db.user.find({
"role": {
$bitsAnySet: [0, 1, 2]
}
})
3-7位置全部等于0
db.user.find({
"role": {
$bitsAllClear: [3, 4, 5, 6, 7]
}
})
3-7位置任意等于0
db.user.find({
"role": {
$bitsAnyClear: [3, 4, 5, 6, 7]
}
})
(2) 計算操作符
| 操作符 | 含義 | 操作 |
|---|---|---|
| and | 位與 | 查詢角色 |
| or | 位或 | 新增角色 |
| xor | 位異或 | 刪除角色 |
user3新增超級管理員角色
db.user.update({
"userId": "user3"
}, {
$bit: {
"role": {
or: NumberLong(4)
}
}
})
user4刪除普通用戶角色
db.user.update({
"userId": "user4"
}, {
$bit: {
"role": {
xor: NumberLong(1)
}
}
})
4.5.2 代碼實例
@Service
public class UserBizService {
/*
* 新增角色
*/
public boolean addRoleBit(String userId, Long addRole) {
Query query = new Query();
Criteria criteria = Criteria.where("userId").is(userId);
query.addCriteria(criteria);
Update update = new Update();
update.bitwise("role").or(addRole);
mongoTemplate.updateFirst(query, update, User.class);
return true;
}
/**
* 刪除角色
*/
public boolean removeRoleBit(String userId, Long addRole) {
Query query = new Query();
Criteria criteria = Criteria.where("userId").is(userId);
query.addCriteria(criteria);
Update update = new Update();
update.bitwise("role").xor(addRole);
mongoTemplate.updateFirst(query, update, User.class);
return true;
}
/**
* 查詢rolePosition位置全部等于0的用戶
*
* 表示不具有rolePositions中所有角色的用戶
*/
public List<User> queryRoleAllClear(List<Integer> rolePositions) {
Criteria criteria = Criteria.where("role").bits().allClear(rolePositions);
List<User> users = mongoTemplate.query(User.class).matching(criteria).all();
return users;
}
/**
* 查詢rolePosition位置任一等于0的用戶
*
* 表示不具有rolePositions中任一角色的用戶
*/
public List<User> queryRoleAnyClear(List<Integer> rolePositions) {
Criteria criteria = Criteria.where("role").bits().anyClear(rolePositions);
List<User> users = mongoTemplate.query(User.class).matching(criteria).all();
return users;
}
/**
* 查詢rolePosition位置全部等于1的用戶
*
* 表示具有rolePositions中所有角色的用戶
*/
public List<User> queryRoleAllSet(List<Integer> rolePositions) {
Criteria criteria = Criteria.where("role").bits().allSet(rolePositions);
List<User> users = mongoTemplate.query(User.class).matching(criteria).all();
return users;
}
/**
* 查詢rolePosition位置任一等于1的用戶
*
* 表示具有rolePositions中任一角色的用戶
*/
public List<User> queryRoleAnySet(List<Integer> rolePositions) {
Criteria criteria = Criteria.where("role").bits().anySet(rolePositions);
List<User> users = mongoTemplate.query(User.class).matching(criteria).all();
return users;
}
}
5 文章總結(jié)
本文我們從一個簡單案例開始,分析了直接新增字段的優(yōu)缺點。新增字段方案遇到最多問題就是在復(fù)雜業(yè)務(wù)場景中,需要新增數(shù)據(jù)對接工作量,增加了開發(fā)維護(hù)成本。
我們又介紹了位圖法,一個字段就可以表示多種業(yè)務(wù)含義,減少了字段冗余,節(jié)省了對接開發(fā)成本。同時位圖法增加了代碼理解成本,數(shù)據(jù)庫字段含義不直觀,需要進(jìn)行轉(zhuǎn)義,大家可以根據(jù)業(yè)務(wù)需求場景選擇。
JAVA前線
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
