Redis(八) - Redis企业实战之优惠券秒杀

本文详细探讨了在分布式环境中如何生成全局唯一ID,利用Redis自增ID和全局ID生成器。接着介绍了如何实现优惠券秒杀下单,包括添加优惠券和下单逻辑,重点解决了超卖问题和一人一单并发安全挑战。最后讨论了在集群模式下的并发问题及解决方案,涉及乐观锁、分布式锁的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、全局唯一ID

1. 全局ID生成器

每个店铺都可以发布优惠券:

在这里插入图片描述
当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显
  • 受单表数据量的限制

所以tb_voucher_order表的主键不能用自增ID:

create table tb_voucher_order
(
    id          bigint                                        not null comment '主键'
        primary key,
    user_id     bigint unsigned                               not null comment '下单的用户id',
    voucher_id  bigint unsigned                               not null comment '购买的代金券id',
    pay_type    tinyint(1) unsigned default 1                 not null comment '支付方式 1:余额支付;2:支付宝;3:微信',
    status      tinyint(1) unsigned default 1                 not null comment '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
    create_time timestamp           default CURRENT_TIMESTAMP not null comment '下单时间',
    pay_time    timestamp                                     null comment '支付时间',
    use_time    timestamp                                     null comment '核销时间',
    refund_time timestamp                                     null comment '退款时间',
    update_time timestamp           default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
);

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

在这里插入图片描述

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
在这里插入图片描述
ID的组成部分:

  • 符号位:1bit,永远为0,表示正数

  • 时间戳:31bit,以秒为单位,可以使用69年

  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

编写全局ID生成器代码:

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳,以2022.1.1为基准计算时间差
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 生成带有业务前缀的redis自增id
     * @param keyPrefix
     * @return
     */
    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        // 加上日期前缀,可以存更多同一业务类型的数据,并且还能通过日期获取当天的业务数量,一举两得
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        // 用于是数字类型的拼接,所以不能像拼接字符串那样处理,而是通过位运算将高32位存 符号位+时间戳,低32位存 序列号
        return timestamp << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long second = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println(second);// 1640995200
    }
}

测试全局ID生成器:

@SpringBootTest
class HmDianPingApplicationTests {
	@Resource
    private RedisIdWorker redisIdWorker;

    private ExecutorService executorService = Executors.newFixedThreadPool(500);

	@Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(300);

        // 每个线程生成100个id
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown();
        };
        // 300个线程
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
            executorService.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time = " + (end - begin));
    }
}

测试结果:

在这里插入图片描述

在这里插入图片描述

2. 全局唯一ID生成策略

  • UUID(不是递增的)
  • Redis自增
  • 雪花算法(snowflake)
  • 数据库自增(单独建一张表存自增id,分配到分库分表后的表中)

3. Redis自增ID策略

  • 以日期作为前缀的key,方便统计订单量
  • 自增ID的结构:时间戳 + 计数器

二、实现优惠券秒杀下单

1. 添加优惠券

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:

在这里插入图片描述
优惠券表信息:

  • tb_voucher:优惠券的基本信息,优惠金额、使用规则等(tb_voucher表的type字段区分是普通券还是秒杀券)
  • tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间(秒杀券才需要填写这些信息),同时秒杀券拥有普通券的基本信息(秒杀券表tb_seckill_voucher的主键id绑定的是普通券表tb_voucher的id)
create table tb_voucher
(
    id           bigint unsigned auto_increment comment '主键'
        primary key,
    shop_id      bigint unsigned                               null comment '商铺id',
    title        varchar(255)                                  not null comment '代金券标题',
    sub_title    varchar(255)                                  null comment '副标题',
    rules        varchar(1024)                                 null comment '使用规则',
    pay_value    bigint(10) unsigned                           not null comment '支付金额,单位是分。例如200代表2元',
    actual_value bigint(10)                                    not null comment '抵扣金额,单位是分。例如200代表2元',
    type         tinyint(1) unsigned default 0                 not null comment '0,普通券;1,秒杀券',
    status       tinyint(1) unsigned default 1                 not null comment '1,上架; 2,下架; 3,过期',
    create_time  timestamp           default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time  timestamp           default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
);
create table tb_seckill_voucher
(
    voucher_id  bigint unsigned                     not null comment '关联的优惠券的id'
        primary key,
    stock       int(8)                              not null comment '库存',
    create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
    begin_time  timestamp default CURRENT_TIMESTAMP not null comment '生效时间',
    end_time    timestamp default CURRENT_TIMESTAMP not null comment '失效时间',
    update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '秒杀优惠券表,与优惠券是一对一关系';

2. 编写添加秒杀券的接口

主要代码:

@RestController
@RequestMapping("/voucher")
public class VoucherController {

    @Resource
    private IVoucherService voucherService;

    /**
     * 新增秒杀券
     * @param voucher 优惠券信息,包含秒杀信息
     * @return 优惠券id
     */
    @PostMapping("seckill")
    public Result addSeckillVoucher(@RequestBody Voucher voucher) {
        voucherService.addSeckillVoucher(voucher);
        return Result.ok(voucher.getId());
    }
}
@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
    }
}

