前言

参照 《redis深度历险-核心原理与应用实践》

一、redis基础结构简介

1. 五种基础数据结构

a. string

简介:字符串是redis最简单的数据结构,内部表示是一个字符数组。redis所有的数据结构都以唯一的key字符串作为名称,通过key获取value。

特性:

  • 动态字符串,采用预分配冗余空间方式减少内存的频繁分配(当字符串长度小于1MB时,扩容是加倍现有空间;大于1MB时,每次多扩1MB)
  • 字符串最大长度是512MB

操作:get/set/mget/mset/setex(设置过期时间)/setnx(不存在则执行创建)/incr(整形+1)/incrby(整形+部分数值)

位图:字符串由多个字节组成,每个字节由8bit组成,可将一个字符串看成很多bit的组合,这便是bitmap

b. list

简介:双向链表,插入和删除操作很快,时间复杂度为O(1),索引定位较慢

操作:rpush/lpop(队列);rpush/rpop(栈);lindex/lrange/ltrim(O(n)慎用);

底层优化(快速列表):

redis的list底层不是简单的linkedlist

  • 在列表元素较少的情况下,会使用连续的内存存储(ziplist),它将所有的元素彼此挨在一起存储,分配的是连续的内存空间
  • 当数据较多时,转换成quicklist。由于普通链表中,指针消耗存储空间较大,还会加重内存碎片化,因此redis将链表和ziplist结合起来,组成quicklist,如下图所示。quicklist既满足了快速插入删除的性能,又不会出现太大的空间冗余

c. hash

简介:无序字典,内部存储很多键值对,实现使用数组+链表形式

特点:

  • redis字典的值只能是字符串,rehash的方式与java的hashmap不一样,采用渐进式rehash策略
  • 当hash移除了最后一个元素后,旧的hash的数据结构自动删除,内存被回收

渐进式rehash:

  • 在rehash的同时,保留新旧两个hash结构
  • 查询时会查询两个hash结构,在后续的定时任务和hash操作指令中,渐进式将旧的hash内容一点点迁移到新的hash结构中。迁移完成后,使用新的hash取代之

操作:hset/hgetall/hlen/hget/hmset/hincrby(用于整形增加数值)

d. set

简介:内部是无序键值对,相当于特殊的字段,value为null

特点:set最后一个元素被移除后,数据结构被删除,内存被回收

操作:sadd/smembers/smember(查询元素是否存在)/scard(取长度)/spop(弹出一个)

e. zset

简介:类似于java的scortedSet和hashmap的结合体

特征:

  • 是一个set,可以保证内部value的唯一性
  • 可以赋予每个value一个值score,代表这个value的排序权重,内部实现为跳跃列表
  • zset最后一个元素被删除后,数据结构自动删除,内存被回收

操作:zadd/zrange(按score排序输出)/zrevrange(逆序输出)/zscard(获取长度)/zscore(获取指定值的score)/zrank(排名)/zrangebyscore(按分数区间遍历)

跳跃列表:由于zset需要支持快速随机插入和删除,因此不使用数组实现;由于需要按数值排序,链表不支持二分查找,纯链表不能满足需求。此时,便使用跳表。跳表特征如下:

  • 最下面一层元素串起来,每隔几个元素挑选出一个代表,再将代表串起来,按此方法选出二级代表,三级代表...
  • 定位插入点时,先从顶层开始,逐渐下潜到下一级,最后下潜到最底层

2. 容器型数据结构的通用规则

list/set/hash/zset四种数据结构共享以下两条规则:

  • create if not exist:如果容器不存在,就创建一个再进行操作
  • drop if no elements:容器中元素没有数据,就删除容器

3. 过期时间

特点:

  • redis所有数据类型都可以设置过期时间,时间到了,redis自动删除对象
  • 过期时间是以对象为单位的,例如,hash结构过期是整个hash对象的过期,不是某一个子key过期
  • 如果一个字符串已经设置了过期时间,如果调用set方法修改它,它的过期时间会消失

