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 | $ redis-cli |
输入指令即可。
关闭 redis
1 | $ redis-cli shutdown |
Linux 下使用
中文官方网址:http://www.redis.cn
第一步:安装 c 需要的 GCC 环境
1 | yum install -y gcc-c++ |
第二步:下载解压 Redis 源码压缩包
1 | wget http://download.redis.io/releases/redis-5.0.5.tar.gz tar |
第三步:编译 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 | vim redis.conf |
- 启动服务
1 | ./redis-server redis.conf |
- 其他命令
1 | redis-server # 启动 redis 服务 |
Redis 设置密码和查看密码
找到配置文件 redis.conf
1 | requirepass 123456 |
修改后重启即可
- 不需要重启的方法
1 | config set requirepass 123456 |
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 | key user |
- 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 | key hobby |
- 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 | <parent> |
模板,jedis 中 API 的方法名跟 Redis 命令名称是完全一致的,调用 jedis 对象对应的方法就能完成相应的操作。
set name bunny ==> jedis.set(“name”, “bunny”);
get name ==> String val = jedis.get(“name”);
1 | public void testJedisPool() { |
Spring Data Redis
代码
1 | # application.properties |
1 | // 直接注入框架封装好的StringRedisTemplate |
Redis 的应用场景
缓存
项目中,对于不常变动的数据,比如全国省市地区等,就能缓存起来,下次需要查询的时候,可以先从缓存中查询,有则直接返回结果,避免去查询关系型数据库,如果没有缓存再从关系型数据库中查询,并且缓存到 Redis 中,然后再返回
1 | // 业务层方法 |
实时统计点赞总数
项目启动时,从关系型数据库中查出点赞数量后,把该数据存入 redis 中,当用户每次点赞时,使用 redis 提供的 incr 增加点赞总是,当用户查询点赞总数时,直接从 redis 中查询点赞总数,显示到页面中,再配合定时器,每隔一段时间,把 redis 中的点赞总数存入到关系型数据库中,完成数据同步。
朋友圈点赞
内容存入格式 key:user:id:post:id value: 内容。
点赞的存入格式, key {user: id, post: id: news} value:list
- 发一条朋友圈:set user:1 post:2 “hello redis”
- 点赞 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"}
- 查询多少人点赞:llen user:1:post:2:news
- 查询那些人点赞: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 推荐可能认识的人:
- 遍历 user:1 的好友
- 把 user:2 / user:3 所有的好友去并集,存起来 sunionstore tmp user:2 user:3
- 删除自己 srem tmp user:1
- 把 user:1 已经认识的人删除 sdiffstore tmp tmp user:1:friends
- 随机推荐 srandmember tmp 2
Redis 的缓存穿透
上述代码中,如果正常逻辑是没问题的,但是如果id = -1,不存在的id 被传过来以后,result 永远为空,则如果不停的发送该请求,就会一直访问 redis 数据库,但是缓存并没有拦截这种操作,导致数据库被一直访问。这种现象被称为缓存穿透。最终导致数据库被拖垮了。
解决方案一:
1 | boolean flag = redis.exist(key); |
这样当非法访问时,会在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次,如果要做到只查询一次数据,则要用到分布式锁。
- 多个请求只能有一个请求获得锁
- 锁需要有失效的时间,不然会出现死锁的情况。
- 谁加的锁只能谁解。
setNx("Lock")
访问 redis,redis 是单线程的,只有第一次访问可以获得锁,没获得锁的线程都返回了。做完业务, delete key。
但是有可能,做业务的时候失败了,则不会解锁,造成死锁,以后任何请求访问 key1,都会被驳回。则必须在 setNx()
后加入 setEx()
设置有效期,大概比业务时长要长一点。而且,setNx 和 setEx 必须同为一个原子操作。redis 操作数据库有一个方法可以将两个方法作为一个原子操作。
1 | jedis.set(String key, String value, String nxxx, String expx, long time) |
所以步骤为
String set(String key, String value, String nxxx, String expx, long time)
,加锁时,每个请求都要 uuid,value 存的就是 uuid。- 做业务,如果业务时长超过了,锁的失效时间。则会有新的请求获取锁,此时要区分请求和请求之间的锁的不同,需要加入 uuid 来标识是哪个请求的所,防止别的请求删除了其他请求的锁。
- del key ,删除时需要判断 redis 的uuid == uuid ,如果等于,再删锁。
常见的面试问题:
- 做了微服务,你们有哪些服务?
- 商品服务、订单服务、积分服务。。。
- 项目的并发量,访问量。
- 如果是外包项目,有测试人员进行压测,不是我们负责。
- 如果是产品项目,要看app年限,
- 做产品的,每个月更新做哪些事情?
- 新功能的迭代,除了改 bug,还会做活动。百度用户增长策略
- 项目线上出问题,排除问题的流程
- 普通程序,普通程序员没有线上服务器权限。logbag,aop方式加入日志。根据客户反馈的截图,大概猜测哪些微服务出问题,拿到对应微服务的日志,logbag,把关键参数通过 aop,把入参、出参排查问题。修正、打补丁升级。
- 线上出过哪些生产的事故。
- 写日志引发的故障,日志级别导致服务器硬盘爆了
- 线上频发报警,网关频繁转发超时,系统很多服务响应时间过长,服务cpu过载,数据库死锁等不正常现象,目标范围缩小在近两日上线功能,并锁定在新上线的操作redis缓存功能上。
- 印象最深刻的bug