Как обойти отсутствие транзакций в MongoDB?


141

Я знаю, что здесь есть аналогичные вопросы, но они либо говорят мне вернуться к обычным системам РСУБД, если мне нужны транзакции, либо использовать атомарные операции или двухфазную фиксацию . Второе решение кажется лучшим выбором. Третье я не хочу следовать, потому что кажется, что многое может пойти не так, и я не могу проверить его во всех аспектах. Мне сложно рефакторинг моего проекта для выполнения атомарных операций. Я не знаю, исходит ли это с моей ограниченной точки зрения (пока я работал только с базами данных SQL), или это действительно невозможно.

Мы хотели бы провести пилотное тестирование MongoDB в нашей компании. Мы выбрали относительно простой проект - SMS-шлюз. Это позволяет нашему программному обеспечению отправлять SMS-сообщения в сотовую сеть, а шлюз выполняет грязную работу: фактически общается с провайдерами через различные протоколы связи. Шлюз также управляет выставлением счетов за сообщения. Каждый покупатель, который обращается за услугой, должен купить кредиты. Система автоматически уменьшает баланс пользователя при отправке сообщения и отказывает в доступе, если баланс недостаточен. Кроме того, поскольку мы являемся клиентами сторонних поставщиков SMS, у нас также может быть собственный баланс на их счетах. Мы также должны следить за ними.

Я начал думать о том, как я могу хранить необходимые данные с помощью MongoDB, если я уменьшу некоторую сложность (внешний биллинг, отправка SMS в очереди). Исходя из мира SQL, я бы создал отдельную таблицу для пользователей, еще одну для SMS-сообщений и одну для хранения транзакций, касающихся баланса пользователей. Скажем, я создаю отдельные коллекции для всех в MongoDB.

Представьте себе задачу отправки SMS со следующими шагами в этой упрощенной системе:

  1. проверьте, достаточно ли у пользователя средств на балансе; отказать в доступе, если недостаточно кредита

  2. отправить и сохранить сообщение в коллекции SMS с подробностями и стоимостью (в действующей системе сообщение будет иметь statusатрибут, и задача заберет его для доставки и установит цену SMS в соответствии с его текущим состоянием)

  3. уменьшить баланс пользователя на стоимость отправленного сообщения

  4. зарегистрировать транзакцию в коллекции транзакций

В чем проблема? MongoDB может выполнять атомарные обновления только в одном документе. В предыдущем потоке могло случиться, что закрадывалась какая-то ошибка, и сообщение сохранялось в базе данных, но баланс пользователя не обновлялся и / или транзакция не регистрировалась.

У меня возникли две идеи:

  • Создайте единую коллекцию для пользователей и сохраните баланс как поле, транзакции и сообщения, связанные с пользователем, как вложенные документы в документе пользователя. Поскольку мы можем обновлять документы атомарно, это фактически решает проблему транзакции. Недостатки: если пользователь отправляет много SMS-сообщений, размер документа может стать большим и может быть достигнут предел документа в 4 МБ. Может быть, я смогу создавать исторические документы в таких сценариях, но я не думаю, что это будет хорошей идеей. Также я не знаю, насколько быстрой будет система, если я буду помещать все больше и больше данных в один и тот же большой документ.

  • Создайте одну коллекцию для пользователей и одну для транзакций. Транзакции могут быть двух видов: покупка в кредит с положительным изменением баланса и сообщения, отправленные с отрицательным изменением баланса. У транзакции может быть вложенный документ; например, в отправленных сообщениях детали SMS могут быть встроены в транзакцию. Недостатки: я не храню текущий баланс пользователя, поэтому мне приходится вычислять его каждый раз, когда пользователь пытается отправить сообщение, чтобы узнать, может ли сообщение пройти или нет. Боюсь, что этот расчет может замедлиться по мере роста количества хранимых транзакций.

Я немного не понимаю, какой метод выбрать. Есть ли другие решения? Я не смог найти в Интернете никаких передовых методов решения подобных проблем. Я полагаю, что многие программисты, которые пытаются познакомиться с миром NoSQL, вначале сталкиваются с аналогичными проблемами.