二、分布式锁

1. redis分布式锁概述

简介:为了限制分布式程序并发问题,需要使用分布式锁

思路:

方法1. setnx(set if not exists)设置锁;del释放锁

问题:del指令若没有被调用,则会陷入死锁

方法2. setnx,之后运行expire,为锁加上过期时间,避免陷入死锁

问题:expire没有顺利执行,会造成死锁

方法3. redis为解决上述问题,提供了 setnx name value ex expiretime 指令,setnx和expire组成了原子指令

2. 超时时间

超时问题概述:方法3解决expire与setnx非原子的问题,但不能解决超时问题。问题如下:

  1. 如果加锁和释放锁之间的逻辑执行的时间过长,容易导致锁超时,此时第一个持有锁的线程没有处理完,第二个线程就重新持有锁,导致临界区代码不能严格串行执行
  2. 其他线程可能会释放redis的锁

解决方法:

对于问题2,可采用在 setnx时,设置随机数,在执行删除指令时,匹配随机数,避免别的线程误删的方法

tag = random.nextint()
if redis.set(key, tag, nx=True, ex=5):do_something()redis.del_if_equal(key,tag)

由于redis中没有del_if_equal,因此可以采用lua脚本实现

if redis.call("get",KEYS[1])==ARVG[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

3. 可重入性

概述:一个线程在持有一个锁后,可多次进入临界区(例如执行递归调用时,无需再加锁)

实现:redis若需要实现可重入,需要客户端对set封装,使用 ThreadLocal存储当前持有锁数量

package redis.RedisWithReentranLock;import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;import java.util.HashMap;
import java.util.Map;public class RedisWithReentranLock {private ThreadLocal<Map<String, Integer>> locks = new ThreadLocal<>(); // 每个线程维护一个锁计数器private Jedis jedis = new Jedis(new HostAndPort("127.0.0.1", 6379));private long expire;public RedisWithReentranLock(long expire) {this.expire = expire;}// 加锁private boolean _lock(String key, long expire) {String rsp = jedis.set(key, "1", new SetParams().nx().ex(expire));return rsp != null && rsp.equals("OK");}// 解锁private boolean _unlock(String key) {return jedis.del(key) == 1;}// 获取锁private Map<String, Integer> getLock() {Map<String, Integer> lock = locks.get();if (lock == null) {lock = new HashMap<>();locks.set(lock);}return locks.get();}// 加锁public boolean lock(String key) {// 判断线程是否已经加锁,加了则无需继续加Map<String, Integer> lock = getLock();Integer lockCnt = lock.get(key);if (lockCnt != null && lockCnt > 0) {lock.put(key, ++lockCnt);return true;}// 未加锁,则请求加boolean dolock = _lock(key, expire);// 加锁成功,设置已经被线程持有一次if (dolock) {lock.put(key, 1);return true;}return false;}public boolean unlock(String key) {Map<String, Integer> lock = getLock();Integer lockCnt = lock.get(key);// 未加锁if (lockCnt == null || lockCnt == 0) {return false;}// 已被线程持有,若加锁数量大于1,则只需要计数器减1lockCnt--;if (lockCnt > 0) {lock.put(key, lockCnt);} else {_unlock(key);lock.remove(key);}return true;}public static void main(String[] args) throws InterruptedException {RedisWithReentranLock redisWithReentranLock = new RedisWithReentranLock(100);System.out.println(redisWithReentranLock.lock("reTest"));Thread.sleep(5000);System.out.println(redisWithReentranLock.lock("reTest"));System.out.println(redisWithReentranLock.unlock("reTest"));Thread.sleep(10000);System.out.println(redisWithReentranLock.unlock("reTest"));}
}

注意:以上并不是可重入锁的全部,精确一些,还需要考虑内存锁计数器的过期时间

三、延迟队列

适用场景:只有一组消费者组,不考虑消息的高可靠性(无ack保证)

1. 异步消息队列

操作:lpop/rpop lpush/rpush

redis支持多个生产者/消费者并发写入/读取数据

2. 队列空了怎么办

队列空后,消费者端会陷入 pop空转,拉高消费者的CPU消耗,redis的QBS也会被拉高。

a. 解决方法1:通常可以使用sleep解决这个问题

问题:若有多个消费者,即使每个消费者sleep,并发依旧大

b. 解决办法2:阻塞读

思路:使用blpop/brpop替代前面的 lpop/rpop (blpop key waittime)

问题:空闲连接问题。如果线程一直阻塞,redis客户端成了闲置连接,blpop/brpop会抛出异常(解决方法:捕获异常并重试)

3. 锁冲突处理

客户端请求加锁时,若加锁失败,一般有一下处理策略:

  1. 直接抛出异常,通知用户(适合用户直接发起请求的场景)
  2. sleep一会,然后重试(如果抢占锁的线程较多,会导致后续处理延迟)
  3. 将请求转移到延迟队列,稍后重试(适合异步消息处理,可以避开锁冲突)

4. 延迟队列的实现

思路:通过redis的zset实现。将消息序列化为zset的value,消息的到期处理时间作为socre,多线程轮询zset。

代码:

package redis.DelayingQueue;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import redis.clients.jedis.Jedis;import java.lang.reflect.Type;
import java.util.Set;
import java.util.UUID;public class DelayingQueue<T> {// 每个延迟任务类static class TaskItem<T> {public String id;public T data;@Overridepublic String toString() {return "TaskItem{" +"id='" + id + '\'' +", data=" + data +'}';}}private final Jedis jedis;private final String keys;private final long sleepMicroseconds;private final Type taskType = new TypeReference<TaskItem<T>>() {}.getType();  // 泛型序列化需要使用TypeReferencepublic DelayingQueue(Jedis jedis, String keys, long sleepMicroseconds) {this.jedis = jedis;this.keys = keys;this.sleepMicroseconds = sleepMicroseconds;}// 将任务加入延迟队列public void addDelayQueue(T data) {// 1. 序列化TaskItem<T> taskItem = new TaskItem<>();taskItem.id = UUID.randomUUID().toString();taskItem.data = data;String jsonTask = JSON.toJSONString(taskItem);// 2. 加入队列this.jedis.zadd(this.keys, System.currentTimeMillis() + this.sleepMicroseconds, jsonTask);}// 工作方法,轮询延迟队列,获取元素public void worker() {// 线程未被中断,就轮询while (!Thread.interrupted()) {// 获取当前时间已经到期的任务Set<String> tasks = jedis.zrangeByScore(this.keys, 0, System.currentTimeMillis());// 无任务,睡眠500毫秒if (tasks.isEmpty()) {try {Thread.sleep(500);} catch (Exception e) {e.printStackTrace();}continue;}// 获取一个任务,并抢占String taskJson = tasks.iterator().next();if (jedis.zrem(this.keys, taskJson) > 0) {TaskItem<T> taskItem = JSON.parseObject(taskJson, taskType);System.out.println("get task:" + taskItem.id);handler(taskItem.data);}}}// 任务执行句柄private void handler(T data) {System.out.println("get task data:" + data.toString());}public static void main(String[] args) throws InterruptedException {Jedis jedis = new Jedis("127.0.0.1", 6379);DelayingQueue<String> queue = new DelayingQueue<>(jedis, "testList", 5000);// 生产者队列, 放入数据Thread producer = new Thread(() -> {for (int i = 0; i < 10; i++) {queue.addDelayQueue("job:" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread worker = new Thread(queue::worker);producer.start();worker.start();producer.join();Thread.sleep(20000);worker.interrupt();System.out.println("工作线程退出");worker.join();}
}

优化:上述代码中,同一个任务可能会被多个进程取的,并在zrem中竞争,导致线程浪费了一次迭代。可以考虑使用lua脚本将zrangebyscore和zrem整合成原子操作,减少redis请求次数

四、位图

概述:redis提供位图,节省存储空间。位图不是特殊的数据结构,其实只是普通的字符串(byte数组),使用者可以用get/set直接获取或设置整个位图的内容,也可以用getbit/setbit设置位图某一位

1. 基本用法

特点:redis 的位数组是自动扩展的,如果设置了某个偏移超过了现有的内容范围,会自动将位数组进行0扩充

例子:使用位操作设置"he"字符串

h的ascii码为104(0110 1000)    e的ascii码为101(0110 0101)

127.0.0.1:6379> setbit s 1 1
(integer) 0
127.0.0.1:6379> setbit s 2 1
(integer) 0
127.0.0.1:6379> setbit s 4 1
(integer) 0
127.0.0.1:6379> get s
"h"
127.0.0.1:6379> setbit s 9 1
(integer) 0
127.0.0.1:6379> setbit s 10 1
(integer) 0
127.0.0.1:6379> setbit s 13 1
(integer) 0
127.0.0.1:6379> setbit s 15 1
(integer) 0
127.0.0.1:6379> get s
"he"

上述例子看视为零存整取,还可以进行零存零取与整存零取

  • 零存零取:getbit w 1 1; getbit w 1;
  • 整存零取:set w h; getbit w 1;

2. 统计和查找

常用指令:

  • 统计指令:bitcount位图统计 (bitcount  key [start end])
  • 查找指令:bitpos位图查找 (bitpos 键 需要查找的位 [起始字符] [结束字符]  )

3. 魔术指令 bitfield

概述: getbit和setbit指定的值都是单个位的,如果需要一次操作多个位,需要使用管道处理。redis在3.2版本之后,新增了bitfield,可以一次性操作多个位

常用指令:get/set/incrby,最多支持64位,超过64位需要使用多个子指令

例子:

键s存储如图:

1) 查询操作

