位图法在mongodb中的应用

共 15064字,需浏览 31分钟

 ·

2022-11-29 11:22


JAVA前线 


欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习




1 需求背景

假设系统用户一共有三种角色:普通用户、管理员、超级管理员,现在需要设计一张用户角色表记录这类信息。我们不难设计出如下方案:


idnamesuperadminnormal
101用户一100
102用户二010
103用户三001
104用户四111

用户一具有超级管理员角色,用户二具有管理员角色,用户三具有普通用户角色,用户四同时具有三种角色。


2 发现问题

如果新增加一种角色呢?可以新增一个字段:


idnamesuperadminnormalnew
101用户一1000
102用户二0100
103用户三0010
104用户四1110

按照上述一个字段表示一种角色设计表,功能没有问题,优点是容易理解结构清晰,但是我们想一想有没有什么问题?笔者遇到过如下问题:

在复杂业务环境一份数据可能会使用在不同场景,例如上述数据存储在MySQL数据库,这一份数据还会被用在如下场景:

  • 检索数据需要同步一份到ES
  • 使用此表通过Flink计算业务指标
  • 订阅此表Binlog消息进行业务处理

如果表结构发生变化,数据源之间需要重新对接,业务方也要进行代码修改,这样开发成本非常高。有没有办法避免此类问题?


3 解决方案

我们可以使用位图法,同一个字段可以表示多个业务含义。先设计如下数据表,userFlag字段暂时不填:


idnameuser_flag
101用户一暂时不填
102用户二暂时不填
103用户三暂时不填
104用户四暂时不填

位图每一个bit表示一种角色:

使用位图法表示如下数据:

idnamesuperadminnormal
101用户一100
102用户二010
103用户三001
104用户四111

用户一位图如下,十进制数值等于4:


用户二位图如下,十进制数值等于2:


用户三位图如下,十进制数值等于1:


用户四位图如下,十进制数值等于7:


现在可以填写数据表第三列:


idnameuser_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」一起交流学习


浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报