Distributed (1) -- distributed lock

Distributed encounters concurrency

In the previous chapters, concurrent operations either occur in a single application, and JVM-based locks are generally used to solve concurrency problems, or they occur in the database, and database-level locks can be considered. In distributed scenarios, multiple application instances need to be guaranteed. If both can execute synchronous code, some additional work needs to be done. One of the most typical distributed synchronization solutions is to use distributed locks.

Distributed locks are implemented in many ways, but they are all similar in nature, that is, relying on shared components to achieve lock query and acquisition. If the Monitor in the monolithic application is provided by the JVM, then the distributed monitor is the Provided by shared components, and typical shared components are not unfamiliar to everyone, including but not limited to: Mysql, Redis, Zookeeper. They also represent three types of shared components: databases, caches, and distributed coordination components. Consul-based distributed locks are actually similar to Zookeeper-based distributed locks. They all use distributed coordination components to realize locks. Larger and larger, these three types of distributed locks have similar principles, except that, The characteristics and implementation details of locks vary.

Redis implements distributed locks

Defining requirements: Application A needs to complete the operation of adding inventory, deploy multiple instances of A1, A2, and A3, and ensure synchronization of operations between instances.

Analysis of requirements: Obviously, the lock that relies on the JVM can no longer solve the problem at this time. A1 adds a lock, and the synchronization of A2 and A3 cannot be guaranteed. In this scenario, distributed locks can be considered.

Create a Stock table, including two fields of id and number, and let A1, A2, and A3 operate on them concurrently to ensure thread safety.

1
2
3
4
5
6
@Entity
public class Stock {
     @Id
     private String id;
     private Integer number;
}

Define the database access layer:

1
2
public interface StockRepository extends JpaRepository<Stock,String> {
}

The protagonist of this section, the redis distributed lock, is implemented using the open source redis distributed lock: Redisson.

Introduce Redisson dependencies:

1
2
3
4
5
<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version> 3.5 . 4 </version>
</dependency>

Define the test class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController
public class StockController {
     @Autowired
     StockRepository stockRepository;
     ExecutorService executorService = Executors.newFixedThreadPool( 10 );
     @Autowired
     RedissonClient redissonClient;
     final static String id = "1" ;
     @RequestMapping ( "/addStock" )
     public void addStock() {
         RLock lock = redissonClient.getLock( "redisson:lock:stock:" + id);
         for ( int i = 0 ; i < 100 ; i++) {
             executorService.execute(() -> {
                 lock.lock();
                 try {
                     Stock stock = stockRepository.findOne(id);
                     stock.setNumber(stock.getNumber() + 1 );
                     stockRepository.save(stock);
                 } finally {
                     lock.unlock();
                 }
             });
         }
     }
}

The above code allows concurrency to occur at multiple levels. First, within the application, enabling the thread pool to complete the inventory addition operation is inherently thread-unsafe. Second, among multiple applications, such an operation of adding 1 is more unconstrained. If the initialized id is 1, the number of Stocks is 0. Enable the three applications A1 (8080), A2 (8081), and A3 (8082) locally, and execute addStock() concurrently at the same time. If the thread is safe, the Stock in the database must be 300. This is our detection. in accordance with.

Briefly interpret the above code, use redisson to obtain an RLock, RLock is the implementation class of the java.util.concurrent.locks.Lock interface, Redisson helps us shield the implementation details of Redis distributed locks, and has used java.util.concurrent. Friends of locks.Lock will know that the following code can be called a synchronized starting paradigm. After all, this is the code given in Lock's java doc:

1
2
3
4
5
6
7
Lock l = ...;
l.lock();
try {
    // access the resource protected by this lock
} finally {
   l.unlock();
}

And redissonClient.getLock("redisson:lock:stock:" + id) is a Monitor that synchronizes with the string "redisson:lock:stock:" + id, which ensures that different ids do not block each other.

In order to ensure concurrency, I added Thread.sleep(1000) in the actual test to make the competition happen. Test Results:

Redis distributed locks do work.

Notes on locks

It is not difficult to implement a Redis distributed lock that can be used for demo, but why do people prefer to use open source implementations? The main thing is usability and stability. We make things work is what I keep in mind when I write a blog and code. If I really want to study how to implement a distributed lock by myself, or use locks to ensure concurrency, what should I pay attention to? point? To name a few: blocking, timeouts, reentrancy, availability, other features.

block

It means waiting between various operations. When A1 is executing to increase inventory, other threads of A1 are blocked, and all threads in A2 and A3 are blocked. In Redis, the polling strategy and the CAS primitive provided by the bottom layer of redis can be used ( Such as setnx) to achieve. (Beginners can understand that: set a key in redis, when you want to execute the lock code, first ask if there is the key, if there is, it means that other threads are in the execution process, if not, set the key, and execute the code, After the execution is completed, the key is released, and setnx guarantees the atomicity of the operation)

overtime time

In special cases, the lock may not be released, such as deadlock, infinite loop and other unexpected situations. It is necessary to set the lock timeout time. A very intuitive idea is to set the expiration time for the key.

如在Redisson中,lock提供了一个重载方法lock(long t, TimeUnit timeUnit);可以自定义过期时间。

可重入

这个特性很容易被忽视,可重入其实并不难理解,顾名思义,一个方法在调用过程中是否可以被再次调用。实现可重入需要满足三个特性:

  1. 可以在执行的过程中可以被打断;
  2. 被打断之后,在该函数一次调用执行完之前,可以再次被调用(或进入,reentered)。
  3. 再次调用执行完之后,被打断的上次调用可以继续恢复执行,并正确执行。

比如下述的代码引用了全局变量,便是不可重入的:

1
2
3
4
5
6
7
int t;
void swap( int x, int y) {
     t = x;
     x = y;
     y = t;
     System.out.println( "x is" + x + " y is " + y);
}

一个更加直观的例子便是,同一个线程中,某个方法的递归调用不应该被阻塞,所以如果要实现这个特性,简单的使用某个key作为Monitor是欠妥的,可以加入线程编号,来保证可重入。

使用可重入分布式锁的来测试计算斐波那契数列(只是为了验证可重入性):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping ( "testReentrant" )
public void ReentrantLock() {
     RLock lock = redissonClient.getLock( "fibonacci" );
     lock.lock();
     try {
         int result = fibonacci( 10 );
         System.out.println(result);
     } finally {
         lock.unlock();
     }
}
int fibonacci( int n) {
     RLock lock = redissonClient.getLock( "fibonacci" );
     try {
         if (n <= 1 ) return n;
         else
             return fibonacci(n - 1 ) + fibonacci(n - 2 );
     } finally {
         lock.unlock();
     }
}

最终输出:55,可以发现,只要是在同一线程之内,无论是递归调用还是外部加锁(同一把锁),都不会造成死锁。

可用性

借助于第三方中间件实现的分布式锁,都有这个问题,中间件挂了,会导致锁不可用,所以需要保证锁的高可用,这就需要保证中间件的可用性,如redis可以使用哨兵+集群,保证了中间件的可用性,便保证了锁的可用性、

其他特性

除了可重入锁,锁的分类还有很多,在分布式下也同样可以实现,包括但不限于:公平锁,联锁,信号量,读写锁。Redisson也都提供了相关的实现类,其他的特性如并发容器等可以参考官方文档。

新手遭遇并发

基本算是把项目中遇到的并发过了一遍了,案例其实很多,再简单罗列下一些新手可能会遇到的问题。

使用了线程安全的容器就是线程安全了吗?很多新手误以为使用了并发容器如:concurrentHashMap就万事大吉了,却不知道,一知半解的隐患可能比全然不懂更大。来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ConcurrentHashMapTest {
     static Map<String, Integer> counter = new ConcurrentHashMap();
     public static void main(String[] args) throws InterruptedException {
         counter.put( "stock1" , 0 );
         ExecutorService executorService = Executors.newFixedThreadPool( 10 );
         CountDownLatch countDownLatch = new CountDownLatch( 100 );
         for ( int i = 0 ; i < 100 ; i++) {
             executorService.execute( new Runnable() {
                 @Override
                 public void run() {
                     counter.put( "stock1" , counter.get( "stock1" ) + 1 );
                     countDownLatch.countDown();
                 }
             });
         }
         countDownLatch.await();
         System.out.println( "result is " + counter.get( "stock1" ));
     }
}

counter.put(“stock1″, counter.get(“stock1″) + 1)并不是原子操作,并发容器保证的是单步操作的线程安全特性,这一点往往初级程序员特别容易忽视。

总结

项目中的并发场景是非常多的,而根据场景不同,同一个场景下的业务需求不同,以及数据量,访问量的不同,都会影响到锁的使用,架构中经常被提到的一句话是:业务决定架构,放到并发中也同样适用:业务决定控制并发的手段,如本文未涉及的队列的使用,本质上是化并发为串行,也解决了并发问题,都是控制的手段。了解锁的使用很简单,但如果使用,在什么场景下使用什么样的锁,这才是价值所在。

同一个线程之间的递归调用不应该被阻塞,所以如果要实现这个特性,简单的使用某个key作为Monitor是欠妥的,可以加入线程编号,来保证可重入。

 

(原文地址:http://www.importnew.com/27278.html 。 尊重原创,感谢作者!)

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324522533&siteId=291194637