How to use Redis locks to solve high concurrency problems in detail

  • 2020-06-12 10:58:27
  • OfStack

Use of redis technology:

redis is a really good technology, it can solve the concurrent volume of site 1 in a certain degree, such as the commodity buying and killing activities...

The reason why redis can solve high concurrency is that it can directly access the memory. In the past, we used the database (hard disk), which improves the access efficiency and solves the pressure of the database server.

Why redis is getting more and more important, why we don't choose memcache, it is because memcache can only store strings, and redis has a lot of storage types (such as strings, LIST, SET, etc.), Each value of memcache can only store 1M at most, with very limited storage resources and 10 minutes of memory consumption, while redis can store 1G. The most important thing is that memcache is not as safe as redis. In case of server failure or unexpected shutdown,redsi will backup the data in memory to the hard disk, while memcache will lose everything. This also shows that memcache is not suitable for database use, but can be used for caching.

The introduction

Here we mainly use the setnx command of Redis to handle high concurrency.

[

setnx has two parameters. The first parameter represents the key. The second parameter represents the value. If the current key does not exist, the current key is inserted, with the second parameter as the value. Returns 1. If the current key exists, 0 is returned.

]

Create inventory table


CREATE TABLE `storage` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `number` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

Set the initial inventory to 10

Create order table


CREATE TABLE `order` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `number` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

Test when locks are not used


$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'root');
$sql="select `number` from storage where id=1 limit 1";
$res = $pdo->query($sql)->fetch();
$number = $res['number'];
if($number>0)
{
 $sql ="insert into `order` VALUES (null,$number)";

 $order_id = $pdo->query($sql);
 if($order_id)
 {

 $sql="update storage set `number`=`number`-1 WHERE id=1";
 $pdo->query($sql);
 }
}

The ab test simulated concurrency and found that the inventory was correct.


mysql> select * from storage;
+----+--------+
| id | number |
+----+--------+
| 1 | 0 |
+----+--------+
1 row in set (0.00 sec)

Look at the order sheet


mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
| 1 | 10 |
| 2 | 10 |
| 3 | 9 |
| 4 | 7 |
| 5 | 6 |
| 6 | 5 |
| 7 | 5 |
| 8 | 5 |
| 9 | 4 |
| 10 | 1 |
+----+--------+
10 rows in set (0.00 sec)

It is found that several orders are operated on the same inventory data, which may cause an oversold situation.

Modify the code to add redis lock for data control


<?php
/**
 * Created by PhpStorm.
 * User: daisc
 * Date: 2018/7/23
 * Time: 14:45
 */
class Lock
{
 private static $_instance ;
 private $_redis;
 private function __construct()
 {
 $this->_redis = new Redis();
 $this->_redis ->connect('127.0.0.1');
 }
 public static function getInstance()
 {
 if(self::$_instance instanceof self)
 {
  return self::$_instance;
 }
 return self::$_instance = new self();
 }

 /**
 * @function  lock 
 * @param $key  The lock name 
 * @param $expTime  Expiration time 
 */
 public function set($key,$expTime)
 {
 // Preliminary lock 
 $isLock = $this->_redis->setnx($key,time()+$expTime);
 if($isLock)
 {
  return true;
 }
 else
 {
  // If the lock fails. Determine if the lock already exists. If the lock has expired, delete the lock. Relock 
  $val = $this->_redis->get($key);
  if($val&&$val<time())
  {
  $this->del($key);
  }
  return $this->_redis->setnx($key,time()+$expTime);
 }
 }


 /**
 * @param $key  unlock 
 */
 public function del($key)
 {
 $this->_redis->del($key);
 }

}



$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'root');
$lockObj = Lock::getInstance();
// The judgement is that the lock can be successfully added 
if($lock = $lockObj->set('storage',10))
{
 $sql="select `number` from storage where id=1 limit 1";
 $res = $pdo->query($sql)->fetch();
 $number = $res['number'];
 if($number>0)
 {
 $sql ="insert into `order` VALUES (null,$number)";

 $order_id = $pdo->query($sql);
 if($order_id)
 {

  $sql="update storage set `number`=`number`-1 WHERE id=1";
  $pdo->query($sql);
 }
 }
 // unlock 
 $lockObj->del('storage');

}
else
{
 // Locking did not succeed in executing other operations. 
}

Run the ab test again to see the results


mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
| 1 | 10 |
| 2 | 9 |
| 3 | 8 |
| 4 | 7 |
| 5 | 6 |
| 6 | 5 |
| 7 | 4 |
| 8 | 3 |
| 9 | 2 |
| 10 | 1 |
+----+--------+
10 rows in set (0.00 sec)

It was found that the order sheet did not operate the same inventory data. So using redis locks can effectively handle high concurrency.

In fact, there is no need to judge the expiration time when adding the lock. In order to avoid deadlock, we add an expiration time judgment. Actively delete the lock when it expires.

conclusion


Related articles: