The cases in this article will be uploaded to git, please feel free to browse git address: github.com/muxiaonong/… This paper will use three independent Redis servers, which can be built in advance

preface

In Java, we are familiar with locking, such as synchronized and Lock. In Java concurrent programming, we use locking to realize data inconsistency caused by multiple threads competing for the same shared resource or variable. However, JVM locking can only be used for a single application service. With the development of our business needs, the monomer standalone deployment system has evolved into a distributed system, as a result of the distributed system multithreading, multi-process and distribution on different machines, this time the JVM locking concurrency control, there is no effect, in order to solve the cross JVM lock and the ability to control access to a Shared resource, and the birth of a distributed lock.

What is distributed locking

Distributed locking is a way to control synchronous access to shared resources between distributed systems. In distributed systems, they often need to coordinate their actions. If different systems or hosts on the same system share one or a group of resources, the access to these resources must be mutually exclusive to prevent interference and ensure consistency. In this case, distributed locks are required

Why can’t JVM locks be distributed?

We can look at the code to see why the JVM lock is not reliable under the cluster. We simulate the commodity to snap up the scene, there are ten users to A service for the goods and services B has ten users to snapping up the goods, when there is one of the users for success, other users may not be in order for the goods for operation, so what is A service will get the B will get the the goods, we take A look at

The status changes to 1 when one of the users succeeds

GrabService:

public interface GrabService {

    /** ** **@param orderId
     * @param driverId
     * @return* /
    public ResponseResult grabOrder(int orderId, int driverId);
}
Copy the code

GrabJvmLockServiceImpl:

@Service("grabJvmLockService")
public class GrabJvmLockServiceImpl implements GrabService {
	
	@Autowired
	OrderService orderService;
	
	@Override
	public ResponseResult grabOrder(int orderId, int driverId) {
		String lock = (orderId+"");
		
		synchronized (lock.intern()) {
			try {
				System.out.println("Users."+driverId+"Execute order logic");
				
	            boolean b = orderService.grab(orderId, driverId);
	            if(b) {
	            	System.out.println("Users."+driverId+"Order successful");
	            }else {
	            	System.out.println("Users."+driverId+"Order failed"); }}finally{}}return null; }}Copy the code

OrderService :

public interface OrderService {
	
	public boolean grab(int orderId, int driverId);
	
}
Copy the code

OrderServiceImpl :

@Service
public class OrderServiceImpl implements OrderService {
	
	@Autowired
	private OrderMapper mapper;
	
	public boolean grab(int orderId, int driverId) {
		Order order = mapper.selectByPrimaryKey(orderId);
		 try {
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
		if(order.getStatus().intValue() == 0) {
			order.setStatus(1);
			mapper.updateByPrimaryKeySelective(order);
			
			return true;
		}
		return false; }}Copy the code

Here we simulate the cluster environment, start two ports, 8004 and 8005 for access. Here we test with Jmeter. If you can’t use Jmeter, you can see my previous article on tomcat pressure test: Tomcat optimization

Start the Server- Eureka registry first and port 8004 and port 8005





Test results:



Here we can see 8004 service and 8005 serviceAt the same time, there is a user to order this product successfullyHowever, this commodity can only be grabbed by one user. Therefore, if the JVM lock is in a cluster or distributed environment, it cannot guarantee that only one thread can access the data of shared variables at the same time, which cannot solve the problem of distributed and clustered environment. So you need to use distributed locks.

Distributed lock three ways to achieve

There are three ways to implement distributed locks:

  • Implement distributed lock based on database
  • Distributed lock based on cache (Redis)
  • Distributed lock based on Zookeeper

Today, we will focus on distributed locking based on Redis implementation

Reids implements distributed locks in three ways

1, based on redis SETNX to achieve distributed lock 2, Redisson to achieve distributed lock 4, using redLock to achieve distributed lock

Directory structure:

Method 1: Implement distributed lock based on SETNX

Set the value of key to value if and only if the key does not exist. If the given key already exists, SETNX does nothing. Setnx: This parameter is set only when the key does not exist and no operation is performed

Lock:

SET orderId driverId NX PX 30000 If the preceding command is executed successfully, the client successfully obtains the lock, and then can access the shared resource. If the preceding command fails to be executed, the lock fails to be obtained.

Release the lock: the key, determine whether you put the lock.

GrabService:

public interface GrabService {

    /** ** **@param orderId
     * @param driverId
     * @return* /
    public ResponseResult grabOrder(int orderId, int driverId);
}
Copy the code

GrabRedisLockServiceImpl:


@Service("grabRedisLockService")
public class GrabRedisLockServiceImpl implements GrabService {

	@Autowired
	StringRedisTemplate stringRedisTemplate;
	
	@Autowired
	OrderService orderService;
	
    @Override
    public ResponseResult grabOrder(int orderId , int driverId){
        / / to generate the key
    	String lock = "order_"+(orderId+"");
    	/* * If the lock has not been released, e.g., the business logic has been executed halfway, the o&M service has been restarted, or the server has been hung, finally, what can be done? * Add timeout */
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// if(! lockStatus) {
// return null;
/ /}
    	
    	/* * Case 2: if the timeout period is added, it will not be added. Operation and maintenance restart */
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// stringRedisTemplate.expire(lock.intern(), 30L, TimeUnit.SECONDS);
// if(! lockStatus) {
// return null;
/ /}
    	
    	/* * case 3: The timeout should be added once, instead of 2 lines, * */
    	boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"".30L, TimeUnit.SECONDS);
    	if(! lockStatus) {return null;
    	}
    	
    	try {
			System.out.println("Users."+driverId+"Execute order snatching logic");
			
            boolean b = orderService.grab(orderId, driverId);
            if(b) {
            	System.out.println("Users."+driverId+"Win the order.");
            }else {
            	System.out.println("Users."+driverId+"Failed to get the order"); }}finally {
        	/** * This type of release lock may release someone else's lock. * /
// stringRedisTemplate.delete(lock.intern());
        	
        	/** * The following code to avoid releasing someone else's lock */
        	if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) { stringRedisTemplate.delete(lock.intern()); }}return null; }}Copy the code

