Redis和苍穹外卖学习笔记
在我来个人平台:
https://www.wolai.com/vB9b7m2Wi2YxF6kaV7dt4C
ThreadLocal用于线程间的数据隔离,多线程情况下,每个线程只能访问自己内部的ThreadLocal变量,保证数据的安全性。
原理:每一个线程都会维护一个ThreadLoaclMap,这个Map是一个哈希散列结构,每一个元素(Entry)都是一个键值对,key尾ThreadLocal,Value为存储的数据。

ThreadLocal在多线程中可能存在的问题?
内存泄漏问题:不及时清理——没有合适的调用remove()方法清理线程局部变量,占用内存(解决方法 手动释放)
长时间运行的线程池——线程池中的线程对象可能会被反复使用,导致一个ThreadLocal变量在不同任务之间传递,在执行任务后如果没有及时清理,可能会导致变量泄露。(线程池中不适用ThreadLocal)
秒杀优化思路
优惠券秒杀库存超卖问题使用改良乐观锁,对于一人多单问题使用synchronized,但对集群来说synchronized是无效的,此时使用分布式锁(redis的setnx互斥命令)。
同时分布式锁也存在一定问题,比如获取锁的线程超时释放后该线程执行完又再一次释放锁,但此时释放的不是自己的锁,从而导致的超卖问题。
解决方法:在释放锁时判断锁的线程标识是否与当前线程一致
此时依旧存在判断锁标识是否相等和释放锁操作不是原子操作的问题,一旦判断线程标识相等之后阻塞,又会出现上面一样的问题。
解决方法:将释放锁的操作存为lua脚本。
但是上面的分布式锁仍然存在四个问题:不可重入,不可重试,超时释放和主从一致性问题。
针对上面四个问题,使用了Redisson,直接不用上面的了,哈哈哈
Redisson

Redisson的可重入锁原理(借鉴ReentrantLock)
替换String setNX EX,使用Hash结构,将线程标识和可重入次数都存入Hash中。

释放锁时会判断重入次数是否为0,如果是0才会删除锁。
没有setNX因此需要自己手动判断锁是否存在(Exist),并且手动设置锁有效期:
释放锁时,需要先判断锁是否是自己的,如果是且锁的重入次数不为0,需要重置锁有效期。
获取锁和释放锁的流程:



Redisson的可重试原理(获取锁尝试不止一次,有重试机制)
一旦在tryLock()方法中传入waitTime时间,则默认在waitTime时间内会一直尝试获取锁。
当获取锁失败时返回指定key的剩余有效期ttl(通过异步回调函数),计算尝试获取锁之后剩余的有效的等待时间time,订阅线程释放锁的消息通知,如果在等待期间获得了释放锁的消息通知,再次计算剩余有效等待时间有效期,如果剩余时间<=0获取锁失败,否则继续尝试获取锁。 (消息订阅)
尝试获取锁也有一个最大等待时间,当ttl< time时,等待ttl时间,否则等待time时间。(信号量机制)
Redisson的超时释放(确保锁在业务执行完之后释放,而不是因为阻塞释放)
当获取锁成功之后,会自动对线程进行续约,后台执行一个延时定时任务,releaseTime/3的时间后会自动更新锁的有效期;当锁释放时,会取消更新任务。
只有leaseTime不填(-1)的情况下才会有看门狗机制

Redisson的主从一致性 ?
解决单台Redis发生故障,所有业务都会出现问题。
多台Redis,主节点用来处理Redis的增删改,从节点处理Redis的读请求,主节点不断的把数据同步给从节点,但可能会存在延时。
解决方法:所有节点都变成独立的Redis节点,此时必须在每一个节点都拿到锁才能获取锁

将锁列表转为一个集合,只有获取集合中所有的锁才算获取锁
实际应用在项目中:
生产者确定:
通过配置文件开启Publisher Confirm和Publisher Return机制;
定义ConfirmCallback和ReturnCallback;
ConfirmCallback中MQ的回执通过Future来返回,addCallback
消费者确认:当业务正常执行时自动返回ack,当业务出现异常时,根据异常判断返回不同结果:如果是业务异常,会自动返回nack(重新投递给消费者),如果是消息处理或校验异常,自动返回reject(消息被删除)。

