Redis

Redis


键值对存储的 NoSQL 数据库,提供高兴能的键值存储。可以理解为一个高性能的超大 Map。

特点:

  • Redis 中事务只有成功,没有失败
  • 没有表的概念和相关操作
  • 单线程执行。没有线程安全。
  • 性能高,达到 10w 次读取 /s,8万次写/s
  • 每个操作都是原子操作。

安装 Redis


Redis 默认端口是 6379,安装好后会自动启动服务器,没有密码。

mac 下使用

1
$ brew install redis

安装 redis,安装完后输入命令启动

1
$ redis-server /usr/local/etc/redis.conf

安装完后,redis 默认的配置文件 redis.conf 位于 /usr/local/etc,同时 redis-setinel.conf 也在这里。

如果启动时,不指定配置文件,redis 会使用程序中内置的默认配置,只有在开发和测试阶段才考虑使用内置的默认配置,正式环境最好还是提供配置文件,并且一般命名为 redis.conf。

启动 redis

1
$ redis-server /usr/local/etc/redis.conf

检测 redis 服务器是否启动

1
$ redis-cli ping

终端输出 pong 说明服务器正常运行,输出Could not connect to Redis at 127.0.0.1:6379: Connection refused 说明服务器关闭

启动后使用命令行操作 redis

1
2
3
$ redis-cli
127.0.0.1:6379> select 1
OK

输入指令即可。

关闭 redis

1
$ redis-cli shutdown

Linux 下使用

中文官方网址:http://www.redis.cn

第一步:安装 c 需要的 GCC 环境

1
2
yum install -y gcc-c++
yum install -y wget

第二步:下载解压 Redis 源码压缩包

1
2
wget http://download.redis.io/releases/redis-5.0.5.tar.gz tar 
tar -zxf redis-5.0.5.tar.gz

第三步:编译 Redis 源码,进入 redis-3.2.8 目录,执行编译命令

1
cd redis-5.0.5

第四步:安装 Redis,需要通过 PREFIX 指定安装路径

1
make install PREFIX=/usr/local/redis/

Redis 启动

  • 拷贝 redis-5.0.5/redis.conf 配置文件到 /usr/local/redis/bin 目录
1
cp /usr/software/redis-5.0.5/redis.conf /usr/local/redis/bin/
  • 修改 redis.conf
1
2
3
4
5
6
7
8
vim redis.conf
# 将`daemonize`由`no`改为`yes`
daemonize yes
# 默认绑定的是回环地址,默认不能被其他机器访问
# 绑定自己的服务器 ip 供外界访问
bind 115.236.96.46
# 是否开启保护模式,由yes该为no
protected-mode no
  • 启动服务
1
2
3
4
5
6
7
8
9
10
./redis-server redis.conf

# 关闭
./redis-cli shutdown
ps aux | grep redis // 关闭 Redis进程
./redis-cli -h [ip] -p [6379] -a [password]
./redis-cli
auth [password]
PING
PONG
  • 其他命令
1
2
3
4
5
6
redis-server # 启动 redis 服务
redis-cli # 进入 redis 命令客户端
redis-benchmark # 性能测试工具
redis-check-aof # aof 文件进行检查的工具
redis-check-dump # rdb 文件进行检查的工具
redis-sentinel # 启动哨兵监控服务

Redis 设置密码和查看密码

找到配置文件 redis.conf

1
requirepass 123456

修改后重启即可

  • 不需要重启的方法
1
2
config set requirepass 123456
config get requirepass # 查看密码

Redis 支持的数据类型


String,hash,list,set,zset

String

  • set key value -> 存入键值对
  • get key -> 根据键取出值
  • getset key value -> 返回旧值后存入新值
  • incr key -> 把值递增1
  • decr key -> 把值递减1
  • incrby key num -> 在原有值的基础上 + num
  • append key ‘value’ -> 原值后拼接新内容
  • setnx key value -> 存入键值对,键存在时不存入
  • setex key timeout value -> 存入键值对,timeout表示失效时间,单位s
  • setrange key index value -> 修改键对应的值,index表示开始的索引位置
  • mset k1 v1 k2 v2 … -> 批量存入键值对
  • mget k1 k2 … -> 批量取出键值
  • del key -> 根据键删除键值对

hash

hash 类型,存储对象,其实就是 Map 类型,map 中的 value 有可以有多个键值对。

1
2
3
4
5
6
7
key user
value {
key name
value bunny,
key age
value 18
}
  • hset key hashkey hashvalue -> 存入一个hash对象
  • hget key hashkey -> 根据hash对象键取去值
  • hincrby key hashkey 递增值 -> 递增hashkey对应的值
  • hexists key hashkey -> 判断hash对象是含有某个键
  • hlen key -> 获取hash对象键的数量
  • hkeys key -> 获取hash对象的所有键
  • hvals key -> 获取hash对象的所有值
  • hgetall key -> 获取hash对象的所有数据
  • hdel key hashkey -> 根据hashkey删除hash对象键值对 同样有hsetnx,其作用跟用法和setnx一样

list

list 类型更多表示队列,可以直接操作首尾元素。

1
2
key hobby
value [java, js, dota2, lol]
  • rpush key value -> 往列表右边添加数据
  • lpush key value -> 往列表左边添加数据
  • lpop key -> 弹出列表最左边的数据
  • rpop key -> 弹出列表最右边的数据
  • lrange key start end -> 范围显示列表数据,全显示则设置0 -1
  • linsert key before/after refVal newVal -> 参考值之前/后插入数据
  • lset key index value -> 根据索引修改数据
  • lrem key count value -> 在列表中按照个数删除数据
  • ltrim key start end -> 范围截取列表
  • lindex key index -> 根据索引取列表中数据
  • llen key -> 获取列表长度

set

和 Set 集合性质一样,底层使用哈希表实现,存入元素是无序不可重复,通过 Redis 提供的命令来取交集、并集、差集

  • sadd key value -> 往set集合中添加元素
  • smembers key -> 列出set集合中的元素
  • srem key value -> 删除set集合中的元素
  • spop key count -> 随机弹出集合中的元素
  • sdiff key1 key2 -> 返回key1中特有元素
  • sdiffstore var key1 key2 -> 返回key1中特有元素存入另一个set集合
  • sinter key1 key2 -> 返回两个set集合的交集
  • sinterstore var key1 key2 -> 返回两个set集合的交集存入另一个set集合
  • sunion key1 key2 -> 返回两个set集合的并集
  • sunionstore var key1 key2 -> 返回两个set集合的并集存入另一个set集合
  • smove key1 key2 value -> 把key1中的某元素移入key2中
  • scard key -> 返回set集合中元素个数
  • sismember key value -> 判断集合是否包含某个值
  • srandmember key count -> 随机获取set集合中元素

zset(Sorted Sets)

zset 是 set 的一个升级版本,在 set 的基础上增加了一个顺序属性,这一属性在修改元素的时候可以指定,每次指定后,zset 会自动重新按新的值调整顺序。

  • zadd key num name -> 存入数值和名称
  • zrange key start end -> 按照数值升序输出名称
  • zrangebyscore key min max [withscores] -> 按照数值范围升序输出名称
  • zrevrange key start end -> 按照数值降序输出名称
  • zrevrangebyscore key max min [withscores] -> 按照数值范围降序输出名称
  • zrem key name -> 删除名称和数值
  • zincrby key num name -> 偏移名称对应的数值
  • zrank key name -> 升序返回排名
  • zrevrank key name -> 降序返回排名
  • zremrangebyscore key max min [withscores] -> 根据分数范围删除元素
  • zremrangebyrank key start end -> 根据排名删除元素
  • zcard key -> 返回元素个数
  • zcount key min max -> 按照分数范围统计个数

Redis 的管理命令

管理 key 的命令

  • exists key -> 判断某个key是否存在
  • expire key second -> 设置key的过期时间
  • persist key -> 取消key的过期时间
  • select index -> 切换数据库索引,范围是0 ~ 15共16个分区
  • move key index -> 把某个key-value移动到其他索引中
  • rename oldKey newKey -> 把oldKey重命名为
  • newKey info -> 查看当前服务器信息
  • flushdb -> 清空当前库中的数据
  • flushall -> 清空所有库中的数据

设置密码

修改安装目录 /redis.windows-service.conf 配置文件,在 443 行位置,去掉注释
此时就需要通过密码认证才能执行命令。redis-cli - a 密码

Jedis


redis 的 java 端的驱动。

准备环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<parent> 
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version>
</parent>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- SpringBoot整合Spring Data Redis -->
<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- redis的驱动包:jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>

<build>
<plugins> <!-- SpringBoot打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

模板,jedis 中 API 的方法名跟 Redis 命令名称是完全一致的,调用 jedis 对象对应的方法就能完成相应的操作。

