全局唯一ID
【问题
】:如果订单表使用数据库自增id会存在以下问题:
- id的规律太明显,容易让用户猜测到一些信息;
- 随着时间的推移,单张表的压力会很大,所以需要将数据分到多张表,但是如果每张表都采用自增的id,就会导致id出现重复
【解决
】:全局ID生成器,是一种在分布式系统下用来生成全局唯一id的工具,需要满足以下几个特性:
- 唯一性:id肯定要唯一
- 递增性:这个id是来替代数据库id,这样有利于数据库创建索引
- 高性能、高可用、安全性
@Component
public class RedisIdWorker {
private static final long BEGIN_TIMESTAMP = 1735689600L; // 开始时间戳
private static final int COUNT_BITS = 32; // 序列号的位数
@Resource
private StringRedisTemplate stringRedisTemplate;
public long nextId(String keyPrefix) {
// 1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = nowSecond - BEGIN_TIMESTAMP;
// 2. 生成序列号(redis单个值的自增长有个上限:2^64),所以这里不能永远用同一个key,可以考虑拼接一个日期字符串
// 获取当前的日期
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
// redis自增长
long cont = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3. 拼接并返回
return timeStamp << COUNT_BITS | cont; // 时间戳 左移32位 把低32位空出来,然后去 或 count
}
}
实现优惠券秒杀下单
@Transactional
public Long seckillVoucher(Long voucherId) {
SeckillVoucher seckillVoucher = seckillVoucherMapper.selectById(voucherId);
LocalDateTime now = LocalDateTime.now();
Assert.isTrue(seckillVoucher.getBeginTime().isBefore(now) && seckillVoucher.getEndTime().isAfter(now), ()->new RuntimeException("当前优惠券不在规定的时间"));
Assert.isTrue(seckillVoucher.getStock() > 0, ()->new RuntimeException("库存不足"));
// 扣减库存
Assert.isTrue(seckillVoucherMapper.update(Wrappers.<SeckillVoucher>lambdaUpdate().setSql("stock = stock - 1").eq(SeckillVoucher::getVoucherId, voucherId)) > 0, ()->new RuntimeException("抢光啦"));
// 下单
VoucherOrder voucherOrder = new VoucherOrder().setId(redisIdWorker.nextId("order")).setUserId(UserHolder.getUser().getId()).setVoucherId(voucherId);
Assert.isTrue(voucherOrderMapper.insert(voucherOrder) > 0, ()->new RuntimeException("抢光啦"));
return voucherOrder.getId();
}
超卖问题
【问题
】由于多个线程同时访问同一个共享资源,但是并没有对共享资源进行加锁处理,这就导致线程1查询库存后还没更新库存,线程2也去查询库存,此时查询的是线程1还没更新之前的数据,所以就会出现超卖问题。
【解决
】:
- 悲观锁:认为线程安全问题一定会发送,所以在操作数据之前先获取锁,确保线程串行执行。(Synchronized、Lock都属于悲观锁)
- 乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其他线程对数据做了修改。
- 如果没有修改:认为安全,才能更新数据
- 如果已经被其他线程修改:说明发生了安全问题,可以重试或抛异常
乐观锁(更新数据)
乐观锁的关键是:之前查询到的数据是否被修改过
版本号法
给数据加上一个版本号(version),在修改库存的同时还要去修改版本号。
CAS法
【优化
】:从上图可以直观看到,version字段和stock字段做的事其实是一样的,所以只需要在更新库存的时候,看看和之前的库存相比是否有变化,如果有变化就放弃更新,如果没有变化再更新。(这种方式相当于是用数据本身来代替版本号)
和原库存做对比
seckillVoucherMapper.update(
Wrappers.<SeckillVoucher>lambdaUpdate()
.setSql("stock = stock - 1")
.eq(SeckillVoucher::getVoucherId, voucherId)
.eq(SeckillVoucher::getStock, seckillVoucher.getStock())
)
【
现象
】使用foxApi做测试,200个线程,只有23个线程是下单成功的,剩下的全部下单失败了。
【原因
】因为CAS的乐观锁机制,更新的条件是stock = 查询时的stock,如果并发时多个线程读取到相同的stock,只有一个线程能更新成功,所以这样会导致大量的失败。
【改进
】
seckillVoucherMapper.update(
Wrappers.<SeckillVoucher>lambdaUpdate()
.setSql("stock = stock - 1")
.eq(SeckillVoucher::getVoucherId, voucherId)
.gt(SeckillVoucher::getStock, 0)
)
一人一单问题
需求:一个用户只能下一单
悲观锁(新增数据)
// 一人一单:
@Transactional
public VoucherOrder createVoucherOrder(Long voucherId) {
// 判断这个用户是否已经下过单
Long userId = UserHolder.getUser().getId();
/**
* 希望同一个用户是同一把锁,这样不同的用户就不会被锁定,锁的范围变小,性能也会得到很大的提升
* 注意:一个请求过来,这个userId都是一个新的对象,所以这里要变成String类型
* 但是如果光变成String类型还是不行,toString方法的底层还是一个全新的对象
* 因此还需要额外调用intern()方法,这个方法相当于是去字符串常量池中去找一个一样的字符串
* 这样就能保证是一个对象了
*/
synchronized (String.valueOf(userId).intern()) {
Long count = voucherOrderMapper.selectCount(Wrappers.<VoucherOrder>lambdaQuery().eq(VoucherOrder::getVoucherId, voucherId).eq(VoucherOrder::getUserId, userId));
Assert.isTrue(count == 0, ()->new RuntimeException("您已经买过此优惠券啦"));
// 扣减库存
Assert.isTrue(
seckillVoucherMapper.update(
Wrappers.<SeckillVoucher>lambdaUpdate()
.setSql("stock = stock - 1")
.eq(SeckillVoucher::getVoucherId, voucherId)
.gt(SeckillVoucher::getStock, 0)
) > 0, ()->new RuntimeException("优惠券购买失败"));
// 下单
VoucherOrder voucherOrder = new VoucherOrder().setId(redisIdWorker.nextId("order")).setUserId(userId).setVoucherId(voucherId);
Assert.isTrue(voucherOrderMapper.insert(voucherOrder) > 0, ()->new RuntimeException("优惠券购买失败"));
return voucherOrder;
}
}
但是以上方法还是存在一点小问题:
【问题
】因为这个是操作多张表,涉及到事务。
现在这个锁是加在方法内部,如果这个线程已经购买到消费券,锁释放了,但是事务还没有提交。
此时如果又有一个新的线程进来,由于事务还没有提交,这个新的线程仍认为此时还未购买消费券,然后也会下单。
【解决
】:我们不应该将锁加在这个方法内部,应该加在调用这个方法的地方。
在这个方法内部不加锁,在方法的调用处:
synchronized (String.valueOf(userId).intern()) {
return createVoucherOrder(voucherId).getId();
}
但是这样还是存在事务的问题:
【问题
】由于我们只给了createVoucherOrder添加事务,但是外层调用它的函数并没有添加事务,但是此时调用createVoucherOrder方法时,只能拿到当前类的对象,而不是拿到他的代理对象。
事务之所以能够生效,是因为是使用当前类的代理对象去做的事务处理。
【解决
】:需要拿到当前对象的代理对象
- 使用当前类的代理对象去调用
synchronized (String.valueOf(userId).intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId).getId();
}
- 还需要引入一个依赖(底层是基于aspectj实现的)
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
- 启动类上需要添加注解,暴露代理对象
@EnableAspectJAutoProxy(exposeProxy=true)
分布式锁
上边写的代码还是有问题的。
【存在问题
】:在集群模式下,由于集群模式下会有多个JVM,每个JVM都会有自己的锁监视器,这就导致就会有多个锁监视器,因此每一个JVM内会有一个线程是成功的
【解决
】:主要还是因为不同的JVM会有不同的锁监视器,所以只需要让多个JVM使用同一个锁监视器就可以了(跨进程的锁)
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
分布式锁的实现
基于Redis的分布式锁
思路分析
- 获取锁:
- 互斥
- 确保同一时刻只有一个线程获得锁
- 超时释放(获得锁的时候添加一个过期时间)
- 非阻塞获取:成功 - 返回true、失败 - 返回false
# 添加锁,利用setnx的互斥特性
setnx lock thread1
# 添加过期时间、避免服务宕机引起死锁
expire lock 10
要保证setnx和expire这两条命令的原子性:
set lock thread1 ex 10 nx
- 释放锁:手动释放
del lock
代码实现
@Data
@Component
@Accessors
public class SimpleRedisLock implements ILock{
@Resource
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX = "lock:";
/**
* 获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return
*/
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
long threadId = Thread.currentThread().getId();
// setIfAbsent:不存在,才执行
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
/**
* 这里会存在一个自动拆箱,如果Boolean是null,就会存在空指针问题,所以这里要用这种写法
* flag = True : return true
* flag = False || flag = null : return false
*/
return Boolean.TRUE.equals(flag);
}
/**
* 释放锁
*/
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
调用
:
simpleRedisLock = simpleRedisLock.setName("order:" + userId); // 锁的范围应该是用户
Assert.isTrue(simpleRedisLock.tryLock(10), ()->new RuntimeException("您已经买过此优惠券啦"));
// 获得锁成功
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
Long id = proxy.createVoucherOrder(voucherId).getId();
// 释放锁
simpleRedisLock.unlock();
极端情况1
- 线程1先获取锁,结果线程1的业务还在执行,线程1的锁已经被超时释放了(锁有时间),线程1还不知道自己的锁被释放了
- 此时线程2来了,线程2获取锁成功,线程2还在执行业务
- 这时线程1执行完自己的业务逻辑,线程1去释放锁,但是线程1释放的锁却是线程2的锁!线程2还在执行自己的业务结果锁被释放了。
- 此时线程3也来了,由于此时锁被线程1释放了,线程3获取锁也成功,也开始执行自己的业务…
【产生原因
】线程释放锁的时候,没有进行判断当前锁是不是自己的锁,因此很容易释放别的线程的锁。
【解决
】获取锁的时候要存储线程的标识;释放锁的时候,需要判断锁标识是不是自己的。
改进的Redis分布式锁(线程释放锁前判断是否是自己的锁)
- 获取锁的时候存入线程标识(可以用UUID标识)
- 释放锁的时候先获取锁中的线程标识,判断是否与当前线程标识一致,一致才释放锁。
线程id是一个递增的数字,在JVM内部,每创建一个线程,线程id就会递增。
在集群模式下,不同的JVM都会维护不同的递增数字。因此两个JVM很可能出现线程id冲突的情况。
所以应该使用UUID + 线程id
来作为线程的标识
- UUID:区分不同的JVM
- 线程ID:区分不同的线程
@Data
@Component
@Accessors(chain = true)
public class SimpleRedisLock implements ILock{
@Resource
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
/**
* 获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return
*/
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// setIfAbsent:不存在,才执行
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
/**
* 这里会存在一个自动拆箱,如果Boolean是null,就会存在空指针问题,所以这里要用这种写法
* flag = True : return true
* flag = False || flag = null : return false
*/
return Boolean.TRUE.equals(flag);
}
/**
* 释放锁
*/
@Override
public void unlock() {
String threadId = ID_PREFIX + Thread.currentThread().getId();// 当前线程id
if(threadId.equals(stringRedisTemplate.opsForValue().get(KEY_PREFIX + name))) { // 判断标识是否一致
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
极端情况2
- 线程1释放锁的时候,会判断这个锁是自己的再释放。但是释放锁的时候也有可能会发生阻塞。
- 线程1在“释放锁”之前,“判断锁是自己的”之后,锁超时释放了。
- 锁超时后,线程2来了,线程2获取锁成功,此时线程2开始执行自己的业务逻辑
- 线程2在执行自己的业务逻辑时,线程1苏醒了,线程1直接执行释放锁的动作(因为线程1之前已经判断锁是自己的,但是此时已经变了,锁变成线程2的了),就导致线程1释放了线程2的锁!
【本质原因
】:判断锁标识和释放锁是两个动作,这两个动作之间产生了间隔,所以才出现问题。
【解决
】:判断锁标识和释放锁应该是一个原子动作,这两动作不能出现间隔。
Lua脚本解决多条命令的原子性操作
Lua脚本:在脚本中编写多条Redis命令,确保多条命令执行时的原子性。
如果脚本中的key、value不想写死,可以做为参数传递
- key类型的参数会放入
KEYS
数组中- 其他参数会放入
ARGV
数组
改进的Redis分布式锁(Lua脚本实现判断锁标识和释放锁的原子性)
Lua脚本:判断锁标识 + 释放锁
-- 锁的key
local key = KEYS[1] -- key类型的参数是放在KEYS数组中,下标从1开始
-- 当前线程标识
local threadId = ARGV[1] -- 其他类型的参数是放在ARGV数组中,下标从1开始
-- 获取锁中的线程标识
local id = redis.call('get', key)
-- 比较线程标识和锁中的标识是否一致
if(threadId == id) then
-- 释放锁
return redis.call('del', key) -- 删除成功
end
return 0; -- 删除失败
调用Lua脚本:
private static final DefaultRedisScript<Long> UNLOCKSCRIPT;
static { // 初始化的时候一次读取,这样就不需要每次释放锁的的时候都去读取
UNLOCKSCRIPT = new DefaultRedisScript<>();
UNLOCKSCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCKSCRIPT.setResultType(Long.class); // 返回类型
}
/**
* 释放锁
*/
@Override
public void unlock() {
String threadId = ID_PREFIX + Thread.currentThread().getId();// 当前线程id
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCKSCRIPT, // 脚本
Collections.singletonList(KEY_PREFIX + name), // 锁的key、要求传集合
ID_PREFIX + Thread.currentThread().getId() // 线程标识
);
}
基于Redis的分布式锁优化(Redisson)
setnx实现的分布式锁存在以下问题:
- 不可重入:同一个线程无法多次获取同一把锁
- 不可重试:获取锁只尝试一次就返回false,没有重试机制
- 超时释放:超时释放虽然可以避免死锁,但是这个时间不好控制。如果设置太短,会导致业务还没执行完,就超时释放锁了;如果设置的太短,会导致其他线程也在等待这把锁,锁的阻塞周期过长。
- 主从一致性问题:如果redis提供了主从集群,主从同步存在延迟。当主节点宕机时,此时还未同步数据给从节点,此时其他线程也有可能会拿到锁。
Redisson:在Redis基础上实现的分布式工具的集合。
使用Redisson
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
- 配置Redisson客户端:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
// 配置
Config config = new Config();
config.useSingleServer() // 单节点模式
.setAddress("redis://127.0.0.1:6379");
// 创建RedissionClient对象
return Redisson.create(config);
}
}
- 使用Redisson的分布式锁
RLock lock = redissonClient.getLock("lock:order:" + userId);
Assert.isTrue(lock.tryLock(), ()->new RuntimeException("您已经买过此优惠券啦"));
// 获得锁成功
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
Long id = proxy.createVoucherOrder(voucherId).getId();
// 释放锁
simpleRedisLock.unlock();
直接使用Redisson可以保证原子性,因为它底层是通过lua实现的
Redis优化秒杀
思路分析
由于“判断库存”、“查询订单”、“减库存”、“创建订单”这几个操作都是需要操作数据库的,效率比较低。因此考虑引入Redis:
- 将库存信息、订单信息存入Redis中:主线程先去Redis中查询,完成对于秒杀功能的判断。(Lua脚本)
- 保存优惠券id、订单id、用户id到阻塞队列里,主线程直接返回订单id给用户。
- 另外再开一个线程去异步读取队列中的信息,完成下单操作。
这样整个业务流程会非常短,只需要在Redis中判断用户是否有购买资格,如果有的话,再开一个线程继续执行。
代码实现
- 在添加优惠券的时候需要将stock存入redis中
- 通过lua脚本实现对秒杀功能的判断:
-- 参数列表
local voucherId = ARGV[1] -- 优惠券id
local userId = ARGV[2] -- 用户id
-- 数据key
local stockKey = 'seckill:stock:' .. voucherId -- 库存key
local orderKey = 'seckill:order:' .. voucherId -- 订单key
-- 1.判断库存是否充足
local stockNum = tonumber(redis.call('get', stockKey))
if(stockNum <= 0) then
-- 库存不足:返回1
return 1
end
-- 2.判断用户是否下单 sismember orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 存在:重复下单、返回2
return 2
end
-- 3. 扣库存 incrby stockKey - 1
redis.call('incrby', stockKey, -1)
-- 4. 下单(将userId添加到orderKey中)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
- 调用lua脚本,如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static { // 初始化的时候一次读取,这样就不需要每次释放锁的的时候都去读取
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class); // 返回类型
}
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024); // 阻塞队列
@Override
public Long seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
// 1. 执行lua脚本
/**
* res = 1:库存不足
* res = 2:重复下单
* res = 0:成功
*/
Long res = stringRedisTemplate.execute(
SECKILL_SCRIPT, // 脚本
Collections.emptyList(), // key类型参数
String.valueOf(voucherId), String.valueOf(userId) // 其他类型参数
);
int r = res.intValue();
// 2. 判断结果
// 3. 不为0 - 没有购买资格
if (r != 0) {
throw new RuntimeException(r == 1 ? "库存不足" : "不可重复下单");
}
// 4. 为0 - 有购买资格 - 把下单信息保存到阻塞队列中
long orderId = redisIdWorker.nextId("order");
VoucherOrder voucherOrder = new VoucherOrder().setId(redisIdWorker.nextId("order")).setUserId(userId).setVoucherId(voucherId);
// 保存阻塞队列
orderTasks.add(voucherOrder);
// 5. 返回订单id
return orderId;
}
DefaultRedisScript的泛型类似应该是Long,如果改成Integer就会报错:
Redis exception; nested exception is io.lettuce.core.RedisException: java.lang.IllegalStateException
这是因为:在 Redis 内部,所有数值(包括 Lua 脚本的返回值)都以 64 位有符号整数(long) 的形式处理,即使用 return 1 这样的语句,Redis 也会将其转换为 Long 类型返回给客户端。
- 开启线程,不断从阻塞队列中获取信息,实现异步下单
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
private IVoucherOrderService proxy;
@PostConstruct // 当前类初始化的时候执行
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); // 提交任务
}
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1. 获取队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take(); // 如果队列中没有数据,就会阻塞在这
// 2. 创建订单
// 扣减库存
Assert.isTrue(
seckillVoucherMapper.update(
Wrappers.<SeckillVoucher>lambdaUpdate()
.setSql("stock = stock - 1")
.eq(SeckillVoucher::getVoucherId, voucherOrder.getVoucherId())
.gt(SeckillVoucher::getStock, 0)
) > 0, ()->new RuntimeException("优惠券购买失败"));
// 下单
Assert.isTrue(voucherOrderMapper.insert(voucherOrder) > 0, ()->new RuntimeException("优惠券购买失败"));
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
总的来说,秒杀优化的思路就是,变成异步下单为同步下单。
- 如果用户有抢单资格,直接返回成功的信息给用户
- 剩下下单的动作,就交给另一个线程异步处理
Redis消息队列实现异步秒杀
上边的阻塞队列用的是JDK的阻塞队列,这个阻塞队列使用的是JVM的内存。
【问题
】:
- 内存限制问题:如果不加以限制,在高并发的场景下,会有无数的订单对象创建、放入内存,这样下去很有可能会导致内存溢出。所以要求说创建阻塞队列的时候要指定阻塞队列的长度。
- 数据安全问题:如果从阻塞队列中拿到一个订单任务后发生了异常,那这个订单任务就再也没有机会被处理了
redis提供了三种不同的方式来实现消息队列:
- list结构:基于List结构模拟消息队列
- PubSub:基于点对点消息模型
- Stream:比较完善的消息队列模型
基于List结构模拟消息队列
list数据结构:双向链表
队列的入口和出口不在同一边,利用lpush结合rpop、rpush结合lpop来实现
【优】:
- 利用Redis存储、不受JVM内存上限
- 基于Redis持久化机制,数据安全性有保证
- 可以满足消息有序性
【缺】:
- 无法避免消息丢失:push命令执行后,直接从消息队列中移除,此时如果消息丢失了,其他消费者也拿不到该消息
- 只支持单消费者
【注】当队列中没有消息时,rpop和lpop会返回null,不会像JVM的阻塞队列一样阻塞并等待消息。
因此应该使用brpop、blpop来实现阻塞效果
基于PubSub的消息队列
消费者可以订阅一个或多个channel,生产者向对应的channel发送消息,所有的订阅者都能收到相关消息。
【相关命令】:
- subscribe channel [channel]:订阅一个或多个频道
- publish channel msg:向一个频道发送消息
- psubscribe pattern [pattern]:订阅与pattern格式匹配的所有频道
【优】:采用发布订阅模型,支持多生产、多消费
【缺】:
- 不支持数据持久化、无法避免消息丢失:发布消息时,如果这个频道没有被任何消费者订阅,那么这个消息就会丢失。发出的消息不会在redis中保存
- 消息堆积有上限:发送消息时,如果有消费者监听,会在消费者那边有个缓冲区(有上限),把消息缓存下来
基于Stream的消息队列
Stream是Redis5.0引入的一种新的数据类型,是一个功能完善的消息队列。
单消费者模式
发送消息:xadd命令
读取消息:xread命令
消息读取后不会消失,仍然在消息队列中
消息漏读风险:当我们指定起始ID为$时,代表读取最新的消息,如果处理一条消息的过程中,又有超过1条以上的消息到达队列,下次获取时也只能获取最新的一条消息,会出现消息漏读的问题
消费者组模式
消费者组:将多个消费者划分到一个组中,监听一个队列。
【特点
】:
- 消息分流:队列中的多个消费者是竞争关系,队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息的处理速度
- 消息标识:消费者组会维护一个标识,记录最后一个被处理的消息,确保每个消息都会被消费
- 消息确认:消费者获取消息后,消息处于pending状态,并存入pending-list队列。当处理完消息通过XACK来确认消息,标识消息为已处理,才会从pending-list移除。
xgroup命令
创建消费者组
:xgroup create key groupName ID
删除消费者组
:xgroup destory key groupName
给消费者组添加消费者
:xgroup createconsumer key grouName consumerName
删除消费者组中指定的消费者
:xgroup delconsumer key groupName consumerName
xreadgroup命令
从消费者组读取消息: