Redis implements the principle of distributed locks and the Redisson framework implements distributed locks, the most detailed explanation on the entire network

Disclaimer: Most of my space talks about the principle and implementation of distributed locks. If you want to directly use the Redisson framework to implement distributed locks, you can directly turn to the end

Regarding distributed locks, it is suitable for microservice clusters with a particularly large amount of concurrency, and can achieve resource acquisition synchronously

In fact, I have not experienced the practice of distributed locks in real projects. The following is used as a reference for my study, but as far as I know, the specific implementation redisof companies that generally use distributed locks is also the same as the framework I described below redisson, as long as it is not like Taobao and Jingdong. Projects with particularly high concurrency are basically applicable. If I have the opportunity to use distributed locks in the future, I will update this article.

I will analyze the principle, code, and framework of distributed locks one by one. The code part of this article is for reference only

The knowledge points that need to be used (Must be able to use

1. JMeter is used for stress testing: jmeter installation and use, full graphic explanation

2. Nginx is used for load balancing (multiple services): Windows installs Nginx and configures load balancing

3. Redis is used to store Mock data: Install Redis on Windows to double-click to start

4. Start two projects with the same service but different ports: idea implements two projects with the same service but different ports at the same time, full diagram

Five, redis tool class: Redis tool class (redisTemplate) and usage of redisTemplate

Graphical distributed lock (spike scenario)

Diagram why distributed locks are needed (I forgot to draw the data operation that returns to the user, but it doesn’t affect it, knowing that it needs to return the message to the user in the end)

Scenario 1. A single service does not require distributed locks (to save trouble, I only draw two users)

1. Without locking, it will cause dirty reading of data

insert image description here

2. Use synchronized to implement lock processing

insert image description here

Scenario 2. Cluster (distributed lock implementation) (multiple projects with the same service but different ports, after load balancing to different services) (to save trouble, I only draw two services)

1. Only locking at the service level will not achieve the effect

insert image description here

2. Need to use the setnx request of redis

principle

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"

As shown in the above code setnx, the request means first to find out whether there is mykeythis key, if not, put one Helloand valuereturn 1(that is, the table name is inserted successfully), if mykeyit already exists, keythen return 0(indicates that the insertion failed)

Since redis is single-threaded, all incoming requests will be queued in the redis method. The following user 1 request is faster than user 2's request to access redis to achieve distributed locks.

  • User 1 uses this method in service 1 setnxto insert a keyreturn success, and indicates that user 1 gets the distributed lock

  • setnxAt the same time, user 2 uses this method to insert the same one in service 2. keyAt this time, it prompts that the insertion failed, and the return fails, and then service 2 spins to get the lock or directly returns to the user to try again later (directly returning to the user is not friendly)

  • After that, as soon as the user finishes processing the request, modify the redis data, and finally keydelete it, so that other services can get the lock, and then use setnxthe command to try to add the lock, and the data operated after getting the redislock can be executed without the lock. One step (isn't there three points here, that is to execute the content after the second point)

insert image description here

Code Analysis Distributed Lock Principle (Second Kill Scenario)

Initialize Redis data (used to store products used by seckill)

1. Use redis to configure a mock data for the seckill service. Here I set the key to goods and the value to 50. Every time the data is used up, the goods data needs to be changed to 50 again (tool: Another Redis Desktop Manager)

insert image description here

Scenario 1. Realization of seckill under single service (locking) (distributed lock is not required) (corresponding to diagram scenario 1)

SpringBootAs the basic framework for realizing distribution, when only running a single service, one is used jvmto control the code. Let's assume that a seckill project is implemented:

1. Write an interface for testing seckill

@RestController
public class DemoController {
    
    
    @Autowired
    private RedisUtil redisUtil;

    @RequestMapping("/demo")
    public String demo(){
    
    
        try {
    
    
//            取goods的值
            Integer goods = (Integer) redisUtil.get("goods");
            if (goods <= 0){
    
    
                System.err.println(Thread.currentThread().getName() +
                        Thread.currentThread().getId() + "商品已经取完");
                return "商品已经取完";
            }
//            这里模拟一下延时 0.1秒 (因为数据量太少,这样可以很直观的看出加锁和不加锁的区别)
            Thread.sleep(100);
//            用户拿到了这个商品,所以这个商品需要自减一
//            使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
            System.err.println(Thread.currentThread().getName() +
                    Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
//            由于用户已经取到了商品,所以redis中的数据也需要更新
            redisUtil.set("goods",goods);
        } catch (InterruptedException e) {
    
    
            return "错误";
        }
        return "你已经成功获取商品";
    }
}

2. Configure JMeter

In an instant, 20 users rushed to grab goodsthis product

insert image description here

insert image description here

3. Test

It stands to reason that each user only grabs one item, so there will be 30 items left. Let’s take a look at the following situation and find that all users get the 50th data, so subtract 1, and then they are all in redis The stored value is 49, which causes dirty reading of the data, and it is very simple to modify the code here

Console:

insert image description here

redis:

insert image description here

4. Solve the dirty read problem (locking) under a single service, remember to modify the goods data in redis to 50, or request 20 times

You only need to modify the code and add the lock, so that the locking based on the jvm level is realized

@RestController
public class DemoController {
    
    
    @Autowired
    private RedisUtil redisUtil;

    @RequestMapping("/demo")
    public String demo(){
    
    
        try {
    
    
//            加锁
            synchronized (this){
    
    

//            取goods的值
                Integer goods = (Integer) redisUtil.get("goods");
                if (goods <= 0){
    
    
                    System.err.println(Thread.currentThread().getName() +
                            Thread.currentThread().getId() + "商品已经取完");
                    return "商品已经取完";
                }
//            这里模拟一下延时 0.1秒 (因为数据量太少,这样可以很直观的看出加锁和不加锁的区别)
                Thread.sleep(100);
//            用户拿到了这个商品,所以这个商品需要自减一
//            使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
                System.err.println(Thread.currentThread().getName() +
                        Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
//            由于用户已经取到了商品,所以redis中的数据也需要更新
                redisUtil.set("goods",goods);
            }
        } catch (InterruptedException e) {
    
    
            return "错误";
        }
        return "你已经成功获取商品";
    }
}

5. Test

Console:

insert image description here

redis:

insert image description here

Scenario 2. Realization of seckill in the cluster (realization of distributed locks) (corresponding to scenario 2 in the diagram)

1. Start Nginx, configure load balancing, and start two services (8080 port, 9090 port), 测试 场景一 4 的代码what kind of error will occur, remember to modify the goods data in redis to 50 (I don’t know how to deal with the following operations, you can see what you need to use to the knowledge points)

Nginx configuration (nginx.conf), and start:

worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
	# 负载均衡配置访问路径 serverList名字随便取
	upstream serverList{
	   # 这个是tomcat的访问路径
	   server localhost:8080;
	   server localhost:9090;
	}
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
			proxy_pass http://serverList;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

Reconfigure JMeter:

Let the two services handle 25 requests each

insert image description here

insert image description here

Both projects start, and the console is emptied:

insert image description here

2. Test

There are many two items in the back that took out the same product, so I won’t open the display here, just know that there is a problem

Port 8080:

insert image description here

Port 9090:

insert image description here

redis: It's not 0 as we imagined, so it won't work to use the lock under a single jvm

insert image description here

3. Modify the code to realize adding redis distributed lock

@RestController
public class DemoController {
    
    
    @Autowired
    private RedisUtil redisUtil;

    @RequestMapping("/demo")
    public String demo(){
    
    
//        设置自旋超时时间
        long timeoutAt = System.currentTimeMillis();
        try {
    
    
//            自旋
            while (true){
    
    
                long now = System.currentTimeMillis();
//                5秒超时,退出
                if (now - timeoutAt > 5000){
    
    
                    System.out.println("连接超时请重试");
                    return "连接超时请重试";
                }
//               加锁
                boolean lock = redisUtil.setnx("lock", "先随便输入什么东西都可以啦~");
                if (!lock){
    
    
                    continue;
                }

//               取goods的值
                Integer goods = (Integer) redisUtil.get("goods");
                if (goods <= 0){
    
    
                    System.err.println(Thread.currentThread().getName() +
                            Thread.currentThread().getId() + "商品已经取完");
                    return "商品已经取完";
                }
//               这里加了锁就不需要
//                Thread.sleep(100);

//               用户拿到了这个商品,所以这个商品需要自减一
//               使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
                System.err.println(Thread.currentThread().getName() +
                        Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
//               由于用户已经取到了商品,所以redis中的数据也需要更新
                redisUtil.set("goods",goods);
//            解锁
                redisUtil.delete("lock");
                return "你已经成功获取商品";
            }
        } catch (Exception e) {
    
    
            return "错误";
        }
    }
}

4. Test, remember to modify the goods data in redis to 50

Port 8080:

insert image description here

Port 9090:

insert image description here

redis:

insert image description here

The basis of a redis distributed lock has been completed, but the distributed lock implemented in this way has many problems, which are not enough to see under high concurrency conditions. The following is an advanced teaching (the following solutions have been tested)

Problem 1. If an exception occurs before unlocking, resulting in the inability to unlock, then other services cannot access redis

insert image description here

Solution: Modify the unlocked code to the finally code block

@RestController
public class DemoController {
    
    
    @Autowired
    private RedisUtil redisUtil;

    @RequestMapping("/demo")
    public String demo(){
    
    
//        设置自旋超时时间
        long timeoutAt = System.currentTimeMillis();
//            自旋
        while (true){
    
    
                long now = System.currentTimeMillis();
//                5秒超时,退出
                if (now - timeoutAt > 5000){
    
    
                    System.out.println("连接超时请重试");
                    return "连接超时请重试";
                }
//               加锁
                boolean lock = redisUtil.setnx("lock", "先随便输入什么东西都可以啦~");
                if (!lock){
    
    
                    continue;
                }

            try {
    
    
//               取goods的值
                Integer goods = (Integer) redisUtil.get("goods");
                if (goods <= 0){
    
    
                    System.err.println(Thread.currentThread().getName() +
                            Thread.currentThread().getId() + "商品已经取完");
                    return "商品已经取完";
                }

//               用户拿到了这个商品,所以这个商品需要自减一
//               使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
                System.err.println(Thread.currentThread().getName() +
                        Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
//               由于用户已经取到了商品,所以redis中的数据也需要更新
                redisUtil.set("goods",goods);
                return "你已经成功获取商品";
            } catch (Exception e) {
    
    
                e.printStackTrace();
                return "请重试";
            } finally {
    
    
//            解锁
                redisUtil.delete("lock");
            }
        }
    }
}

Question 2: If the service hangs up directly before it is unlocked, no other service can access redis

Solution: Set the cache time (10s)

@RestController
public class DemoController {
    
    
    @Autowired
    private RedisUtil redisUtil;

    @RequestMapping("/demo")
    public String demo(){
    
    
//        设置自旋超时时间
        long timeoutAt = System.currentTimeMillis();
//            自旋
        while (true){
    
    
                long now = System.currentTimeMillis();
//                5秒超时,退出
                if (now - timeoutAt > 5000){
    
    
                    System.out.println("连接超时请重试");
                    return "连接超时请重试";
                }
//               加锁,并设置缓存时间 10秒
                boolean lock = redisUtil.setnx("lock", "先随便输入什么东西都可以啦~",10, TimeUnit.SECONDS);
                if (!lock){
    
    
                    continue;
                }

            try {
    
    
//               取goods的值
                Integer goods = (Integer) redisUtil.get("goods");
                if (goods <= 0){
    
    
                    System.err.println(Thread.currentThread().getName() +
                            Thread.currentThread().getId() + "商品已经取完");
                    return "商品已经取完";
                }

//               用户拿到了这个商品,所以这个商品需要自减一
//               使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
                System.err.println(Thread.currentThread().getName() +
                        Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
//               由于用户已经取到了商品,所以redis中的数据也需要更新
                redisUtil.set("goods",goods);
                return "你已经成功获取商品";
            } catch (Exception e) {
    
    
                e.printStackTrace();
                return "请重试";
            } finally {
    
    
//            解锁
                redisUtil.delete("lock");
            }
        }
    }
}

Question 3. If the first service runs for 11 seconds, but after 10 seconds, then the lock time is up, then the second service can get the lock and access redis, so that during the second service, the first After the service is completed, the first service will release the lock of the second service, which leads to the failure of the lock

Solution 1: Set a unique identifier (uuid), and judge whether it is the lock set by the request when deleting.but, there is a problem here, that is, after 10 seconds, although the first service cannot delete the lock other than itself, but other services get the lock for redis operation, if the first service modifies it in the second service Modify redis after redis data, then there is a problem with the third service getting redis data, which will also cause lock failure

@RestController
public class DemoController {
    
    
    @Autowired
    private RedisUtil redisUtil;

    @RequestMapping("/demo")
    public String demo(){
    
    
//        设置自旋超时时间
        long timeoutAt = System.currentTimeMillis();
//        设置唯一标识
        String uuid = UUID.randomUUID().toString();
//            自旋
        while (true){
    
    
                long now = System.currentTimeMillis();
//                5秒超时,退出
                if (now - timeoutAt > 5000){
    
    
                    System.out.println("连接超时请重试");
                    return "连接超时请重试";
                }
//               加锁,设置超时时间和唯一标识
                boolean lock = redisUtil.setnx("lock", uuid,10, TimeUnit.SECONDS);
                if (!lock){
    
    
                    continue;
                }

            try {
    
    
//               取goods的值
                Integer goods = (Integer) redisUtil.get("goods");
                if (goods <= 0){
    
    
                    System.err.println(Thread.currentThread().getName() +
                            Thread.currentThread().getId() + "商品已经取完");
                    return "商品已经取完";
                }

//               用户拿到了这个商品,所以这个商品需要自减一
//               使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
                System.err.println(Thread.currentThread().getName() +
                        Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
//               由于用户已经取到了商品,所以redis中的数据也需要更新
                redisUtil.set("goods",goods);
                return "你已经成功获取商品";
            } catch (Exception e) {
    
    
                e.printStackTrace();
                return "请重试";
            } finally {
    
    
                if (uuid.equals(redisUtil.get("lock"))){
    
    
//            解锁
                    redisUtil.delete("lock");
                }
            }
        }
    }
}

Solution 2: Set up heartbeat detection, you can go to see this Redis distributed lock how to solve the lock timeout problem?

Question 4. What to do if the serial is too slow

Solution: Split: Use different locks and different keywords. For example, goods is 50 items, which can be split into: goods_1:10, goods_2:10, goods_3:10, goods_4:10, goods_5:10

Question 5. When using a Redis cluster, the master node hangs up, but the child node just does not synchronize to the key just uploaded, causing the lock to fail

Solution: Use zookeeper to implement distributed locks

Etc., etc. . . . . .

Use the Redisson framework to implement distributed locks

Schematic of Redisson

insert image description here

Add jar package

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.20.0</version>
</dependency>

configuration beans

@Configuration
public class RedissonConfig {
    
    

    @Bean
    public RedissonClient redisson(){
    
    
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0).setPassword("123456");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

Test (don't look at the lack of code, in fact, this is guaranteed after years of actual combat)

@RestController
public class DemoController {
    
    
    @Autowired
    private RedisUtil redisUtil;
    @Resource
    private Redisson redisson;

    @RequestMapping("/test")
    public String Test(){
    
    
        RLock lock = redisson.getLock("lock");
//        设置超时时间30秒
        lock.lock(30,TimeUnit.SECONDS);
        try {
    
    
//               取goods的值
            Integer goods = (Integer) redisUtil.get("goods");
            if (goods <= 0){
    
    
                System.err.println(Thread.currentThread().getName() +
                        Thread.currentThread().getId() + "商品已经取完");
                return "商品已经取完";
            }

//               用户拿到了这个商品,所以这个商品需要自减一
//               使用System.err输出一下数据,这样显示控制台输出是红色比较好观察
            System.err.println(Thread.currentThread().getName() +
                    Thread.currentThread().getId() + "取出了第" + goods-- + "商品");
//               由于用户已经取到了商品,所以redis中的数据也需要更新
            redisUtil.set("goods",goods);
            return "你已经成功获取商品";
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return "请重试";
        }finally {
    
    
            lock.unlock();
        }

    }
}

Port 8080:

insert image description here

Port 9090:

insert image description here

redis:

insert image description here

Guess you like

Origin blog.csdn.net/qq_57581439/article/details/130047205