61
Простите меня, если я ошибаюсь, но похоже, что этот проект будет использовать хранилище данных NoSQL независимо от того, выиграет он от этого или нет. NoSQL не является альтернативой SQL как «модному» варианту, за исключением тех случаев, когда технология реляционных СУБД не подходит для проблемного пространства, а нереляционное хранилище данных подходит. Многие из ваших вопросов включают «Если бы это был SQL, то ...», и это меня предупреждает. Все NoSQL возникли из-за необходимости решить проблему, которую SQL не мог, а затем они были несколько обобщены, чтобы упростить использование, и затем, конечно же, победа начала катиться.
PurplePilot 09

4
Я знаю, что этот проект не совсем лучший для опробования NoSQL. Однако я боюсь, если мы начнем использовать его с другими проектами (скажем, программное обеспечение для управления коллекциями библиотек, потому что мы занимаемся управлением коллекциями), и вдруг придет какой-то запрос, в котором нужны транзакции (и он на самом деле там, представьте, что книги передается из одной коллекции в другую), нам нужно знать, как решить эту проблему. Может быть, это просто я ограничен во взглядах и считаю, что транзакции нужны всегда. Но может быть есть способ как-то их преодолеть.
NagyI 09

3
Я согласен с PurplePilot, вы должны выбрать технологию, которая подходит для решения, а не пытаться привить решение, которое не подходит для проблемы. Моделирование данных для графовых баз данных - это совершенно другая парадигма, чем проектирование РСУБД, и вам нужно забыть все, что вы знаете, и заново изучить новый способ мышления.

10
Я понимаю, что должен использовать подходящий инструмент для этой задачи. Однако для меня - когда я читаю подобные ответы - мне кажется, что NoSQL не годится ни для чего, где важны данные. Это хорошо для Facebook или Twitter, где, если некоторые комментарии теряются, мир продолжает жить, но все, что выше этого, не работает. Если это правда, я не понимаю, почему другие заботятся о строительстве, например. Интернет-магазин с MongoDB: kylebanker.com/blog/2010/04/30/mongodb-and-ecommerce В нем даже упоминается, что большинство транзакций можно преодолеть с помощью атомарных операций. Я ищу как.
NagyI 09

2
Вы говорите: «Кажется, что NoSQL не годится ни для чего, где важны данные», неверно там, где это не хорошо (возможно), это транзакционная обработка транзакций типа ACID. Кроме того, NoSQL предназначены для распределенных хранилищ данных, которые могут быть очень сложными при использовании хранилищ типа SQL, когда вы попадаете в сценарии репликации главного подчиненного устройства. В NoSQL есть стратегии для обеспечения согласованности в конечном итоге и обеспечения использования только самого последнего набора данных, но не ACID.
PurplePilot 09

Ответы:


24

Начиная с 4.0, MongoDB будет иметь транзакции ACID с несколькими документами. План состоит в том, чтобы сначала включить те, которые находятся в развертывании набора реплик, а затем сегментированные кластеры. Транзакции в MongoDB будут выглядеть так же, как транзакции, с которыми разработчики знакомы по реляционным базам данных - они будут многопользовательскими, с аналогичной семантикой и синтаксисом (например, start_transactionи commit_transaction). Важно отметить, что изменения в MongoDB, которые позволяют выполнять транзакции, не влияют на производительность рабочих нагрузок, для которых они не требуются.

Подробнее см. Здесь .

Наличие распределенных транзакций не означает, что вы должны моделировать свои данные, как в табличных реляционных базах данных. Воспользуйтесь мощью модели документа и следуйте передовым и рекомендуемым практикам моделирования данных.


1
Сделки прибыли! 4.0 GA'ed. mongodb.com/blog/post/…
Григорий Мельник

Транзакции MongoDB по-прежнему имеют ограничение на размер транзакции 16 МБ, недавно у меня был случай использования, когда мне нужно было поместить 50 тыс. Записей из файла в mongoDB, поэтому для сохранения атомарного свойства я думал об использовании транзакций, но поскольку записи json 50 тыс. при превышении этого предела возникает ошибка «Общий размер всех транзакционных операций должен быть меньше 16793600. Фактический размер - 16793817». для получения дополнительной информации вы можете пройти официальный билет jira, открытый на mongoDB jira.mongodb.org/browse/SERVER-36330
Гаутам Малик

MongoDB 4.2 (в настоящее время находится в стадии бета-тестирования, RC4) поддерживает крупные транзакции. Представляя транзакции в нескольких записях журнала операций, вы сможете записать более 16 МБ данных в одной транзакции ACID (с учетом существующего максимального времени выполнения по умолчанию 60 секунд). Вы можете попробовать их прямо сейчас - mongodb.com/download-center/community
Григорий Мельник,

MongoDB 4.2 теперь является GA с полной поддержкой распределенных транзакций. mongodb.com/blog/post/…
Григорий Мельник

83

Жизнь без сделок

Транзакции поддерживают свойства ACID, но, хотя в них нет транзакций MongoDB, у нас есть атомарные операции. Что ж, атомарные операции означают, что когда вы работаете над одним документом, эта работа будет завершена до того, как кто-либо другой увидит этот документ. Они увидят все внесенные нами изменения или ни одного из них. А с помощью атомарных операций вы часто можете выполнить то же самое, что мы сделали бы, используя транзакции в реляционной базе данных. Причина в том, что в реляционной базе данных нам нужно вносить изменения в несколько таблиц. Обычно таблицы, которые нужно соединить, и поэтому мы хотим сделать это сразу. И для этого, поскольку существует несколько таблиц, нам нужно будет начать транзакцию и выполнить все эти обновления, а затем завершить транзакцию. Но сMongoDB, мы собираемся встроить данные, так как мы собираемся предварительно объединить их в документы, а это эти богатые документы с иерархией. Часто мы можем добиться того же. Например, в примере с блогом, если мы хотим убедиться, что мы обновили сообщение в блоге атомарно, мы можем это сделать, потому что мы можем обновить все сообщение в блоге сразу. Там, где, как если бы это была связка реляционных таблиц, нам, вероятно, пришлось бы открыть транзакцию, чтобы мы могли обновить коллекцию сообщений и коллекцию комментариев.

Итак, какие наши подходы мы можем использовать, MongoDBчтобы преодолеть нехватку транзакций?

  • реструктуризация - реструктуризация кода, чтобы мы работали в рамках одного документа и использовали атомарные операции, которые мы предлагаем в этом документе. И если мы так поступаем, то обычно все готово.
  • реализовать в программном обеспечении - мы можем реализовать блокировку в программном обеспечении, создав критический раздел. Мы можем построить тест, протестировать и установить, используя поиск и изменение. При необходимости мы можем построить семафоры. И в каком-то смысле так и работает большой мир. Если подумать, если одному банку нужно перевести деньги в другой, они не живут в одной системе отношений. И каждая из них часто имеет свои собственные реляционные базы данных. И они должны иметь возможность координировать эту операцию, даже если мы не можем начать транзакцию и завершить транзакцию в этих системах баз данных, только в одной системе в одном банке. Так что в программном обеспечении, безусловно, есть способы обойти эту проблему.
  • терпеть - последний подход, который часто работает в современных веб-приложениях и других приложениях, которые принимают огромные объемы данных, - просто терпеть небольшую несогласованность. Например, если мы говорим о ленте друзей в Facebook, не имеет значения, увидят ли все обновления вашей стены одновременно. Если все в порядке, если один человек отстает на несколько секунд и догоняет. Во многих проектах систем часто не критично, чтобы все было идеально согласовано и чтобы у всех было совершенно согласованное и одинаковое представление о базе данных. Таким образом, мы могли просто терпеть небольшую несогласованность, которая в некоторой степени временна.

Update, findAndModify, $addToSet( В пределах обновления) и $push( в пределах обновления) операции выполняют атомарна в одном документе.


2
Мне нравится, как это делает этот ответ, вместо того, чтобы продолжать сомневаться, следует ли нам вернуться к реляционной БД. Спасибо @xameeramir!
DonnyTian

3
критическая часть кода не будет работать, если у вас более 1 сервера, и вам придется использовать внешнюю распределенную службу блокировки
Александр Миллс

@AlexanderMills Не могли бы вы уточнить, пожалуйста?
Zameer Ansari

ответ, кажется, расшифровка видео отсюда: youtube.com/watch?v=_Iz5xLZr8Lw
Fritz

Я думаю, что это нормально, пока мы не ограничены необходимостью работать с одной коллекцией. Но мы не можем поместить все в один документ по разным причинам (размер документа или использование ссылок). Думаю, тогда нам могут понадобиться транзакции.
user2488286

24

Проверьте это Tokutek. Они разрабатывают плагин для Mongo, который обещает не только транзакции, но и повышение производительности.


@ Джованни Битлинер. Tokutek был с тех пор приобретен Percona, и в предоставленной вами ссылке я не вижу ссылки на какую-либо информацию о том, что произошло с момента публикации. Вы знаете, что случилось с их усилиями? Я отправил электронное письмо на адрес этой страницы, чтобы узнать.
Тайлер Кольер

Что вам конкретно нужно? Если вам нужна технология toku, примененная к Mongodb, попробуйте github.com/Tokutek/mongo , если вам нужна версия mysql, возможно, они добавили ее в свою стандартную версию Mysql, которую они обычно предоставляют
Джованни Битлинер

Как интегрировать токутек с nodejs.
Manoj Sanjeewa

11

Подводя итог: если целостность транзакций является обязательной , не используйте MongoDB, а используйте только компоненты в системе, поддерживающие транзакции. Чрезвычайно сложно создать что-то поверх компонента, чтобы обеспечить функциональность, аналогичную ACID, для компонентов, не совместимых с ACID. В зависимости от индивидуальных вариантов использования может иметь смысл каким-то образом разделить действия на транзакционные и нетранзакционные действия ...


1
Я думаю, вы имеете в виду, что NoSQL можно использовать в качестве вспомогательной базы данных с классической СУБД. Мне не нравится идея смешивать NoSQL и SQL в одном проекте. Это увеличивает сложность и, возможно, также создает некоторые нетривиальные проблемы.
NagyI

1
Решения NoSQL редко используются в одиночку. Хранилища документов (монго и кушетка), пожалуй, единственное исключение из этого правила.
Karoly Horvath

7

В чем проблема? MongoDB может выполнять атомарные обновления только в одном документе. В предыдущем потоке могло случиться, что какая-то ошибка закрадывалась и сообщение сохранялось в базе данных, но баланс пользователя не уменьшался и / или транзакция не регистрировалась.

Это не проблема. Упомянутая вами ошибка является либо логической (ошибка), либо ошибкой ввода-вывода (сеть, сбой диска). Такая ошибка может привести к тому, что хранилища без транзакций и транзакционные хранилища окажутся в несогласованном состоянии. Например, если он уже отправил SMS, но при сохранении сообщения возникла ошибка - он не может откатить отправку SMS, а это значит, что он не будет зарегистрирован, баланс пользователя не будет уменьшен и т. Д.

Настоящая проблема здесь в том, что пользователь может воспользоваться состоянием гонки и отправить больше сообщений, чем позволяет его баланс. Это также относится к СУБД, если вы не отправляете SMS внутри транзакции с блокировкой поля баланса (что было бы большим узким местом). В качестве возможного решения для MongoDB можно было бы findAndModifyсначала уменьшить баланс и проверить его, если он отрицательный, запретить отправку и возместить сумму (атомарное приращение). Если положительный, продолжайте отправку и в случае неудачи верните сумму. Также можно вести сбор истории баланса, чтобы помочь исправить / проверить поле баланса.


