全局唯一ID: 经常会需要全局唯一ID,比如订单表的生成。因为

  • 如果ID自增,则规律性太明显
  • 并且表中可能存在大量的数据,会受到表单数据量的限制

全局唯一ID生成器特性:

  1. 唯一性
  2. 高可用
  3. 高性能
  4. 递增性
  5. 安全性

超卖问题

  • 原本库存为1,线程1查询后发现库存满足要求。要去扣除库存,但在此之前,线程2,线程3同时查询,发现库存充足。同样满足扣除库存的要求,此时产生了超卖问题
  • oversell

解决方法: 加锁

  1. 乐观锁: 认为线程安全问题不一定发生,因此不加锁,只是在更新数据时判断有没有其他线程对数据进行了修改

    • 如果没有修改则认为自己安全,才能更新数据
    • 如果已经被其他线程修改说明发生了安全问题,此时重试或返回异常
  2. 悲观锁: 认为线程安全问题一定会发生,因此操作数据之前先获取数据,确保线程串行执行。(性能差)

    • 比如Synchronized、Lock都是悲观锁

乐观锁

  1. 版本号法
    • 用版本标识数据更新,如果版本version更新了,则表示数据已经被更新了。
  2. CAS法
    • 用之前查询到的数据判断是否存在相同的数据,如果不存在,则表示数据已经更新。实际上就是简化了版本字段。

这样存在问题,因为如果有一百个线程,初始情况下stock=100,第一个线程首先执行查询,满足,然后修改数据库stock - 1。在修改数据库之前,其他线程查询stock=100,也满足查询条件,但是实际上查询数据库只有stock=99了,发现被修改了,因此直接错误退出。
所以应该使用stock>0而不是等于前一个值

分布式锁

  • 满足分布式系统或集群模式下多进程可见并且互斥的锁

要求

  1. 多进程可见即所有JVM可见
  2. 互斥
  3. 高可用
  4. 高性能
  5. 安全性
MySQL Redis Zookeeper
互斥 利用MySQL本身互斥锁机制 利用SETNX互斥命令 利用节点的唯一性和有序性实现互斥
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超市时间,到期释放 临时节点,断开连接自动释放

基于Redis的分布式锁

分布式锁需要实现两个方法:获取锁和释放锁

distributedlock

  1. 获取锁
    • 互斥: 确保只有一个线程能够获取锁setnx lock thread1expire lock 5
    • 但是上面这种两条语句的方法不是原子的,有可能执行了一条后线程中断
    • 可以使用set lock thread1 nx ex 5合并成一条语句
  2. 释放锁
    • 手动释放: del key
    • 超时释放: 获取锁的时候设置超时时间

Redisson分布式锁原理

  1. 可重入: 利用hash结构记录线程ID和重入次数
  2. 可重试: 利用信号量和pubsub功能实现等待、唤醒,获取锁失败的重试机制
  3. 超时续约: 利用watchDog,每隔一段时间重置超时时间

Redis消息队列

基于List的消息队列的优缺点

  • 优点:
    1. 利用Redis存储,不受限与JVM内存上限
    2. 基于Redis持久化机制,保证数据安全性
    3. 满足消息有序性
  • 缺点:
    1. 无法避免消息丢失
    2. 只支持单消费者

基于PubSub的消息队列

  • 发布订阅,一个消费者可以订阅一个或多个channel,生产者向对应的channel发送消息后,所有订阅者可以收到相关信息
  • subscribe channel [channel]: 订阅一个或多个频道
  • publish channel msg: 向一个频道发送消息
  • psubscribe pattern [pattern]: 订阅与pattern格式匹配的所有频道
  • 优点:
    1. 采用发布订阅模型,支持多生产,多消费
  • 缺点:
    1. 不支持持久化
    2. 无法避免消息丢失,不安全
    3. 消息堆积有上限,超出时数据丢失

基于Stream的消息队列

  • 发送消息: xadd key [NOMKSTREAM] [MAXLEN|MINID [=|~] threadhold [LIMIT count]] *|ID field value [field value ...]
    • [NOMKSTREAM]: 如果队列不存在,是否自动创建队列,默认是动创建
    • [MAXLEN|MINID [=|~] threadhold [LIMIT count]]: 设置消息队列的最大消息数量
    • *|ID: 消息的唯一ID,*代表由Redis自动生成,格式是时间戳-递增数字
    • field value [field value ...]: 发送到队列中的消息,称为Entry,格式就是多个键值对
  • 读取消息: XREAD [COUNT count] [BLOCK milliseconds] STREAMS key[key ...] ID [ID ...]
    • [COUNT count]: 每次读取消息的最大数量
    • [BLOCK milliseconds]: 没有消息时,是否阻塞,阻塞时长
    • STREAM key [key ...]: 要从哪个队列读取消息,key就是队列名
    • ID [ID ...]: 起始ID,只返回大于该ID的消息。0表示从第一条消息开始,$表示从最新的消息开始