set name bunny ==> jedis.set(“name”, “bunny”);
get name ==> String val = jedis.get(“name”);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test public void testJedisPool() {
// 1:创建Jedis连接池
JedisPool pool = new JedisPool("localhost", 6379);
// 2:从连接池中获取Jedis对象
Jedis jedis = pool.getResource();
/* 设置密码
jedis.auth(密码);
*/
// 3:TODO
System.out.println(jedis);
// 4:关闭资源
jedis.close();
pool.destroy();
}

Spring Data Redis


代码

1
2
3
4
# application.properties 
spring.redis.host=localhost #默认值
spring.redis.port=6379 #默认值
spring.redis.password=admin #根据情况配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 直接注入框架封装好的StringRedisTemplate 
@Autowired private
StringRedisTemplate redisTemplate;

@Test
public void testRedisTemplate() {
// 操作string
redisTemplate.opsForValue().xx();
// 操作hash
redisTemplate.opsForHash().xx();
// 操作list
redisTemplate.opsForList().xx();
// 操作set
redisTemplate.opsForSet().xx();
// 操作zset
redisTemplate.opsForZSet().xx();

// 设置 key 的失效时间
redisTemplate.expire(key, Consts.VERIFY_CODE_TIMEOUT, TimeUnit.MILLISECONDS);
}

注意:以上的xx表示命令的名称,如:set / put / get 等

Redis 的应用场景


缓存

项目中,对于不常变动的数据,比如全国省市地区等,就能缓存起来,下次需要查询的时候,可以先从缓存中查询,有则直接返回结果,避免去查询关系型数据库,如果没有缓存再从关系型数据库中查询,并且缓存到 Redis 中,然后再返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 业务层方法
public User get(Long id) {
User user = null;
// 避免多个对象的key一致,采用全限定名:id的形式来作为缓存的key
String cacheKey = "cn.wolfcode.xxx.domain.User:"+id;
// 从redis中取出数据
String cacheVal = jedis.get(cacheKey);
if (cacheVal == null) {
// redis中没有数据,再从关系型数据库中查询
user = userMapper.selectByPrimaryKey(id);
// 把查询到的结果存到redis中
jedis.set(cacheKey, JSON.toJSONString(user));
} else {
// redis中有数据,再把该数据解析成一个User对象
user = JSON.parseObject(cacheVal, User.class);
}
return user;
}

实时统计点赞总数

项目启动时,从关系型数据库中查出点赞数量后,把该数据存入 redis 中,当用户每次点赞时,使用 redis 提供的 incr 增加点赞总是,当用户查询点赞总数时,直接从 redis 中查询点赞总数,显示到页面中,再配合定时器,每隔一段时间,把 redis 中的点赞总数存入到关系型数据库中,完成数据同步。

朋友圈点赞