Спасибо за отличный ответ! Я знаю, что если я использую хранилища, поддерживающие транзакции, данные могут быть повреждены из-за системы SMS, которую я не контролирую. Однако с Mongo есть вероятность, что ошибка данных может произойти и внутри компании. Допустим, код изменяет баланс пользователя с помощью findAndModify, баланс становится отрицательным, но прежде чем я смогу исправить ошибку, возникает ошибка, и приложение необходимо перезапустить. Я думаю, вы имеете в виду, что я должен реализовать что-то похожее на двухфазную фиксацию на основе коллекции транзакций и регулярно проверять исправления в базе данных.
NagyI

9
Неправда, транзакционные хранилища откатятся, если вы не сделаете окончательную фиксацию.
Karoly Horvath

9
Кроме того, вы не отправляете SMS, а затем входите в БД, это просто неправильно. Сначала сохраните все в БД и сделайте окончательную фиксацию, затем вы можете отправить сообщение. На этом этапе что-то все еще может выйти из строя, поэтому вам нужно задание cron, чтобы проверить, действительно ли сообщение было отправлено, если не пытаться отправить. Возможно, для этого лучше всего подойдет выделенная очередь сообщений. Но все сводится к тому, можете ли вы отправлять SMS-сообщения транзакционным способом ...
Кароли Хорват

@NagyI да, я это имел в виду. Для простоты масштабирования нужно торговать преимуществами транзакций. В основном приложение должно ожидать, что любые два документа в разных коллекциях могут находиться в несовместимом состоянии, и быть готовым справиться с этим. @yi_H он откатится, но состояние больше не будет актуальным (информация о сообщении будет потеряна). Это не намного лучше, чем просто иметь частичные данные (например, уменьшенный баланс, но отсутствие информации в сообщениях или наоборот).
pingw33n

Понимаю. На самом деле это непростое ограничение. Возможно, мне стоит узнать больше о том, как системы РСУБД выполняют транзакции. Можете ли вы порекомендовать какой-нибудь онлайн-материал или книгу, где я могу прочитать об этом?
NagyI

6

Проект прост, но вы должны поддерживать транзакции для оплаты, что все усложняет. Так, например, сложная система портала с сотнями коллекций (форум, чат, реклама и т. Д.) В некотором отношении проще, потому что, если вы потеряете форум или запись в чате, никого не волнует. Если вы, с другой стороны, потеряете платежную транзакцию, это серьезная проблема.

Итак, если вам действительно нужен пилотный проект для MongoDB, выберите простой в этом отношении.


Спасибо за объяснение. Печально это слышать. Мне нравится простота NoSQL и использование JSON. Мы ищем альтернативу ORM, но, похоже, какое-то время мы должны ее придерживаться.
NagyI 09

Можете ли вы назвать какие-либо веские причины, почему MongoDB лучше SQL для этой задачи? Пилотный проект звучит немного глупо.
Karoly Horvath

Я не говорил, что MongoDB лучше SQL. Мы просто хотим знать, лучше ли это SQL + ORM. Но сейчас становится все яснее, что они неконкурентоспособны в подобных проектах.
NagyI 09

6

Транзакции отсутствуют в MongoDB по уважительным причинам. Это одна из тех вещей, которые делают MongoDB быстрее.

В вашем случае, если транзакция является обязательной, mongo не подходит.

Может быть RDMBS + MongoDB, но это добавит сложности и затруднит управление и поддержку приложения.


1
Теперь существует дистрибутив MongoDB под названием TokuMX, который использует фрактальную технологию для повышения производительности в 50 раз и в то же время обеспечивает полную поддержку транзакций ACID: tokutek.com/tokumx-for-mongodb
OCDev

10
Как может сделка никогда не быть "обязательной". Как только вам когда-нибудь понадобится 1 простой случай, когда вам нужно обновить 2 таблицы, mongo вдруг больше не подходит? Это совсем не оставляет много вариантов использования.
Mr_E 02 окт.15,

1
@Mr_E согласен, поэтому MongoDB такой тупой :)
Александр Миллс

6

Это, вероятно, лучший блог, который я нашел о реализации транзакционной функции для mongodb.!

Флаг синхронизации: лучше всего подходит для простого копирования данных из главного документа

Очередь заданий: очень общего назначения, решает 95% случаев. В любом случае большинству систем требуется хотя бы одна очередь заданий!

Двухэтапная фиксация: этот метод гарантирует, что у каждой сущности всегда есть вся информация, необходимая для перехода в согласованное состояние

Сверка журналов: самый надежный метод, идеально подходящий для финансовых систем

Управление версиями: обеспечивает изоляцию и поддерживает сложные структуры

Прочтите это для получения дополнительной информации: https://dzone.com/articles/how-implement-robust-and


Пожалуйста, включите в свой ответ соответствующие части связанного ресурса, необходимые для ответа на вопрос. Как есть, ваш ответ очень подвержен гниению ссылок (например, если связанный веб-сайт выйдет из строя или изменится, ваш ответ потенциально бесполезен).
мех

Спасибо @mech за предложение
Vaibhav

4

Это поздно, но думаю, это поможет в будущем. Я использую Redis для создания очереди для решения этой проблемы.

  • Требование: на
    изображении ниже показано, что 2 действия необходимо выполнять одновременно, но этап 2 и этап 3 действия 1 должны завершиться до начала этапа 2 действия 2 или наоборот (этап может быть запросом REST api, запросом базы данных или выполнением кода javascript ... ). введите описание изображения здесь

  • Как очередь помогает вам?
    Очередь убедитесь, что каждый блочный код между многими функциями lock()и release()во многих функциях не будет выполняться одновременно, сделайте их изолированными.

    function action1() {
      phase1();
      queue.lock("action_domain");
      phase2();
      phase3();
      queue.release("action_domain");
    }
    
    function action2() {
      phase1();
      queue.lock("action_domain");
      phase2();
      queue.release("action_domain");
    }
    
  • Как создать очередь
    Я сосредоточусь только на том, как избежать части состояния гонки при построении очереди на внутреннем сайте. Если вы не знаете основы очереди, иди сюда .
    В приведенном ниже коде показана только концепция, которую необходимо правильно реализовать.

    function lock() {
      if(isRunning()) {
        addIsolateCodeToQueue(); //use callback, delegate, function pointer... depend on your language
      } else {
        setStateToRunning();
        pickOneAndExecute();
      }
    }
    
    function release() {
      setStateToRelease();
      pickOneAndExecute();
    }
    

Но вам нужно isRunning() setStateToRelease() setStateToRunning()изолировать его, иначе вы снова столкнетесь с условиями гонки. Для этого я выбираю Redis для ACID и масштабируемость.
В документе Redis говорится о транзакции:

Все команды в транзакции сериализуются и выполняются последовательно. Никогда не может случиться так, что запрос, отправленный другим клиентом, будет обслужен в середине выполнения транзакции Redis. Это гарантирует, что команды выполняются как единая изолированная операция.

P / s:
Я использую Redis, потому что мой сервис уже использует его, вы можете использовать любой другой способ поддержки изоляции для этого.
В action_domainмоем коде выше показано, когда вам нужно только действие 1, вызванное пользователем A, заблокировать действие 2 пользователя A, не блокировать другого пользователя. По идее ставится уникальный ключ для блокировки каждого пользователя.


Вы бы получили больше голосов, если бы ваш счет уже был выше. Так думает большинство здесь. Ваш ответ полезен в контексте вопроса. Я за тебя проголосовал.
Мукус

3

Транзакции теперь доступны в MongoDB 4.0. Образец здесь

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

// Updates two collections in a transactions

function updateEmployeeInfo(session) {
    employeesCollection = session.getDatabase("hr").employees;
    eventsCollection = session.getDatabase("reporting").events;

    session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

    try{
        employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
        eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
    } catch (error) {
        print("Caught exception during transaction, aborting.");
        session.abortTransaction();
        throw error;
    }

    commitWithRetry(session);
}

// Start a session.
session = db.getMongo().startSession( { mode: "primary" } );

try{
   runTransactionWithRetry(updateEmployeeInfo, session);
} catch (error) {
   // Do something with error
} finally {
   session.endSession();
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.