Why do we need distributed locks

In the era of traditional single application, in order to solve the problem of inconsistent data when accessing shared resources under concurrent conditions, traditional enterprise-class Java applications usually use Synchronized or concurrent tool classes provided by JDK to control concurrent access.

However, today’s enterprise applications are mostly deployed in clusters and distributions. Services are divided into multiple subsystems and deployed independently. Usually, each system deploys multiple instances. While improving performance and efficiency, it also brings some problems. The traditional locking method can not solve the problem of concurrent access. Synchronized or Lock RetreenLock controls access to shared resources by concurrent threads only applies to single applications or single deployed service instances. However, such clustered or distributed service instances are generally deployed on different machines, leading to their own independent hosts and JDK. In this case, the traditional locking mechanism can no longer solve the problem of accessing shared resources across JVM processes, so distributed locking is needed.

As mentioned above, when multiple clients make requests, they will be forwarded to the corresponding service. Assuming that they operate on the member variable A under different instances of the same service, A has A separate memory space on each instance. Each request will modify the value of A in its own instance, but will not be synchronized to other instances.

A distributed lock

Distributed locking is not a middleware or component, but a mechanism and a solution. In a distributed deployment environment, multiple clients or service processes are mutually exclusive to access shared resources through a lock mechanism to avoid concurrency security problems.

The common implementation of distributed locks includes optimistic and pessimistic locks based on database level, atomic operations based on Redis, mutually exclusive locks based on Zookeeper, and distributed locks based on Redisson.

Implementation of distributed lock

1, Redis

Redis does not provide a direct distributed lock component, but implements it indirectly with the help of Redis atomic operations. Redis is able to achieve distributed locks mainly because of the single-thread mechanism adopted by Redis. No matter how many requests are initiated by the external system, only one thread can perform certain operations at the same time, and other threads enter the waiting queue.

Based on the redis to realize distributed mainly use the lock is SET KEY VALUE [EX seconds] [PX milliseconds] [NX | XX] command

  • [EX seconds] : setkeyExpiration time, in seconds
  • [PX milliseconds] : setkeyIn milliseconds
  • XX] [NX | : NX:keyThere is no thevalue, returns OK on success, nil on failure:keyExistential settingvalue, returns OK on success, nil on failure

code

The simulation is goods inventory reduction operation

@Autowired
StringRedisTemplate redisTemplate;

/** ** simulate merchandise inventory reduction operation *@param productCode
     */
@PutMapping("reduce/{product-code}")
public void reduce(@PathVariable(value = "product-code") String productCode) {
    String lockKey = "lock:" + productCode;
    try {
        Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, productCode, 10, TimeUnit.MILLISECONDS);

        if (isSuccess) {
            // The inventory reduction operation was performed successfully
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(productCode));
            if (count > 0) { redisTemplate.opsForValue().increment(productCode); }}}finally {
        / / releases the lockredisTemplate.delete(lockKey); }}Copy the code

Although the above code can achieve distributed lock, but there are still many problems. For example,

Lock misunderstanding except

If two threads, thread 1 and thread 2, operate on a shared resource at the same time, thread 1 acquires the lock and sets the timeout period to 10s. When the service process is executed, thread 1 releases the lock and thread 2 acquires the lock. Thread 1 and thread 2 execute concurrently. When thread 1 completes execution and deletes the lock, thread 2 does not complete execution and deletes the lock held by thread 2.

2, Redisson

Redisson is a comprehensive middleware that implements Java resident memory data grid on the basis of Redis. The reason why Redisson provides distributed lock is that there are certain defects in the distributed implementation of atomic operation based on Redis, and Redisson makes up these defects well.

The Redisson integration is primarily based on Spring Boot

  1. pom.xml

    Redisson’s dependencies need to be introduced

    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
    <dependency>
      <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.14.0</version>
      </dependency>
    </dependencies>
    Copy the code
  2. The configuration file

Server: port: 9000 Spring: redis: host: 47.102.218.26 password: root Port: 6379 cluster: failed-attempts: 3 master-connection-pool-size: 64 nodes: '' read-mode: SLAVE retry-attempts: 3 retry-interval: 1500 scan-interval: 1000 slave-connection-pool-size: 64 pool: conn-timeout: 3000 max-active: 8 max-idle: 16 max-wait: 3000 min-idle: 8 size: 10 so-timeout: 3000 sentinel: fail-max: '' master: business-master master-onlyWrite: true nodes: '' single: Address: 192.168.60.23:6379Copy the code