One might ask here, what happens if my business takes longer to execute than the lock is released? We can use a daemon thread, as long as we have the current thread is holding the lock, when the 10 s, daemon thread automatically overtime on this thread for operation, will continue on the 30 s of the expiration time, until the lock is released, is not in the contract, open a child thread, the time is N, every N / 3, N on to continue

Focus:

  1. Key is our target to lock, such as the order ID.

  2. DriverId is our commodity ID, which is guaranteed to be unique among all lock requests from all clients for a long enough period of time. That is, an order is grabbed by a user.

  3. NX indicates that the SET will succeed only if the orderId does not exist. This ensures that only the first client can acquire the lock, and no other client can acquire the lock until it is released.

  4. PX 30000 indicates that the lock has an automatic expiration time of 30 seconds. Of course, 30 seconds is just an example, and the client can choose the appropriate expiration time.

  5. This lock must be set to an expiration time. Otherwise, when a client acquires a lock, if it crashes or is unable to communicate with the Redis node due to network partitioning, it will hold the lock forever, and other clients will never be able to acquire the lock. Antirez also emphasized this point in the later analysis, and called this expiration time the lock validity time. The client that acquires the lock must complete access to the shared resource within this time.

  6. This operation cannot be split.

    SETNX orderId driverId EXPIRE orderId 30 Although these two commands have the same effect as one of the SET commands described in the previous algorithm, they are not atomic. If the client crashes after executing SETNX, there is no chance to execute EXPIRE, causing it to hold the lock forever. Cause a deadlock.

Method 2: Implement distributed lock based on Redisson

Flow chart:



Code implementation:

@Service("grabRedisRedissonService")
public class GrabRedisRedissonServiceImpl implements GrabService {

	@Autowired
	RedissonClient redissonClient;
	
	@Autowired
	OrderService orderService;
	
    @Override
    public ResponseResult grabOrder(int orderId , int driverId){
        / / to generate the key
    	String lock = "order_"+(orderId+"");
    	
    	RLock rlock = redissonClient.getLock(lock.intern());
    	
    	
    	try {
    		// By default, this code sets the key timeout time to 30 seconds, after 10 seconds, then delay
    		rlock.lock();
			System.out.println("Users."+driverId+"Execute order snatching logic");
			
            boolean b = orderService.grab(orderId, driverId);
            if(b) {
            	System.out.println("Users."+driverId+"Win the order.");
            }else {
            	System.out.println("Users."+driverId+"Failed to get the order"); }}finally {
        	rlock.unlock();
        }
        return null; }}Copy the code