127.0.0.1:6379> bitfield s get u5 0     // 第一位开始取5位 01101=13
1) (integer) 13
127.0.0.1:6379> bitfield s get u4 1     // 第二位开始取4位 1101=13
1) (integer) 13
127.0.0.1:6379> bitfield s get u3 1     // 第二位开始取3位 110=6
1) (integer) 6
127.0.0.1:6379> bitfield s get i3 1     // 第二位开始取3位(有符号) 110=-2
1) (integer) -2
127.0.0.1:6379> bitfield s get i4 1     // 第二位开始取4位(有符号) 1101=-3
1) (integer) -3

当获取位数超过限制时(无符号数63位,有符号数64位),redis会报参数错误

127.0.0.1:6379> set w testtesttesttest
OK
127.0.0.1:6379> bitfield w get u63 0
1) (integer) 4193618412526811578
127.0.0.1:6379> bitfield w get u64 0
(error) ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.
127.0.0.1:6379> bitfield w get i64 0
1) (integer) 8387236825053623156
127.0.0.1:6379> bitfield w get i65 0
(error) ERR Invalid bitfield type. Use something like i16 u8. Note that u64 is not supported but i64 is.

此时,可以使用多个子指令获取

127.0.0.1:6379> bitfield w get u63 0 get u63 64
1) (integer) 4193618412526811578
2) (integer) 4193618412526811578

