redis概述
Redis是开源的kv存储系统,nosql数据库
注:nosql泛指非关系性数据库,不遵守sql标准,不支持acid,远超sql性能
nosql适用于:数据高并发读写、海量数据读写,对数据高可扩展性
nosql不适用:事务,基于sql的结构化查询存储,处理复杂的关系,需要即席查询
value支持string,list,set,zset(sorted set --有序集合)和hash
操作是原子性的 ,并且支持不同方式的排序
redis可以配合关系型数据库做高速缓存
redis简单操作:
启动:进入/usr/local/bin 运行redis-server /conf文件位置
结束:redis-cli shutdown, 也可以进入终端输入shutdown
多实例关闭:redis-cli -p portNumber shutdown
6379由来:6379在是手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。Redis 作者 Antirez 早年看电视节目,觉得 Merz 在节目中的一些话愚蠢可笑,Antirez 喜欢造“梗”用于平时和朋友们交流,于是造了一个词 "MERZ",形容愚蠢,与 "stupid" 含义相同。MERZ长期以来被Redis作者antirez及其朋友当作愚蠢的代名词。后来 Antirez 重新定义了 "MERZ" ,形容”具有很高的技术价值,包含技艺、耐心和劳动,但仍然保持简单本质“。
redis默认提供了16个数据库,从0开始,默认使用0号库
使用命令select <dbid>来切换数据库,如:select 8
redis使用单线程+多路IO复用
可以比方买票,许多人买票,通过代理买票,代理从柜台再买票。
graph TD
1 --> 黄牛
2 --> 黄牛
3 --> 黄牛
黄牛 --> 火车站
这里黄牛买票是单线程的,多个io复用体现在多个用户,谁的票买到了,再来拿,其他时候继续干自己的活,提高cpu运行效率。
key * 查看当前库中所有key
exists key 判断key是否存在,返回0/1
tpye key 看key是什么类型
del key 删除指定key数据
unlink key根据value选择非阻塞删除,仅将key从keyspace元数据中删除,真正的删除会在后续异步操作中。
expire key 10 给key设置10s的过期时间
ttl key 查看还有多少秒过期 -1永不过期, -2已过期
dbsize查看当前数据库的key数量
flushdb 清空当前库
flushall 通杀全部库
五大常用数据类型
String
简介
String是redis最基本的类型,一个key对应一个value,是二进制安全的,redis的string可以包含任何数据,比如jpg图片或者序列化的对象。
String类型是redis最基本的数据类型,一个redis字符串value最多512M
常用命令
set <key> <value> 添加键值对
get <key>通过key查询value
append <key><value>将给定的value追加到原值的末尾
strlen <key> 得到对应value的长度
setnx <key><value> key不存在的时候才设置key的值
incr <key> 将key中存储的数字+1,只能对数字值操作,为空则设置位1
decr <key> 反之
incrby/decrby <key> <length> 将key中的值+/-length。
注:incr是原子操作,不会被线程调度机制打断的操作,一旦开始,中间不会有任何context switch--切换到另一个线程,因为redis是单线程操作。
mset <key><value><key><value> 同时设置多个kv
mget <key1><key2> 同时获取多个vlue
msetnx <key><value><key><value>同时设置一个或多个kv,当且仅当所有key都不存在,由于原子性,一个失败都失败。
getrange <开始位置><结束位置> 获取值的范围,左闭右闭
setrange <开始位置><结束位置>覆写
setxt <key><过期时间><value>设置kv同时设置过期时间,单位秒
getset<k><v> 新换旧,设置新值同时获取旧值
数据结构
String的数据结构为简单动态字符串,是可修改的字符串,内部类似于java的arrayList,草用预分配冗余空间的方式来减少内存的频繁分配
扩容:字符串长度小于1m,扩容是加倍现有空间,如果超过1mb,扩容只会+1mb,字符串最大长度512mb
List
简介
单键多值,按照插入顺序排序,可以调价一个元素到列表的头/尾,底层是双向链表,对两端操作性能高,索引下标性能差。
常用命令
lpush/rpush <key><value><value> 从左/右插入一个或多个值
lpop/rpop <key>左边或右边吐出一个值,值在键在,值亡键亡
rpoplpop <key1><key2> 从key1列表右边吐出一个值,插到key2左边
lrange <key><start><stop> 按照索引下标获取元素,从左到右 0,-1表示取所有值
lindex <key><index> 按照索引下标获取元素 从左到右
llen <key> 获取列表长度
insert <key> before/after <value><newvalue> 在value的前/后面插入newvalue值
lrem <key> <n> <value> 从左边删除n个value
lset <key> <index> <value> 将列表key下标为index值替换为value
数据结构
List结构为快速链表quickList
元素少的时候会使用一块连续的内存存储,结构是ziplist,压缩列表
多个压缩列表用链表形式组成一个quickList
将所有的元素挨着一起存储,分配的是一块连续的内存
数量较多的时候改为quicklist
由于普通链表需要附加指针太大,浪费空间,这里的列表只存int类型的数据,结构上还需要两个额外的指针prev和next
redis将链表和ziplist组成quicklist,也就是将多个ziplist使用双指针穿起来使用,挤满足了快速插入删除新能,又不会有太大的空间冗余(用链表穿起来多个数组)
Set
简介
和list类似的一个列表功能,set可以自动排重(不能重复,且无序)
是string类型的无序集合,底层是value为null的hash表,所以crud的复杂度是O(1)
常用命令
sadd <key> <value1> <value2> 将一个或多个value元素加入key中,已经存在则忽略
smembers <key> 取出key的值
sismember <key> <value> 判断key中是否有value
scard <key> 返回元素个数
srem <key> <value1> <value2> 删除集合中某个元素
spop <key> 随机从集合中吐出一个值
srandmember <key><n> 随机从集合中取出n个值,不会从集合中删除
smove <source><destination><value> 把集合中一个值从一个集合移动到另一个集合
sinter<key1><key2> 返回交集
sunion <key1><key2> 返回并集
sdiff <key1><key2> 返回差集key1中的不包含key2中的
数据结构
set数据结构是dict字典,hash表实现。
Hash
简介
value是一个string对应的field和value映射表,适合存储对象。
用户id位查找的key,存储的value用户对象包括了姓名,年龄,生日等,而用户id这个key对应了一个redis中的key。相当于value是个键值对。
常用命令
hset <key><field><value> key集合中的filed键赋值value
hget <key><field> 从key集合field取出value
hmset <key><field1><value1><field2><value2> 批量设置hash的值
hexists <key><field> 查看hash表key中,给定域field是否存在
hkeys <key>列出hash集合中所有field
hvals <key> 列出hash集合中所有value
hincrby<key><field><increment>为哈希表key中域field加上增量
hsetnx <key><field><value>将哈希表key中的域field的值设置为value,当且仅当field不存在。
数据结构
hash对应的数据结构适量中,ziplist,hashtable,field-value长度短,个数少,使用ziplist,否则是用hashtable
有序集合Zset
简介
zset和set相似,不能重复。有序集合的每个成员关联了一个评分score,这个评分备用来按照最低分到最高分排序集合成员,成员唯一,但是评分可以重复,可以通过评分或者次序快速获得一个范围的元素。访问有序集合中间元素也很快,因此可以使有序集合作为一个没有重复成员的智能列表。
常用命令
zadd <key><score1><value1><score2><value2>将一个或多个member元素及其score加入到有序集合中。
zrange<key><start><stop> [WITHSCORES] 返回有序集key中下标在start到stop之间的元素,带有WITHSCORES可以让分数和值一起返回
zrangebyscore key minmax [WITHSCORES] [limit offset count] 返回有序集key中,所有score值介于min和max之间的成员(闭区间)。
zrangebyscore key maxmin [WITHSCORES] [limit offset count] 从大到小排列
zincrby <key><increment><value> 元素score加增量
zrem <key><value>删除集合下指定值的元素
zcount<key><min><max>统计分数区间元素个数
zrank<key><value>返回该值在集合中的排名,0开始
数据结构
等价java Map<String, Double> 给每个value权重,又类似TreeSet,元素按照score排序。
底层使用了hash和跳跃表,hash作用是关联score和value,保障value元素性,通过value可以找到score。跳跃表目的给元素value通过score排序。
Redis配置文件
unit单位:只支持bytes,不支持bit,大小写不敏感
bind 127.0.0.1 只能本机访问
protected-mode no 关闭保护模式 支持远程访问
tcp-backlog 511 队列总和是未完成三次握手+完成三次握手队列
timeout 0 多久不操作超时 0 永不超时
tcp-keeplive 检测心跳 看看连接是否活着
daemonize yes 后台启动redis
.....
Redis发布订阅
简介
发布订阅是消息通信模式,发送者pub发送消息,订阅这sub接收消息,redis客户端可以订阅任意数量频道
使用
客户端1订阅:SUBSCRIBE 频道名称
客户端2发布:publish 频道名称 内容
Redis新数据类型
Bitmaps
简介
如存abc使用二进制存储其对应ASCII码,97,98,99(01100001,01100010,01100011)。合理使用操作位可以有效提高内存使用率和开发效率。
把bitmaps想象成存0和1的数组,数组的下标在bitmaps叫偏移量。
操作:
setbit <key><偏移量><0/1> 存放
getbit <key><偏移量> 取值
bitcount <key><start><end> 开始到结束1的个数
bitop and(or/not/xor) <key><key>逻辑操作
HyperLogLog
简介
便于计算基数(不重复元素个数),如访问网站,相同ip只算一个。重复元素不再添加
操作:
pfadd <key><element> [element] 添加指定元素
pfcount <key>[key] 基数数量
pfmerge <key><key>
Geospatial
简介
地理信息,元素二维坐标,经纬度设置,查询,范围查询,距离查询,经纬度hash等。
操作:
geoadd <key> <longitude><latitude><member>[long lati member] 添加
geopos <key><member>[member] 获取指定地区坐标值
geodist<key><member1><member2>[m|km|ft|mi] 获取两个位置之间的直线距离
georadius <key><long><lati> radius[m|km|ft|mi] 以给定经纬度为中心,找某一半径内的元素
Jedis操作Redis6
- 添加dependency
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
- java代码
//创建jedis对象
Jedis jedis = new Jedis("localhost", 6379);
//测试
System.out.println(jedis.ping());
返回PONG
注:redis.conf要注释bind 127.0.0.1,否则只能本机访问ip无法访问到,protected-mode no改为yes,否则不能远程访问,只能本机访问。
具体操作调用jedis各个方法,方法名称基本和命令行一致。
简单实现手机验证码功能
//随机生成六位验证码
public static String getCode() {
Random random = new Random();
String code = "";
for (int i = 0; i < 6; i++) {
int rand = random.nextInt(10);
code += rand;
}
System.out.println(code);
return code;
}
//把随机生成的验证码存入redis
//一个手机号每天可以获取三个验证码,每个验证码有效期2分钟
public static String saveToRedis(String phone, String code) {
Jedis jedis = new Jedis("localhost", 6379);
// 手机发送次数的key
String countKey = "VerifyCode" + phone + ":count";
// 验证码的key
String codeKey = "VerifyCode" + phone + ":code";
// 每个手机每天只能发送三次验证码
String count = jedis.get(countKey);
if(count == null) {
//没发送过,设置为1
jedis.setex(countKey, 24 * 3600, "1");
// 把验证码存入redis
jedis.setex(codeKey, 30, code);
jedis.close();
} else if(Integer.parseInt(count) <= 2){
//验证码有效期间不能再获取验证码
//查询不到redis中的kv
if(jedis.get(codeKey) == null) {
jedis.setex(codeKey, 120, code);
jedis.incr(countKey);
} else {
//查询到redis中验证码还在,不更新验证码
System.out.println("验证码获取速度过快");
}
jedis.close();
} else {
//发送三次了不能在发送了
System.out.println("超过三次");
jedis.close();
}
return "seccess";
}
//检测输入的验证码是否正确
public static void getRedisCode(String phone, String code) {
Jedis jedis = new Jedis("localhost", 6379);
String codeKey = "VerifyCode" + phone + ":code";
if(jedis.get(codeKey).equals(code)) {
System.out.println("验证码正确");
} else {
System.out.println("验证码错误");
}
}
SpringBoot操作redis
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
- application.properties
#Redis服务器地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
- config Class
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
- ControllerDemo
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String) redisTemplate.opsForValue().get("name");
return name;
}
}
Redis事务
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 Redis事务的主要作用就是串联多个命令防止别的命令插队。
Multi、Exec、discard
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
事务冲突的问题
悲观锁
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
执行multi前,执行watch key1 key2... 如果在事务执行之前这些key被修改,事务将被打断。
unwatch可以解除对所有key的监视。
Redis事务三特性
单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
持久化
RDB
指定时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,他恢复时是将快照文件直接读到内存中。
有一定规则,多少秒内多少个key发生变化会写入磁盘。在redis.conf文件改变
设置持久化方式 手动/自动:redis.conf bgsave
redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程结束,用这个临时文件取代上次持久化好的文件,整个过程中,主进程是不用进行任何IO操作,保证高性能。相当于复制主进程,并且称为主进程的子进程,子进程进行持久化,主进程继续做自己的事儿。如果进行大规模数据恢复,且对于数据恢复的完整性不是很敏感,RDB币AOF更加高效。其最后一次持久化后的数据可能丢失,因为是设置多少个值发生变化后持久化,如果变化一部分,但是还没到持久化设置的值,服务崩掉,此时会丢失这部分数据。
RBD恢复
config get dir查询到rdb文件目录,关闭redis,将rdb拷贝到工作目录下,再次启动redis会直接加载数据。
优势
- 适合大规模数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
劣势
- Fork时候内存中数据被克隆了一份,大约两倍的膨胀性要考虑
- redis在fork时使用了写时拷贝基数,但是数据庞大消耗性能
- 备份周期在一定间隔时间做一次备份,如果redis挂掉,丢失部分数据。
AOF(Append Only File)
日志形式记录每个写操作,将redis执行过的所有指令记录下(不包括读操作),只许追加文件,但是不能修改文件。AOF默认不开启。conf中appendonly修改。
AOF和RDB同时开启默认取AOF数据。
备份恢复和RDB相同,文件是appendonly.aof
缓存穿透
redis查询不到数据,出现很多非正常url访问(db里没有)
解决方案:
- 对空值缓存,如果查询返回的数据为空,仍然把这个空结果缓存,过期时间可能会很短。
- 设置可访问的白名单,bitmaps,id作为偏移量,和bitmap里面id比较,如果id不在其中,进行拦截。(每次都访问bitmaps,效率一般)
- 布隆过滤器,底层也是bitmaps
- 实时监控,发现redis命中率急剧降低,排查访问对象和访问数据,设置黑名单。
缓存击穿
数据库访问压力瞬时增加,redis中的key没有出现大量过期,redis正常运行。redis某个key过期了,而大量访问查询这个key
解决方案
- 预先设置热门数据到redis中
- 实时调整key的过期时长
- 使用锁,但是会降低效率
缓存雪崩
数据库压力变大,极少时间内,出现了大量key过期
解决方案
- 构建多级缓存架构:nginx缓存+redis缓存+其他缓存
- 使用锁或队列:避免大量请求落在底层存储上,但是会降低效率,不适合高并发
- 设置过期标志更新缓存,设置提前量,如果过期会触发通知另外的线程在后台更新key的缓存
- 将缓存失效时间分散开,设置成随机值。
分布式锁
java api不能提供分布式锁问题,而系统分布在不同的服务器上,单机的并发锁失效,需要一种跨jvm的互斥机制来控制共享资源的访问,这就是分布式锁。
主流方案:
- 基于数据库实现分布式锁
- 基于缓存 如redis
- 基于zookeeper
每一种分布式锁都有各自优缺点
- 性能:redis最高
- 可靠性: zookeeper最高
解决方案
redis命令:
SETNX key value
expire key time
如果担心原子性问题
set key value nx ex time
注意:
SETEX key second value
= SET key value EX second
释放锁:del,或者设置过期时间等他过期也能自动释放锁
可能出现的问题
abc三个线程同时操作想要上锁,a先上锁了,过期时间为10s,在具体操作之前A卡住了,并且超过10s,锁过期释放了,但是A的操作并没有结束,而此时B抢到了锁,进行B的具体操作,此时A反应过来了,手动释放锁,本来想释放A的锁,但是实际释放了B的锁
A获得锁-A卡顿-A锁过期-B获得锁-A进行具体操作-A释放锁----此时A释放的是B的锁。
- 解决:UUID防误删,设置锁的val时候设置为唯一的uuid,解锁的时候判断uuid是否一致再进行释放,防止错误释放。
另一个问题:a和b两个操作,a先上锁,再进行具体操作,之后释放锁(del)的时候,已经判断号uuid一样了,但是此时锁正好到了过期时间,自动释放了,而此时B获取到了这个锁,进行具体操作,而A还没结束,可能会释放掉B的锁(原因缺乏原子性)
解决:使用lua脚本(支持原子操作)