Подробный Redis — SpringBoot интегрирует использование Redis, RedisTemplate и аннотаций.

Redis задняя часть база данных NoSQL

В этой статье в основном рассказывается об использовании 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.

Есть два предварительных шага к использованию кеша

  1. существуетpom.xmlимпортировать зависимости

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. Аннотировать класс запуска@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")

Уведомление

  1. cacheNamesиkeyследовать@Cacheable()Если они непротиворечивы, они будут обновлены правильно.

  2. @CachePut()и@Cacheable()Возвращаемое значение аннотированного метода должно быть согласованным

3.3 Удалить кеш

Аннотируйте метод, который должен удалить кеш:@CacheEvict(cacheNames = "prodcut", key = "123"), после выполнения этого метода соответствующая запись в Redis будет удалена.

3.4 Другие общие функции

  1. 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();
        }
    }
    
  2. Ключ также может быть динамически установлен как параметр метода.

    @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"

  3. Кэширование также может устанавливать условия.

    Установите для кэширования только тогда, когда длина 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

  1. StringRedisTemplate расширяет RedisTemplate.

  2. RedisTemplate — это универсальный класс, а StringRedisTemplate — нет.

  3. StringRedisTemplate может работать только с парами ключ-значение ключ=строка, значение=строка, а RedisTemplate может работать с любым типом пары ключ-значение.

  4. Их соответствующие методы сериализации различаются, но в итоге все они получают массив байтов, и достигается одна и та же цель: StringRedisTemplate использует класс StringRedisSerializer, а RedisTemplate использует класс JdkSerializationRedisSerializer. Для десериализации один получает String, а другой — Object.

  5. Данные двух не являются общими. 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/ из…


Ваша поддержка - самая большая движущая сила для меня, чтобы двигаться вперед, добро пожаловать, чтобы поставить лайк, добро пожаловать, чтобы отправить маленькие звездочки ✨ ~