2) 设置操作

set操作

127.0.0.1:6379> bitfield s set u8 8 97     // 将第8位开始的8位(第二个字符)设置为97(a)
1) (integer) 101
127.0.0.1:6379> get s
"ha"

incrby操作

127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w incrby u4 1 1     // 取 1101(13) 并加1
1) (integer) 14
127.0.0.1:6379> bitfield w incrby u4 1 1     // 取 1110(14) 并加1
1) (integer) 15
127.0.0.1:6379> bitfield w incrby u4 1 1     // 取 1111 并加1,此时溢出,折返
1) (integer) 0

bitfield提供了溢出策略子指令overflow,用户可以选择溢出行为,默认为wrap,可供选择的还有失败(fail)(报错不执行),以及饱和截断(sat)(超过范围就停留在最大或最小值)。overfilow指令只影响接下来的第一条指令,这条指令执行完后,策略会变成wrap

127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w overflow sat incrby u4 1 1
1) (integer) 14
127.0.0.1:6379> bitfield w overflow sat incrby u4 1 1
1) (integer) 15
127.0.0.1:6379> bitfield w overflow fail incrby u4 1 1
1) (nil)
127.0.0.1:6379> bitfield w incrby u4 1 1
1) (integer) 0

五、HyperLogLog

概述:统计uv时,需要对用户去重,可以采用set保存用户id。但当用户数量较多时,十分浪费空间,若统计结果不需要十分精确,可以使用HyperLogLog去重统计(标准误差在0.81%)

