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

OSCHINA-MIRROR/evan_chen_1-jseckill

Клонировать/Скачать
SOURCE-README.md 15 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 09.06.2025 04:18 85ee06e

Анализ исходного кода 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-хэша можно сформировать действительный запрос на распродажу. После окончания периода распродажи, интерфейс перестает возвращать md5-хэши.
Данные, предоставляемые внешними интерфейсами для проведения распродажи, являются горячими данными, и их значения, за исключением количества товара, остаются неизменными. Мы храним их в Redis, который является базой данных на основе памяти с использованием неблокирующего многоканального ввода-вывода, использующего технологию epoll.
Код см. в методе public Exposer exportSeckillUrl(long seckillId) класса SeckillServiceImpl.java
Перед сохранением в Redis, объект Seckill сериализуется в двоичный формат с помощью фреймворка 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);
        }
    }
}
```## 3. Обработка скидок сзади### 3.1 Ограничение скорости на Java-сервере
Использование RateLimiter из библиотеки Google Guava для ограничения скорости <br/>
Например: разрешено только 10 человек в секунду для входа в процесс скидок. (возможно, блокировка 90% запросов пользователей, после блокировки возвращается "К сожалению, вы не успели") <br/>
Код AccessLimitServiceImpl.java <br/>
```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

Схема шагов выполнения скидок![](doc/image/arch-seckill.png)

1. Шаг 1 в схеме: сначала проходит через балансировку нагрузки и разделение трафика Nginx

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

3. Redis проверяет, был ли уже выполнен сейл. Для предотвращения повторного сейла. Если сейл еще не был выполнен, имя пользователя (в данном случае номер телефона) и seckillId упаковываются в сообщение и отправляются в RabbitMQ, запросы становятся последовательными. Немедленно возвращается состояние "в очереди" на клиентскую сторону, на клиентской стороне отображается "в очереди..."

4. На серверной стороне слушается сообщение в RabbitMQ, каждое сообщение извлекается по одному и после его анализа запрашивается Redis для уменьшения количества на 1 (команда decr). И вручную подтверждается сообщение (ACK). Если уменьшение количества прошло успешно, то в Redis записывается номер телефона пользователя userPhone, который успешно уменьшил количество

5. Шаг 2 в схеме: после успешного добавления в очередь клиентская сторона периодически запрашивает сервер о выполнении сейла, затем проверяет Redis о выполнении сейла

### 3.3 Уменьшение запасов после оплатыИсходный код см. в <code>SeckillServiceImpl.java</code>
Принцип:
в методе <code>public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)</code>
сначала вызывается <code>insertSuccessKilled()</code>, затем <code>reduceNumber()</code>
<b>сначала вставляется запись о сейле, затем уменьшается запас. Таким образом, блокировка строки действует только на этапе уменьшения запаса, что повышает производительность параллельных операций с базой данных.</b>
(в противном случае, если сначала уменьшается запас, а затем вставляется запись о сейле, блокировка строки, созданная операцией update, будет действовать на протяжении всего времени транзакции, что снижает производительность)
Исходный код
```java
@Override
@Transactional
/**
 * Сначала вставляется запись о сейле, затем уменьшается запас.
 */
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
        throws SeckillException, RepeatKillException, SeckillCloseException {
    if (md5 == null || !md5.equals(getMD5(seckillId))) {
        logger.info("seckill data rewrite! ! ! . seckillId={},userPhone={}", seckillId, userPhone);
        throw new SeckillException("seckill data rewrite");
    }
    //Выполнение логики сейла: уменьшение запаса + запись о покупке
    Date nowTime = new Date();
```        try {
            // Вставка записи о скидке (запись о покупке)
            int insertCount = successKilledDAO.insertSuccessKilled(seckillId, userPhone);
            // Уникальность: seckillId, userPhone
            if (insertCount <= 0) {
                // Повторная попытка скидки
                logger.info("seckill repeated. seckillId={},userPhone={}", seckillId, userPhone);
                throw new RepeatKillException("seckill repeated");
            } else {
```                // Уменьшение запаса товара, конкуренция за популярные товары
                 // reduceNumber - это операция update, которая открывает строковый блок для таблицы seckill
                 int updateCount = seckillDAO.reduceNumber(seckillId, nowTime);
                 if (updateCount <= 0) {
                     // Запись не обновлена, скидка закончена, откат
                     throw new SeckillCloseException("seckill is closed");
                 } else {
                     // Успешная скидка, коммит
                     SuccessKilled payOrder = successKilledDAO.queryByIdWithSeckill(seckillId, userPhone);
                     logger.info("seckill SUCCESS->>>.  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("seckill inner error:" + e.getMessage());
         }
     }
 ```## 4. Настройка кластера```- Настройка кластера RabbitMQ

```text
# Настройки 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/evan_chen_1-jseckill.git
git@api.gitlife.ru:oschina-mirror/evan_chen_1-jseckill.git
oschina-mirror
evan_chen_1-jseckill
evan_chen_1-jseckill
master