###Redis版本:Redis5.0.14
###人生建议,一定要按照对应的版本阅读,否则会相当难受
一、什么是NoSQL
NoSQL=not only sql(不仅仅是SQL)
关系型数据库:列+行,同一个表下数据结构是一样的
非关系数据库:数据存储没有固定的格式,并且可以进行横向扩展
NoSQL泛指非关系型数据库,在超大规模的并发时代,关系型数据库很难应付。
· RDBMS:
1. 组织化结构
2. 固定SQL
3. 数据和关系都存在单独的表中(行列)
4. DML(数据操作语言)、DDL(数据定义语言)
5. 严格的一致性:原子性、一致性、隔离性、持久性
6. 基础的事务
· NoSQL:
1. 不仅仅是数据
2. 没有固定的查询语言(受存储的结构影响)
3. 键值对存储(redis)、列存储(HBase)、文档存储(MongoDB)、图形数据库(Neo4j,不是存图形,放的是关系)
4. 最终一致性:基本可用、软状态/柔性事务、最终一致性
二、redis是什么?
Redis=Remote Dictionary Server(远程字典服务)
C语言编写的、支持网络、基于内存、可持久化的日志型、k-v数据库,支持多种语言的API
为了保证效率,在工作时数据都是缓存在内存中。
三、redis的五大基本类型
· String字符串
string是redis最常用的数据结构,也是最经常使用到的类型,并且其他类型都是在string的基础上构建的;
string最大存储的值是512MB,string类型的底层数据结构不一定是字符串。
应用场景:
1. 缓存功能
2. 计数器
3. 统计多单位的数据量
4. 保存用户session
· List列表
list是用于存储多个有序的字符串,有序是基于index有序,不是基于value大小的;
一个list中可以存储有一个或多个元素,支持存储2^32-1个元素;
redis可以从list的两端push和pop元素,支持读取指定范围的元素,支持读取指定下标的元素;
list是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的。
应用场景:
1. 消息队列
2. 文章列表或数据分页展示
· Set集合
set和list类型相似,都可以用于存储多个字符串元素的集合;
set中不允许出现重复的元素。而且set中的元素是没有顺序的,不支持下标操作;
set使用的是hashTable构造的,因此复杂度为O(1),支持集合内的增删改查,支持多个集合之间的交集、并集、差集操作。
set的底层数据结构也不只有一种
应用场景:
1. 标签
2. 共同好友
3. 统计网站的独立IP
· Zset(有序集合)
zset保留了set中元素不能重复的特性,但是zset给每个元素新增了一个point来支持有序;
应用场景:
1. 排行榜
2. 利用point来做带权重的队列
· hash哈希
hash是一个k-v集合,它是一个string类型的field(字段)和value的映射表;
前面提到了redis是一个k-v型数据库,因此hash数据结构相当于在redis的大hash的value中又套了一层k-v型数据
应用场景:
1. 用于存储关系型数据库中表记录
2. 用于存储用户相关信息,优化用户相关信息的获取
四、redis的对象和底层数据结构
· redis整体的存储结构
redis内部是一个大的hashmap,内部是数组实现的hash,key冲突通过拉链的方法去解决,
每个dictEntry为一个k-v对象,value为定义的redisObject
typedef struct dictEntry {
void *key; // key
union { // 用union存储value
void *val; // 指向redisObjet
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 拉链解决哈希冲突,所以需要有指向下一个dictEntry的指针
} dictEntry;
· redisObject
redisObject用于存储一个实际的对象,使用位域存储信息,可以尽可能的压缩内存。
typedef struct redisObject {
unsigned type:4; // 表示该对象的类型(string/list/hash/set/zset)
unsigned encoding:4; /* 表示编码方式,即使两个对象的type相同,
可能也会因为两者的大小不同采用的底层存储方式不同,
编码就是用来表示底层数据结构的 */
unsigned lru:LRU_BITS; /* 内存回收有关,用conf文件选择:
LRU:最近最久未使用算法,当空间满的时候,优先回收最久没有使用的;
LFU:最近最少使用算法,当空间满时,优先回收最小访问频率的 */
int refcount; // 记录被引用次数
void *ptr; // 指向具体的数据结构
} robj;
· redisObject的八种基础数据结构
某些类型的对象(比如string、hash)可以在内部以多种方式表示,
redisObject中的encoding字段就是用来表示实际存储结构的。
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
其中quicklist是ziplist的封装,stream在5.0.14还没实现,所以底层数据结构有八种:
int、embstr、raw、ht、linkedlist(被quicklist取代)、ziplist、intset、skiplist
对应五种类型:
1. string:int、embstr、raw
2. list:ziplist、quicklist
3. hash:ziplist、hashtable(ht)
4. set:hashtable、intset
5. zset:ziplist、skiplist
五、String存储结构
· String的保存方式
1. 如果长度小于等于20位且是数字,则用int来存储;
确保在一个long整型范围内,2^64的长正好是20位。
2. 当长度不超过44字节,使用embstr编码;
3. 其他的使用raw编码。
· SDS(简单动态字符串):
embstr和raw都是SDS的编码;
redis针对不同长度的字符做了相应的数据结构
#define SDS_TYPE_8 1
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 字符串长度
uint8_t alloc; // 字符串最大长度
unsigned char flags; /* 表示柔性数组的类型,标识给柔性数组分配的大小
用的是第三位保存一个数的方式记录,这里是SDS_TYPE_8,所以在第三位保存001就行;*/
char buf[]; // 柔性数组,实际存储数据的地方
};
· embstr:
embstr只分配一次内存空间,使redisObject和sds是连续的内存,查询的效率会快很多(内存连续加快寻址速度);
也正是因为redisObject和SDS是连续的,所以当字符串增加时长度会增加,需要整个Object和SDS都需要重新分配内存,会影响性能;
所以redis用embstr实现一次分配之后,只允许读,如果修改数据,那么它就会转成raw编码,不再使用embstr编码
· 为什么使用SDS结构体,不使用C语言的字符串?
1. SDS获取字符串长度快,直接读取len字段,复杂度为O(1);
C语言查询字符串长度需要遍历整个字符串,复杂度为O(N)。
2. 因为有alloc字段记录最大长度,len记录当前长度;
所以不用担心buf溢出的情况。
3. 减少了修改字符串对性能的影响,C语言修改字符串要重新分配内存。
4. 二进制安全:C语言字符串以"\0"作为结尾,导致一些二进制文件无法正确读取;
SDS采用len字段控制字符串的读取长度,支持使用"\0"。
5. SDS的字符串结尾仍然是"\0"结尾,保证了支持C语言的部分函数。
· SDS空间分配
新增:
1. 当前alloc>= newlen,直接返回
2. 如果alloc< newlen< SDS_MAX_PREALLOC (SDS_MAX_PREALLOC==1024*1024,也就是1M,64位)
翻倍扩容
3. 如果alloc< newlen && newlen> SDS_MAX_PREALLOC
扩容到SDS_MAX_PREALLOC
5. 判断newlen的SDS_TYPE与旧的SDS_TPPE是否一致;
如果是一致,在原SDS上remalloc;
如果不一致,重新分配内存,再free掉旧的内存。
缩容:
不会立即使用内存重新分配来回收缩短后多余的字节,而是将这些字节的数量记录下来,等待后续可能的使用。
同时,redis不允许string超过512M
· 为什么小于44字节的使用embstr编码?
1. 查询速度快(寻址快)
2. embstr用的是sdshdr8结构
已知redisObject占用空间为4+4+24+32+64= 128bits= 16btyes
已知sdshdr8头占用空间为1+1+1+1=4字节
有因为jemalloc最小的arean为64字节,所以embstr为64-16-4=44bytes
六、List存储结构
· List的保存方式
1. 在redis3.2之前:
当list中元素长度小于64字节,list中数据个数小于512个,使用ziplist
不然使用linkedlist
2. redis3.2 ~ redis5.0.14(文章版本):
使用基于ziplist的quicklist
· ziplist(压缩列表):
ziplist是一种压缩链表,它所存储的内容都是在连续的内存中;
ziplist的数据结构并没有被实际定义出来,而是通过宏定义从内存中直接提取出来;
适合存储对象元素不大而且每个元素也不大的情况。
ziplist的存储格式:
zlbtyes:用于记录整个ziplist占用的内存
zltail:记录列表尾节点距离起始地址有多少字节
zllen:记录ziplist中包含的节点数量
entry:各个节点
zlend:用于标记ziplist末尾
· zlentry:
ziplist中的entry和ziplist一样没有一个实际的存储结构;
但是可以从接收entry的结构体zlentry中了解如何存储的:
typedef struct zlentry {
unsigned int prevrawlensize; // 前一个节点len字段的长度
unsigned int prevrawlen; // 前一个节点的长度
unsigned int lensize; // 当前节点len的长度
unsigned int len; // 当前节点的长度
unsigned int headersize; // prevrawlensize + lensize.
unsigned char encoding; // 编码方式
unsigned char *p; // 指向节点起始位置
} zlentry;
· ziplist为什么不适合量多的元素:
1. ziplist存储内容在内存中是连续的,所以插入的复杂度是O(N),即每次插入都要重新进行remalloc做内存扩展;
2. 如果remalloc超过了ziplist内存大小,还会重新分配内存,并将内容复制到新的地址;
3. 如果数量大的话,重新分配内存和copy内容会消耗大量时间。
· zlentry字段prevrawlen解析:
prevrawlen:记录前一个节点的长度,单位是字节;
前一个节点字节长度小于254时,prevrawlen只需要一个字节(2^8);
否则需要五个字节来进行存储,第一个固定为0xFE(254);
作用是可以根据这个属性和当前的位置快速定位到上一个节点的位置,支持反向遍历
· quicklist(快速列表):
quicklist是ziplist和linkedlist的结合,将likedlist按段切分,每一段用ziplist来紧凑内存,ziplist之间使用双向指针链接。
typedef struct quicklist {
quicklistNode *head; // quicklist头
quicklistNode *tail; // quicklist尾
unsigned long count; // 所有quicknode中ziplist的数量
unsigned long len; // quicklist中quicknode的数量,也就是quicklist的长度
int fill : 16; // ziplist的大小,由conf决定
unsigned int compress : 16; // quicklist的压缩深度,由conf决定
} quicklist;
· quicklist字段解析:
1. fill:控制着ziplist大小,存放着list-max-ziplist-szie的大小;
qicklist内部默认单个ziplist长度为8bytes,超过这个字节数,就会新起一个ziplist。
2. compress:压缩深度;
quicklist下面是用ziplist组成的,为了更进一步节约空间,Redis还会对ziplist使用LZF算法压缩存储;
一般情况下最容易访问的是两端的数据,中间的数据被访问的频率比较低;
压缩深度的作用就是控制用LZF不压缩多少个ziplist,
比如为了支持快速pop/push,quicklist的首尾的ziplist一般是不压缩的,compress就是1。
quicklistLZF(被LZF压缩后的ziplist)
typedef struct quicklistLZF {
unsigned int sz; // 被LZF压缩后的大小
char compressed[]; // 压缩后的数据
} quicklistLZF;
· quicklistNode:
quicklistNode是quicklist中实际的节点,里面封装着存折数据的ziplist(quicklistLZF)
quicklistNode也是双向性的体现
typedef struct quicklistNode {
struct quicklistNode *prev; // 前一个节点
struct quicklistNode *next; // 后一个节点
unsigned char *zl; // 指向ziplist或者quicklistLZF
unsigned int sz; // ziplist的总字节数
unsigned int count : 16; // ziplist中entry的总个数
unsigned int encoding : 2; // 是否采用了LZF压缩,1表示压缩过,2表示为压缩过
unsigned int container : 2; // 是否启用ziplist来存储,1表示不启用,2表示启用
unsigned int recompress : 1; // 记录是否被压缩过
unsigned int attempted_compress : 1; // node can't compress; too small
unsigned int extra : 10; // 预留空间
} quicklistNode;
· 为什么会出现quicklist这种数据结构?
1. 双向链表,插入和删除效率高,但是对内存不友好,而且还需要存储额外的前后指针;
2. 数组为代表的连续内存,插入和删除时间复杂度高,但是因为局部性原理对内存友好;
3. 于是利用ziplist的内存连续性,再封装只使用一个双向链表,就有了quicklist数据结构。
七、Hash存储结构
· Hash的保存方式
和之前的list一样,hash再存储时也考虑了连续内存和非连续内存的优缺点。
1. 当Hash中数据项比较少的情况下,使用ziplist进行追加保存,先保存key再保存value;
2. 随着数据的增加,ziplist就可能会转成dict,使用key-value键值对保存。
根据conf决定:
1. hash-max-ziplist-entries
2. hash-max-zip-value
· dict(字典/hashtable):
dict字典记录的是key-value的对应关系
typedef struct dict {
dictType *type; // dict的相关操作函数
void *privdata;
dictht ht[2]; // 两个dictht,ht[0]是实际使用的dictht,ht[1]是rehash时的副本
long rehashidx; // 记录rehash状态,-1表示没有进行rehash
unsigned long iterators; /* number of iterators currently running */
} dict;
· dictht(dict hashtable)
每个dict中都有两个dictht,通常情况下只有一个hashtable是有值的;
子啊dict扩容缩容的时候,需要渐进式rehash,把ht[0]搬迁到ht[1]中;
搬迁结束后,删除旧的ht,使用新的dictht。
typedef struct dictht {
dictEntry **table; // hashtable
unsigned long size; // 表的大小
unsigned long sizemask; // 用于计算key放到哪个桶,大小为size-1
unsigned long used; // 表示已经使用了的entry的个数
} dictht;
size:dickht中的size每次都是尽量按照2^n来分配的
sizemask:用于计算key应该放到哪个桶,用&与运算代替取模运算
· dictEntry
真正用于存储key-value键值对的结构
typedef struct dictEntry {
void *key; // key
union { // 用union存储value
void *val; // 指向value
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 拉链解决哈希冲突,所以需要有指向下一个dictEntry的指针
} dictEntry;
· rehash
当遇到哈希冲突时,解决方法是采用的是链地址法,这样相比于开放地址法来说比较快,
但是当一条链过长,还是会损耗性能,就需要rehash扩容;
当hash表中的元素逐渐删除的越来越少,说明一维数组空洞太多,浪费了空间,就需要缩容rehash。
扩容rehash条件:
判断是否正在进行持久化,选择不同的负载因子;
一般情况下当元素个数==第一位数组长度(used/size)的时候,就会开始扩容
再redis做bgsave(RDB后台持久化)时,为了减少内存页的过多分离(Copy On Write),redis不回去扩容;
但是如果负载因子到达了5时,就会强制扩容,不管是不是再持久化
缩容rehahs:
缩容的条件时元素低于数组长度的10%,并且缩容不考虑是否再持久化
缩容不需要考虑持久化的原因:缩容申请的空间比较小,同时会释放一些已经使用的内存,不会最大系统压力;
扩容考虑持久化的原因:尽可能减少内存页过多分离,系统需要更多的开销去回收内存。
· rehash步骤
1. 为ht[1]分配内存,2的幂
2. 修改rehashidx,用于记录rehash的进度
3. 将ht[0]中的节点迁移到ht[1]中修改rehashidx
4. 迁移完毕,rehashidx置为-1
5. 释放ht[0],将ht[1]作为ht[0]
渐进式rehash:
如果dictentry过多,一次性完成迁移工作会长时间阻塞redis的运行。
1. 所以在每次增删改查之后,迁移最多10*n个桶,把总时间分散到每一次对dict的操作中
2. 如果服务器长时间处于空闲状态,为了重复利用时间,设置了定时函数每次拿出1ms执行rehash操作
注意点:
1. rehash过程中,dictAdd,只插入到ht[1]中,确保ht[0]只减不增。
2. rehash过程dictFind和dictDelete,会在两个dictht中都操作
3. dict默认的hash算法是SipHash
4. redis在持久化时,负载因子不同
· 迭代器
迭代器按照字段int safe分成两种:
1. safe==1 安全迭代器:
支持边遍历边修改,不支持rehash
2. sefe==0 非安全迭代器:
不支持边遍历边修改,但是支持rehash
· 注意点:
1. ziplist转为dict的操作是不可逆的
2. 尽可能的使用ziplist来作为hash底层实现;
长度尽量控制在1000以内,否则由于存取操作时间复杂度O(n),长列表会消耗大量CPU性能;
3. ziplist和dict之间的转化条件可以在conf中修改;
八、Set存储结构
· Set的保存方式
Redis的集合内部的键值对是无序、唯一的。
它的内部实现相当于一个特殊的字典,字典所有的value都是null;
set类型的底层编码包括hashtable和intset。
1. 当存储的数据都是整数 且 存储的数据元素小于 set-max-intset-entries使用intset
2. 不能同时满足这两个条件,Redis就是用dict来存储集合中的数据
· intset(整数集合)
intset是redis由于保存整数值的集合抽象数据结构,它可以保存16、32、64位的整数值,
且集合中不会出现重复数据,数据也是从小到大存储的。
typedef struct intset {
uint32_t encoding; // 表示元素类型
uint32_t length; // 元素个数
int8_t contents[]; // 柔性数组,没有重复元素,从小到大排列
} intset;
· intset的操作
查询:
intset采用的是折半查找时间复杂度位O(logN)
插入:
因为intset是按照不同类型存储的,所以当新插入的元素大于当前intset时,为防止溢出,会对其进行升级。
1. 计算newValue的编码类型
2. 比较newencoding是否大于当前intset的encoding
3. 如果不大于,说明不会溢出,查找到插入位置,remalloc内存,更新intset的len字段
4. 如果大于,说明可能溢出,更新当前intset编码
5. remalloc新的内存空间,更新len字段
6. 按照新的encoding,移动以前的元素到新的内存地址
7. 插入元素
· intset的特点
1. 提升灵活性
自动升级数据类型来适应新元素,可以将任意类型的整数添加至集合,不需要担心类型错误
2. 节省内存
会根据存储对象的类型尽可能的使用少的内存
3. 不支持降级
4. 添加和删除都需要remalloc,性能损耗大
· 注意点:
1. intset转dict是不可逆的
2. set是不允许重复的
3. 支持交并补
4. set-max-intset-entries可以在conf中修改
九、Zset存储结构
· Zset的保存方式
Zset有序集合和Set集合有着必然的联系,他保留了集合不能有重复成员的特性,但不同的是,
Zset中的元素是有序的,但是他和列表的根据index有序不同,Zset给每一个元素设置了一个point作为排序的依据。
Zset的底层编码有两种数据结构,ziplist和skiplist
· ziplist排序
每个集合元素使用两个紧挨着的entry来保存,第一个entry保存member,第二个元素保存point
· skiplist(跳表)
skiplist是与dict结合来使用的
typedef struct zset {
dict *dict; // 存储skiplist中的字段和socre的对应关系
zskiplist *zsl; // 用于排序
} zset;
skiplist结构:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头节点、尾节点
unsigned long length; // 节点数量
int level; // 当前表内节点的最大层数
} zskiplist;
一般的链表,使用next指针跳转到下一个节点;
跳表的节点有多层指针,使用不同层的指针就能跨过中间的多个节点,实现加速。
header:指向跳表的头节点,通过这个指针直接定位表的头节点;
tail:指向跳跃表的表尾节点,通过这个指针直接定位到表的尾节点;
level:记录当前表内,层数最高的那个节点有多少层
length:可以直接返回跳表中元素的个数
· skiplistNode
skiplistNode和双向链表的节点类似,只是有多层指针
typedef struct zskiplistNode {
sds ele; // member对象
double score; // point分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 这一层跨越的节点数量
} level[]; // 层
} zskiplistNode;
level:一个数组,每一层都有自己的前进指针和跨度;
每次创建一个新的跳表,程序都根据幂次定律(类似于随机丢0和1,丢到1就加一层,丢到0就停止)选择层数
backward:用于从表尾向表头遍历时使用,与前进指针不同,一个节点只有一个后退指针,一次只能退一个
score:用于比较大小排序
ele:成员是唯一的,但是score是可以重复的;当score相同时,按照字典序中的大小排序。
· skiplist特点:
1. 排序按照score排序,如果scor相等,按照ele来排序
2. 平均查询时间为O(logN)
· 注意点:
1. 在skiplist1的基础上创建dict原因是,skiplist是根据score查询的,
dict中存放对象和socre的映射关系,可以在O(1)查询到;
2. skiplist和dict利用指针复制的方法共享元素和分值;
3. zset-max-ziplist-entries和zset-max-ziplist-value用于切换ziplist和skiplist;
4. Zset不允许重复
十、三大特殊数据结构
· geospatial(地理位置)
1. geospatial将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。
这些数据将会存储到sorted set这样的目的是为了方便使用GEORADIUS或者GEORADIUSBYMEMBER命令对数据进行半径查询等操作。
2. sorted set使用一种称为Geohash的技术进行填充。经度和纬度的位是交错的,以形成一个独特的52位整数。
sorted set的double score可以代表一个52位的整数,而不会失去精度。(有兴趣的同学可以学习一下Geohash技术,使用二分法构建唯一的二进制串)
3. 有效的经度是-180度到180度
有效的纬度是-85.05112878度到85.05112878度
应用场景:
1. 查看附近的人
2. 微信位置共享
3. 地图上直线距离的展示
· Hyperloglog(基数)
就是不重复的数
hyperloglog 是用来做基数统计的,
其优点是:输入的提及无论多么大,hyperloglog使用的空间总是固定的12KB,利用12KB,它可以计算2^64个不同元素的基数!非常节省空间!
但缺点是估算的值,可能存在误差;
应用场景:
网页统计UV (浏览用户数量,同一天同一个ip多次访问算一次访问,目的是计数,而不是保存用户)
传统的方式,set保存用户的id,可以统计set中元素数量作为标准判断。
但如果这种方式保存大量用户id,会占用大量内存,我们的目的是为了计数,而不是去保存id。
· Bitmaps(位存储)
Redis提供的Bitmaps这个“数据结构”可以实现对位的操作。
Bitmaps本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作。
可以把Bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做偏移量。
单个bitmaps的最大长度是512MB,即2^32个比特位。
应用场景:
两种状态的统计都可以使用bitmaps,例如:统计用户活跃与非活跃数量、登录与非登录、上班打卡等等。
十一、数据库事务与Redis事务
事务的本质:一组命令的集合
· 数据库事务:
1. 数据库事务通过ACID(原子性、一致性、隔离性、持久性)来保证
2. 数据库中除了查询以外的操作都会对数据造成影响,事务处理能够保证一系列操作完全执行或者完全不执行;
当一个事务被提交之后,该事务中的任何一条SQL语句在被执行的时候,都会生成一条撤销日志。
· Redis事务:
1. redis事务提供了"将命令打包并顺序执行"的机制,并且事务在执行的期间不会中断;
服务器在执行完事务中所有命令之后,才会继续处理其他客户端的洽谈命令(Redis是单线程的)
2. redis事务由开始(muiti)、执行(exec)、撤销(discard)
3. redis事务为了追求高效,牺牲了一定了原子性
· Redis事务的特点
1. 代码语法错误(编译时异常),所有命令都不执行
2. 代码逻辑错误(运行时异常),其他命令都正常执行(不保证原子性)
· 为什么redis事务不保证原子性
1. 语法错误是由编程错误造成的,而这些错误应该是在开发过程中被发现,而不是出现在生产环境中
2. 不采用回滚来保证原子性,可以保持简单和快速
· 并发控制:
1. 悲观锁:认为什么时候都会出现其他线程的干扰,无论做什么操作都会加锁;
2. 乐观锁:认为不会出现问题,所以不上锁,在更新的时候检查一下是否有人修改过;
redis利用Watch监控某个变量和Unwatch来实现乐观锁
03-01
940
![](https://csdnimg.cn/release/blogv2/dist/pc/img/readCountWhite.png)
“相关推荐”对你有帮助么?
-
非常没帮助
-
没帮助
-
一般
-
有帮助
-
非常有帮助
提交