1. 使用方法

a. pfadd与pfcount

指令:pfadd(增加计数)和pfcount(获取计数),pfadd用法与set类似,把id增加进去就可以,pfcount和scard用法类似,直接获取计数值即可

测试例子:

package redis.HyperLogLogLearn;import redis.clients.jedis.Jedis;public class PfTest {public static void main(String[] args) {Jedis jedis = new Jedis("127.0.0.1", 6379);for (int i = 0; i < 1000000; i++) {jedis.pfadd("pftest", "user" + i);}System.out.println(jedis.pfcount("pftest"));jedis.close();}
}

运行上述代码,可见与精确值相差了 1788个,误差为0.1788%

将程序再执行一遍,发现结果不变,仍为1788,去重功能起了作用

b. pfmerge

指令:pfmerge用于将多个pf计数值累计在一起,形成新的pf值。例如,需要将两个页面的uv值合并到一起,便可使用pfmerge

例子:

更改上述程序,使用pfadd往'"pftest1"中增加999000-1000100号元素,再执行下列指令

127.0.0.1:6379> pfcount pftest1
(integer) 1097
127.0.0.1:6379> pfmerge pftest1 pftest
OK
127.0.0.1:6379> pfcount pftest1
(integer) 1001869

可以看到,经过pfmerge之后,pftest1变成了1001869,相比pftest,增加了约100

2. HyperLogLog特性与原理

a. 特性

HyperLogLog需要占用12kb,因此不适合单个用户相关的数据统计。redis对HyperLogLog数据结构进行了优化,在计数值较小时,存储空间采用稀疏矩阵,空间很小,当计数值变大,稀疏矩阵占用空间超过阈值时,才会一次性转变成稠密矩阵,才会占用12kb

b. 原理

实验一:

给定一系列随机整数,记录下低位连续零位的最大长度K(maxbit),通过K值预测随机数的数量N

