1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/frank_liu_1-jseckill

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
SOURCE-README.md 16 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 28.11.2024 14:58 a81ea0f

Анализ исходного кода Jseckill

1. Общая архитектура

Система развёрнута по схеме, представленной на рисунке:

  • Процесс проведения «секунда-килла» включает два шага:
    • Шаг 1 (секунда-килл): в Redis выполняется секунда-килл. Этот шаг характеризуется большим количеством одновременных запросов. После успешного выполнения запроса пользователю предоставляется 30 минут для оплаты. Если оплата не произведена, то значение в Redis увеличивается на 1, и считается, что пользователь отказался от оплаты.
    • Шаг 2 (оплата): после успешной оплаты пользователем, данные о платеже сохраняются в MySQL. Количество одновременных запросов на этом шаге меньше, чем на первом. Для обеспечения целостности данных используется транзакция базы данных.

Статические ресурсы сайта секунды-килла, такие как статические веб-страницы, ссылки на JavaScript, CSS, изображения, аудио и видео, размещаются на CDN (сети распространения контента).

Если компания хочет снизить затраты, статические ресурсы можно развернуть на nginx. Благодаря высокой производительности nginx при обслуживании статических ресурсов, можно максимально увеличить скорость доступа к этим ресурсам.

Через nginx осуществляется обратный прокси-сервер, который предоставляет доступ только к порту 80. Кроме того, nginx обеспечивает балансировку нагрузки, распределяя её между несколькими узлами jseckill-backend. Стратегия балансировки нагрузки основана на распределении нагрузки в соответствии с производительностью каждого приложения.

Для MySQL используется репликация Master-Slave, которая позволяет разделить чтение и запись, повышая тем самым способность базы данных обрабатывать большое количество одновременных запросов.

2. Экспозиция секунд-килл интерфейса

Цель экспозиции секунд-килл интерфейсов — предоставить доступ к md5-кодам товаров только после начала периода секунды-килла. После окончания этого периода интерфейс больше не возвращает md5-коды.

Данные, предоставляемые через секунд-килл интерфейсы, являются «горячими» данными, которые не изменяются (за исключением количества товаров), и хранятся в Redis, базе данных в памяти.

Используется неблокирующий мультиплексор epool, который работает быстрее, чем операции с диском или базой данных. Код можно найти в методе SeckillServiceImpl.java public Exposer exportSeckillUrl(long seckillId).

Перед сохранением в Redis данные сериализуются в двоичный код с помощью фреймворка Protostuff.

Исходный код:

@Override
public Exposer exportSeckillUrl(long seckillId) {
// Оптимизация точки: кеширование оптимизации: на основе тайм-аута для поддержания согласованности
//1. Доступ к Redis
Seckill seckill = redisDAO.getSeckill(seckillId);
if (seckill == null) {
//2. Доступ к базе данных
seckill = seckillDAO.queryById(seckillId);
if (seckill == null) {
return new Exposer(false, seckillId);
} else {
//3. Сохранение в Redis
redisDAO.putSeckill(seckill);
}
}

Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
// Текущее время системы
Date nowTime = new Date();
if (nowTime.getTime() < startTime.getTime()
|| nowTime.getTime() > endTime.getTime()) {
return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(),
endTime.getTime());
}
// Преобразование процесса конкретной строки, необратимое
String md5 = getMD5(seckillId);
return new Exposer(true, md5, seckillId);
}

3. Обработка секунды-килла на стороне сервера

3.1 Ограничение потока на Java-сервере

Ограничение потока реализуется с использованием RateLimiter из Google guava. Например, допускается только 10 человек в секунду для входа в процесс секунды-килла (возможно, это приведёт к отклонению 90% запросов пользователей, а отклоненные запросы будут возвращать сообщение «К сожалению, вы не успели»).

Доступ к LimitServiceImpl.java:

package com.liushaoming.jseckill.backend.service.impl;

import com.google.common.util.concurrent.RateLimiter;
import com.liushaoming.jseckill.backend.service.AccessLimitService;
import org.springframework.stereotype.Service;

/**
 * Ограничение перед секундой-киллом.
 * Используется RateLimiter от Google guava
 */
@Service
public class AccessLimitServiceImpl implements AccessLimitService {
/**
* Каждые 10 токенов выдаются в секунду, и только запросы, получившие токены, могут войти в процесс секунды-килла
*/
private RateLimiter seckillRateLimiter = RateLimiter.create(10);

/**
* Попытка получить токен
* @return
*/
@Override
public boolean tryAcquireSeckill() {
return seckillRateLimiter.tryAcquire();
}
}

Использование ограничения потока, SeckillServiceImpl.java

@Override
@Transactional
/**
* Выполнение секунды-килла
*/
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException {
if (accessLimitService.tryAcquireSeckill()) { // Если ограничение потока не применяется, выполнить обработку секунды-килла
return updateStock(seckillId, userPhone, md5);
} else { // Если применено ограничение потока, напрямую вызвать исключение ограничения доступа
logger.info("--->ACCESS_LIMITED-->seckillId={},userPhone={}", seckillId, userPhone);
throw new SeckillException(SeckillStateEnum.ACCESS_LIMIT);
}
}

3.2 Выполнение секунды-килла в Redis