测试添加:
在这里插入图片描述

测试结果:
在这里插入图片描述

三、实现秒杀下单

在这里插入图片描述

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

在这里插入图片描述

主要代码:

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    @Resource
    private IVoucherOrderService voucherOrderService;

    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return voucherOrderService.seckillVoucher(voucherId);
    }
}
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).update();
        if (!success) {
            // 扣减库存失败
            return Result.fail("库存不足!");
        }
        // 6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2.用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        return Result.ok(orderId);

    }
}

简单测试秒杀成功:

在这里插入图片描述
扣减库存成功:

在这里插入图片描述

四、超卖问题

当有大量请求同时访问时,就会出现超卖问题

在这里插入图片描述
在这里插入图片描述

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

在这里插入图片描述

1. 加锁方式 - 乐观锁

乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:

(1)版本号法

在这里插入图片描述

(2)CAS法

  • 用库存代替了版本号,可以少加一个字段
  • 扣库存时,与查询时的库存比较,没被修改则可以扣减库存
    在这里插入图片描述

2. 乐观锁解决超卖问题

乐观锁方式,通过CAS判断前后库存是否一致,解决超卖问题:

// 之前的代码
boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).update();
                
// 乐观锁方式,通过CAS判断前后库存是否一致,解决超卖问题                
boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1") // set stock = stock -1
            .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); // where id = ? and stock = ?

又出现新的问题:

  • 假设100个线程同时请求,但通过CAS判断后,只有一个线程能扣减库存成功,其余99个线程全部失败
  • 此时,库存剩余99,但是实际业务可以满足其余99个线程扣减库存
  • 虽然能解决超卖问题,但是设计不合理

所以为了解决失败率高的问题,需要进一步改进:

  • 通过CAS 不再 判断前后库存是否一致,而是判断库存是否大于0
boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0

3. 小结

超卖这样的线程安全问题,解决方案有哪些?
(1)悲观锁:添加同步锁,让线程串行执行

  • 优点:简单粗暴
  • 缺点:性能一般

(2)乐观锁:不加锁,在更新时判断是否有其它线程在修改

  • 优点:性能相对悲观锁好(但是仍然需要同时查数据库,影响性能)
  • 缺点:存在成功率低的问题(可以采用分段锁方式提高成功率)

五、一人一单问题

  • 需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

在这里插入图片描述
在扣减库存之前,加上一人一单的逻辑:

// 5.一人一单逻辑
Long userId = UserHolder.getUser().getId();
  // 5.1.查询订单数量
  int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  // 5.2.判断是否下过单
  if (count > 0) {
      // 用户已经购买过了
      return Result.fail("用户已经购买过一次!");
  }

此处仍会出现并发问题,当同一用户模拟大量请求同时查询是否下过单时,如果正好都查询出count为0,就会跳过判断继续执行扣减库存的逻辑,此时就会出现一人下多单的问题

解决方法:

  • 由于是判断查询的数据是否存在,而不是像之前判断查询的数据是否修改过
  • 所以这里只能加悲观锁

1. 加锁分析

  • 首先将一人一单之后的逻辑全部加锁,所以将一人一单之后的逻辑抽取出一个方法进行加锁,public Result createVoucherOrder(Long voucherId)
  • 如果直接在方法上加锁,则锁的是this对象,锁的对象粒度过大,就算是不同的人执行都会阻塞住,影响性能,public synchronized Result createVoucherOrder(Long voucherId)
  • 所以将锁的对象改为userId,但是不能直接使用synchronized (userId),因为每次执行Long userId = UserHolder.getUser().getId();虽然值一样,但是对象不同,因此需要这样加锁 synchronized (userId.toString().intern()),intern()表示每次从字符串常量池中获取,这样值相同时,对象也相同
  • 为了防止事务还没提交就释放锁的问题,则不能将锁加在createVoucherOrder方法内部,例如:
@Transactional
public Result createVoucherOrder(Long voucherId) {
	synchronized (userId.toString().intern()) {
		。。。
	}
}

而是需要等事务提交完再释放锁,例如:

synchronized (userId.toString().intern()) {
 	// 获取代理对象(事务)
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

2. 事务分析

  • 由于只有一人一单之后的逻辑涉及到修改数据库,所以只需对该方法加事务
    @Transactional
    public Result createVoucherOrder(Long voucherId)
  • 由于只对createVoucherOrder方法加了事务,而该方法是在seckillVoucher方法中被调用,seckillVoucher方法又没有加事务,为了防止事务失效,则不能直接在seckillVoucher方法调用createVoucherOrder方法,例如:
@Override
public Result seckillVoucher(Long voucherId) {
	。。。。
	synchronized (userId.toString().intern()) {
        return this.createVoucherOrder(voucherId);
    }
}

而是需要通过代理对象调用createVoucherOrder方法,因为@Transactional事务注解的原理是通过获取代理对象执行目标对象的方法,进行AOP操作,所以需要这样:

@Override
public Result seckillVoucher(Long voucherId) {
	。。。。
	// 获取代理对象(事务)
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

并且还要引入依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

还要开启注解暴露出代理对象:

@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }
}

完整VoucherOrderServiceImpl代码:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }

        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            // 获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
        
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {

        // 5.一人一单逻辑
        Long userId = UserHolder.getUser().getId();
        // 5.1.查询订单数量
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否下过单
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6,扣减库存
        // 乐观锁方式,通过CAS判断库存是否大于0,解决超卖问题:
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0

        if (!success) {
            // 扣减库存失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 8.返回订单id
        return Result.ok(orderId);
    }
    
}

六、集群模式下并发安全问题

  • 通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
  1. 我们将服务启动两份,端口分别为8081和8082:
    在这里插入图片描述
  2. 然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
    在这里插入图片描述
    修改完后,重新加载nginx配置文件:
    在这里插入图片描述
    配置2个端口不同的服务:
    在这里插入图片描述
    在这里插入图片描述
    模拟同一用户下单2次:
    在这里插入图片描述

现在,用户请求会在这两个节点上负载均衡,再次测试下是否存在线程安全问题:

  • 访问8081端口的线程进入了synchronized中
    在这里插入图片描述

  • 访问8082端口的线程也进入了synchronized中
    在这里插入图片描述

  • 最终同一个用户下了2单扣了2个库存,所以在集群模式下,出现了一人多单的问题:
    在这里插入图片描述
    分析:

  • 锁的原理是每个JVM中都有一个Monitor作为锁对象,所以当对象相同时,获取的就是同一把锁

  • 但是不同的JVM中的Monitor不同,所以获取的不是同一把锁

  • 因此集群模式下,加synchronized锁也会出现并发安全问题,需要加分布式锁
    在这里插入图片描述