package redis.HyperLogLogLearn;import java.util.concurrent.ThreadLocalRandom;public class pfPrincipleTest1 {private int maxBits = 0;public void random() {// 取32位的数long value = ThreadLocalRandom.current().nextLong(2L << 32);int bits = lowZeros(value);if (this.maxBits < bits) {this.maxBits = bits;}}// 获取最低位为1的位偏移public int lowZeros(long val) {int bitCnt = 1;long valDiv = val;while (valDiv > 0) {long bitNum = valDiv % 2;if (bitNum == 1) {return bitCnt - 1;}valDiv = valDiv >> 1;bitCnt++;}return bitCnt - 2;}static class Exp {private int loopNum;private pfPrincipleTest1 pfPrinciple;public Exp(int loopNum) {this.loopNum = loopNum;this.pfPrinciple = new pfPrincipleTest1();}public void work() {for (int i = 0; i < loopNum; i++) {this.pfPrinciple.random();}}public void printf() {System.out.printf("loop num:%d  log loop num:%.2f  maxbits:%d\n",this.loopNum, Math.log(this.loopNum) / Math.log(2), this.pfPrinciple.maxBits);}}public static void main(String[] args) {int step = 100;for (int loopNum = 100; loopNum < 10000000; loopNum = loopNum + step) {Exp exp = new Exp(loopNum);exp.work();exp.printf();step = step * 2;}}
}

运行上述程序,可得:

实验二:

由上述结果可见,K和N之间存在一定的线性相关性,N约等于2^K。由于N为整数,若N介于2^K与2^(K+1)之间,采用这种方法结果均为2^K,误差较大。为增加结果可靠性,可使用多个pfPrincipleTest1 类进行加权

改进版代码如下:

package redis.HyperLogLogLearn;import java.util.concurrent.ThreadLocalRandom;public class pfPrincipleTest2 {private int maxBits = 0;public void random(long value) {// 取32位的数int bits = lowZeros(value);if (this.maxBits < bits) {this.maxBits = bits;}}// 获取最低位为1的位偏移public int lowZeros(long val) {int bitCnt = 1;long valDiv = val;while (valDiv > 0) {long bitNum = valDiv % 2;if (bitNum == 1) {return bitCnt - 1;}valDiv = valDiv >> 1;bitCnt++;}return bitCnt - 2;}static class Exp {private final int loopNum;private final pfPrincipleTest2[] pfPrinciple;public Exp(int loopNum, int bucketNum) {this.loopNum = loopNum;this.pfPrinciple = new pfPrincipleTest2[bucketNum];for (int i = 0; i < bucketNum; i++) {this.pfPrinciple[i] = new pfPrincipleTest2();}}public void work() {for (int i = 0; i < loopNum; i++) {long value = ThreadLocalRandom.current().nextLong(2L << 32);// 随机取一个bucketint bucketKey = (int) (((value & 0x0fff000) >> 12) % this.pfPrinciple.length);pfPrincipleTest2 pf = this.pfPrinciple[bucketKey];pf.random(value);}}public double calN() {double result = 0.0;for (pfPrincipleTest2 pfPrinciple : this.pfPrinciple) {result = result + 1.0 / (float) pfPrinciple.maxBits;}double avgResult = (double) this.pfPrinciple.length / result;return Math.pow(2, avgResult) * this.pfPrinciple.length;}public void printf() {double est = this.calN();System.out.printf("loop num:%d  est:%.2f  sub:%.2f\n",this.loopNum, est, Math.abs(est - loopNum) / (double) loopNum);}}public static void main(String[] args) {int step = 100;for (int loopNum = 100000; loopNum < 1000000; loopNum = loopNum + step) {Exp exp = new Exp(loopNum, 1024);exp.work();exp.printf();step = (int) (step * 1.5);}}
}

如图,预测的值与真实值相差较小

注意:

  • 此处均值计算使用调和平均数(普通平均数容易受到离群值影响)
  • 上述算法在随机数量较少时,会因为某些bucket的maxbits=0导致倒数不可求,因此真实的HyperLogLog要比上诉代码复杂

3. pf的内存占用为什么是12KB

redis在HyperLogLog实现中用16384个桶,maxbits需要6个bit,最大可以表示maxbit=63,于是共占用内存 2^14*6/8=12KB

redis篇-基础与应用篇(上)相关推荐

  1. redis 哨兵 异步_Redis稍微往上一点点写点集群

    上篇文章记录了一点Redis的基础用法 这篇写的更往上一点点写点集群. 主从复制 一台Redis服务器有可能会崩掉出现故障,Redis向其他数据库一样,提供了复制机制,复制可以提高系统的容错能力,同时 ...

  2. 《精通QTP——自动化测试技术领航》—第1章1.5节QTP精华—对象库(上)之基础攻略篇...

    本节书摘来自异步社区<精通QTP--自动化测试技术领航>一书中的第1章1.5节QTP精华-对象库(上)之基础攻略篇,作者余杰 , 赵旭斌,更多章节内容可以访问云栖社区"异步社区& ...

  3. Redis笔记-基础篇(黑马视频教程)

    写在开头 这是我在观看黑马Redis视频教程中根据PPT和上课内容,个人写的笔记,中间有部分来源于百度,如有侵权,联系我删除. 文章目录 写在开头 NoSQL数据库简介 技术发展 NoSQL数据库 R ...

  4. Redis学习笔记1-理论篇

    目录 1,Redis 数据类型的底层结构 1.1,Redis 中的数据类型 1.2,全局哈希表 1.3,数据类型的底层结构 1.4,哈希冲突 1.5,rehash 操作 2,Redis 的 IO 模型 ...

  5. Redis学习笔记(实战篇)(自用)

    Redis学习笔记(实战篇)(自用) 本文根据黑马程序员的课程资料与百度搜索的资料共同整理所得,仅用于学习使用,如有侵权,请联系删除 文章目录 Redis学习笔记(实战篇)(自用) 1.基于Sessi ...

  6. Webpack系列-第一篇基础杂记

    系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 公司的前端项目基本都是用Webpack来做工程化的,而Webpack虽然 ...

  7. 学霸现身!博士生发18篇SCI,4篇CNS子刊,开学典礼上全场震撼

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 近日,在南京工业大学2020级研究生新生开学典礼上,一位博士研究生的发言,赢得了现场阵阵掌声.&quo ...

  8. python turtle基本语法_Python 基础语法-turtle篇

    Python 基础语法-turtle篇 今天这节课主要讲了类的概念,并引出turtle中的函数和Turtle类. -创建一个Turtle类:brad=turtle.Turtle() -定义Turtle ...

  9. JNI学习开始篇 基础知识 数据映射及学习资料收集

    JNI学习开始篇 基础知识 数据映射及学习资料收集 JNI介绍 JNI(Java Native Interface) ,Java本地接口. 用Java去调用其他语言编写的程序,比如C或C++. JNI ...

最新文章

  1. Google推出的新服务:Docs Spreadsheets
  2. android百度地图覆盖物异步加载图片,Android 百度地图marker中图片不显示的解决方法(推荐)...
  3. 11旋转编码器原理_旋转编码器的原理是什么?增量式编码器和绝对式编码器有什么区别?...
  4. centos7+svn+mysql_Linux下安装SVN服务(CentOS7下)
  5. python删除列表中的元素
  6. ubuntu16.04 使用 rc.local 自启动加载 python 脚本
  7. 两个平面的位置关系和判定方程组解_精品获奖教案 1.2.4平面与平面的位置关系(2)教案 苏教版必修2...
  8. mini2440的串口在Qt上实现
  9. 2022年成考(专升本)考试政治练习题及答案
  10. shareX截图工具提示:shareX\Tools\ffmpeg.exe不存在。解决方案2020年
  11. lineageos信号叉号_Z1刷lineage os 14.1 15.1官方版后信号上叉号的清除教程
  12. 概率论基础知识(三) 参数估计
  13. 十年磨一剑,你要的低代码平台在这里
  14. win2000修改主机名称
  15. 差分放大电路——直接耦合放大电路基本元件
  16. 通过 iso 重装阿里云 ECS
  17. mysql字符集和校对规则
  18. 怎么样去提升网站长尾词在百度搜狗360的排名?
  19. oracle 标示符太长,Oracle PLS-00114: 标识符 ' ' 太长
  20. kubectl查看node状态_K8S故障排除方法 - 笃行之 - 博客园

热门文章

  1. 掌握了 Kubernetes 这 16 个核心概念后,女朋友再也不担心我玩不转容器集群化了!...
  2. 博途软件中多重背景块的建立_怎么理解多重背景数据块?
  3. 中国商用单相电能智能表行业市场供需与战略研究报告
  4. 电动机保护器的c语言程序设计解释,电动机保护器(GY102-C)
  5. NNDL 作业5:卷积
  6. Spring Boot 整合 阿里开源中间件 Canal 实现数据增量同步!
  7. 比微信小程序早出发的PWA
  8. SRAM6264和EPROM2764相互兼容
  9. 一位电子工程师从学校到工作岗位的项目经历,或许你可以借鉴
  10. crontab -e修改默认编辑器