redis实现分布式锁

1. 环境

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
spring.redis.host=127.0.0.1
spring.redis.port=6379

测试redis是否链接成功

我在redis里面有1000块钱

127.0.0.1:6379> keys *
1) "money:woms"
127.0.0.1:6379> get money:woms
"1000"
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("name")
    public Object name() {



        String womsMoney = stringRedisTemplate.opsForValue().get("money:woms");


        return womsMoney;


    }

}
$ curl localhost:8080/name -s|jq
1000

为什么要用分布式锁

当多个线程同时对value进行操作的话,就会有问题

模拟10个并发并发请求,每个请求的目的是扣除我的100快,这样1000块钱被10个请求执行,应该最后是0元,但是结果呢?

刚开始,我有1000

127.0.0.1:6379> set money:woms 1000
OK

10个线程同时消费,每个线程消费100

import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("name")
    public Object name() throws Exception {

        for(int i=0; i<10; i++) {
            Thread thread = new Thread(() -> {

                    Integer womsMoneyInteger = Integer.parseInt(stringRedisTemplate.opsForValue().get("money:woms"));
                    stringRedisTemplate.opsForValue().set("money:woms", String.valueOf(womsMoneyInteger - 100));

            });
            thread.start();
        }

        TimeUnit.SECONDS.sleep(2);
        String now_money = stringRedisTemplate.opsForValue().get("money:woms");
        return now_money;
    }

}

最后看结果,发现我还有900

127.0.0.1:6379> get money:woms
"900"

为什么会出现这种情况呢?就是第一个用户get的时候,get的是1000,第二个用户get的时候,还是1000,第一个用户-100,剩900,第二个用户在之前的查询的1000的基础上-100,相当于应该-200,这里只-100

redis实现分布式锁

import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.StringRedisTemplate;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class RedisLock {

    public boolean addLock(StringRedisTemplate redisTemplate, String key, String value, long timeout, int expire) {
        long nano = System.nanoTime();// 开始时间

        while ((System.nanoTime() - nano) < timeout) {
            if (redisTemplate.opsForValue().setIfAbsent(key, value, expire, TimeUnit.SECONDS)) {
                log.info(Thread.currentThread().getName());
                return true;
            }
        }
        return false;
    }

    public void unlock(StringRedisTemplate stringRedisTemplate, String key, String value) {

        String keyValue = stringRedisTemplate.opsForValue().get(key);
        if (value.equals(keyValue)) {
            log.info(Thread.currentThread().getName());
            stringRedisTemplate.delete(key);
        }

    }
}
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController
@Slf4j
public class TestController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("name")
    public Object name() throws Exception {


        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                String uuid = UUID.randomUUID().toString();
                RedisLock redisLock = new RedisLock();
                if (redisLock.addLock(stringRedisTemplate, "money:woms:lock", uuid, 1000 * 1000 * 1000L, 180)) {
                    log.info(Thread.currentThread().getName());
                    Integer womsMoneyInteger = Integer.parseInt(stringRedisTemplate.opsForValue().get("money:woms"));
                    stringRedisTemplate.opsForValue().set("money:woms", String.valueOf(womsMoneyInteger - 100));
                } 
                redisLock.unlock(stringRedisTemplate, "money:woms:lock", uuid);
            });
            thread.start();
        }

        TimeUnit.SECONDS.sleep(10);
        String now_money = stringRedisTemplate.opsForValue().get("money:woms");
        return now_money;
    }

}

其原理就是使用另一个key作为当前操作的锁,给另一个key设置值,如果设置成功,可以对目标key的值进行操作,操作完成以后把作为锁的key删除。 其他用户由于给目标key设置值的时候,锁key值已近存在,就会在超时时间内一直设置值,如果超时时间内设置成功,则可以进行操作,设置不成功,就不进行操作,其目的就一个就是在同一个时间只能有一个用户对值进行修改。

如果使用lua脚本怎么实现呢?

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController
@Slf4j
public class TestController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private DefaultRedisScript<String> getRedisScript;

    @PostConstruct
    public void init() {
        getRedisScript = new DefaultRedisScript<String>();
        getRedisScript.setResultType(String.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("222.lua")));
    }

    @GetMapping("name")
    public Object name() throws Exception {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                log.info(Thread.currentThread().getName());
                stringRedisTemplate.execute(getRedisScript, Arrays.asList("money:woms"));

            });
            thread.start();
        }

        TimeUnit.SECONDS.sleep(10);
        String now_money = stringRedisTemplate.opsForValue().get("money:woms");
        return now_money;
    }

}
local money = redis.call("get",KEYS[1])
redis.call("set", KEYS[1], money - 100)

lua本身就是原子性,可以不用任何锁,直接操作

Last updated

Was this helpful?