В этой статье в основном рассказывается об использовании Redis, о том, как интегрироваться с проектом SpringBoot и как реализовать кеширование с помощью аннотаций и RedisTemplate. Наконец, я приведу пример использования Redis для реализации распределенных блокировок в шиповой системе.
Для более практических сценариев применения Redis обратите внимание на проекты с открытым исходным кодом.coderiver
адрес проекта:GitHub.com/cache Cats/ из…
1. Обзор NoSQL
Что такое NoSQL?
NoSQL (NoSQL = Not Only SQL), что означает «не толькоSQL», который обычно относится к нереляционной базе данных.
Зачем вам нужен NoSQL?
С интернетомweb2.0С появлением веб-сайтов традиционные реляционные базы данных имеют дело с веб-сайтами Web 2.0, особенно с крупномасштабными и высококонкурентными.SNSТип web2.0 чистыйдинамическая сетьВеб-сайт стал бессильным и выявил множество непреодолимых проблем, а нереляционная база данных развивалась очень быстро в силу своих особенностей. База данных NoSQL была создана для решения проблем, связанных с крупномасштабным сбором данных и несколькими типами данных, особенно с применением больших данных. -- Энциклопедия Байду
Четыре основные категории баз данных NoSQL
- хранилище ключей-значений
- хранилище столбцов
- база данных документов
- граф базы данных
Классификация | сопутствующие товары | типичное приложение | модель данных | преимущество | недостаток |
---|---|---|---|---|---|
ключ-значение | Токио, Кабинет/Тиран, Редис, Волдеморт, Беркли БД | Кэширование контента, в основном используемое для обработки высоких нагрузок доступа к большим объемам данных. | массив пар ключ-значение | Быстрый поиск | Сохраняемые данные не структурированы |
база данных хранилища столбцов | Cassandra, HBase, Riak | распределенная файловая система | Хранится в кластере столбцов, сохраняя одни и те же данные столбцов вместе | Быстрый поиск, сильная масштабируемость и более простое распределенное расширение | Относительно ограниченный функционал |
база данных документов | CouchDB, MongoDB | Веб-приложение (аналогично Key-Value, значение структурировано) | массив пар ключ-значение | Требования к структуре данных не являются строгими | Производительность запросов невысокая, отсутствует унифицированный синтаксис запросов. |
База данных графов | Neo4J, InfoGrid, Infinite Graph | Социальные сети, рекомендательные системы и т.д. Сосредоточьтесь на построении реляционных графов | структура графа | Использование алгоритмов корреляции структуры графа | Для получения результата необходимо просчитать весь граф, а сделать распределенное кластерное решение непросто |
Особенности NoSQL
- Легко расширить
- Гибкая модель данных
- Большой объем данных, высокая производительность
- Высокая доступность
2. Обзор Redis
Сценарии применения Redis
- тайник
- очередь задач
- Статистика посещений сайта
- Таблица лидеров приложений
- Обработка истечения срока действия данных
- Разделение сеансов в архитектуре распределенного кластера
Установка Redis
В Интернете есть много руководств по установке Redis, поэтому я не буду здесь много говорить, просто расскажу о методе установки Docker:
Установка Docker и запуск Redis
docker run -d -p 6379:6379 redis:4.0.8
Если вы хотите запустить службу Redis позже, откройте командную строку и введите следующую команду.
redis-server
Внедряйте зависимости перед использованием
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3. Метод аннотации использует кеш Redis.
Есть два предварительных шага к использованию кеша
-
существует
pom.xml
импортировать зависимости<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
Аннотировать класс запуска
@EnableCaching
@SpringBootApplication @EnableCaching public class SellApplication { public static void main(String[] args) { SpringApplication.run(SellApplication.class, args); } }
Обычно используемые аннотации следующие
-
@Cacheable
Свойства показаны ниже
Он используется для запроса и добавления кеша.Возвращаемое значение этого метода возвращается при первом запросе, а данные сохраняются на сервере Redis.
Вызовите этот метод в будущем, чтобы проверить, есть ли данные в Redis и есть ли данные, возвращаемые напрямую в кэш Redis, без выполнения кода в методе. Если нет, код в теле метода выполняется нормально.
Атрибут value или cacheNames используется в качестве ключа, а атрибут key можно рассматривать как подраздел значения Значение может иметь несколько ключей для формирования разных значений и существовать на сервере Redis.
Было проверено, что value и cacheNames имеют одну и ту же функцию, идентифицируя первичный ключ. Два свойства не могут быть определены одновременно, может быть определено только одно из них, иначе будет сообщено об ошибке.
Condition и If являются условиями, которые будут использоваться позже. Несколько других свойств обычно не используются, на самом деле, я не знаю, как их использовать...
-
@CachePut
Обновите значение соответствующего ключа в Redis. свойства и
@Cacheable
такой же -
@CacheEvict
Удалите значение соответствующего ключа в Redis.
3.1 Добавить кеш
Добавьте аннотации к методам, которые необходимо кэшировать@Cacheable(cacheNames = "product", key = "123")
,
cacheNames
иkey
необходимо заполнить, если не заполненоkey
,дефолтkey
имя текущего метода. При обновлении кеша произойдет сбой обновления, поскольку имя метода отличается.
Например, добавление кеша в список заказов
@RequestMapping(value = "/list", method = RequestMethod.GET)
@Cacheable(cacheNames = "product", key = "123")
public ResultVO list() {
// 1.查询所有上架商品
List<ProductInfo> productInfoList = productInfoService.findUpAll();
// 2.查询类目(一次性查询)
//用 java8 的特性获取到上架商品的所有类型
List<Integer> categoryTypes = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());
List<ProductCategory> productCategoryList = categoryService.findByCategoryTypeIn(categoryTypes);
List<ProductVO> productVOList = new ArrayList<>();
//数据拼装
for (ProductCategory category : productCategoryList) {
ProductVO productVO = new ProductVO();
//属性拷贝
BeanUtils.copyProperties(category, productVO);
//把类型匹配的商品添加进去
List<ProductInfoVO> productInfoVOList = new ArrayList<>();
for (ProductInfo productInfo : productInfoList) {
if (productInfo.getCategoryType().equals(category.getCategoryType())) {
ProductInfoVO productInfoVO = new ProductInfoVO();
BeanUtils.copyProperties(productInfo, productInfoVO);
productInfoVOList.add(productInfoVO);
}
}
productVO.setProductInfoVOList(productInfoVOList);
productVOList.add(productVO);
}
return ResultVOUtils.success(productVOList);
}
Может быть сообщено о следующей ошибке
Объект не сериализован. заставить объект реализоватьSerializable
метод
@Data
public class ProductVO implements Serializable {
private static final long serialVersionUID = 961235512220891746L;
@JsonProperty("name")
private String categoryName;
@JsonProperty("type")
private Integer categoryType;
@JsonProperty("foods")
private List<ProductInfoVO> productInfoVOList ;
}
В IDEA есть плагин для генерации уникального идентификатора:GenerateSerialVersionUID
Более удобно.
Перезапустите проект, чтобы получить доступ к списку заказов, проверьте кеш Redis в rdm, естьproduct::123
Указывает, что кеширование выполнено успешно.
3.2 Обновить кеш
Аннотируйте метод, который должен обновить кеш:@CachePut(cacheNames = "prodcut", key = "123")
Уведомление
cacheNames
иkey
следовать@Cacheable()
Если они непротиворечивы, они будут обновлены правильно.
@CachePut()
и@Cacheable()
Возвращаемое значение аннотированного метода должно быть согласованным
3.3 Удалить кеш
Аннотируйте метод, который должен удалить кеш:@CacheEvict(cacheNames = "prodcut", key = "123")
, после выполнения этого метода соответствующая запись в Redis будет удалена.
3.4 Другие общие функции
-
cacheNames
Его также можно записать единообразно по классу,@CacheConfig(cacheNames = "product")
, вам не нужно писать конкретный метод.@CacheConfig(cacheNames = "product") public class BuyerOrderController { @PostMapping("/cancel") @CachePut(key = "456") public ResultVO cancel(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ buyerService.cancelOrder(openid, orderId); return ResultVOUtils.success(); } }
-
Ключ также может быть динамически установлен как параметр метода.
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
Если параметр является объектом, вы также можете установить свойство объекта в качестве ключа. Например, одним из параметров является пользовательский объект, а ключ можно записать в виде
key="#user.id"
-
Кэширование также может устанавливать условия.
Установите для кэширования только тогда, когда длина openid больше 3
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
можно также указать
unless
То есть кешировать, когда условие не истинно.#result
Представляет возвращаемое значение, что означает, что когда код возврата не равен 0, он не кэшируется, то есть кэшируется, когда он равен 0.@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3", unless = "#result.code != 0") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
4. RedisTemplate использует кеш Redis
В отличие от метода аннотации, метод аннотации можно настроить без настройки, просто введите зависимости и добавьте их в класс запуска.@EnableCaching
Можно использовать аннотации, использование RedisTemplate более проблематично и требует некоторой настройки.
4.1 Конфигурация Redis
Первый шаг — ввести зависимости и добавить их в класс запуска.@EnableCaching
аннотация.
затем вapplication.yml
Настройте Redis в файле
spring:
redis:
port: 6379
database: 0
host: 127.0.0.1
password:
jedis:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
timeout: 5000ms
затем написатьRedisConfig.java
класс конфигурации
package com.solo.coderiver.user.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import java.net.UnknownHostException;
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
Настройка Redis завершена.
4.2 Типы структур данных Redis
Redis может хранить сопоставления между ключами и 5 различными типами структур данных: String (строка), List (список), Set (набор), Hash (хэш) и Zset (упорядоченный сбор).
Ниже приводится краткое введение в эти пять типов структур данных:
тип структуры | значение, хранящееся в структуре | Структурная грамотность |
---|---|---|
String | Может быть строкой, целым числом или числом с плавающей запятой | Выполнять операции над целыми строками или частями строк; выполнять увеличение или уменьшение объектов и чисел с плавающей запятой. |
List | Связанный список, каждый узел в связанном списке содержит строку | Вталкивание или извлечение элементов с обоих концов связанного списка; обрезка связанного списка по смещению; чтение одного или нескольких элементов; поиск или удаление элементов по значению |
Set | Неупорядоченная коллекция, содержащая строки, и каждая содержащаяся строка уникальна и различна. | Добавить, получить, удалить один элемент; проверить, существует ли элемент в наборе; вычислить пересечение, объединение, разность; показать случайные элементы из набора |
Hash | Неупорядоченная хэш-таблица, содержащая пары ключ-значение | Добавить, получить, удалить одну пару ключ-значение; получить все пары ключ-значение |
Zset | Упорядоченное сопоставление между элементами строки (элементами) и оценками с плавающей запятой (оценками), где порядок элементов определяется размером оценок. | Добавить, получить, удалить один элемент; получить элементы на основе диапазона или члена |
4.3 StringRedisTemplate и RedisTemplate
RedisTemplate определяет операции для пяти структур данных
-
redisTemplate.opsForValue();
строка действия
-
redisTemplate.opsForHash();
хэш операции
-
redisTemplate.opsForList();
Список действий
-
redisTemplate.opsForSet();
набор операций
-
redisTemplate.opsForZSet();
Работа с упорядоченным набором
Если вы работаете со строками, рекомендуется использоватьStringRedisTemplate
.
Разница между StringRedisTemplate и RedisTemplate
-
StringRedisTemplate расширяет RedisTemplate.
-
RedisTemplate — это универсальный класс, а StringRedisTemplate — нет.
-
StringRedisTemplate может работать только с парами ключ-значение ключ=строка, значение=строка, а RedisTemplate может работать с любым типом пары ключ-значение.
-
Их соответствующие методы сериализации различаются, но в итоге все они получают массив байтов, и достигается одна и та же цель: StringRedisTemplate использует класс StringRedisSerializer, а RedisTemplate использует класс JdkSerializationRedisSerializer. Для десериализации один получает String, а другой — Object.
-
Данные двух не являются общими. StringRedisTemplate может управлять данными только в StringRedisTemplate, а RedisTemplate может управлять данными только в RedisTemplate.
4.4 Использование в проекте
Если вам нужно использовать Redis, используйте@Autowired
вводить
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
Поскольку в настоящее время в проекте используется только хэш-структура StringRedisTemplate и RedisTemplate, StringRedisTemplate является относительно простым, и код публиковаться не будет.Ниже приведен только пример работы с хэшем.
Что касается подробного использования RedisTemplate, есть статья, очень подробная и очень хорошая, я не думаю, что нужно писать ее снова.портал
Управление хэшем с помощью RedisTemplate
package com.solo.coderiver.user.service.impl;
import com.solo.coderiver.user.dataobject.UserLike;
import com.solo.coderiver.user.dto.LikedCountDTO;
import com.solo.coderiver.user.enums.LikedStatusEnum;
import com.solo.coderiver.user.service.LikedService;
import com.solo.coderiver.user.service.RedisService;
import com.solo.coderiver.user.utils.RedisKeyUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class RedisServiceImpl implements RedisService {
@Autowired
RedisTemplate redisTemplate;
@Autowired
LikedService likedService;
@Override
public void saveLiked2Redis(String likedUserId, String likedPostId) {
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());
}
@Override
public void unlikeFromRedis(String likedUserId, String likedPostId) {
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());
}
@Override
public void deleteLikedFromRedis(String likedUserId, String likedPostId) {
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
}
@Override
public void incrementLikedCount(String likedUserId) {
redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1);
}
@Override
public void decrementLikedCount(String likedUserId) {
redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1);
}
@Override
public List<UserLike> getLikedDataFromRedis() {
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
List<UserLike> list = new ArrayList<>();
while (cursor.hasNext()) {
Map.Entry<Object, Object> entry = cursor.next();
String key = (String) entry.getKey();
//分离出 likedUserId,likedPostId
String[] split = key.split("::");
String likedUserId = split[0];
String likedPostId = split[1];
Integer value = (Integer) entry.getValue();
//组装成 UserLike 对象
UserLike userLike = new UserLike(likedUserId, likedPostId, value);
list.add(userLike);
//存到 list 后从 Redis 中删除
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
}
return list;
}
@Override
public List<LikedCountDTO> getLikedCountFromRedis() {
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
List<LikedCountDTO> list = new ArrayList<>();
while (cursor.hasNext()) {
Map.Entry<Object, Object> map = cursor.next();
//将点赞数量存储在 LikedCountDT
String key = (String) map.getKey();
LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());
list.add(dto);
//从Redis中删除这条记录
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
}
return list;
}
}
5. Redis реализует распределенные блокировки
Поговорив об основных операциях, давайте поговорим о практических приложениях, использующих Redis для реализации распределенных блокировок.
Перед реализацией распределенных блокировок взгляните на две команды Redis:
-
SETNX
будет
key
установить значение наvalue
,еслиkey
не существует, в этом случае он эквивалентенSETЗаказ. когдаkey
Когда он существует, ничего не делайте.SETNX
Да"SET if Not eXсокращение от «исты».возвращаемое значение
Integer reply, конкретное значение:
-
1
если ключ установлен -
0
Если ключ не установлен
пример
redis> SETNX mykey "Hello" (integer) 1 redis> SETNX mykey "World" (integer) 0 redis> GET mykey "Hello" redis>
-
-
GETSET
Автоматически сопоставьте ключ со значением и верните значение, соответствующее исходному ключу. Если ключ существует, но соответствующее значение не является строкой, возвращается ошибка.
Шаблоны проектирования
GETSETможет иINCRИспользуются вместе для реализации функции подсчета, поддерживающей сброс. Например: всякий раз, когда происходит событие, программа будет вызыватьINCRДобавьте 1 к ключу mycounter, но иногда нам нужно получить значение счетчика и автоматически сбросить его на 0. Этого можно добиться с помощью GETSET mycounter "0":
INCR mycounter GETSET mycounter "0" GET mycounter
возвращаемое значение
bulk-string-reply: вернуть старое значение раньше, если раньше
Key
не существует вернетсяnil
.пример
redis> INCR mycounter (integer) 1 redis> GETSET mycounter "0" "1" redis> GET mycounter "0" redis>
Эти две команды соответствуют в java какsetIfAbsent
иgetAndSet
Реализация распределенной блокировки:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Slf4j
public class RedisLock {
@Autowired
StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value 当前时间 + 超时时间
* @return
*/
public boolean lock(String key, String value){
if (redisTemplate.opsForValue().setIfAbsent(key, value)){
return true;
}
//解决死锁,且当多个线程同时来时,只会让一个线程拿到锁
String currentValue = redisTemplate.opsForValue().get(key);
//如果过期
if (!StringUtils.isEmpty(currentValue) &&
Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一个锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key, String value){
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
log.error("【redis锁】解锁失败, {}", e);
}
}
}
использовать:
/**
* 模拟秒杀
*/
public class SecKillService {
@Autowired
RedisLock redisLock;
//超时时间10s
private static final int TIMEOUT = 10 * 1000;
public void secKill(String productId){
long time = System.currentTimeMillis() + TIMEOUT;
//加锁
if (!redisLock.lock(productId, String.valueOf(time))){
throw new SellException(101, "人太多了,等会儿再试吧~");
}
//具体的秒杀逻辑
//解锁
redisLock.unlock(productId, String.valueOf(time));
}
}
Для более конкретных сценариев использования Redis обратите внимание на проекты с открытым исходным кодом.CodeRiver
, стремится создать полноплатформенный бутик-проект с открытым исходным кодом.
coderiver Китайское название 河CODE — это платформа, на которой программисты и дизайнеры могут совместно работать над проектами. Независимо от того, являетесь ли вы разработчиком интерфейса, бэкенда или мобильного приложения, дизайнером или менеджером по продукту, вы можете публиковать проекты на платформе и сотрудничать с партнерами-единомышленниками для завершения проектов.
Coderiver River Code похож на гостиницу программиста, но его основная цель — способствовать техническому обмену между талантами в различных подобластях, расти вместе и совместно выполнять проекты. Никаких денежных операций не происходит.
Планируется, что это будет полноплатформенный проект с полным стеком, включая терминал для ПК (Vue, React), мобильный H5 (Vue, React), гибридную разработку ReactNative, родной Android, апплет WeChat и бэкэнд java. Добро пожаловать, обратите внимание.
адрес проекта:GitHub.com/cache Cats/ из…
Ваша поддержка - самая большая движущая сила для меня, чтобы двигаться вперед, добро пожаловать, чтобы поставить лайк, добро пожаловать, чтобы отправить маленькие звездочки ✨ ~