redis缓存数据库
redis简单介绍
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API,其实当前最热门的NoSQL数据库之一,NoSQL还包括了Memcached和mongodb。关于Redis和Memcached的比较请点击:Redis 和 Memcached 各有什么优缺点。
Redis是NoSQL的一种,NoSQL(Not Only SQL),意即不仅仅是SQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题,包括超大规模数据的存储。
redis命令简介
Redis默认有16个数据库,序号分别是0-15,默认数据库序号为0,以下命令是其相关命令。
dbsize 返回数据库key数量
select db-index 选择数据库,redis默认16个,编号0-15
move key db-index 把key移到db-index数据库下
rename oldkey newkey 原子重命名
renamenx oldkey newkey 同上,newkey存在则失败
flushdb 删除当前db的所有key
flushall 删除所有db的所有key
ping 测试连接是否存活(返回PONG表示连接成功)
echo xxx 在命令行打印xxx消息
info 返回redis服务器信息
config get key 返回对应的配置信息,config get * 返回所有配置信息
clear 清屏操作
keys pattern 返回匹配模式的所有keys
keys * : 查看该数据库中所有的key
type key 查看key类型
ttl key 返回key的剩余过期时间,-1表示key不存在或者没有设置过过期时间
exists key 测试key是否存在del key1 key2 … keyN
randomkey 返回当前数据库随机选择的一个key,如数据库是空的,则返回空串
persist key : 取消掉key的过期时间exprie key seconds 为key设置过期时间,单位s。返回1成功,0表示key已设置过过期时间或者不存在
redis的数据类型
有String / Hash / List / Set / Ordered Set五种数据结构
缓存到内存中;key-value结构;可持久化(将数据保存到磁盘上);支持丰富的数据类型
列表类型的lpush、rpush,lpop、rpop,lset,lrange,阻塞操作:blpop、brpop
memcache:缓存到内存中;key-value结构;只支持字符串;
string类型:
set() ---设置键值对
mset() ---批量设置键值对
get() ---获取键值
mget() ---批量获取键值
getset(name, value) ---#设置新值,打印原值
实例:
print(r.getset("name1","wangwu")) #输出:zhangsan
print(r.get("name1")) #输出:wangwu
getrange(key, start, end) #根据字节获取子序列
setrange(name, offset, value) #修改字符串内容,从指定字符串索引开始向后替换,如果新值太长时,则向后添加
strlen(name) #返回name对应值的字节长度(一个汉字3个字节)
append(name, value) #在name对应的值后面追加内容
实例:
r.set("name","zhangsan")
print(r.get("name")) #输出:'zhangsan
r.append("name","lisi")
print(r.get("name")) #输出:zhangsanlisi
hash数据类型:
hset(name, key, value) hget(name,key) 设置一个获取一个键值对
hgetall(name) #获取name对应hash的所有键值
hmset(name, mapping) #在name对应的hash中批量设置键值对,mapping:字典
hmget(name, keys, *args) # 在name对应的hash中获取多个key的值
hlen(name)、hkeys(name)、hvals(name) #hlen(name) 获取hash中键值对的个数、#hkeys(name) 获取hash中所有的key的值、#hvals(name) 获取hash中所有的value的值
hexists(name, key) #检查name对应的hash是否存在当前传入的key
hdel(name,*keys) #删除指定name对应的key所在的键值对
list操作:
lpush(name,values) # 在name对应的list中添加元素,每个新的元素都添加到列表的最左边
rpush(name,values) #同lpush,但每个新的元素都添加到列表的最右边
lpushx(name,value) #在name对应的list中添加元素,只有name已经存在时,值添加到列表的最左边
rpushx(name,value) #在name对应的list中添加元素,只有name已经存在时,值添加到列表的最右边
llen(name) # name对应的list元素的个数
r.lset(name, index, value) #对list中的某一个索引位置重新赋值
lpop(name) #移除列表的左侧第一个元素,返回值则是第一个元素
lindex(name, index) #根据索引获取列表内元素
lrange(name, start, end) #分片获取元素 示例:print(r.lrange("list_name",0,-1))
ltrim(name, start, end) #移除列表内没有在该索引之内的值
rpoplpush(src, dst) 从一个列表取出最右边的元素,同时将其添加至另一个列表的最左边
set数据类型:可用来去重
sadd(name,values) #给name对应的集合中添加元素
smembers(name) #获取name对应的集合的所有成员
scard(name) #获取name对应的集合中的元素个数
smove(src, dst, value) #将某个元素从一个集合中移动到另外一个集合
spop(name) #从集合的右侧移除一个元素,并将其返回
redis设置键值的过期时间:
expire keys time #设置键的存活时间为time(单位以秒计)
pexpire keys time #设置键的时间为time(以ms计)
expireat key timesstamp #将key的存活时间设置为unix的时间戳,单位为秒s
pexpireat key timestamp #将可以的存活时间设置为unix 的时间戳,单位为毫秒。
常见的redis问题
1、redis服务挂掉之后数据会消失吗?
会。redis基于内存的。如果服务宕机,内存就会清空。这是时候考虑加入redis持久化
redis持久化
RDB持久化
RDB持久化机制的优点:
1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,
非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,
在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据
(2)RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork
一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可
(3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速
RDB持久化机制的缺点:
(1)如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,
都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据
(2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端
提供的服务暂停数毫秒,或者甚至数秒
AOF持久化
优点:
(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,
最多丢失1秒钟的数据
(2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且
文件不容易破损,即使文件尾部破损,也很容易修复
(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,
会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件
还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如
某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF
文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
缺点:
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,
当然,每秒一次fsync,性能也还是很高的
(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据
快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite
并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
缓存穿透
要查询的数据根本就不存在,这里说的不存在说的是数据里本身就没有。比如一张表里的自增ID才到100,但是大量请求查询ID=200
解决方案:
布隆过滤器
缓存空对象,即对不存在的key也进行缓存。但是也会带来问题,恶意攻击传送不存在的key。
布隆过滤器:
将数据库中所有的查询条件,放入布隆过滤器中。当一个查询请求过来时,先经过布隆过滤器进行查,如果判断请求查询值存在,则继续查;如果判断请求查询不存在,直接丢弃。原理就是一个对一个key进行k个hash算法获取k个值,在比特数组中将这k个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。
当一个查询请求过来时,先经过布隆过滤器进行查,如果判断请求查询值存在,则继续查;如果判断请求查询不存在,直接丢弃。
缓存击穿
要查询的数据存在数据库里有,缓存里也有。但是我们往往在setKey的时候都会设置时间。如果key属于热点数据,那么在这个key过期的一瞬间,大量请求就会直达数据库。
解决方案:
1、设置热点数据永不过期:对于某个需要频繁获取的信息,换存在redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景不太合适。
2、定时更新:比如热点数据过期时间是1h,那么没到59分钟时就是定时任务去更新热点key,重新设置过期时间
3、分布式锁:互斥锁简单来说就是redis中根据key获得的value只为空时,先上锁,然后从数据库加载,加载完毕,释放所。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间。
缓存雪崩
这个包含两种情况。一种是缓存服务宕机,一种是缓存服务没有宕机。
第一种:宕机。这叫缓存不可用问题----论如何搭建高可用缓存服务。
第二种:非宕机。缓存服务中的key同时出现了大面积的缓存失效。----热点数据集中失效问题。
解决方案:
第一种:搭建高可用缓存服务,以及业务接口的限流和降级。
第二种:key过期时间+随机值,让过期时间错开。
第二种:用分布式锁控制访问的线程。使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
RDB和AOF到底该如何选择
(1)不要仅仅使用RDB,因为那样会导致你丢失很多数据
(2)也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快
; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug
(3)综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程
度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复
redis事务处理
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
Redis事务没有隔离级别的概念:
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
Redis事务的三个阶段:
开始事务
命令入队
执行事务
Redis事务相关命令:
watch key1 key2 … : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
multi : 标记一个事务块的开始( queued )
exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
discard : 取消事务,放弃事务块中的所有命令
unwatch : 取消watch对所有key的监控
使用示例:
redis的发布订阅模式
发布者:
subscribe channel message 订阅消息
publish channel message 发布订阅消息
解决并发问题
redis的乐观锁
-
利用watc实现redis乐观锁
乐观锁基于CAS(compare and swap)思想实现(比较并替换),这点在讲mysqlMVCC多版本并发控制也提到过,原理是一样的,它不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的尝试重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用reids来实现乐观锁。
-
思路如下
1、利用reids的watch的功能,监控这个redisKey的状态值
2、获取redisKey的值
3、创建redis事务
4、给这个key的值+1
5、然后去执行这个事务,如果key的值被修改则会滚(其实就是事务执行失败),key不加1
redis的分布式锁
实现原理
共享资源互斥;共享资源串行化;单应用中使用锁:(单进程多线程);synchronized、ReentrantLock;分布式应用中使用锁:(多进程多线程);分布式锁是控制分布式系统之间同步访问共享资源的一种方式;利用redis的单线程特性对共享资源进行串行化处理
实现方式-获取锁
- 方式一 (推荐)
使用set命令实现
set(lockkey, requestId, "NX", "EX", expireTime)
- 方式二
使用setnx命令实现 –并发会产生问题
# 先设置锁
setnx(lockkey, requestId) # 执行完进程down
# 设置成功,进程down 永久有效。比的进程无法获取锁
setnx(lockkey, expireTime) # 这一步没有执行
实现方式-释放锁
- 方式一(del命令实现) – 并发
先获取key,再删除key
问题:如果删除的时候,这把锁已经不属于单前客户端的时候会解除他人的锁。案例:如果客户端A加锁,一段时间后A解锁,执行del之前锁突然过期,然后客户端B尝试加锁成功,A在执行del,会将B的锁给解除,这是因为分布式锁是有生命周期的
- 方式二(redis+lua脚本实现) – 推荐
代码示范-python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
使用lua脚本解决redis分布式锁存在的错误释放问题
"""
import redis
import time
import hashlib
class DistributedLock:
def __init__(self, redis_instance, lock_key, value):
"""
:param redis_instance:
:param lock_key:
:param value: A unique identifier
"""
self.redis_instance = redis_instance
self.lock_key = lock_key
self.value = value
def set_lock(self, ex=3):
"""
:param ex: sets an expire flag on key ``name`` for ``ex`` seconds.
:return: True -> success or None -> fail
"""
return self.redis_instance.set(self.lock_key, self.value, ex=ex, nx=True)
def del_lock(self):
"""
:return: 1 -> success or 0 -> fail
"""
try:
return self.redis_instance.evalsha(self.script_sha1_str(), 1, self.lock_key, self.value)
except redis.exceptions.NoScriptError:
return self.redis_instance.eval(self.lua_script(), 1, self.lock_key, self.value)
@staticmethod
def lua_script():
lua_str = """
if redis.call("GET",KEYS[1]) == ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
"""
return lua_str
def load_script(self):
return self.redis_instance.script_load(self.lua_script())
def script_sha1_str(self):
script_sha1 = hashlib.sha1(self.lua_script())
return script_sha1.hexdigest()
def is_script_exist(self):
"""
:return: True or False
"""
return self.redis_instance.script_exists(self.script_sha1_str())[0]
def flush_script(self):
return self.redis_instance.script_flush()
@classmethod
def unique_value(cls):
return int(time.time() * 1000)
if __name__ == '__main__':
"""
def redis_cache():
connection_pool = redis.ConnectionPool(
host='127.0.0.1',
port='6379',
db=0,
)
return redis.Redis(connection_pool=connection_pool)
distributed_lock = DistributedLock(
redis_instance=redis_cache(),
lock_key='distributed_lock',
value=DistributedLock.unique_value()
)
print(distributed_lock.set_lock(ex=30))
print(distributed_lock.del_lock())
"""
如果您觉得文章对您有所帮助,可以请囊中羞涩的博主吃个鸡腿饭,万分感谢。愿每一个来到这里的人生活幸福美满。
微信赞赏
支付宝赞赏