//线程池,以单线程的方式执行订单的创建(在mysql中扣减库存后新增一个订单) /开启线程池/ private static final ExecutorService CACHE_THREADPOOl1 = Executors.newSingleThreadExecutor();public void run() { while (true){ try { //获取消息队列中的消息,对应的命令XREADGROUP GROUP g1 ‘消费者名称’ COUNT 1 BLOCK 2000 STREAMS stream.order List<MapRecord<String, Object, Object>> r1 = stringRedisTemplate.opsForStream().read( Consumer.from(“group1”, “c1”), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create(“stream.order”, ReadOffset.lastConsumed()) ); //判别消息是否获取成功了 if(r1 == null || r1.isEmpty()){ //如果获取失败了,继续下次的循环 continue; } //取出第一条消息 MapRecord<String, Object, Object> objectRecord = r1.get(0); Map<Object, Object> value = objectRecord.getValue(); tb_vouther_order tbVoutherOrder = BeanUtil.fillBeanWithMap(value, new tb_vouther_order(), true); //获取消息成功了,开始下一步操作 //创建锁对象,redisson对象创建锁对象时,自动生成一个唯一值作为value,后续释放锁redisson自动通过该key"lock:order:" + tbVoutherOrder.getUid()获取value,比较锁是否被修改,才能进行释放 RLock redisLock = redissonClient.getLock("lock:order:" + tbVoutherOrder.getUid().toString()); //通过redisson的获取redis的全局锁 boolean b = redisLock.tryLock(2000, TimeUnit.MICROSECONDS); //成功获取了锁 try { //判别是否获取了锁成功(通过redis的nex命令概念建立了互斥锁的概念,因此不判别也没事,因为nex命令只能对一个key执行一次的,因此并发下,只能有一个请求获取全局锁) if(!b){ //并发下单业务,避免重复下单,直接返回失败(不等待直接退出)该步骤是异步处理的,因此不需要返回给前端 log.error("该用户:" + tbVoutherOrder.getUid() + ",已经下单了"); } //消息的确认(保证了消息的可靠性) Long g1 = stringRedisTemplate.opsForStream().acknowledge("stream.order", "g1", objectRecord.getId()); //通过开启的线程任务,异步的操控mysql数据库完成库存的扣减和下单的操作 tbvoutherorderservice.addseckillVouther2(tbVoutherOrder); }finally{ //释放锁 redisLock.unlock(); } }}你帮我分析一下异步下单的代码,线程池采用了单线程,需要考虑分布式全局互斥锁保证线程之间的安全
最新发布
03-18
<think>嗯,用户让我分析他们写的异步下单代码,特别是线程池用了单线程的情况下,是否需要分布式全局互斥锁来保证线程安全。首先,我得仔细看看他们提供的代码结构和逻辑。 用户使用了一个单线程的线程池ExecutorService,然后在run方法里有一个无限循环,不断从Redis Stream读取消息。读取到消息后,处理订单创建,使用Redisson的锁来防止重复下单。这里的关键问题是,虽然线程池是单线程的,但用户担心是否需要分布式锁来保证安全。 首先,单线程的线程池在同一个JVM内确实是顺序执行任务的,不会有并发问题。但用户可能部署了多个实例,比如多个服务节点,每个节点都有自己的单线程池处理消息。这时候,多个实例同时处理同一个用户的订单请求,就需要分布式锁来保证同一时间只有一个实例能处理该用户的下单操作。所以,即使用单线程,分布式锁还是必要的。 接下来,我需要检查代码中的Redisson锁使用是否正确。用户获取锁的时候用了tryLock(2000, TimeUnit.MICROSECONDS),这里的时间单位应该是毫秒更合理,因为2000微秒只有2毫秒,可能太短了,容易导致锁获取失败。需要提醒用户检查单位是否正确,避免因超时时间过短导致不必要的失败。 另外,在锁获取失败后,用户只是记录日志然后继续,但消息没有被确认,可能导致消息重新被消费。这里应该处理ACK,或者在获取锁失败后重新放回队列,否则可能会重复处理消息。需要指出这一点,建议在锁获取失败时进行消息确认或者重试机制。 还有,代码中在finally块释放锁是正确的,确保锁一定会被释放,避免死锁。但需要确认tryLock的参数是否正确,比如等待时间和租约时间。如果使用默认的锁配置,可能没有设置自动续期,长时间任务可能导致锁过期,任务还没完成锁就被释放了。建议用户设置合理的锁超时时间,或者使用看门狗自动续期。 此外,消息确认的位置在获取锁之后,如果在处理订单过程中发生异常,可能导致消息未被确认,从而导致重复消费。应该将消息确认放在订单处理成功之后,或者在异常处理中处理消息的状态。需要检查代码的逻辑流程,确保消息只有在处理成功后才被确认。 还有,用户使用的是单线程处理,如果订单处理耗时较长,可能会导致消息积压。虽然单线程保证了顺序,但可能需要评估处理能力是否足够,或者考虑使用多线程提高吞吐量,但要注意分布式锁的必要性依然存在。 最后,代码中从Redis读取消息后,直接转换为实体对象,这里需要确保字段映射正确,避免反序列化错误。此外,异常处理部分是否完善,比如数据库操作失败后的重试或记录日志,也需要检查。 总结来说,用户正确使用了分布式锁来应对多实例情况下的并发问题,但需要注意锁的超时时间设置、消息确认的时机以及异常处理,以确保系统的可靠性和一致性。</think>### 代码分析及改进建议 #### 一、单线程与分布式锁的关系 ```java private static final ExecutorService CACHE_THREADPOOl1 = Executors.newSingleThreadExecutor(); ``` 1. **单线程池特性**: - 单个线程顺序执行任务 - 同一JVM内**天然线程安全**(无需synchronized等同步机制) 2. **为何需要分布式锁**: - 当服务部署多个实例时,多个单线程池会**并行消费消息** - 分布式场景下需要**跨进程互斥**(例如防止用户重复下单) #### 二、当前代码问题分析 (以下标记对应原始代码段落) ##### 问题1:锁时间单位错误 ```java boolean b = redisLock.tryLock(2000, TimeUnit.MICROSECONDS); // 2000微秒=2毫秒 ``` - **风险**:锁等待时间过短,容易造成锁获取失败 - **建议修改**: ```java boolean b = redisLock.tryLock(2, TimeUnit.SECONDS); // 合理设置等待时间 ``` ##### 问题2:消息确认时机 ```java Long g1 = stringRedisTemplate.opsForStream().acknowledge(...); tbvoutherorderservice.addseckillVouther2(...); ``` - **风险**:若下单操作失败,消息已被确认会导致数据丢失 - **正确顺序**: ```java // 先执行业务逻辑 tbvoutherorderservice.addseckillVouther2(...); // 成功后确认消息 stringRedisTemplate.opsForStream().acknowledge(...); ``` ##### 问题3:异常处理不足 ```java } finally { redisLock.unlock(); } ``` - **隐患**:未处理解锁时的`IllegalMonitorStateException` - **改进方案**: ```java } finally { if (redisLock.isHeldByCurrentThread()) { redisLock.unlock(); } } ``` #### 三、线程安全优化方案 ##### 方案1:增强锁有效性验证 ```java // 增加锁持有者校验(防止误删其他线程的锁) if (b) { try { String lockValue = redissonClient.getBucket("lock:order:"+uid).get(); // 对比当前线程ID与锁值(需存储线程标识) } finally { redisLock.unlock(); } } ``` ##### 方案2:消息处理幂等性设计 ```java // 在数据库订单表添加唯一约束 ALTER TABLE tb_vouther_order ADD UNIQUE (uid, voucher_id); ``` ##### 方案3:完善错误处理流程 ```java try { // 业务逻辑 } catch (Exception e) { // 记录错误消息到死信队列 stringRedisTemplate.opsForStream().add("stream.order.dlq", value); // 重置消费进度 stringRedisTemplate.opsForStream().acknowledge(..., "0-0"); } ``` #### 四、架构层面思考 1. **单线程池瓶颈**: - 优点:天然保证消息顺序处理 - 缺点:吞吐量上限=1线程处理能力 - 改进建议:使用固定线程池+分布式锁组合 2. **锁粒度优化**: ```java // 原锁:用户维度 "lock:order:" + uid // 优化锁:商品+用户维度(减少锁竞争) "lock:item:" + itemId + ":user:" + uid ``` 3. **Redis Stream特性利用**: ```java // 使用Pending Entries List处理未确认消息 stringRedisTemplate.opsForStream().pending("stream.order", "g1"); ``` #### 五、最终改进代码示例 ```java public void run() { while (!Thread.currentThread().isInterrupted()) { try { List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(...); if (CollectionUtils.isEmpty(records)) continue; MapRecord<String, Object, Object> record = records.get(0); Map<Object, Object> data = record.getValue(); // 反序列化校验 TbVoutherOrder order = validateAndConvert(data); RLock lock = redissonClient.getLock("lock:order:" + order.getUid()); try { if (lock.tryLock(2, 30, TimeUnit.SECONDS)) { // 等待2秒,锁持有30秒 try { // 业务处理 boolean success = tbvoutherorderservice.addseckillVouther2(order); if (success) { // 消息确认 stringRedisTemplate.opsForStream().acknowledge(...); } else { // 记录失败日志 } } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } catch (Exception e) { log.error("订单处理异常", e); } } } ``` #### 六、监控指标建议 1. **关键指标**: - 线程池队列堆积量:`CACHE_THREADPOOl1.getQueue().size()` - 锁竞争次数:Redisson `waitTime`监控 - 消息处理延迟:记录从消息产生到数据库写入的时间差 2. **告警阈值**: ```properties # 线程任务堆积超过1000 threadpool.queue.size > 1000 # 锁获取失败率超过10% (lock_fail_count / lock_total_count) > 0.1 ``` 通过以上优化,可在保持单线程顺序处理特性的同时,确保分布式环境下的线程安全与系统可靠性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值