失败重试机制:使得返回nack时并不是一直无条件的重新入队列,而是在本地重试,重试达到最大次数后,返回reject,消息被丢弃。

失败消息处理策略:对于消息可靠性要求高的业务场景,不能当测试达到最大重试次数后就丢弃。
@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
// 处理失败消息的交换机和队列
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}
// 定义一个RepublishMessageRecoverer,关联队列和交换机
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
1、在秒杀业务中先执行lua脚本判断当前用户是否具有购买资格,确定有购买资格之后,将订单信息存到消息队列中。
秒杀业务代码
public Result seckillVoucher(Long voucherId) {
//1.执行lua脚本判断当前用户的购买资格
Long userId = UserHolder.getUser().getId();
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString());
//2.判断结果是不是为0
int r=result.intValue();
if (result!=0){
//2.1 不为0,代表没有购买资格
return Result.fail(r==1?"库存不足":"不能重复下单");
}
//3.走到这一步说明有购买资格,将订单信息存到消息队列
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
//4. 存入消息队列等待异步消费
// 4.1 创建CorrelationData
CorrelationData cd = new CorrelationData();
// 4.2 给Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>(){
@Override
public void onSuccess(CorrelationData.Confirm confirm) {
// 4.3 消息发送成功时的处理逻辑
if(confirm.isAck()){
log.debug("消息发送成功,收到ack!");
}else{
log.error("消息发送失败,收到nack!"+ confirm.getReason());
}
}
@Override
public void onFailure(Throwable throwable) {
// 4.2.2 消息发送失败时的处理逻辑
log.error("消息发送失败,发生异常!"+throwable.getMessage());
}
});
//向交换机发送信息
rabbitTemplate.convertAndSend("hmdianping.direct","direct.seckill",voucherOrder);
return Result.ok(orderId);
}
2、在工具包中声明listener类,用来声明队列和交换机,并从指定队列里获取消息,使用@RabbitListener注解。
一旦获得新消息,将会执行注解下面的方法。
@Component
public class seckillvoucherListener {
@Resource
IVoucherOrderService voucherOrderService;
//声明队列和交换机
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name="direct.sekill.queue"),//指定队列
key = "direct.sekill",//默认key
exchange = @Exchange(name = "hmdianping.direct",type = ExchangeTypes.DIRECT)//指定交换机
))
public void receiveMessage(Message message, AMQP.Channel channel, VoucherOrder voucherOrder){
voucherOrderService.handleVoucherOrder(voucherOrder);
System.out.println("监听到了"+message);
}
}
3、获得新消息之后,调用优惠卷下单服务中的handleVoucherOrder()方法,将订单信息中的用户Id作为reddison锁的key值,成功获取锁之后将执行创建createVoucherOrder()方法,加锁是解决一人多单的问题确保一个userId只能下一单,Tranctional是保证扣除订单和保存订单操作的原子性,确保数据库数据的一致性。
@Transactional
public void handleVoucherOrder(VoucherOrder voucherOrder){
//1.所有信息从当前消息实体中拿
//获取用户
Long userId=voucherOrder.getUserId();
//创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock=lock.tryLock();
if(!isLock){
//获取锁失败,直接返回失败或者重试
log.error("不允许重复下单!");
return;
}
try{
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
proxy.creatVoucherOrder(voucherOrder);
}finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
//
}
@Transactional
public void creatVoucherOrder(VoucherOrder voucherOrder){
// 5.一人一单逻辑
// 5.1.用户id
//6.扣减库存
Long voucherId = voucherOrder.getVoucherId();
// 先查找是否存在订单,从而确保消费幂等性
if(seckillVoucherService.getById(voucherId)!=null){
return;
}
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1") // set stock = stock -1
.eq("voucher_id", voucherId)
.gt("stock",0)// where id = ? and stock > 0
.update();
if (!success) {
//扣减库存
log.error("库存不足!");
return ;
}
save(voucherOrder);
}


浙公网安备 33010602011771号