19

如何优雅地运用位运算实现产品需求? | 梁桂钊的博客

 3 years ago
source link: http://blog.720ui.com/2020/bitdesign/#?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何优雅地运用位运算实现产品需求?

发表于 2020-07-09 | 功能设计 | 架构

本文总阅读量 248 次

在开始正文之前,我们先来说一下 Linux 的系统权限设计。在 Linux 系统中,为了保证文件的安全,对文件所有者、同组用户、其他用户的访问权限进行了分别管理。其中,文件所有者,即建立文件或目录的用户。同组用户,是所属组群中的所有用户。其他用户,指的是既不是文件所有者,也不是同组用户的其他用户。每个文件和目录都具有读取权限、写入权限和执行权限,这三个权限之间相互独立。

image.png

在 Linux 系统中,每个文件的访问权限可以用 9 个字母表示,每 3 个字母表示一类用户权限,分别代表文件创建者、同组用户、其他用户。其中,r 表示读取权限,w 表示写入权限,x 表示执行权限。通过功能模式修改文件权限,有三个部分组成,包括对象、操作和权限。

image.png

假设需要增加同组用户写入权限,下面来看一个例子。



  1. chmod g+w /root/install.log

此外,每一类用户的访问也可以通过数字的方式进行表示。

image.png

那么,通过数字模式就可以对常见的 Linux 文件权限操作进行归纳。

image.png

假设需要设置创建者可读可写可执行、同组用户可读、其他用户可读,我们可以这样写:



  1. chmod 755 /root/install.log

事实上,Linux 的文件访问权限就是非常经典的位运算使用场景。无独有偶,我们再来看下 Java 中的 java.lang.reflect.Modifier 。其中, Modifier 类采用 16 进制定义了静态常量。



  1. public static final int PUBLIC = 0x00000001;
  2. public static final int PRIVATE = 0x00000002;
  3. public static final int PROTECTED = 0x00000004;
  4. public static final int STATIC = 0x00000008;
  5. public static final int FINAL = 0x00000010;
  6. public static final int SYNCHRONIZED = 0x00000020;
  7. public static final int VOLATILE = 0x00000040;
  8. public static final int TRANSIENT = 0x00000080;
  9. public static final int NATIVE = 0x00000100;
  10. public static final int INTERFACE = 0x00000200;
  11. public static final int ABSTRACT = 0x00000400;
  12. public static final int STRICT = 0x00000800;

紧接着,Modifier 类提供了很多静态方法,例如 isPublic() 方法的返回值 & PUBLIC 对应的 16 进制值,如果非 0,则说明含有 public 修饰符。



  1. public static boolean isPublic(int mod) {
  2. return (mod & PUBLIC) != 0;

这里有一个重要的知识点,采用 & 运算,两位同时为 1,结果才为 1,否则为 0。即 0&0=0; 0&1=0; 1&0=0; 1&1=1。例如:3&1 即 0000 0011 & 0000 0001 = 00000001,值为 1。



  1. 0000 0011
  2. & 0000 0001
  3. = 0000 0001

与此同时,Modifier 类还采用 | 运算,确保参加运算的两个对象只要有一个为 1,其值为 1。即 0|0=0; 0|1=1; 1|0=1;1|1=1。例如 Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE | Modifier.ABSTRACT | Modifier.STATIC | Modifier.FINAL | Modifier.STRICT 的结果是 3103,即 110000011111。



  1. private static final int CLASS_MODIFIERS =
  2. Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE |
  3. Modifier.ABSTRACT | Modifier.STATIC | Modifier.FINAL |
  4. Modifier.STRICT;
  5. 0000 0000 0000 0001
  6. | 0000 0000 0000 0010
  7. | 0000 0000 0000 0100
  8. | 0000 0000 0000 1000
  9. | 0000 0000 0001 0000
  10. | 0000 0100 0000 0000
  11. | 0000 1000 0000 0000
  12. = 0000 1100 0001 1111

书归正传,我们站在前辈们的肩上,通过位运算设计优雅的多选标识,例如通过位运算实现权限控制或多状态管理,它的好处在于易扩展,避免数据库设计过程中字段膨胀,减少磁盘存储空间。

假设,我们现在有一个有一个业务需求:在任务中添加一个通知方式,可选项包括 IM 消息、系统提醒、邮箱、短信。选择 IM 消息后,支持 IM 即时发送;选择系统提醒后,支持站内信推送;选择选择邮箱后,该任务后续相关提醒内容,可通过发送邮件至相关人邮箱中进行通知;选择短信后,该任务后续相关提醒内容,可通过发送短信至相关人进行通知。

image.png

我们在设计数据库库表时,通常情况下,将多个标识字段合并成一个字段,并把这个字段改成字符串型方式保存,例如,存在 1 时表示支持 IM,2 时表示支持系统消息,3 表示支持邮箱,4 表示支持短信。此时,如果同时都满足,它的存储形式就是以逗号分隔的字符串:“1,2,3,4”。这样设计的好处在于,不仅消除相同字段的冗余,而且当增加新的渠道类别时,不需增加新的字段。



  1. IM(1, "IM消息"),
  2. SYSTEM(2, "系统提醒"),
  3. MAIL(3, "邮箱"),
  4. SMS(4, "短信");

但在数据查询时,我们需要对字符串进行分隔。并且字符串类型的字段在查询效率和存储空间上不如整型字段。因此,我们可以用“位”来解决这个问题。我们采取不同的位来分别表示不同类别的标识字段。

image.png

因此,当某个任务支持 IM 时,则保存 1(0000 0001);支持系统消息时,则保存 2(0000 0010),支持邮箱时,则保存 4(0000 0100);支持短信时,则保存 8(0000 1000)。四种都支持,则保存 15 (0000 11111)。

说明
00000001 1 支持IM
00000010 2 支持系统消息
00000011 3 支持IM、系统消息
00000100 4 支持邮箱
00000101 5 支持邮箱、IM
00000110 6 支持邮箱、系统消息
00000111 7 支持邮箱、IM、系统消息
00001000 8 支持短信
00001111 15 支持邮箱、IM、系统消息、短信

紧接着,我们通过封装常用方法来实现增删改。



  1. * @param mod 用户当前值
  2. * @param value 需要判断值
  3. * @return 是否存在
  4. public static boolean hasMark(long mod, long value) {
  5. return (mod & value) == value;
  6. * @param mod 已有值
  7. * @param value 需要添加值
  8. * @return 新的状态值
  9. public static long addMark(long mod, long value) {
  10. if (hasMark(mod, value)) {
  11. return mod;
  12. return (mod | value);
  13. * @param mod 已有值
  14. * @param value 需要删除值
  15. * @return 新值
  16. public static long removeMark(long mod, long value) {
  17. if (!hasMark(mod, value)) {
  18. return mod;
  19. return mod ^ value;

总结一下,我们在数据库设计时,将多个标识字段合并成一个字段,并把这个字段改成字符串型方式保存,不仅消除相同字段的冗余,而且当增加新的渠道类别时,不需增加新的字段,但是字符串类型的字段在查询效率和存储空间上不如整型字段。因此,我们可以参考用“位”来解决这个问题。我们采取不同的位来分别表示不同类别的标识字段。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK