A better implementation method than Redis distributed locks in large-scale distributed scenarios. Source code analysis of the implementation process of etcd distributed locks

Let's cut into the scenario simulation that will be used in this example: commodity spike, or high concurrency, for commodity inventory deduction operations. I use a small SpringBoot project to simulate this operation.
The technology stack used in this example:

SpringBoot
Redis
etcd's
latest 2020 collection of some interview questions (all organized into documents), there are many dry goods, including detailed explanations of netty, spring, thread, spring cloud, etc., there are also detailed learning plans, interview questions, etc. I feel that The interview is very clear: to get the interview information, just: click here to get it!!! Password: CSDNInsert picture description here

Before the formal liver code, let's first understand the mechanism and principle of etcd distributed lock implementation.

The basic mechanism of etcd distributed lock implementation

Lease mechanism
Lease mechanism (TTL, Time To Live), etcd can set a lease for stored key-value pairs. When the lease expires, the key-value will be invalidated and deleted;
at the same time, it also supports renewal, and the lease expires through the client Renew the contract before
to avoid expiration of the key-value pair.
The Lease mechanism can ensure the security of distributed locks and configure a lease for the key corresponding to the lock.
Even if the lock holder fails to actively release the lock due to a failure, the lock will be automatically released when the lease expires.
Revision mechanism
Each key has a Revision number, which is +1 for each transaction. It is globally unique
. The order of writing operations can be known through the size of the Revision.
When implementing distributed locks, multiple clients grab locks at the same time
and obtain locks in turn according to the size of the Revision number, which can avoid the "herding effect" and achieve fair locks.

Sheep effect: Sheep are a very disorganized organization. Usually, we are blindly rushing to the left and right when we are together. However, once one sheep moves, other sheep will rush forward without thinking. Wolves and better grass not far away.
The Revision mechanism of etcd can perform write operations according to the order of the size of the Revision number, thus avoiding the "herding effect".
This is consistent with the principle that zookeeper's temporary sequence node + monitoring mechanism can avoid the herd effect.

The Prefix mechanism
is the prefix mechanism.
For example, for a lock named /etcd/lock, two clients competing for it write operations. The
actual keys written are: key1="/etcd/lock/UUID1", key2="/etcd/lock /UUID2".
Among them, UUID represents a globally unique ID to ensure the uniqueness of the two keys.
The write operation will be successful, but the returned Revision is different.
So, how to determine who has obtained the lock? Query through the prefix /etcd/lock, and return a KeyValue list containing two key-value pairs,
as well as their Revision. Through the Revision size, the client can determine whether it has acquired the lock.
Watch mechanism
is the monitoring mechanism.
The Watch mechanism supports a fixed key of Watch and a range of Watch (prefix mechanism).
When the watched key or range changes, the client will receive a notification; when implementing distributed locks, if the lock fails,
the Key-Value list returned by the Prefix mechanism can be used to obtain the key whose Revision is smaller and the smallest difference ( Called pre-key),
monitor the pre-key, because only it releases the lock, it can obtain the lock, if Watch to the DELETE event of
the pre-key , it means that the pre-key has been released, and it will hold the lock.

etcd distributed lock principle diagram

Insert picture description here

Implementation process of etcd distributed lock

establish connection

The client connects to etcd and creates a globally unique key with the prefix /etcd/lock.
Assume that the first client corresponds to key="/etcd/lock/UUID1", and the second is key="/etcd/lock/UUID2 "; The
client creates a lease for its own key respectively-Lease, the length of the lease is determined according to the business time;

Create a scheduled task as the "heartbeat" of the lease

While a client holds the lock, other clients can only wait. In order to avoid the lease expiration during the waiting period, the
client needs to create a timed task as a "heartbeat" for renewal. In addition, if the client crashes while holding the lock and the
heartbeat stops, the key will be deleted due to the expiration of the lease, so that the lock is released and deadlock is avoided;

The client writes its globally unique key into etcd

Execute the put operation and write the key binding lease created in step 1 into Etcd. According to Etcd's Revision mechanism,
assuming that the Revisions returned by the two client put operations are 1, 2 respectively, the client needs to record the Revision for the
next judgment Whether to obtain the lock;

The client judges whether to obtain the lock

The client reads the key-Value list with the prefix /etcd/lock/, and judges whether the Revision of its key is the
smallest in the current list, and if it is, the lock is considered; otherwise, it monitors the deletion of the previous Revision in the list that is smaller than its own key. Event, once a delete event or an event deleted due to lease expiration is monitored, the lock will be acquired by itself;

Perform business

After obtaining the lock, operate the shared resource and execute the business code

Release lock

After completing the business process, delete the corresponding key to release the lock

Code

With the above theory as the foundation, we started the code implementation of etcd distributed lock.

jetcd client

Jetcd is a Java client of etcd, it provides a rich interface to operate etcd, which is easy to use.

Insert picture description here

Redis data preparation

Initialize the inventory stock=300, and then set a lucky=0, which means that the person who grabs the inventory can be the user's order information in the actual scene. For every deduction of an inventory, lucky will increase by 1.Insert picture description here

Implementation of etcd distributed lock

Since the Lock interface of etcd has its own set of implementations, and the Lock interface of zookeeper also has its own set of implementations, redis...Various distributed lock implementations have their own Locks, so I encapsulated a template method:

/**
 * @program: distributed-lock
 * @description: 各种分布式锁的基类,模板方法
 * @author: 行百里者
 * @create: 2020/10/14 12:29
 **/
public class AbstractLock implements Lock {
    
    
    @Override
    public void lock() {
    
    
        throw new RuntimeException("请自行实现该方法");
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    
    
        throw new RuntimeException("请自行实现该方法");
    }

    @Override
    public boolean tryLock() {
    
    
        throw new RuntimeException("请自行实现该方法");
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    
    
        throw new RuntimeException("请自行实现该方法");
    }

    @Override
    public void unlock() {
    
    
        throw new RuntimeException("请自行实现该方法");
    }

    @Override
    public Condition newCondition() {
    
    
        throw new RuntimeException("请自行实现该方法");
    }
}

With this template method, subsequent implementations of distributed locks can inherit this template method class.

Implementation of etcd distributed lock:

@Data
public class EtcdDistributedLock extends AbstractLock {
    
    
    private final static Logger LOGGER = LoggerFactory.getLogger(EtcdDistributedLock.class);

    private Client client;
    private Lock lockClient;
    private Lease leaseClient;
    private String lockKey;
    //锁路径,方便记录日志
    private String lockPath;
    //锁的次数
    private AtomicInteger lockCount;
    //租约有效期。作用 1:客户端崩溃,租约到期后自动释放锁,防止死锁 2:正常执行自动进行续租
    private Long leaseTTL;
    //续约锁租期的定时任务,初次启动延迟,默认为1s,根据实际业务需要设置
    private Long initialDelay = 0L;
    //定时任务线程池
    ScheduledExecutorService scheduledExecutorService;
    //线程与锁对象的映射
    private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();

    public EtcdDistributedLock(Client client, String lockKey, Long leaseTTL, TimeUnit unit) {
    
    
        this.client = client;
        this.lockClient = client.getLockClient();
        this.leaseClient = client.getLeaseClient();
        this.lockKey = lockKey;
        this.leaseTTL = unit.toNanos(leaseTTL);
        scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
    }

    @Override
    public void lock() {
    
    
        
    }

    @Override
    public void unlock() {
    
    
        
    }
}

The realization of the lock method:

@Override
public void lock() {
    
    
    Thread currentThread = Thread.currentThread();
    LockData existsLockData = threadData.get(currentThread);
    //System.out.println(currentThread.getName() + " 加锁 existsLockData:" + existsLockData);
    //锁重入
    if (existsLockData != null && existsLockData.isLockSuccess()) {
    
    
        int lockCount = existsLockData.lockCount.incrementAndGet();
        if (lockCount < 0) {
    
    
            throw new Error("超出etcd锁可重入次数限制");
        }
        return;
    }
    //创建租约,记录租约id
    long leaseId;
    try {
    
    
        leaseId = leaseClient.grant(TimeUnit.NANOSECONDS.toSeconds(leaseTTL)).get().getID();
        //续租心跳周期
        long period = leaseTTL - leaseTTL / 5;
        //启动定时续约
        scheduledExecutorService.scheduleAtFixedRate(new KeepAliveTask(leaseClient, leaseId),
                initialDelay,
                period,
                TimeUnit.NANOSECONDS);

        //加锁
        LockResponse lockResponse = lockClient.lock(ByteSequence.from(lockKey.getBytes()), leaseId).get();
        if (lockResponse != null) {
    
    
            lockPath = lockResponse.getKey().toString(StandardCharsets.UTF_8);
            LOGGER.info("线程:{} 加锁成功,锁路径:{}", currentThread.getName(), lockPath);
        }

        //加锁成功,设置锁对象
        LockData lockData = new LockData(lockKey, currentThread);
        lockData.setLeaseId(leaseId);
        lockData.setService(scheduledExecutorService);
        threadData.put(currentThread, lockData);
        lockData.setLockSuccess(true);
    } catch (InterruptedException | ExecutionException e) {
    
    
        e.printStackTrace();
    }
}

In short, the lock code is based on the following steps:

Check lock reentrancy
Set up lease
Open timed task heartbeat check
Block acquire lock Lock
successfully, set lock object

After business processing is completed (deduction of inventory), unlock:

@Override
public void unlock() {
    
    
    Thread currentThread = Thread.currentThread();
    //System.out.println(currentThread.getName() + " 释放锁..");
    LockData lockData = threadData.get(currentThread);
    //System.out.println(currentThread.getName() + " lockData " + lockData);
    if (lockData == null) {
    
    
        throw new IllegalMonitorStateException("线程:" + currentThread.getName() + " 没有获得锁,lockKey:" + lockKey);
    }
    int lockCount = lockData.lockCount.decrementAndGet();
    if (lockCount > 0) {
    
    
        return;
    }
    if (lockCount < 0) {
    
    
        throw new IllegalMonitorStateException("线程:" + currentThread.getName() + " 锁次数为负数,lockKey:" + lockKey);
    }
    try {
    
    
        //正常释放锁
        if (lockPath != null) {
    
    
            lockClient.unlock(ByteSequence.from(lockPath.getBytes())).get();
        }
        //关闭续约的定时任务
        lockData.getService().shutdown();
        //删除租约
        if (lockData.getLeaseId() != 0L) {
    
    
            leaseClient.revoke(lockData.getLeaseId());
        }
    } catch (InterruptedException | ExecutionException e) {
    
    
        //e.printStackTrace();
        LOGGER.error("线程:" + currentThread.getName() + "解锁失败。", e);
    } finally {
    
    
        //移除当前线程资源
        threadData.remove(currentThread);
    }
    LOGGER.info("线程:{} 释放锁", currentThread.getName());
}

Unlocking process:

Reentrancy check
Remove the node path of the current lock Release the lock
Clear the reentrant thread resources

Interface test

/**
 * @program: distributed-lock
 * @description: etcd分布式锁演示-高并发下库存扣减
 * @author: 行百里者
 * @create: 2020/10/15 13:24
 **/
@RestController
public class StockController {
    
    

    private final StringRedisTemplate redisTemplate;

    @Value("${server.port}")
    private String port;

    @Value("${etcd.lockPath}")
    private String lockKey;

    private final Client etcdClient;

    public StockController(StringRedisTemplate redisTemplate, @Value("${etcd.servers}") String servers) {
    
    
        //System.out.println("etcd servers:" + servers);
        this.redisTemplate = redisTemplate;
        this.etcdClient = Client.builder().endpoints(servers.split(",")).build();
    }

    @RequestMapping("/stock/reduce")
    public String reduceStock() {
    
    
        Lock lock = new EtcdDistributedLock(etcdClient, lockKey, 30L, TimeUnit.SECONDS);
        //获得锁
        lock.lock();
        //扣减库存
        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
    
    
            int realStock = stock - 1;
            redisTemplate.opsForValue().set("stock", String.valueOf(realStock));
            //同时lucky+1
            redisTemplate.opsForValue().increment("lucky");
        } else {
    
    
            System.out.println("库存不足");
        }
        //释放锁
        lock.unlock();
        return port + " reduce stock end!";
    }
}

This is very simple. When a request comes in, first try to lock it. After the lock is successful, execute the business and deduct the inventory. At the same time, the order information is +1. After the business processing is completed, the lock is released.
Stress test The
test interface has been completed. Use JMeter to simulate a high concurrency scenario, send 500 requests at the same time (the inventory is only 300), and observe the results.
Start two services first, one 8080 and one 8090:

Insert picture description here
Configure nginx (mainly to facilitate the simulation of high concurrency and distribution): Insert picture description here
The IP address of nginx is 192.168.2.10: Insert picture description here
Therefore, for our stress test, we only need to send a request to the http://192.168.2.10/stock/reduce interface.

Insert picture description here
Insert picture description here
Insert picture description here
Execution pressure test results: The Insert picture description here
Insert picture description here
results show that our etcd distributed lock is successful!

to sum up

Some interview questions collected in the latest 2020 (all organized into documents), there are a lot of dry goods, including detailed explanations of netty, spring, thread, spring cloud, etc., there are also detailed learning plans, interview questions, etc. I feel that I am in the interview. Speaking very clearly: to get the interview information, just: click here to get it!!! Password: CSDN
Insert picture description here

Guess you like

Origin blog.csdn.net/a3961401/article/details/109249069