Redis

个人仓库:

https://gitee.com/luzhenxiong/bilibili-database/tree/master/redis

持久化

持化双雄

  • RDB(Redis DataBase)

  • AOF(Append Only File)

RDB

(默认开启)

RDB持久性以指定的时间间隔执行数据集的时间点快照

实现类似照片记录效果的方式,就是把某一时刻的数据和状态以文件的形式写到磁盘上,也就是快照。 这样一来即使故障宕机,快照文件也不会丢失,数据的可靠性也得到了保证。 这个快照文件就称为RDB文件(dump.rdb), 其中,RDB就是Redis DataBase的缩写。

小技巧

物理恢复,一定要服务和备份分机隔离, 不可以把备份文件dump.rdb和生产redis服务器放在同一台机器,必须分开各自存储,以防生产物理机损坏后备份文件也挂了。

手动触发

Redis提供了两个命令来生成RDB文件,分别是save和bgsave

SAVE

在主程序中执行会阻塞当前redis服务器,直到持久化工作完成

执行save命令期间,Redis不能处理其他命令,线上禁止使用

BGSAVE

Redis会在后台异步进行快照操作,不阻塞快照同时还可以响应客户端请求,该触发方式会fork一个子进程由子进程复制持久化过程

LASTSAVE

可以通过lastsave命令获取最后依次成功执行快照的时间

除了缓存,redis的其它用法

  1. 数据共享, 分布式session

  2. 分布式锁

  3. 全局ID

  4. 计算器、点赞

  5. 位统计

  6. 轻量级消息队列

  7. 抽奖

  8. 点赞、签到、打卡

  9. 差集交集并集,用户关注、可能认识的人,推荐模型

  10. 热点新闻、热搜排行榜

计数器

def test_counter(self, c):
    """场景3: 计数器

    https://redis.io/commands/incr/

    在双十一或者其他大型节日中,业务服务器的资源有限,但是在高并发的场景下,过多的请求可能会导致服务器宕机等问题,
    所以会对接口请求做一些限制,比如限制每秒请求总数为200次,超过200次就等待,等下一秒再次请求,这里用Redis作为计数器
    的模式来实现。
    """
    from datetime import datetime
    # 1) 首先在接到请求之后设置一个键,然后自增1,如果不存在,则值会被初始化为0。
    # 2) 当接口得到请求后,执行incryyyyMMddHHmmss:mykey, 如果返回结果小于200则进行处理,超过200将等待下一秒处理。
    now = datetime.now().strftime("%Y%m%d%H%M%S")
    key = f'{now}:mykey'
    assert c.incr(key) == 1
    assert c.incr(key) == 2
    assert c.incr(key) == 3
    assert c.incr(key) == 4
    # 3) 因为每秒会生成一个键, 为了节省内存空间,可能需要一个定时任务,定时删除这些已经使用过的键。
    c.delete(key)

全局ID

def test_unique_id(self, c):
    """场景4: 唯一自增的ID或者流水号

    在有些业务中,为了提高数据库的读写能力,可能会将一个库根据业务拆分成多个小库、将一个大表拆分成几个小表,
    比如user01表、user02表,但是又要保证两张表中的ID是全局统一自增的,所以数据库自带的自增属性是无法使用的。

    这时可以利用Redis的String数据类型实现自增。

    incrby: https://redis.io/commands/incrby/
    """
    # Redis单机的性能是很不错的,但如果想拓展Redis的服务器,可以这样实现:
    # 假设目前有5台Redis服务器, 第一台的生成方式
    assert c.incrby("id", 1) == 1
    assert c.incrby("id", 5) == 6
    assert c.incrby("id", 5) == 11
    assert c.incrby("id", 5) == 16
    assert c.incrby("id", 5) == 21

    c.delete("id")
    # 第2台的生成方式
    assert c.incrby("id", 2) == 2
    assert c.incrby("id", 5) == 7
    assert c.incrby("id", 5) == 12
    assert c.incrby("id", 5) == 17
    assert c.incrby("id", 5) == 22

    c.delete("id")
    # 第3台的生成方式
    assert c.incrby("id", 3) == 3
    assert c.incrby("id", 5) == 8
    assert c.incrby("id", 5) == 13
    assert c.incrby("id", 5) == 18
    assert c.incrby("id", 5) == 23

    c.delete("id")
    # 第4台的生成方式
    assert c.incrby("id", 4) == 4
    assert c.incrby("id", 5) == 9
    assert c.incrby("id", 5) == 14
    assert c.incrby("id", 5) == 19
    assert c.incrby("id", 5) == 24

    c.delete("id")
    # 第5台的生成方式
    assert c.incrby("id", 5) == 5
    assert c.incrby("id", 5) == 10
    assert c.incrby("id", 5) == 15
    assert c.incrby("id", 5) == 20
    assert c.incrby("id", 5) == 25

    # 更优秀的实现方式: 雪花算法

文章阅读数或者网页浏览数统计

def test_statistics(self, c):
    """场景5: 文章阅读数或者网页浏览数统计

    用户的总点赞数、关注数、粉丝数、帖子的评论数、热度、文章的阅读数和收藏数等,
    实现这些需求并想要减轻数据库压力,而且对数据的时效性和写文章的频率要求高时,可以使用Redis。
    """
    # 文章第一次被阅读
    c.incr("mypageurl")
    # 查看文章的阅读数量
    assert c.get("mypageurl") == b'1'
    # 访问时每次都自增1
    c.incr("mypageurl")

分布式锁

def test_distributed_lock(self, c):
    """场景6: 分布式锁

    分布式系统可以借助于Redis中的setnx命令来实现分布式锁。

    流程: 如果键没有值就能新增成功,就表示抢到锁了,否则失败。
    等到操作成功时释放锁,也就是删除键。
    """
    # 给键seckill001赋值,如果之前不存在, 则创建
    assert c.setnx("seckill001", "true")
    # 设置过期时间为20毫秒, 解决死锁问题
    c.expire("seckill001", 20)
    # 当其他客户端进来之后执行如下命令,返回False则表示锁被占用,只能后期继续尝试再次执行
    assert c.setnx("seckill001", "true") is False
    # 当第一个抢到锁的线程执行完业务之后,便可以删除键,让其他线程能够抢到锁
    c.delete("seckill001")
    # 其他线程执行如下命令时返回的结果是1,表示拿到了锁
    assert c.setnx("seckill001", "true")

分布式锁

一个靠谱分布式锁需要具备的条件和刚需

  • 独占性: OnlyOne, 任何时刻只能有且仅有一个线程持有

  • 高可用: 1-若集群环境下, 不能因为某一个节点挂了而出现获取锁和释放锁失败的情况; 2-高并发请求下, 依旧性能OK好使

  • 防死锁: 杜绝死锁, 必须有超时控制机制或者撤销操作,有个兜底终止跳出方案

  • 不乱抢: 防止张冠李戴, 不能私下unlock别人的锁, 只能自己加锁自己释放。

  • 重入性: 同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

实现

中小厂代码

127.0.0.1:6379> setnx k1 true
127.0.0.1:6379> EXPIRE k1 60

大厂差评, setnx + expire不安全, 两条命令非原子性的。

可重入锁

是指在同一个县城在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提, 锁对象得是同一个对象),不会因为之前已经获取得过还没释放而阻塞。

核心是计数器,lock了几次就要unlock几次

用redis的hset字典计数

key=zzyyLock

field=uuid:threading_id

value=lock的次数

后端项目踩坑