指定$表示读取最新的消息,但是如果一下到来几条消息,依然只会读取最新的消息,导致漏读

  • 优点:
    1. 消息可回溯
    2. 一个消息可以被多个消费者读取
    3. 可以阻塞读取
  • 缺点:
    1. 有消息漏镀的风险

消费者组

  • 将多个消费者划分到一个组中,监听同一个队列

  • 特点:

    1. 消息分流: 队列中的消息会分流给组内不同的消费者,不是重复的消费,从而加快消息处理的速度
    2. 消息标识: 消费者组维护一个标识,记录最后一个被处理的消息,哪怕消费者宕机,还会从标识之后读取消息,确保每一个消息都会被消费
    3. 消息确认: 消费者获取消息后,消息处于pending状态,存入pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list中移除(可以确保所有的消息都能被消费到)
  • 创建消费者组: xgroup create key groupName ID [MKSTREAM]

    • key: 队列名称
    • groupName: 消费者组名称
    • ID: 起始ID标识,$标识队列中最后一个消息,0标识队列中第一个消息
    • MKSTREAM: 队列不存在时自动创建队列
  • 删除消费者组: xgroup destroy key groupName

  • 删除消费者组中指定的消费者: xgroup delconsumer key groupname consumername

  • 给指定的消费者组添加消费者: xgroup createconsumer key groupname consumername

  • 从消费者组读取消息: xreadgroup GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]

    • group: 消费者组名称
    • consumer: 消费者名称,如果消费者不存在,则会自动创建一个
    • count: 本次查询的最大数量
    • BLOCK milliseconds: 当没有消息时最长等待时间
    • NOACK: 无需手动ACK,获取到消息后自动确认
    • STREAMS key: 指定队列名称
    • ID: 获取消息的起始ID
      • ‘>’: 从下一个未消费的消息开始
      • 其他: 根据指定ID从pending-list中获取已经消费但是没有确认的消息,比如0,是从pending-list中的第一个消息开始
  • 特点:

    1. 消息可回溯
    2. 可以多消费者争抢消息,加快消费速度
    3. 可以阻塞读取
    4. 没有消息漏读风险
    5. 有消息确认机制,保证消息至少被消费一次
List PubSub Stream
消息持久化 支持 - 支持
阻塞读取 支持 支持 支持
消息堆积处理 受限于内存空间,可以利用多消费者加快处理 受限于消费者缓冲区 受限于队列长度,可以利用消费者组提高消费速度,减少堆积
消息确认机制 - - 支持
消息回溯 - - 支持

GEO

  • GEOADD: 添加一个地理空间信息,包括经度、纬度、值
  • GEODIST: 计算指定两个点之间的距离并返回
  • GEOHASH: 将值的坐标转为哈希字符串形式并返回
  • GEOPOS: 返回指定值的坐标
  • GEORADIUS: 指定圆心、半径,找到院内包含的所有值,按照圆心之间的距离排序后返回,6.2版本以后废弃
  • GEOSEARCH: 指定范围内搜索值,按照与指定点之间的距离排序后返回,范围可以是圆形或矩形
  • GEOSEARCHSTORE: 与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key中

BitMap

  • Redis中使用String类型实现BitMap,最大上限512M,即232bits2^{32} bits
  • SETBIT: 向指定位置offset存入一个0 or 1
  • BITCOUNT: 统计BitMap中值为1的bit位的数量
  • BITFIELD: 操作BitMap中bit数组中的指定位置offset的值,实现了查询修改以及自增,一般用来查询
  • BITFIELD_RO: 获取BitMap中bit数组,并以十进制形式返回
  • BITOP: 将多个BitMap的结果位运算
  • BITPOS: 查找bit数组中指定范围内第一个0 or 1出现的位置

HyperLogLog(HLL)

  • UV: Unique Visitor独立访客量,通过互联网访问,浏览这个网页的自然人。一天内同一个用户多次访问,只记录一次

  • PV: Page View页面访问量或点击量,用户没访问网站的一个页面,记录次PV,用来衡量网站的流量

  • UV在服务端做会麻烦,因为要判断该用户是否统计过了,所以需要保存用户信息。但是如果每个访问的用户都保存到Redis中,则数据量很大

  • HLL是一种概率算法,用于确定非常大的集合的基数,不需要存储其所有的值。基于String实现,单个HLL内存永远小于16K,测量结果是概率性的,误差小于81%。对于UV统计完全可以忽略