位图法在mongodb中的应用
JAVA前线
欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习
1 需求背景
假设系统用户一共有三种角色:普通用户、管理员、超级管理员,现在需要设计一张用户角色表记录这类信息。我们不难设计出如下方案:
id | name | super | admin | normal |
---|---|---|---|---|
101 | 用户一 | 1 | 0 | 0 |
102 | 用户二 | 0 | 1 | 0 |
103 | 用户三 | 0 | 0 | 1 |
104 | 用户四 | 1 | 1 | 1 |
用户一具有超级管理员角色,用户二具有管理员角色,用户三具有普通用户角色,用户四同时具有三种角色。
2 发现问题
如果新增加一种角色呢?可以新增一个字段:
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 |
按照上述一个字段表示一种角色设计表,功能没有问题,优点是容易理解结构清晰,但是我们想一想有没有什么问题?笔者遇到过如下问题:
在复杂业务环境一份数据可能会使用在不同场景,例如上述数据存储在MySQL数据库,这一份数据还会被用在如下场景:
检索数据需要同步一份到ES 使用此表通过Flink计算业务指标 订阅此表Binlog消息进行业务处理
如果表结构发生变化,数据源之间需要重新对接,业务方也要进行代码修改,这样开发成本非常高。有没有办法避免此类问题?
3 解决方案
我们可以使用位图法,同一个字段可以表示多个业务含义。首先设计如下数据表,userFlag字段暂时不填:
id | name | user_flag |
---|---|---|
101 | 用户一 | 暂时不填 |
102 | 用户二 | 暂时不填 |
103 | 用户三 | 暂时不填 |
104 | 用户四 | 暂时不填 |
位图每一个bit表示一种角色:
使用位图法表示如下数据:
id | name | super | admin | normal |
---|---|---|---|---|
101 | 用户一 | 1 | 0 | 0 |
102 | 用户二 | 0 | 1 | 0 |
103 | 用户三 | 0 | 0 | 1 |
104 | 用户四 | 1 | 1 | 1 |
用户一位图如下,十进制数值等于4:
用户二位图如下,十进制数值等于2:
用户三位图如下,十进制数值等于1:
用户四位图如下,十进制数值等于7:
现在可以填写数据表第三列:
id | name | user_flag |
---|---|---|
101 | 用户一 | 4 |
102 | 用户二 | 2 |
103 | 用户三 | 1 |
104 | 用户四 | 7 |
4 代码实例
本文结合mongodb实现思路有两种:
方案一:取出二进制字段在应用层运算 方案二:在数据层直接运算二进制字段
4.1 用户实体
用户实体对应数据表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这类数字,应该采用位移方式进行定义,这样使用者可以明白设计者的意图。
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 数据准备
新增用户一到用户四:
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 应用层运算
应用层运算有三个关键步骤:
查询用户角色 内存计算新角色 更新数据库
@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);
// 更新数据库
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);
// 更新数据库
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 数据层运算
4.5.1 位运算操作符
(1) 查询操作符
操作符 | 含义 |
---|---|
bitsAllClear | 指定二进制位全为0 |
bitsAllSet | 指定二进制位全为1 |
bitsAnyClear | 任意指定二进制位为0 |
bitsAnySet | 任意指定二进制位为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 文章总结
本文我们从一个简单案例开始,分析了直接新增字段的优缺点。新增字段方案遇到最多问题就是在复杂业务场景中,需要新增数据对接工作量,增加了开发维护成本。
我们又介绍了位图法,一个字段就可以表示多种业务含义,减少了字段冗余,节省了对接开发成本。同时位图法增加了代码理解成本,数据库字段含义不直观,需要进行转义,大家可以根据业务需求场景选择。
JAVA前线
欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习