configuration

   @Data
   @ConfigurationProperties(prefix = "redisson")
   public class RedissonProperties {
   
       private String address;
   
       private int database = 0;
   
       private String password;
   
       private int timeout;
   
       /**
        * 池配置
        */
       private RedisPoolProperties pool;
   
       /**
        * 单机
        */
       private RedisSingleProperties single;
   
       /**
        * 哨兵
        */
       private RedissonSentinelProperties sentinel;
   
       /**
        * 主从
        */
       private RedissonMasterSlaveProperties masterSlave;
   
       /**
        * 集群
        */
       private RedissonClusterProperties cluster;
   
   
   }
   

   /**
    * @author xiangjin.kong
    * @date 2021/3/25 10:32
    */
   @Configuration
   @ConditionalOnClass({Redisson.class, RedisOperations.class})
   @EnableAutoConfiguration
   @EnableConfigurationProperties(RedissonProperties.class)
   public class RedissonAutoConfiguration {
   
       @Autowired
       RedissonProperties redisProperties;
       /**
        * 单机
        * @return
        */
       /**
        * 单机模式 redisson 客户端
        */
   
       @Bean
       @ConditionalOnProperty(name = "spring.redis.mode", havingValue = "single")
       RedissonClient redissonSingle() {
           Config config = new Config();
           String node = redisProperties.getSingle().getAddress();
           node = node.startsWith("redis://") ? node : "redis://" + node;
           SingleServerConfig serverConfig = config.useSingleServer()
                   .setAddress(node)
                   .setTimeout(redisProperties.getPool().getConnTimeout())
                   .setConnectionPoolSize(redisProperties.getPool().getSize())
                   .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
           if (StringUtils.isNotBlank(redisProperties.getPassword())) {
               serverConfig.setPassword(redisProperties.getPassword());
           }
           return Redisson.create(config);
       }
   
   
       /**
        * 集群模式的 redisson 客户端
        *
        * @return
        */
       @Bean
       @ConditionalOnProperty(name = "spring.redis.mode", havingValue = "cluster")
       RedissonClient redissonCluster() {
           System.out.println("cluster redisProperties:" + redisProperties.getCluster());
   
           Config config = new Config();
           String[] nodes = redisProperties.getCluster().getNodes().split(",");
           List<String> newNodes = new ArrayList(nodes.length);
           Arrays.stream(nodes).forEach((index) -> newNodes.add(
                   index.startsWith("redis://") ? index : "redis://" + index));
   
           ClusterServersConfig serverConfig = config.useClusterServers()
                   .addNodeAddress(newNodes.toArray(new String[0]))
                   .setScanInterval(
                           redisProperties.getCluster().getScanInterval())
                   .setIdleConnectionTimeout(
                           redisProperties.getPool().getSoTimeout())
                   .setConnectTimeout(
                           redisProperties.getPool().getConnTimeout())
                   .setRetryAttempts(
                           redisProperties.getCluster().getRetryAttempts())
                   .setRetryInterval(
                           redisProperties.getCluster().getRetryInterval())
                   .setMasterConnectionPoolSize(redisProperties.getCluster()
                           .getMasterConnectionPoolSize())
                   .setSlaveConnectionPoolSize(redisProperties.getCluster()
                           .getSlaveConnectionPoolSize())
                   .setTimeout(redisProperties.getTimeout());
           if (StringUtils.isNotBlank(redisProperties.getPassword())) {
               serverConfig.setPassword(redisProperties.getPassword());
           }
           return Redisson.create(config);
       }
   
       /**
        * 哨兵模式 redisson 客户端
        * @return
        */
   
       @Bean
       @ConditionalOnProperty(name = "spring.redis.mode", havingValue = "sentinel")
       RedissonClient redissonSentinel() {
           System.out.println("sentinel redisProperties:" + redisProperties.getSentinel());
           Config config = new Config();
           String[] nodes = redisProperties.getSentinel().getNodes().split(",");
           List<String> newNodes = new ArrayList(nodes.length);
           Arrays.stream(nodes).forEach((index) -> newNodes.add(
                   index.startsWith("redis://") ? index : "redis://" + index));
   
           SentinelServersConfig serverConfig = config.useSentinelServers()
                   .addSentinelAddress(newNodes.toArray(new String[0]))
                   .setMasterName(redisProperties.getSentinel().getMaster())
                   .setReadMode(ReadMode.SLAVE)
                   .setTimeout(redisProperties.getTimeout())
                   .setMasterConnectionPoolSize(redisProperties.getPool().getSize())
                   .setSlaveConnectionPoolSize(redisProperties.getPool().getSize());
   
           if (StringUtils.isNotBlank(redisProperties.getPassword())) {
               serverConfig.setPassword(redisProperties.getPassword());
           }
   
           return Redisson.create(config);
       }
   }
   
   @Data
   public class RedisPoolProperties {
       private int maxIdle;
   
       private int minIdle;
   
       private int maxActive;
   
       private int maxWait;
   
       private int connTimeout;
   
       private int soTimeout;
   
       /**
        * 池大小
        */
       private  int size;
   
   }
   
   
   @Data
   public class RedisSingleProperties {
   
       private String address;
   }
   
   
   @Data
   public class RedissonClusterProperties {
   
       /**
        * 集群状态扫描间隔时间,单位是毫秒
        */
       private int scanInterval;
   
       /**
        * 集群节点
        */
       private String nodes;
   
       /**
        * 默认值: SLAVE(只在从服务节点里读取)设置读取操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里读取。
        * MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取
        */
       private String readMode;
       /**
        * (从节点连接池大小) 默认值:64
        */
       private int slaveConnectionPoolSize;
       /**
        * 主节点连接池大小)默认值:64
        */
       private int masterConnectionPoolSize;
   
       /**
        * (命令失败重试次数) 默认值:3
        */
       private int retryAttempts;
   
       /**
        * 命令重试发送时间间隔,单位:毫秒 默认值:1500
        */
       private int retryInterval;
   
       /**
        * 执行失败最大次数默认值:3
        */
       private int failedAttempts;
   }
   
   
   @Data
   public class RedissonMasterSlaveProperties {
   
       private String masterAddress;
       private String slaveAddress;
   
   }
   
   
   @Data
   public class RedissonSentinelProperties {
   
       /**
        * 哨兵master 名称
        */
       private String master;
   
       /**
        * 哨兵节点
        */
       private String nodes;
   
       /**
        * 哨兵配置
        */
       private boolean masterOnlyWrite;
   
       /**
        *
        */
       private int failMax;
   
   }
 }
Copy the code