内容存入格式 key:user:id:post:id value: 内容。
点赞的存入格式, key {user: id, post: id: news} value:list

  1. 发一条朋友圈:set user:1 post:2 “hello redis”
  2. 点赞 lpush user:1:post:2:news {"id":3,"name":"bigfly","headImg":"xxx.png"} lpush user:1:post:2:news {"id:4,"name":"wangnima","headImg":"xxx.png"}
  3. 查询多少人点赞:llen user:1:post:2:news
  4. 查询那些人点赞:lrange user:1:post:2:news 0 -1

抽奖

参与抽奖的人员不能重复,并且抽到奖后不能再继续参与下轮抽奖
sadd lucky xiaoyao bunny bigfly wangnima zhangquandan tangmaru daduizhang yixiaoxing wangdachui
抽3个三等奖,2个二等奖,1个一等奖。spop lucky count(1, 2, 3)

好友推荐

系统推荐你可能认识的人

user:1:friends [user:2,user:3]
user:2:friends [user:1,user:4,user:5,user:8]
user:3:friends [user:1,user:6,user:7]

向 user:1 推荐可能认识的人:

  1. 遍历 user:1 的好友
  2. 把 user:2 / user:3 所有的好友去并集,存起来 sunionstore tmp user:2 user:3
  3. 删除自己 srem tmp user:1
  4. 把 user:1 已经认识的人删除 sdiffstore tmp tmp user:1:friends
  5. 随机推荐 srandmember tmp 2

Redis 的缓存穿透


上述代码中,如果正常逻辑是没问题的,但是如果id = -1,不存在的id 被传过来以后,result 永远为空,则如果不停的发送该请求,就会一直访问 redis 数据库,但是缓存并没有拦截这种操作,导致数据库被一直访问。这种现象被称为缓存穿透。最终导致数据库被拖垮了。

解决方案一:

http://localhost/query?id=-1

1
2
3
4
5
6
7
8
9
10
11
boolean flag = redis.exist(key);
if (flag) {
return redis.get(key);
} else {
result = mapper.selectByPrimary(id);
if (result == null) {
redis.set(key, result); // 设置短暂的有效时间,5s
} else {
redis.set(key, result);
}
}

这样当非法访问时,会在5秒内不会对数据库进行增删改操作。

解决方案二:

set 前判断key是否合理,把合理的key在系统中存储一份 List。在座查询的时候,先判断查询的key是否在List里面,如果在的情况,才去redis查询。

Redis 雪崩问题


在服务器启动的时候,查询数据库的数据存储到缓存中,刚好这些缓存的失效时间都是一样的,缓存都会在同一时刻同时失效。特点:每个一段时间,服务器压力就会很大,CPU利用率突然很高,呈现周期性。

解决方案一

设置缓存失效时间 + 随机数。

方案二

使用双缓存。两份缓存,一份是有失效时间,一份是永不失效。发现第一份缓存没有,则会先从永不失效的第二份缓存中读取数据。然后把更新缓存的消息放到 MQ 里面,去异步查询数据库查询,异步请求,更新这两份缓存数据。

方案三

设置缓存永不失效,在另一个仓库内设置 key_expire 失效时间。

key1 value1 5分钟失效
key2 value2 8分钟失效
key3 value3 10分钟失效

由于 redis 支持 key 失效的监听事件,监听到这3个 key 的失效时间,可以在监听事件中获取到失效的 key,将缓存失效的 key 放到 MQ 队列中,查询数据库更新对应的 key 的数据。用户永远只读取永不失效的缓存。

Redis 的分布式锁


需求:多台服务器,只需要有一台服务器去执行业务。这时候就需要使用分布式锁。
比如只有key1失效,3000个请求需要 key1、key2、key3,此时,如果多台服务器的,每个请求都要查询一次数据库,则要查询1000次,如果要做到只查询一次数据,则要用到分布式锁。

  1. 多个请求只能有一个请求获得锁
  2. 锁需要有失效的时间,不然会出现死锁的情况。
  3. 谁加的锁只能谁解。

setNx("Lock")访问 redis,redis 是单线程的,只有第一次访问可以获得锁,没获得锁的线程都返回了。做完业务, delete key。

但是有可能,做业务的时候失败了,则不会解锁,造成死锁,以后任何请求访问 key1,都会被驳回。则必须在 setNx() 后加入 setEx() 设置有效期,大概比业务时长要长一点。而且,setNx 和 setEx 必须同为一个原子操作。redis 操作数据库有一个方法可以将两个方法作为一个原子操作。

1
2
jedis.set(String key, String value, String nxxx, String expx, long time)
// nxxx 和 expx 可以保证这个方法的原子性。

所以步骤为

  1. String set(String key, String value, String nxxx, String expx, long time),加锁时,每个请求都要 uuid,value 存的就是 uuid。
  2. 做业务,如果业务时长超过了,锁的失效时间。则会有新的请求获取锁,此时要区分请求和请求之间的锁的不同,需要加入 uuid 来标识是哪个请求的所,防止别的请求删除了其他请求的锁。
  3. del key ,删除时需要判断 redis 的uuid == uuid ,如果等于,再删锁。

常见的面试问题:

  1. 做了微服务,你们有哪些服务?
    • 商品服务、订单服务、积分服务。。。
  2. 项目的并发量,访问量。
    • 如果是外包项目,有测试人员进行压测,不是我们负责。
    • 如果是产品项目,要看app年限,
  3. 做产品的,每个月更新做哪些事情?
    • 新功能的迭代,除了改 bug,还会做活动。百度用户增长策略
  4. 项目线上出问题,排除问题的流程
    • 普通程序,普通程序员没有线上服务器权限。logbag,aop方式加入日志。根据客户反馈的截图,大概猜测哪些微服务出问题,拿到对应微服务的日志,logbag,把关键参数通过 aop,把入参、出参排查问题。修正、打补丁升级。
  5. 线上出过哪些生产的事故。
    • 写日志引发的故障,日志级别导致服务器硬盘爆了
    • 线上频发报警,网关频繁转发超时,系统很多服务响应时间过长,服务cpu过载,数据库死锁等不正常现象,目标范围缩小在近两日上线功能,并锁定在新上线的操作redis缓存功能上。
  6. 印象最深刻的bug
文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2019/09/24/Redis/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论