
本文共 17349 字,大约阅读时间需要 57 分钟。
Redis 学习手册
介绍 Redis 缓存数据的基础知识,包括基础知识、基础操作、数据类型、通用指令、Jedis、缓存预热、缓存雪崩、缓存击穿、缓存穿透、事务、哨兵、主从复制等。
**下载与安装
作为缓存数据库,后期肯定是要部署在 Linux 系统上的,但鉴于是初学,我们先在 Windows 上操作一下,下载地址:
https://github.com/microsoftarchive/redis/releases/download/win-3.2.100/Redis-x64-3.2.100.zip ** 【提示】 此处输入一下连接到浏览器中点击回车然后自己稍稍等待即可,GitHub作为国外的网站,下载速度超慢,希望你能理解! 下载完成后解压即可。执行解压后文件夹中的 redis-server.exe 即可启动 redis:




基本操作
既然是数据库,那就少不了与数据打交道,我们先来看看如何通过redis进行数据的保存和读取。 首先是保存数据:


数据类型
【提示】(面试必备)
redis有五种常用的数据类型: string hash list set sorted_set下面分别介绍这五种类型。
string
string是redis中最简单的数据类型,也是最常用的数据类型,比如:
set name zhangsan
我们还能一次性保存多个数据:
mset name lisi age 30 gender 1
这样我们就同时设置了三个键值,当然也可以一次性取出多个数据:
mget name age gender
还可以获取字符串长度,比如:
strlen name
【注意】需要注意的是当set保存的数据,其键名已经存在的情况下,新的值会覆盖旧的值,redis提供了一种追加方式,以适应更灵活的场景:
append name abc
此时会在key为name字符串后拼接上 abc ,若name不存在,则创建新的数据。
若是数据的键值为数字,则redis会提供一些数字特有的功能,但它本质上还是字符串:set num 1
比如自增操作:
incr num
结果如下:127.0.0.1:6379> set num 1OK127.0.0.1:6379> incr num(integer) 2127.0.0.1:6379> incr num(integer) 3127.0.0.1:6379> incr num(integer) 4
自减操作:
decr key结果如下:127.0.0.1:6379> decr num(integer) 3127.0.0.1:6379> decr num(integer) 2127.0.0.1:6379> decr num(integer) 1
默认自增自减操作为加1或减1,若是想设置步长,则:incrby num 2结果如下:127.0.0.1:6379> incrby num 2(integer) 3127.0.0.1:6379> incrby num 2(integer) 5127.0.0.1:6379> incrby num 2(integer) 7自减也是如此:decrby num 2
步长还支持设置为小数,但incrby是不支持的,我们需要使用incrbyfloat:incrbyfloat num 0.5作为缓存数据库,redis的数据都是存储在内存中的,内存十分宝贵,所以我们不应该让一些垃圾数据还残留在redis中浪费资源,为此,我们需要为数据设置它的时效,即:时间一到,就删除对应的数据信息。redis有两种方式设置数据的时效:setex key seconds valuepsetex key milliseconds value这两种方式的区别在于时间单位不同,第一种设置的是秒,第二种设置的是毫秒,比如:setex number 10 5则5秒后,number数据将会被删除。最后是对数据的删除操作:del name指定需要删除的键名,则执行该操作后,对应的数据会从redis中删除。
hash
对于一个商城的秒杀业务,其访问量无疑是巨大的,为此,我们应该将一些固定不变的信息提前抽取到redis中,而不是在用户进行秒杀活动的时候再去数据库查询。比如商品的名称、商品的描述、商品的秒杀价格等等,这些都是在活动即将开始之前就确定好并且不会改变的,我们将其存放到redis中:
set product:seckill:id:001:name 榨汁机 set product:seckill:id:001:name 美的榨汁机 set product:seckill:id:001:price 120
这是一种较为规范的存储方式,以表名加业务场景加属性名作为数据的键,我们也可以将所有数据封装成一段json数据进行存储:
set product:seckill🆔001 {id:001,name:榨汁机,desc:美的榨汁机,price:120} 然而对于json字符串方式的存储,其弊端是非常明显的,因为若是需要修改商品中的数据,则修改操作就会变得非常麻烦,为此,我们可以使用hash类型来存储。 hash类型不同于string,string的特点是一个键对应一个字符串,而hash一个键会对应一组数据,而这组数组也是以键值对的形式进行存储的,对于hash的保存数据和读取数据,只需要在 set 指令的基础上加一个 h 即可:hset user name zhangsanhset user age 20
其中 user 为整个hash类型数据的键名,而 name zhangsan 和 age 20 均为hash中的数据,这两个数据也是键值对的形式。需要注意的是,hset在保存数据时需要一个属性一个属性地进行保存,而不能这样做:
hset user name zhangsan age 20
若是想同时设置多个属性,需要使用 hmset 指令:
hmset user name zhangsan age 20
通过 hgetall 指令可以将属性一次性读取出来:
hgetall user
结果如下:
127.0.0.1:6379> hgetall user1) "name"2) "zhangsan"3) "age"4) "20"
当然也可以通过 hmget 指令获取所有的属性:
hmget user name age
这种方式相比 hgetall 更加地灵活,也可以读取指定的一个属性:
hget user namehget user age
删除属性也是如此:
hdel user namehdel user age
通过 hlen 可以获取hash中属性的数量:
hlen user 通过 hexists 指令可以判断hash中是否存在指定的属性:hexists user name
存在返回1,不存在返回0。
基于hash的特殊结构,redis也提供了hash的特有功能,比如获取hash中的所有属性名和属性值:hkeys userhvals user
设置hash中的指定属性进行自增:
hincrby user age 1hincrbyfloat user age 0.5hash类型是不支持 hdecrby 和 hdecrbyfloat 的,所以若是想实现自减,可以这么做:hincrby user age -1list
list类型适用于存储多个数据,而且能够保持这些数据间的某些顺序。
list类型保持数据方式如下:lpush key value1 [value2] ......rpush key value1 [value2] ......
其中 lpush 表示从左侧添加, rpush 表示从右侧添加,比如:
lpush nums 1 2 3 4 5
使用 lrange 指令可以读取数据:
lrange nums 0 4
其中 0 4 表示需要读取的索引范围,结果如下:
127.0.0.1:6379> lrange nums 0 41) "5"2) "4"3) "3"4) "2"5) "1"
这说明 lpush 都是从左侧加入,也就是从序列的前面加入数据的,那么相应的:
rpush nums 1 2 3 4 5
rpush 就是从右侧加入,即:从序列的后面加入数据的,其读取顺序应为 1 2 3 4 5 ,结果如下:
127.0.0.1:6379> lrange nums 0 41) "1"2) "2"3) "3"4) "4"5) "5"
list类型读取数据的方式如下:
lpop keyrpop key
分别对应从左边获取和从右边获取,若是保存了这样一个数据:
rpush nums 1 2 3 4 5则 lpop 的效果如下:127.0.0.1:6379> lpop nums"1"
左边的第一个数据 1 就成功被获取了,注意了,通过 lpop 和 rpop 指令获取数据之后,该数据会从list中移除,我们再来试试 rpop 从右边获取,按道理获取到的应该就是 5 :
127.0.0.1:6379> rpop nums"5"
最后看看list中的数据发生了什么变化:
127.0.0.1:6379> lrange nums 0 41) "2"2) "3"3) "4"
有时候我们需要移除中间的某个数据,那么 lpop 和 rpop 肯定是无能为力了,为此,我们需要使用 lrem 指令:
lrem nums 1 2
它表示从nums中移除一个数据 2 ,因为list是允许数据重复出现的,所以需要指定移除的数据数量。
我们还能通过 lindex 指令获取指定索引上的数据:lindex nums 0
通过llen 指令获取list的长度,即:数据的个数:
llen nums
list类型也有其特有的功能,它可以在规定时间内获取并移除数据,实现如下:
blpop key1 [key2] timeoutbrpop key1 [key2] timeout
比如:
blpop nums nums2 num3 30
它表示从nums、nums2、nums3的左边获取数据,在30秒之内,若是30秒过后获取不到数据,则输出nil;若是在某个时间内获取到了数据,则直接结束,输出内容。
setset类型同样提供大量数据的存储,但set的优势在于查询速度更快,list类型底层实质上是一个双向链表,我们都知道,链表的查询效率是比较低的,所以若是出于性能的考虑,set绝对是更胜一筹。
set类型保持数据的方式如下:sadd key member1 [member2]
比如:
sadd nums 1 2 3
使用 smembers 指令可以获取到set中的所有数据:
smember nums删除数据:srem nums 1 2表示删除nums中的数据 1 和 2 。获取set的长度:scard nums判断set中是否包含指定的数据:sismember nums 1
set类型比较特别的地方在于它能够进行随机操作,比如随机获取set中指定数量的数据:srandmember nums 5它表示从nums中随机获取5个数据,这5个数据获取后并不会消失,仍然存在nums中,相较于接下来的这种方式:spop nums 5它表示从nums中随机获取5个数据,但这些数据都会从nums中移除掉。sorted_set
sorted_set类型支持存储大量的数据,同时还提供这些数据按某种方式进行排序的功能。
其保存数据的方式如下:zadd key score1 member1 [score2 member2]
比如:
zadd scores 95 chinese 98 math 85 english
通过 zrange 指令获取全部数据:
zrange scores 0 -1
携带 withscores 还能够将数据的分数输出:
zrange scores 0 -1 withscores
结果如下:
127.0.0.1:6379> zrange scores 0 -1 withscores1) "english"2) "85"3) "chinese"4) "95"5) "math"6) "98"
通过 zrevrange 指令可以以逆序的方式获取数据:
zrevrange scores 0 -1 withscores
删除数据:
zrem scores chinese math
这里需要注意一点,在保存数据的时候每个数据前都跟着一个数字,比如:95 chinese ,这个95其实是一个分数,它并不具有特殊的含义,通过该分数,使得sorted_set类型具有一些特殊的功能。
首先准备一些数据:zadd scores 100 zhangsan 90 lisi 95 wangwu
比如查找分数在95以下的数据:
zrangebyscore scores 0 95
其中scores是key, 0 95 是查找范围,结果如下:
127.0.0.1:6379> zrangebyscore scores 0 951) "lisi"2) "wangwu"
它还能够通过 limit 来限定查询的结果,比如查询分数在100以下的前两个数据:
zrangebyscore scores 0 100 limit 0 2按条件删除数据:zremrangebyscore scores 0 95表示删除95分以下的数据,它也支持按照索引删除数据,比如:zremrangebyrank scores 0 2它表示删除索引0到索引2的数据。
通用指令
对于各个数据类型,redis提供了各自的指令来操作,而对于所有的数据类型,它们都有着一些通用的指令用来控制,一起来看看吧。
首先是删除指定的key:del key判断指定的key是否存在:exists key获取key的类型:type key
key的时效性操作在前面已经简单接触了一下,现在来仔细了解了解,首先是为key设置有效时间:
expire key seconds # 设置有效时间,单位:秒pexpire key milliseconds # 设置有效时间,单位:毫秒expireat key timestamp # 设置时间戳,单位:秒pexpireat key milliseconds-timestamp # 设置时间戳,单位:毫秒
获取key的剩余有效时间: ttl key # 返回有效时间,单位:秒 pttl key # 返回有效时间,单位:毫秒
需要注意的是,这两个指令都能够返回key的剩余有效时间,所以若是key不存在,则返回-2;若是key存在但未设置有效时间,则返回-1;否则返回key的剩余有效时间。 将key从时效性切换为永久性:persist keyredis提供了一些常用的查询指令帮助我们了解key的信息,比如查询指定条件的key:keys pattern其中pattern是匹配模式,若是指定为 * 则查询所有key:keys *
它提供了三种匹配模式:*:匹配任意数量的任意符号?:匹配一个任意符号[]:匹配一个指定符号
为key修改名字:
rename key newkeyrenamenx key newkey
需要注意 rename 指令将当前key修改为已经存在的key时,该key的值会被覆盖,而 renamenx 会报错,所以 renamenx 能够避免覆盖的情况发生。
对所有key排序:sort
随着数据量的逐渐增大,key极易出现重复、出错的情况,大量的数据混杂在一起也很难分别处理,为此,redis提供了数据库的概念。redis为每个服务提供了16个数据库,编号为0~15,每个数据库之间的数据是相互独立的。
切换数据库:select index默认使用的是0号数据库,若是想切换至3号,则:select 3数据移动:move key db
将当前数据库指定的key移动到指定的数据库,比如将 name 移动到3号数据库(移动之后,原数据库的key就不存在了):
move name 3数据清除:dbsize # 返回当前数据库的key数量flushdb # 清空当前数据库的keyflushall # 清空所有数据库的key
Jedis
在项目开发中,我们需要使用Java来帮助我们操作redis,所以来了解一下Java操作redis的工具——Jedis。
引入依赖:redis.clients jedis 3.3.0
首先来测试一下能够连接成功:
@Testpublic void testJedis(){ // 连接redis Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println(jedis.ping());}
输出结果:
PONG
输出 PONG 则说明连接成功了,那么jedis该如何操作redis呢?
操作方法与在redis中的操作一模一样,所以我们可以直接调用同名的方法即可,比如保存一个string类型的数据:@Testpublic void testJedis(){ Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.set("name","zhangsan"); String name = jedis.get("name"); System.out.println(name);}
若是保存list数据,则调用 lpush 或 rpush 方法:
@Testpublic void testJedis(){ Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.lpush("nums","1","2","3","4","5"); // 获取所有数据 Listnums = jedis.lrange("nums",0,-1); for (String num : nums) { System.out.println(num); }}
若是保存hash数据,则调用 hset 方法:
@Testpublic void testJedis() { Jedis jedis = new Jedis("127.0.0.1", 6379); jedis.hset("user", "name", "zhangsan"); jedis.hset("user", "age", "20"); String name = jedis.hget("user", "name"); String age = jedis.hget("user", "age"); System.out.println(name + ":" + age);}
因为需要频繁操作jedis,所以我们可以为其编写一个简单的工具类:
package com.wwj.util;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig;import java.util.ResourceBundle;public class JedisUtil { private static JedisPool jedisPool = null; private static final String host; private static final Integer port; private static final Integer maxTotal; private static final Integer maxIdle; /** * 加载连接池,只执行一次 */ static { // 加載配置文件 ResourceBundle bundle = ResourceBundle.getBundle("redis"); host = bundle.getString("redis.host"); port = Integer.parseInt(bundle.getString("redis.port")); maxTotal = Integer.parseInt(bundle.getString("redis.maxTotal")); maxIdle = Integer.parseInt(bundle.getString("redis.maxIdle")); // 配置连接池信息 JedisPoolConfig config = new JedisPoolConfig(); // 设置最大连接数 config.setMaxTotal(maxTotal); // 设置活动连接数 config.setMaxIdle(maxIdle); jedisPool = new JedisPool(config, host, port); } /** * 获取Jedis连接 * * @return */ public static Jedis getJedis() { return jedisPool.getResource(); }}配置文件:redis.host=127.0.0.1redis.port=6379redis.maxTotal=30redis.maxIdle=10
Linux下的Redis
现在我们已经对redis有了一个大致的认识,下面我们就在linux环境下来看看redis的一些更加高级的操作,首先下载redis的压缩包:wget http://download.redis.io/releases/redis-4.0.0.tar.gz解压一下:tar -zxvf redis-4.0.0.tar.gz进入解压后的目录,然后进行编译安装:cd redis-4.0.0make install首先将redis中的配置文件复制一份:cp redis.conf redis-6379.conf并修改配置文件:daemonize yes # 以守护进程的方式启动logfile "6379.log" # 指定日志文件名dir /opt/redis-4.0.0/logs # 指定日志的存放位置此时以该配置文件启动redis:redis-server redis-6379.conf我们可以查看日志来判断redis是否成功启动了:cd /opt/redis-4.0.0/logscat 6379.log日志内容:8454:C 15 Mar 02:57:25.275 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo8454:C 15 Mar 02:57:25.276 # Redis version=4.0.0, bits=64, commit=00000000, modified=0, pid=8454, just started8454:C 15 Mar 02:57:25.276 # Configuration loaded8455:M 15 Mar 02:57:25.279 * Increased maximum number of open files to 10032 (it was originally set to 1024).8455:M 15 Mar 02:57:25.281 # Creating Server TCP listening socket 127.0.0.1:6379: bind: Address already in use通过日志我们发现,6379端口好像被占用了,我们只需查看哪个应用占用了6379端口,并将其kill掉然后重新启动redis即可。通过配置文件启动的方式可以非常方便地实现多个redis服务的启动,现在复制一个刚才的配置文件:cp redis-6379.conf redis-6380.conf然后修改redis-6380.conf:port 6380把端口修改为6380,然后启动该redis服务:redis-server redis-6380.conflogfile "6380.log"同样检查日志即可判断redis服务是否成功启动。服务启动完成后,若是想连接redis进行操作,则:redis-cli -p 6379若是不指定端口号,则默认使用6379端口进行连接。持久化
redis的数据都是存放在内存中的,所以当出现断电、系统崩溃等异常时,这些数据就丢失了,为此,我们需要利用永久性的存储介质将这些数据保存起来,这样就可以在出现问题的时候再将数据从存储介质上恢复回来。redis提供了两种数据持久化的方式,RDB和AOF,其中RDB是以快照的方式进行保存,而AOF则以日志的方式进行保存,下面分别介绍。
RDB
```java在前面我们已经设置了 dir 的配置值为 /opt/redis-4.0.0/logs ,所以redis生成的rdb文件也会在该目录出现,我们使用客户端连接上一个redis服务:redis-cli -p 6380然后设置几个数据:set name zhangsanset age 20执行 save 指令即可进行持久化,此时在logs目录下即可看到持久化文件:[root@centos-7 logs]# ls6379.log 6380.log dump.rdb该文件的相关信息可以在配置文件中进行设置,比如:dbfilename dump.rdb # 持久化文件名,默认为dump.rdbdir ./ # 文件存放位置,默认为当前目录rdbcompression yes # 设置存储至本地时是否压缩数据,默认为yes,采用LZF压缩rdbchecksum yes # 设置是否进行RDB文件格式校验,默认为yes修改一下6380端口的配置文件:dbfilename dump-6380.rdb通过该文件即可恢复数据。需要注意,save指令的执行会阻塞当前redis服务,直到RDB过程完成为止,有可能会造成长时间的阻塞,线上环境不建议使用save指令。redis提供了第二种RDB指令, bgsave ,该指令的执行将会在后台进行,当我们执行该指令后,redis会调用fork函数生成一个子进程,然后在子进程中创建RDB文件。bgsave指令也有一个可配置项:stop-writes-on-bgsave-error yes 默认为yes,表示后台存储过程中如果发生了错误,是否停止保存操作。我们已经知道通过 bgsave 指令能够备份数据,然而手动执行备份指令并不好,为此,可以让redis自动为我们进行备份,还可以设置备份的频率和触发条件。需要在配置文件中进行配置:save 10 2它表示在10秒的时间内若是有2个key及以上的数据发生了变化,无论是添加、修改还是删除,只要发生了变化,那么就会触发自动备份。
## AOF```javaRDB的方式有着一定的缺陷,因为每次是基于所有数据的备份,所以文件存储大、导致IO性能下降,而且是基于子进程实现的,会额外消耗内存资源。而AOF以独立日志的方式记录每次的写指令,重启时再重新执行AOF文件中记录的指令,以达到恢复数据的目的,与RDB的区别在于:RDB记录的是数据;而AOF记录的是操作数据的指令。AOF方式共有三种写策略,分别是:always:每次写入操作均同步到AOF文件中,数据零误差,性能较低everysec:每秒将缓冲区中的指令同步到AOF文件中,数据准确性较高,性能较高,但在系统突然宕机的情况下会丢失1秒的数据no:由操作系统控制每次同步到AOF文件的周期,整体过程不可控redis默认是关闭了AOF功能的,所以我们需要在配置文件中开启它:appendonly yes # 开启AOFappendfsync always # 指定策略为每次都同步AOF的写策略默认为每秒同步一次,我们可以将其修改为每次都同步,配置完毕后启动redis服务,查看 logs 目录:[root@centos-7 logs]# ll总用量 28-rw-r--r--. 1 root root 4650 3月 15 03:35 6379.log-rw-r--r--. 1 root root 14704 3月 15 19:41 6380.log-rw-r--r--. 1 root root 0 3月 15 19:41 appendonly.aof-rw-r--r--. 1 root root 184 3月 15 19:31 dump-6380.rdb会发现出现了 appendonly.aof 文件,这就是AOF的持久化文件,接下来我们连接客户端,并写入一个数据后,再观察该文件:[root@centos-7 logs]# ll总用量 32-rw-r--r--. 1 root root 4650 3月 15 03:35 6379.log-rw-r--r--. 1 root root 14704 3月 15 19:41 6380.log-rw-r--r--. 1 root root 60 3月 15 19:42 appendonly.aof-rw-r--r--. 1 root root 184 3月 15 19:31 dump-6380.rdb会发现文件大小由原来的0变为了60,这说明我们的配置生效了,该文件只会记录写指令,即添加、修改、删除数据的指令,诸如get指令是不会被它记录的。AOF的持久化文件名也可以通过配置来修改:appendfilename appendonly-6380.aof随着指令不断写入AOF,文件也会越来越大,但有时候会出现这样一种情况:set name zsset name lsset name ww这里虽然执行了三条 set 指令,但事实上,它的效果等价于一条指令:set name ww ,因为后面的数据覆盖了前面的数据,那么如果AOF记录了这三条指令,很显然就造成了资源的浪费。为此,redis提供了AOF重写机制,它能够将对同一个数据的若干条指令转换为最终结果数据对应的指令记录,这样便极大地压缩了AOF文件的体积。
只需执行指令即可实现AOF重写:bgrewriteaof
事务
事务也是一个老生常谈的话题了,这里我们就不说事务的概念了,直接来看看redis中的事务。比如这样的一个现象:127.0.0.1:6380> set name zhangsanOK127.0.0.1:6380> get name"lisi"该客户端设置了一个数据,key为name,值为zhangsan,而在获取的时候却得到了lisi,这是为什么呢?原来,在该客户端获取数据之前,又有别的客户端抢在了它前面修改了数据,由此导致了这样的问题发生,为此,我们需要使用事务控制来避免这一问题:127.0.0.1:6380> multiOK127.0.0.1:6380> set name zhangsanQUEUED127.0.0.1:6380> get nameQUEUED127.0.0.1:6380> exec1) OK2) "zhangsan"首先通过 multi 指令开启事务,然后添加数据,在获取数据之前同样另一个客户端修改了数据,最后执行 exec 指令提交事务,redis便会输出在这次事务中所有指令的结果,可以看到数据并没有被别的客户端修改。当在事务过程中执行了错误的指令时,我们可以使用 discard 指令来取消此时事务,取消之后事务中的所有指令操作都将失效。而且在事务过程中,执行了语法错误的指令,比如 set 打成了 sat ,redis会自动帮助我们取消事务,又比如对string类型的数据执行 incr操作,redis会自动执行事务中正确的操作指令,并取消执行那些错误的指令。锁假设有这样一个需求,天猫双11热卖过程中,需要对已经售罄的商品进行补货,有4个业务员都拥有补货的权限,补货这一过程也涉及到多个操作,那么如何保证这些操作不会重复进行呢?使用 watch指令可以监控某个数据,当数据发生变化时取消所有操作,比如:set name zswatch namemultiset age 20exec在这组指令中,首先添加了一个数据,然后使用watch监视了name,随之开启事务,并在事务中添加了另一个数据,但是在提交事务之前,别的客户端修改了name数据,此时再提交事务便会输出nil:127.0.0.1:6380> set name zsOK127.0.0.1:6380> watch nameOK127.0.0.1:6380> multiOK127.0.0.1:6380> get nameQUEUED127.0.0.1:6380> set age 20QUEUED127.0.0.1:6380> exec(nil)127.0.0.1:6380> keys *1) "name"而且添加的数据age也不存在了,需要注意的是watch操作必须在事务开启之前执行,若在事务中执行则会报错。通过 unwatch 指令可以取消所有数据的监控。
分布式锁
继续看一个场景,双11的网站流量是非常巨大的,一些商家也会在双11推出一些秒杀活动,当然了,秒杀的商品是有数量限制的,比如说,某个手机厂商设置了100个苹果手机用于秒杀活动,而事实上,参与此次秒杀的人是非常多的,它将远远大于手机数量,那么怎么保证商品不会出现超卖的现象,即:最后一件商品不会被多个人同时购买,此时用刚才的watch指令已经无法解决这个问题了,我们需要使用——分布式锁。使用 setnx指令设置一个分布式锁:setnx lock-key value比如这样的一组指令:127.0.0.1:6380> setnx lock-num true(integer) 1127.0.0.1:6380> incrby num -1(integer) 9127.0.0.1:6380> del lock-num(integer) 1这是将商品库存减1的正常流程,首先设置一个lock-num锁,值是无所谓的,然后将库存减1,完成操作后删除锁,假如该客户端在减库存操作完成之前别的客户端也进行减库存操作,则会出现:127.0.0.1:6380> setnx lock-num true(integer) 0这是setnx指令的特性所导致的,setnx指令会判断当前数据库中是否存在lock-num键,若存在则返回0,此时说明别的客户端正在修改库存,那么当前客户端就应该进入等待状态或者做别的操作,只有别的客户端执行完减库存操作并删除了锁之后,setnx就返回了1,这时候就可以正常进行减库存操作了。
死锁
在分布式锁的基础上可能会出现死锁的问题,当客户端添加了分布式锁进行操作后,在删除锁之前出现了突发情况,比如断电、宕机等问题,此时就无法释放该锁,导致别的客户端一直在等待锁的释放。我们可以使用 expire 指令为分布式锁设置一个有效时间,当有效时间过后redis便会自动删除该锁,这样就解决了死锁问题:expire lock-num 60当60秒时间过后,锁会被自动删除。服务器配置到这里关于redis的一些基本操作就学习完了,接下来我们就来看看redis中更加高级的部分,首先是配置文件中的配置信息。配置项 说明daemonize yes nobind 127.0.0.1 绑定主机地址port 6379 设置服务器端口号databases 16 设置数据库数量loglevel debug verboselogfile 端口号.log 设置日志文件名maxclients 0 设置同一时间最大客户端连接数,默认无限制,当客户端连接达到上限时,redis会关闭新的连接timeout 300 客户端闲置等待最大时长,达到最大值后关闭连接,如需关闭该功能, 设置为0include /path/server-端口号.conf 导入并加载指定配置文件信息,用于快速创建redis公共配置较多的redis实例配置文件,便于维护
主从复制
现在我们只是在使用一个redis,它就会出现一些问题,比如服务器宕机后,该服务器上的redis将无法提供服务,而此时应用又只有一个redis服务支撑,那么我们的业务将无法提供正常的服务,为了保证高可用,我们需要为redis搭建集群。redis中以master为主机,slave为从机,一个master可以对应多个slave,而一个slave只能对应一个master。那么首先我们需要建立slave到master的连接,使master能够识别slave,并保存slave的端口号,启动四个窗口模拟这一过程:图片在6380服务和6381服务窗口分别开启6380端口、6381端口的redis服务,然后来到slave窗口:redis-cli -p 6381slaveof 127.0.0.1 6380这里表示使用6381端口连接6380端口,作为它的从机,此时我们再来到master窗口,连接客户端:redis-cli -p 6380set name zs会发现,slave窗口中6381端口的redis也能够获取到该数据,此时证明主从搭建好了。我们也可以在启动redis服务的时候就进行连接:redis-server redis-6381.conf --slaveof 127.0.0.1 6380redis推荐使用配置文件的方式搭建主从结构,修改redis-6381.conf:slaveof 127.0.0.1 6380此时6381就成了6380的从机了。
哨兵
在主从的环境下也可能会产生问题,比如作为主机的master服务宕机了,此时作为它的从机都无法正常工作了,这个时候我们需要在slave中选出一个作为新的master,以支撑主从继续提供服务。哨兵则是为了解决上述问题的,它是一个分布式的系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有的slave连接到新的master。哨兵的启动方式如下:redis-sentinel sentinel.conf哨兵的客户端链接方式:redis-cli -p 26379需要注意的是哨兵客户端不支持数据操作,它只作监控用途。
企业级解决方案
下面介绍一些企业中常用的redis解决方案,这也是一些岗位面试的重点。
缓存预热
缓存预热指的是在系统启动前,提前将相关的缓存数据直接加载到缓存系统,避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,这样用户就可以直接查询到事先被预热的缓存数据。
比如日常例行统计数据访问记录,统计出访问频度较高的热点数据,并将统计结果中的数据分类,根据级别,让redis优先加载级别较高的热点数据,一般使用脚本程序来固定触发数据预热过程。缓存雪崩
缓存在同一时间大面积的失效,导致请求直接落到了数据库上,造成数据库短时间内承受大量请求,很有可能导致数据库直接崩溃。亦或者一些热点数据在某一时刻大面积失效,导致对应的请求直接落到了数据库上也导致数据库直接崩溃。解决办法:
构建多级缓存的架构 优化数据库 耗时的业务 监控redis服务性能指标 对服务进行限流、降级 对于热点数据在某一时刻大面积失效的情况,我们可以为这些数据设置不同的失效时间,比如给它们随机设置失效时间,那么就不会出现数据大面积失效的情况了。缓存击穿
缓存击穿与缓存雪崩非常类似,也是因为大量的请求落到了数据库上导致数据库崩溃,但不同的是,缓存雪崩是大量缓存数据同时失效导致大量的请求落到了数据库;而缓存击穿通常指并发访问同一条数据,解决方案也与缓存雪崩大体相同。
缓存穿透
缓存穿透指的是用户查询的数据在缓存中没有,在数据库中也没有,如发起id等于-1或者id等于非常大的值的请求,这些数据肯定是不存在的,这个时候的用户很有可能是网站的攻击者,这样的攻击请求会导致数据库压力过大进而崩溃。
解决方案: 增加校验逻辑,如检验id是否合法 当数据从缓存和数据库中都未获取到时,将key的value值写为null,这样可以防止攻击者反复使用一个id进行攻击发表评论
最新留言
关于作者
