선착순 문제

김파란·2024년 6월 24일

Spring-Library

목록 보기
1/7

1. 선착순 이벤트

1). 문제점

  • 쿠폰을 100개만 발급하려고 하는데 그 이상 쿠폰이 발급되는 현상이 발생 (레이스 컨디션)
  • 해결법
    -> synchronized: 서버가 여러대 일경우에는 레이스 컨디션이 일어난다
    -> 락을 활용: 우리가 원하는건 쿠폰 개수의 정합성인데 발급된 쿠폰 개수를 가져오는것부터 생성할때까지 락을 걸어야한다 성능에 불이익이 있다
  • 재고시스템은 아이템을 가져오고 감소시키는 데까지 락을 걸어놔야되는데 쿠폰은 그냥 발급만 되면 되기때문에 다르다

2. Redis

  • incr명령어 활용
  • 발급하는 쿠폰의 개수 많아질수록 rdb에 부하를 주게된다
  • 만약 쿠폰전용 db가 아니라면 장애가 일어날 가능성이 크다
  • 예를들어 mysql은 1분에 100개의 insert만 가능하다고 가정하면 쿠폰생성요청과 주문생성 요청, 회원가입 요청이 들어오기 때문에 다 처리할 수가 없다
// repository 계층
@Repository
public class CouponCountRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public CouponCountRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public Long increment(){
        return redisTemplate
                .opsForValue()
                .increment("coupon_count");
    }
}
// service 계층
@Service
public class ApplyService {

    private final CouponRepository couponRepository;
    private final CouponCountRepository couponCountRepository;

    public ApplyService(CouponRepository couponRepository, CouponCountRepository couponCountRepository) {
        this.couponRepository = couponRepository;
        this.couponCountRepository = couponCountRepository;
    }

    public void apply(Long userId) {
        Long count = couponCountRepository.increment();

        if (count > 100) {
            return;
        }
        couponRepository.save(new Coupon(userId));
    }
}

3. Kafka

  • 분산 이벤트 스트리밍 플랫폼
  • 이벤트 스트리밍: 소스에서 목적지까지 이벤트를 실시간으로 스트리밍하는 것
  • 기본구조: Producer, Topic, Consumer
  • Producer가 토픽에 데이터를 삽입하고, Consumer가 토픽에 있는 데이터를 가져갈수 있다

1). Producer

의존성 추가: implementation 'org.springframework.kafka:spring-kafka'

// Producer 설정
@Configuration
public class KafkaProducerConfig {
    // 프로듀서 설정
    @Bean
    public ProducerFactory<String, Long> producerFactory(){
        Map<String, Object> config = new HashMap<>();

        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, LongSerializer.class);

        return new DefaultKafkaProducerFactory<>(config);
    }

    @Bean
    public KafkaTemplate<String, Long> kafkaTemplate(){
        return new KafkaTemplate<>(producerFactory());
    }
}
// Producer 만들기
@Component
public class CouponCreateProducer {

    private final KafkaTemplate<String, Long> kafkaTemplate;

    public CouponCreateProducer(KafkaTemplate<String, Long> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void create(Long userId) {
        kafkaTemplate.send("coupon_create", userId);
    }
}
// Service 계층
@Service
public class ApplyService {

    private final CouponRepository couponRepository;
    private final CouponCountRepository couponCountRepository;
    private final CouponCreateProducer couponCreateProducer;

    public ApplyService(CouponRepository couponRepository, CouponCountRepository couponCountRepository, CouponCreateProducer couponCreateProducer) {
        this.couponRepository = couponRepository;
        this.couponCountRepository = couponCountRepository;
        this.couponCreateProducer = couponCreateProducer;
    }

    public void apply(Long userId) {
        Long count = couponCountRepository.increment();

        if (count > 100) {
            return;

        }
        couponCreateProducer.create(userId);
    }
}

2). Consumer

  • consumer 모듈을 만들어야 한다
// consumer 설정
@Configuration
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory<String, Long> consumerFactory() {
        Map<String, Object> config = new HashMap<>();

        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localHost:9092");
        config.put(ConsumerConfig.GROUP_ID_CONFIG, "group_1");
        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, LongDeserializer.class);

        return new DefaultKafkaConsumerFactory<>(config);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, Long> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, Long> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }

}
// 토픽에 있는걸 가져와서 저장함
@Component
public class CouponCreatedConsumer {
    private final CouponRepository couponRepository;

    public CouponCreatedConsumer(CouponRepository couponRepository) {
        this.couponRepository = couponRepository;
    }

    @KafkaListener(topics = "coupon_create",groupId = "group_1")
    public void listener(Long userId) {
        couponRepository.save(new Coupon(userId));
    }
}

4. 쿠폰개수 1인당 1개로 제한하기

  • redis의 set을 이용
// repository 계층
@Repository
public class AppliedUserRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public AppliedUserRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public Long add(Long userID) {
        return redisTemplate
                .opsForSet()
                .add("applied_user", userID.toString());
    }
}

// service 계층
@Service
public class ApplyService {

    private final CouponRepository couponRepository;
    private final CouponCountRepository couponCountRepository;
    private final CouponCreateProducer couponCreateProducer;
    private final AppliedUserRepository appliedUserRepository;

    public ApplyService(CouponRepository couponRepository, CouponCountRepository couponCountRepository, CouponCreateProducer couponCreateProducer, AppliedUserRepository appliedUserRepository) {
        this.couponRepository = couponRepository;
        this.couponCountRepository = couponCountRepository;
        this.couponCreateProducer = couponCreateProducer;
        this.appliedUserRepository = appliedUserRepository;
    }

    public void apply(Long userId) {
        Long apply = appliedUserRepository.add(userId);
        if (apply != 1) {
            return;
        }
        Long count = couponCountRepository.increment();

        if (count > 100) {
            return;

        }
        couponCreateProducer.create(userId);
    }
}

5. 쿠폰 발급하다가 에러 발생시

  • Consumer에서 쿠폰 발급 에러가 나면 쿠폰개수는 늘어나지만 실제 쿠폰발급은 적게 발급되는 현상
  • Exception이 터지면 그냥 DB에 해당 userId를 저장해두는 식으로 엔티티를 만들고
  • 배치를 통해 확인을 하면서 한꺼번에 저장해두는식으로 하면 된다
@Component
public class CouponCreatedConsumer {
    private static final Logger log = LoggerFactory.getLogger(CouponCreatedConsumer.class);

    private final CouponRepository couponRepository;
    private final FailedEventRepository failedEventRepository;

    public CouponCreatedConsumer(CouponRepository couponRepository, FailedEventRepository failedEventRepository) {
        this.couponRepository = couponRepository;
        this.failedEventRepository = failedEventRepository;
    }

    @KafkaListener(topics = "coupon_create",groupId = "group_1")
    public void listener(Long userId) {
        try {
            couponRepository.save(new Coupon(userId));
        } catch (Exception e) {
            log.error("failed to create coupon ::" + userId);
            failedEventRepository.save(new FailedEvent(userId));
        }

    }
}

0개의 댓글