Focus:

  1. Redis failure. If Redis fails, all clients cannot acquire the lock and the service becomes unavailable. To improve usability. We configure redis with master and slave. When master is unavailable, the system switches to slave, which may result in loss of lock security because Redis’ master/slave replication is asynchronous

    1. Client 1 obtains the lock from the Master. 2. The Master is down, and the key that stores the lock has not been synchronized to the Slave. 3. The Slave is upgraded to Master. 4. Client 2 obtains the lock corresponding to the same resource from the new Master.

    Both client 1 and client 2 hold the lock on the same resource. The security of the lock was breached.

  2. What is the appropriate lock validity time? If set too short, the lock may expire before the client finishes accessing the shared resource and lose protection. If the setting is too long, if one client holding the lock fails to release the lock, all other clients will be unable to acquire the lock and thus cannot function properly for a long time. It should be set slightly shorter, so that if the thread holds the lock, the open thread automatically extends the validity period

Method 3: Distributed lock based on RedLock

In view of the above two points, Antirez designed the Redlock algorithm. Antirez, the author of Redis, gave a better implementation, called Redlock, which is the official guidance specification of Redis for the implementation of distributed lock. Redlock’s algorithm is described on the Redis website: Redis. IO /topics/dist…

Purpose: Mutually exclusive access to shared resources

Therefore, Antirez proposed a new distributed lock algorithm, Redlock, which is based on N completely independent Redis nodes (N can be set to 5 under normal circumstances), meaning that N Redis data is not interconnected, similar to several strangers

Code implementation:

@Service("grabRedisRedissonRedLockLockService")
public class GrabRedisRedissonRedLockLockServiceImpl implements GrabService {

    @Autowired
    private RedissonClient redissonRed1;
    @Autowired
    private RedissonClient redissonRed2;
    @Autowired
    private RedissonClient redissonRed3;
    
    @Autowired
    OrderService orderService;

    @Override
    public ResponseResult grabOrder(int orderId , int driverId){
        / / to generate the key
        String lockKey = (RedisKeyConstant.GRAB_LOCK_ORDER_KEY_PRE + orderId).intern();
        / / red lock
        RLock rLock1 = redissonRed1.getLock(lockKey);
        RLock rLock2 = redissonRed2.getLock(lockKey);
        RLock rLock3 = redissonRed2.getLock(lockKey);
        RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
    
        try {
        	 rLock.lock();
    		// By default, this code sets the key timeout time to 30 seconds, after 10 seconds, then delay
			System.out.println("Users."+driverId+"Execute order snatching logic");
			
            boolean b = orderService.grab(orderId, driverId);
            if(b) {
            	System.out.println("Users."+driverId+"Win the order.");
            }else {
            	System.out.println("Users."+driverId+"Failed to get the order"); }}finally {
        	rLock.unlock();
        }
        return null; }}Copy the code

The client running the Redlock algorithm performs the following steps to acquire the lock:

  1. Gets the current time in milliseconds.
  2. Execute to N Redis nodes in sequenceAcquiring a lockIn the operation. This fetch operation is the same as the previous one based on a single Redis nodeAcquiring a lockThe process is the same, including value driverId and expiration time (e.gPX 30000, i.e. the duration of the lock). To ensure that the algorithm can continue to run if a Redis node is unavailable, thisAcquiring a lockThe operation also has a time out that is much smaller than the duration of the lock (on the order of tens of milliseconds).
  3. When a client fails to acquire a lock from a Redis node, it should immediately try the next Redis node. Failure here should include any type of failure, such as the Redis node being unavailable, or the lock on the Redis node being held by another client
  4. Calculate how long it took to acquire the lock by subtracting the time recorded in step 1 from the current time. If the client successfully obtains the lock from most of the Redis nodes (>= N/2+1), for example, if three of the five machines are successfully locked, the lock is successfully added by default, and the total time consumed in obtaining the lock does not exceed the lock validity time, then the client considers the lock is successfully acquired. Otherwise, the lock fails to be obtained
  5. If the lock is acquired successfully, the lock duration should be recalculated, which is equal to the original lock duration minus the lock duration calculated in step 3.
  6. If the lock acquisition ultimately fails (perhaps because the number of Redis nodes acquiring the lock is less than N/2+1, or the entire lock acquisition process takes longer than the initial validity of the lock), the client should immediately initiate lock release for all Redis nodes (the Redis Lua script described earlier).

The process described above is to acquire the lock, but the process of releasing the lock is relatively simple: the client initiates the lock release operation to all Redis nodes, regardless of whether they were successful or not at the time of acquiring the lock.

conclusion

The specific type of distributed lock to use depends on the business of the company. RedLock can be used to implement distributed lock for heavy traffic, and Redisson can be used for small traffic. Zookeeper can implement distributed lock later. If you have any questions or questions about the content of this article, you can leave a message. The small farmers will reply as soon as they see it. Thank you