Процесс секунды-килла представлен на диаграмме:

  1. Диаграмма Step1: сначала проходит через Nginx балансировщик нагрузки и маршрутизацию.

  2. Запрос обрабатывается программой jseckill. При большом количестве одновременных запросов Google guava RateLimiter ограничивает поток. Часть запросов пользователей отклоняется.

  3. Redis проверяет, была ли секунда-килл уже выполнена. Если нет, сообщение с именем пользователя (здесь это номер телефона) и seckillId отправляется в RabbitMQ, делая запрос последовательной обработкой. Немедленно возвращается статус «В очереди» клиенту, который отображает «В очереди...» на клиенте.

  4. Сервер прослушивает сообщения в RabbitMQ и обрабатывает их одно за другим. После анализа сообщения запрос отправляется в Redis для уменьшения значения на 1 (команда decr). Затем вручную подтверждается очередь.

Если уменьшение значения в Redis успешно, номер телефона пользователя, успешно выполнившего секунду-килл, записывается в Redis.

  1. Диаграмма Step2: после успешного добавления в очередь клиент периодически запрашивает сервер, чтобы проверить, был ли секунда-килл успешным. Позже будет запрошен Redis, чтобы узнать, был ли секунда-килл выполнен успешно.

3.3 Уменьшение количества товаров после оплаты

Код можно найти в SeckillServiceImpl.java. Принцип заключается в том, что в методе public SeckillExecution executeSeckill (long seckillId, long userPhone, String md5), сначала выполняется insertSuccessKilled (), а затем reduceNumber (). Сначала в базу данных записываются данные о выполнении секунды-килла, а затем уменьшается количество товаров. Таким образом, блокировка строк применяется только на этапе уменьшения количества товаров, что повышает производительность операций с базой данных при одновременном выполнении. В запросе скорее всего текст технической направленности из области разработки и тестирования программного обеспечения. Основной язык текста запроса — китайский.

Перевод на русский язык:

@Override
    @Transactional
    /**
     * Сначала вставляем запись о мгновенной распродаже, затем уменьшаем количество товаров на складе
     */
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException {
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            logger.info("Данные о мгновенной распродаже были перезаписаны!!!. seckillId={},userPhone={}", seckillId, userPhone);
            throw new SeckillException("Данные о мгновенной распродаже были перезаписаны");
        }
        //Выполнение логики мгновенной распродажи: уменьшение количества товаров на складе + запись информации о покупке
        Date nowTime = new Date();

        try {
            //Запись информации о мгновенной распродаже (запись информации о покупке)
            int insertCount = successKilledDAO.insertSuccessKilled(seckillId, userPhone);
            //Уникальность: seckillId,userPhone
            if (insertCount <= 0) {
                //Повторная мгновенная распродажа
                logger.info("Мгновенная распродажа повторилась. seckillId={},userPhone={}", seckillId, userPhone);
                throw new RepeatKillException("Мгновенная распродажа повторилась");
            } else {
                //Уменьшение количества товаров на складе, конкуренция за горячие товары
                // reduceNumber - это операция обновления, которая активирует блокировку строки в таблице seckill
                int updateCount = seckillDAO.reduceNumber(seckillId, nowTime);
                if (updateCount <= 0) {
                    //Обновление записи не произошло, мгновенная распродажа завершена, откат
                    throw new SeckillCloseException("Мгновенная распродажа закрыта");
                } else {
                    //Мгновенная распродажа прошла успешно commit
                    SuccessKilled payOrder = successKilledDAO.queryByIdWithSeckill(seckillId, userPhone);
                    logger.info("Успех мгновенной распродажи->>>. seckillId={},userPhone={}", seckillId, userPhone);
                    //Завершение транзакции, снятие блокировки строки в таблице seckill
                    return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, payOrder);
                }
            }
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            //  Все исключения времени компиляции преобразуются в исключения времени выполнения
            throw new SeckillException("Внутренняя ошибка мгновенной распродажи:" + e.getMessage());
        }
    }

4. Конфигурация кластера

  • Конфигурация кластера RabbitMQ
#Конфигурация Rabbitmq
rabbitmq.address-list=192.168.20.3:5672,localhost:5672
rabbitmq.username=myname
rabbitmq.password=somepass
rabbitmq.publisher-confirms=true
rabbitmq.virtual-host=/vh_test
rabbitmq.queue=seckill

Конфигурация адреса кластера RabbitMQ выглядит следующим образом:

rabbitmq.address-list=192.168.20.3:5672,localhost:5672

Правило заключается в том, что каждый адрес должен быть в формате host:port, а несколько адресов серверов MQ должны быть разделены запятыми без лишних пробелов.

Принцип работы кластера: следующий метод может возвращать доступные адреса MQ на основе списка адресов. Если все они недоступны, то будет выброшено исключение.

com.rabbitmq.client.ConnectionFactory#newConnection(List<Address> addrs) throws IOException, TimeoutException {}

Код приложения см. в com.liushaoming.jseckill.backend.config.MQConfig.

Фрагмент кода:

@Bean("mqConnectionSeckill")
    public Connection mqConnectionSeckill(@Autowired MQConfigBean mqConfigBean) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        //Имя пользователя
        factory.setUsername(username);
        //Пароль
        factory.setPassword(password);
        //Путь виртуального хоста (аналогично имени базы данных)
        factory.setVirtualHost(virtualHost);
        //Возвращаем соединение
        return factory.newConnection(mqConfigBean.getAddressList());
    }

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/frank_liu_1-jseckill.git
git@api.gitlife.ru:oschina-mirror/frank_liu_1-jseckill.git
oschina-mirror
frank_liu_1-jseckill
frank_liu_1-jseckill
master