Состоянии гонки(Race condition) на примере счетчика

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

Wikipedia

Простыми словами, когда мы делаем одновременно несколько запросов и записываем в один источник, будь-то файл или база данных, один запрос перезаписывает данные другого запроса.

Счетчик просмотров

Необходимо понимать, куда будем записывать данные. Самая большая ошибка, когда пишут в post_meta или term_meta. Это самый плохой вариант, потому что счет просмотров обновляется постоянно, что лишает вас объектного кеширования данных для поста/термина.

Записывать данные в таблицу options тоже не хорошо, ведь они тоже кешируются, но можно указать, чтобы эти опции не загружались автоматически.

Но лучше всего сделать простую таблицу для хранения при активации плагина:

<?php
class Views {

	public static function table() {
		global $wpdb;

		return $wpdb->prefix . 'views';
	}
	public static function create_table() {
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		global $wpdb;

		$sql = 'CREATE TABLE ' . self::table() . ' (
			`post_id` INT NOT NULL UNIQUE,
			`views` INT UNSIGNED DEFAULT 0
		) ' . $wpdb->get_charset_collate();

		maybe_create_table( self::table(), $sql );
	}
}

register_activation_hook( __FILE__, [ 'Views', 'create_table' ] );

Плохой счетчик

Как обновлять данные? 99% всех счетчиков обновляют следующим образом:

<?php 
class Views {
	//...
	public function get_views( $post_id ) {
		global $wpdb;

		return absint(
			$wpdb->get_var(
				$wpdb->prepare(
					'SELECT views FROM ' . self::table() . ' WHERE post_id=%d',
					$post_id
				)
			)
		);
	}

	public function update( $post_id ) {
		global $wpdb;

		$views = $this->get_views( $post_id );
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE ' . self::table() . ' SET views=%d WHERE post_id=%d', ++$views, $post_id
			)
		)
	}
}

Получили данные, увеличили на 1 и записали обратно в базу данных. Весь метод на update у нас является уязвивым(критическим ) участком кода.

Почему так делать нельзя? Запускаем нагрузочное тестирование на 10000 запросов в течении минуты, для чистоты результата, на странице кроме счетчика, нет кеширования и контента.

В результате вместо 10000 просмотров у нас 7240 просмотров — погрешность 27,5%.

Это произошло из-за того, что несколько запросов одновременно были отправлены с одной и той же информацией. То есть с момента, когда мы данные получили, до момента, когда мы их обновили, другой запрос уже изменил эти данные.

Хороший счетчик

Для того, чтобы этого избежать, в месте, куда вы записываете данные должна быть блокировка до момента записи. К счастью в MySQL базах это уже реализовано.

<?php 
class Views {
	//...
	public function update( $post_id ) {
		global $wpdb;

		$views = $this->get_views( $post_id );
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE ' . self::table() . ' SET views = views + 1 WHERE post_id=%d', $post_id
			)
		)
	}
}

Запускаем нагрузочное тестирование. В результате на 10000 запросов столько же и просмотров. В данном случае все запросы в MySQL выстроились в очередь и пока не выполнится 1 запрос остальные его ждут.

Чтобы счетчик работал со страничным кешированием, запуск его нужно добавить на AJAX. Но это опустим.

Это самый простой пример гонки состояния. В целом все данные, которые имеют состояние (например их необходимо получить из базы, посчитать и записать в базу) лучше делать через MySQL, потому что там есть блокировка состояния.

А теперь представьте ситуацию, где банк при переводе денег с карты на карту точно так же зависит от кол-ва нагрузки и вместо 10000 грн вам будет начислено всего 7240 грн или ничего не начислено.

Всем также рекомендую посмотреть видео от Геннадия Ковшевина о состоянии гонки:

Источник: wp-punk.com

Добавить комментарий

%d такие блоггеры, как: