安装 Spring Boot CLI 默认安装最新版本的(安装位置在$HOME/.sdkman 下):
1
sdk install springboot
切换 Spring Boot CLI 版本
1 2 3
sdk list springboot sdk install springboot XXX.RELEASE sdk use springboot XXX.RELEASE
设置默认版本
1
sdk default springboot XXX.RELEASE
使用本地编译的 springboot
1 2 3
$ sdk install springboot dev /path/to/spring-boot/spring-boot-cli/target/spring-boot-cli-2.0.0.BUILD-SNAPSHOT-bin/spring-2.0.0.BUILD-SNAPSHOT/ $ sdk default springboot dev $ spring --version
正常情况下一个类加载器只能找到加载路径的 jar 包里当前目录或者文件类里面的 *.class 文件,SpringBoot 允许我们使用 java -jar archive.jar 运行包含嵌套依赖 jar 的 jar 或者 war 文件。
传统类加载器的局限
传统类加载器无法加载 jar 包中嵌套的 jar 包。比如项目打包后包含如下这些类和 jar 包,则 c.jar 中的类无法被加载:
1 2 3 4 5 6
a1.class a2.class b.jar b1.class b2.class c.jar
为了能够加载嵌套 jar 里面的资源,之前的做法都是把嵌套 jar 里面的 class 文件和应用的 class 文件打包为一个 jar,这样就不存在嵌套 jar 了,但是这样做就不能很清晰的知道哪些是应用自己的,哪些是应用依赖的,另外多个嵌套 jar 里面的 class 文件可能内容不一样但是文件名却一样时候又会引发新的问题。
加载嵌套 jar 包中的 class 文件
在 Java 中,AppClassLoader 和 ExtClassLoader 都继承自 URLClassLoader,并通过构造函数来传递需要加载的 class 文件所在的目录。 那么只要将 jar 包中嵌套的 jar 包所在的路径作为 URLClassLoader 的扫描路径,就可以实现对嵌套 jar 包的扫描了。 但是默认情况下 Java 使用 AppClassLoader 加载使用命令启动时指定的 classpath 下的类和 jar 包,那么如何使用自定义的 URLClassLoader 来加载这些类和 jar 包呢?具体做法是加一个中间层。 原来是直接使用 AppClassLoader 来加载应用:AppClassLoader -> Application。 现在先加载一个自定义的启动类 Launcher,其中自定义 URLClassLoader,再使用该 URLClassLoader 来加载我们真正的 main 函数:AppClassLoader -> Launcher -> URLClassLoader -> Application。
spring-boot-load 提供的启动器 Launcher
spring-boot-load 模块允许我们加载嵌套 jar 包中的 class 文件,它包含三种类启动器:
@Configuration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(ServletRequest.class) @ConditionalOnWebApplication(type = Type.SERVLET) @EnableConfigurationProperties(ServerProperties.class) @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, ServletWebServerFactoryConfiguration.EmbeddedTomcat.class, ServletWebServerFactoryConfiguration.EmbeddedJetty.class, ServletWebServerFactoryConfiguration.EmbeddedUndertow.class }) public class ServletWebServerFactoryAutoConfiguration {
@Bean public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) { return new ServletWebServerFactoryCustomizer(serverProperties); }
@Bean @ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat") public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer( ServerProperties serverProperties) { return new TomcatServletWebServerFactoryCustomizer(serverProperties); }
/** * Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via * {@link ImportBeanDefinitionRegistrar} for early registration. */ public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
通过自动配置将 TomcatEmbeddedServletContainerFactory 或者 JettyEmbeddedServletContainerFactory 的实例注入 IOC 容器后,我们就可以使用 Web 容器来提供 Web 服务了。 具体的 Web 容器的创建是在容器刷新过程的 onRefresh 阶段进行的(这个阶段是在刷新过程的 invokeBeanFactoryPostProcessors 阶段的后面):
getBeanNamesForType 获取了 IOC 容器中的 EmbeddedServletContainerFactory 类型的 Bean 的 name 集合,如果 name 集合为空或者多个则抛出异常。还记得 Web 容器工厂是通过自动配置注入到 IOC 的吧,并且 TomcatEmbeddedServletContainerFactory 或者 JettyEmbeddedServletContainerFactory 都是实现了 EmbeddedServletContainerFactory 接口。
如果 IOC 里面只有一个 Web 容器工厂 Bean 则获取该 Bean 的实例,然后调用该 Bean 的 getEmbeddedServletContainer 获取 Web 容器,这里假设 Web 容器工厂为 Tomcat ,则创建 Tomcat 容器并进行初始化。
Actuator 模块
Actuator 模块提供了一些组件用于检查应用或中间件的健康状况:
Inspect:检测应用或中间件的健康状况;
Aggregate:聚合所有结果。
基于 Actuator 模块,我们可以开发一个服务探活系统:
Actuator 为业务服务器暴露出一个探活端口healthcheck,借助 Spring 容器获取到中间件的客户端实例;
private static final Logger LOG = Logger.getLogger(ZipkinTest2Application.class); @Autowired private RestTemplate restTemplate; @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); }
@RequestMapping("/miya") public String miya() { LOG.log(Level.INFO, "info is being called"); return restTemplate.getForObject("http://localhost:8988/info", String.class); }
为 Service 注入 Sampler 用于记录 Span
1 2 3 4
@Bean public AlwaysSampler defaultSampler() { return new AlwaysSampler(); }
JVM 规范中定义了 class 文件的格式,但并未定义 Java 源码如何被编译为 class 文件,各厂商在实现 JDK 时通常会将符合 Java 语言规范的源码编译为 class 文件的编译器,例如在 Sun JDK 中就是 javac,javac 将 Java 源码编译为 class 文件的步骤如下图所示:
// The current thread already owns the monitor and it has not yet // been added to the wait queue so the current thread cannot be // made the successor. This means that the JVMTI_EVENT_MONITOR_WAIT // event handler cannot accidentally consume an unpark() meant for // the ParkEvent associated with this ObjectMonitor. } ObjectSynchronizer::wait(obj, ms, CHECK); JVM_END
public class Example { // 类方法 public static int classMethod(int i, long l, float f, double d, Object o, byte b) { return 0; } // 实例方法 public int instanceMethod(char c, double d, short s, boolean b) { return 0; } }
类方法和实例方法栈帧结构有所不同,从图中可以看到它们之间的区别:
类方法帧里没有隐含的 this 引用,而实例方法帧中会隐含一个 this 引用;
并且注意:
byte,char,short,boolean 类型存入局部变量区的时候都会被转化成 int 类型值,当被存回堆或者方法区时,才会转化回原来的类型;
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用 PC 计数器的值以指向方法调用指令后面的一条指令等。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在 Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
public class GCLogTest { public static void main(String[] args) { int _1m = 1024 * 1024; byte[] data = new byte[_1m]; // 将data置null让其可被回收 data = null; System.gc(); } }
在 IDE 中设置 VM 参数-XX:+PrintGCDetails,再运行,可以得到:
1 2 3 4 5 6 7 8 9 10 11 12
[GC (System.gc()) [PSYoungGen: 5051K->776K(38400K)] 5051K->784K(125952K), 0.0014035 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [Full GC (System.gc()) Disconnected from the target VM, address: '127.0.0.1:55472', transport: 'socket' [PSYoungGen: 776K->0K(38400K)] [ParOldGen: 8K->684K(87552K)] 784K->684K(125952K), [Metaspace: 2980K->2980K(1056768K)], 0.0040080 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] Heap PSYoungGen total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000) eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000) from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000) to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000) ParOldGen total 87552K, used 684K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000) object space 87552K, 0% used [0x0000000740000000,0x00000007400ab0d0,0x0000000745580000) Metaspace used 2988K, capacity 4568K, committed 4864K, reserved 1056768K class space used 318K, capacity 392K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Java heap space 表示 Java 堆空间不足。当应用程序申请更多的内存时,若 Java 堆内存已经无法满足应用程序的需要,则将抛出这种异常。
1 2 3 4 5 6 7 8 9 10 11 12 13
/* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { static class OOMObject {}
public static void main(String[] args) { List<OOMObject> list = new ArrayList<>(); while(true) { list.add(new OOMObject()); } } }
java.lang.OutOfMemoryError: PermGen space,表示 Java 永久代(方法区)的空间不足。永久代用于存放类的字节码和常量池,类的字节码被加载后存放在这个区域,这和存放对象实例的堆区是不同的。大多数 JVM 的实现都不会对永久代进行垃圾回收,因此,只要类加载过多就会出现这个问题。一般的应用程序都不会产生这个错误,然而,对于 Web 服务器会产生大量的 JSP,JSP 在运行时被动态地编译为 Java Servlet 类,然后加载到方法区,因此,有很多 JSP 的 Web 工程可能会产生这个异常。
1 2 3 4 5 6 7 8 9 10 11 12
/* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { // 使用List保持对常量池的引用,避免Full GC回收常量池 List<String> list = new ArrayList<String>(); for(int i = 0;; i++) { list.add(String.valueOf(i).intern()); } } }
使用 intern 测试运行时常量池是“永久代”的还是“元空间”的:
1 2 3 4
String str1 = new StringBuilder("计算机").append("软件").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2);
11179 "New I/O worker #168" #299 daemon prio=5 os_prio=0 tid=0x00007f3a75127000 nid=0x5cf runnable [0x00007f37278f7000] 11180 java.lang.Thread.State: RUNNABLE 11181 at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method) 11182 at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269) 11183 at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:79) 11184 at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) 11185 - locked <0x00000000e4669320> (a sun.nio.ch.Util$2) 11186 - locked <0x00000000e4669310> (a java.util.Collections$UnmodifiableSet) 11187 - locked <0x00000000e46691e8> (a sun.nio.ch.EPollSelectorImpl) 11188 at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) 11189 at org.jboss.netty.channel.socket.nio.SelectorUtil.select(SelectorUtil.java:68) 11190 at org.jboss.netty.channel.socket.nio.AbstractNioSelector.select(AbstractNioSelector.java:434) 11191 at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:212) 11192 at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:89) 11193 at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178) 11194 at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108) 11195 at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42) 11196 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) 11197 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) 11198 at java.lang.Thread.run(Thread.java:745)
class SuperClass { public static int value = 123; static { System.out.println("Super!"); } } class SubClass { static { System.out.println("Sub!"); } public static void main(String[] args) { System.out.println(SubClass.value); } }
class ConstClass { static { System.out.println("ConstClass!"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } }
此阶段的主要目的是确保 class 文件的字节流包含的信息符合虚拟机的要求,进行一部分的语义分析,主要是防止字节码中存在一些危险操作(数组越界、错误转型、跳转过头等),后来的 Java 虚拟机规范还规定了文件格式、符号引用等的验证。 不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:
文件格式验证,是要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否为 0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型。 该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。
为类变量(static 变量)分配内存和并初始化为默认值,它们被分配在方法区中。 准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。 比如static int a = 1;在准备阶段后 a 的值为 0,在初始化阶段后才变成 1,但是对常量字段(static final),在准备阶段会直接赋予用户设定的值。
我们知道不同类加载器加载的类位于不同的命名空间,它们之间是相互隔离的,这里说的隔离仅仅指它们存储位置隔离,并不是说一个自定义的类 A 使用了 java.util.List 类就会报错。 自定义的类 A 一般会使用系统类加载器加载,而 java.util.List 则会由启动类加载器加载,当加载类 A 时如果遇到了 java.util.List,会首先尝试通过系统类加载器加载,在它发现自己无法加载后,通过双亲委派模型交给父加载器加载。 如上图所示:
class A { int a; } public class JavaTest extends A { int a; @Test public void test() { a = 1; System.out.println(super.a); } }
为什么下面的报错? 类的初始化阶段有一个细节:类初始化块不能访问定义在其之后的变量
1 2 3 4 5 6 7
public class JavaTest { static { i = 0; System.out.println(i); // 报错 } static int i = 1; }
为什么输出两个’A’? 当我们 new A()时,首先为 A 分配内存空间,此时 A 已经存在了,只是还未初始化,然后调用 A 的构造函数,A 的构造函数又隐式调用了父类的构造函数。 在父类构造函数中使用 this 调用 draw(),this 实际上指向了 a 对象,平常调用方法时 this 也是隐含的。
class B { int a; B() { this.draw(); } void draw() { System.out.println("B"); } } class A extends B { int a; A() { draw(); } @Override void draw() { System.out.println("A"); } public static void main(String[] args) { A a = new A(); } }
为什么最后输出的 count2 为 0?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class SingleTon { private static SingleTon singleTon = new SingleTon(); public static int count1; public static int count2 = 0; private SingleTon() { count1++; count2++; } public static SingleTon getInstance() { return singleTon; } } public class Test { public static void main(String[] args) { SingleTon singleTon = SingleTon.getInstance(); System.out.println("count1=" + singleTon.count1); System.out.println("count2=" + singleTon.count2); } }
class Father { public static void print(String str) { System.out.println("father " + str); } private void show(String str) { System.out.println("father " + str); } } class Son extends Father { } public class Test { public static void main(String[] args) { Son.print("coder"); // 调用的是Father的print()方法 //Father fa = new Father(); //fa.show("cooooder"); // 私有方法无法调用 } }
/** * 重载方法在编译器就可以进行确定,不需要等到运行期间 */ public class StaticDispatch { static class Human { } static class Women extends Human { } static class Men extends Human { }
public void sayHello(Human human) { System.out.println("say human"); } public void sayHello(Women women) { System.out.println("say women"); } public void sayHello(Men men) { System.out.println("say men"); }
public static void main(String[] args) { StaticDispatch ds = new StaticDispatch(); Human women = new Women(); Human men = new Men(); // 编译时确定方法的调用版本是以Human作为参数的方法 ds.sayHello(women); ds.sayHello(men); } }
public class DynamicDispatch { abstract static class Human { abstract public void sayHello(); }
static class Women extends Human { @Override public void sayHello() { System.out.println("say women"); } }
static class Men extends Human { @Override public void sayHello() { System.out.println("say men"); } }
public static void main(String[] args) { Human women = new Women(); Human men = new Men(); women.sayHello(); // 实际类型是Women men.sayHello(); // 实际类型是Men } }
public class Dispatch { static class QQ {}; static class _360{};
public static class Father { public void hardChoice(QQ arg) { System.out.println("father choose qq"); }
public void hardChoice(_360 arg) { System.out.println("father choose 360"); } }
public static class Son extends Father { public void hardChoice(QQ arg) { System.out.println("son choose qq"); }
public void hardChoice(_360 arg) { System.out.println("son choose 360"); } }
public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); } }
public void print(Father father) { System.out.println(father); }
public void print(Child child) { System.out.println(child); }
public static class Father {
@Override public String toString() { return "Father"; } }
public static class Child extends Father {
@Override public String toString() { return "Child"; } }
public static void main(String[] args) { Father father = new Child(); DynamicDispatchTest dynamicDispatchTest = new DynamicDispatchTest(); dynamicDispatchTest.print(father); } }
18:29:28.186 [main] INFO c.t.l.r.RedissonTest - 调用成功, 当前 succeed:15, failed:0 18:29:33.475 [main] INFO c.t.l.r.RedissonTest - 调用失败, 当前 succeed:15, failed:1 18:29:38.750 [main] INFO c.t.l.r.RedissonTest - 调用失败, 当前 succeed:15, failed:2 18:29:39.253 [main] INFO c.t.l.r.RedissonTest - 调用成功, 当前 succeed:16, failed:2 18:29:39.756 [main] INFO c.t.l.r.RedissonTest - 调用成功, 当前 succeed:17, failed:2 18:29:40.259 [main] INFO c.t.l.r.RedissonTest - 调用成功, 当前 succeed:18, failed:2 18:29:40.763 [main] INFO c.t.l.r.RedissonTest - 调用成功, 当前 succeed:19, failed:2 18:29:45.271 [redisson-netty-2-4] INFO o.r.c.MasterSlaveEntry - master 127.0.0.1/127.0.0.1:7004 used as slave 18:29:45.274 [redisson-netty-2-14] INFO o.r.c.p.PubSubConnectionPool - 1 connections initialized for 127.0.0.1/127.0.0.1:7004 18:29:45.280 [redisson-netty-2-4] WARN o.r.c.ClusterConnectionManager - slave: redis://127.0.0.1:7001 has down for slot ranges: [[0-5460]] 18:29:45.285 [redisson-netty-2-28] INFO o.r.c.p.SlaveConnectionPool - 24 connections initialized for 127.0.0.1/127.0.0.1:7004
之后重启7001后,发现7001重新加入到了集群中:
1 2 3 4
18:34:18.539 [redisson-netty-2-29] INFO o.r.c.p.PubSubConnectionPool - 1 connections initialized for 127.0.0.1/127.0.0.1:7001 18:34:18.542 [redisson-netty-2-4] INFO o.r.c.MasterSlaveEntry - master 127.0.0.1/127.0.0.1:7004 excluded from slaves 18:34:18.542 [redisson-netty-2-4] INFO o.r.c.ClusterConnectionManager - slave: redis://127.0.0.1:7001 has up for slot ranges: [[0-5460]] 18:34:18.544 [redisson-netty-2-6] INFO o.r.c.p.SlaveConnectionPool - 24 connections initialized for 127.0.0.1/127.0.0.1:7001
/* If the key already exists in the dict ignore it. */ // c->bpop.keys 是一个集合(值为 NULL 的字典) // 它记录所有造成客户端阻塞的键 // 以下语句在键不存在于集合的时候,将它添加到集合 if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;
incrRefCount(keys[j]);
/* And in the other "side", to map keys -> clients */ // c->db->blocking_keys 字典的键为造成客户端阻塞的键 // 而值则是一个链表,链表中包含了所有被阻塞的客户端 // 以下程序将阻塞键和被阻塞客户端关联起来 de = dictFind(c->db->blocking_keys,keys[j]); if (de == NULL) { // 链表不存在,新创建一个,并将它关联到字典中 int retval;
/* For every key we take a list of clients blocked for it */ l = listCreate(); retval = dictAdd(c->db->blocking_keys,keys[j],l); incrRefCount(keys[j]); redisAssertWithInfo(c,keys[j],retval == DICT_OK); } else { l = dictGetVal(de); } // 将客户端填接到被阻塞客户端的链表中 listAddNodeTail(l,c); } blockClient(c,REDIS_BLOCKED_LIST); }
// 遍历整个 ready_keys 链表 while(listLength(server.ready_keys) != 0) { list *l;
/* Point server.ready_keys to a fresh list and save the current one * locally. This way as we run the old list we are free to call * signalListAsReady() that may push new elements in server.ready_keys * when handling clients blocked into BRPOPLPUSH. */ // 备份旧的 ready_keys ,再给服务器端赋值一个新的 l = server.ready_keys; server.ready_keys = listCreate();
/* First of all remove this key from db->ready_keys so that * we can safely call signalListAsReady() against this key. */ // 从 ready_keys 中移除就绪的 key dictDelete(rl->db->ready_keys,rl->key);
/* If the key exists and it's a list, serve blocked clients * with data. */ // 获取键对象,这个对象应该是非空的,并且是列表 robj *o = lookupKeyWrite(rl->db,rl->key); if (o != NULL && o->type == REDIS_LIST) { dictEntry *de;
/* We serve clients in the same order they blocked for * this key, from the first blocked to the last. */ // 取出所有被这个 key 阻塞的客户端 de = dictFind(rl->db->blocking_keys,rl->key); if (de) { list *clients = dictGetVal(de); int numclients = listLength(clients);
// 还有元素可弹出(非 NULL) if (value) { /* Protect receiver->bpop.target, that will be * freed by the next unblockClient() * call. */ if (dstkey) incrRefCount(dstkey);
// 取消客户端的阻塞状态 unblockClient(receiver);
// 将值 value 推入到造成客户端 receiver 阻塞的 key 上 if (serveClientBlockedOnList(receiver, rl->key,dstkey,rl->db,value, where) == REDIS_ERR) { /* If we failed serving the client we need * to also undo the POP operation. */ listTypePush(o,value,where); }
if (dstkey) decrRefCount(dstkey); decrRefCount(value); } else { // 如果执行到这里,表示还有至少一个客户端被键所阻塞 // 这些客户端要等待对键的下次 PUSH break; } } } // 如果列表元素已经为空,那么从数据库中将它删除 if (listTypeLength(o) == 0) dbDelete(rl->db,rl->key); /* We don't call signalModifiedKey() as it was already called * when an element was pushed on the list. */ }
/* Free this item. */ decrRefCount(rl->key); zfree(rl); listDelNode(l,ln); } listRelease(l); /* We have the new list on place at this point. */ } }
位图
场景
位图数据结构与 java 中的 Set 类似,占用空间小,但是并不能防止冲突,适合数据离散性比较大且对数据准确性不高的场景。
// 从第一个位(0)开始取4个位,结果是无符号数(u) bitfield s get u4 0 // 从第三个位(2)开始取3个位,结果是有符号数(i) bitfield s get i3 2 // 一次性执行多个子命令 bitfield s get u4 0 get u3 2 get i4 0 get i3 2
如果 s 的值是 c,二进制值是0110 0011,取前 4 位结果是 6(0110)。
1 2 3 4 5 6
// +1,结果为0,因为溢出了 bitfield s incrby u2 1 1 // 不执行,返回nil bitfield s overflow fail incrby u2 1 1 // 结果为3 bitfield s overflow sat incrby u2 1 1