redis实现分布式锁

分布式锁介绍

在java的开发中,我们一般在需要并发访问的资源上使用加锁Lock或者synchronized来同步访问,但是只能针对单个jvm内的加锁,当系统需要在多个系统之间访问同一个受保护的资源时,就需要用到分布式锁的机制了,比如在电商网站网站的高并发情况下,大量请求需要扣减库存,而扣减库存的操作需要受保护的。常见的实现分布式锁的方案由通过zookeeper的临时有序节点,数据库的自增主键和今天我们要讲的redis实现。

redis实现分布式锁原理

在redis中实现分布式锁加锁是set lock_key random_value nx px millisecond来实现加锁操作,nx代表当lock_key不存在设置成功,否则表示设置失败,px 代表键的过期时间,防止由于线程长时间持有锁不释放而导致的死锁。random-value表示随机值,每个客户端的都不一样。
解锁操作需要保证原子性,先获取到键key对应的值,判断是否和当前节点设置的值一致,如果不一致则不能解锁(其他客户端不能解锁当前客户端锁定的值),否则可以解锁。因此需要保证原子性。
可以通过lua脚本来执行。

// redis.call中的第一个参数表示要执行的命令,Lua脚本中的下表从1开始,KEYS是要获取的key, ARGV传入的参数值
if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0 
end
redis单机版的分布式锁实现

今天我们讲的是通过单机版实现的分布式锁,即多个线程并发获取同一把锁。

1. 创建springboot工程,引入redis依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>redislock</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>screw</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.3.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
2. 在application.properties中配置redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=13
3. 创建redis配置类
@Configuration
public class RedisConfiguration {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private Integer port;

    public JedisPoolConfig jedisPoolConfig() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 此处的配置可以写在application.properties中,也可以不加
       // jedisPoolConfig.setMaxIdle(100);
       // jedisPoolConfig.setMaxWaitMillis(200);
       // jedisPoolConfig.setMaxTotal(100);
       // jedisPoolConfig.setMinIdle(50);
        return jedisPoolConfig;
    }


    @Bean
    public JedisPool jedisPool() {
        JedisPoolConfig jedisPoolConfig = jedisPoolConfig();
        return new JedisPool(jedisPoolConfig,host,port);
    }
}
4. 创建RedisLock类
@Service
@Slf4j
public class RedisLock {

    private String lock_key = "redis_lock"; // 锁键

    protected long internalLockReleaseTime = 30000; //锁过期时间

    private long timeout = 999999; // 获取锁的超时时间

    //SET的参数
    SetParams params = SetParams.setParams().nx().px(internalLockReleaseTime);

    @Autowired
    private JedisPool jedisPool;

    /**
     * 加锁
     * @param id
     * @return
     */
    public boolean lock(String id) {
        Jedis jedis = jedisPool.getResource();
        Long start = System.currentTimeMillis();
        try{
            for(;;) {
                //SET 命令返回OK,则证明获取锁成功
                String lock = jedis.set(lock_key,id,params);
                if("OK".equals(lock)) {
                    System.out.println("加锁成功: " + id);
                    return true;
                }
                System.out.println("加锁失败等待: " + id);
                //否则循环等待,在timeout时间内仍未获取到锁,则获取失败
                long l = System.currentTimeMillis() - start;
                if(l >= timeout) {
                    return false;
                }
                try{
                    Thread.sleep(100);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            jedis.close();
        }
    }

    /**
     * 解锁
     * @param id
     * @return
     */
    public boolean unlock(String id) {
        Jedis jedis = jedisPool.getResource();
        String script =
                "if redis.call('get', KEYS[1]) == ARGV[1] then" +
                        " return redis.call('del',KEYS[1]) " +
                        "else" +
                        " return 0 " +
                        "end";
        try{
            Object result = jedis.eval(script, Collections.singletonList(lock_key),Collections.singletonList(id));
            if("1".equals(result.toString())) {
                System.out.println("解锁成功: " + id);
                return true;
            }
            System.out.println("解锁失败: " + id);
            return false;
        }finally {
            jedis.close();
        }
    }
}
5. 创建访问控制器类

唯一性id可以使用UUID也可以使用Snowflake雪花算法来生成。
创建1000个线程来执行并发访问。

@Controller
@Slf4j
public class IndexController {

    @Autowired
    private RedisLock redisLock;

    @Autowired
    private SnowFlakeIdWorker snowFlakeIdWorker;

    int count = 0;


    @RequestMapping("/index")
    @ResponseBody
    public String index() throws InterruptedException {
        int clientcount = 1000;
        CountDownLatch countDownLatch = new CountDownLatch(clientcount);

        ExecutorService executorService = Executors.newFixedThreadPool(clientcount);
        long start = System.currentTimeMillis();
        for(int i = 0;i < clientcount;i++) {
            executorService.execute(() -> {
                //通过UUID获取唯一的ID字符串
                String id = UUID.randomUUID().toString();
                try{
                    redisLock.lock(id);
                    count++;
                }finally {
                    redisLock.unlock(id);
                }
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        long end = System.currentTimeMillis();

        log.info("执行线程数:{},总耗时:{},count数为:{}",clientcount,end-start,count);
        return "success";
    }
}
6. 浏览器访问

http://localhost:8080/index 并验证最终的count的是1000次

image.png

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 背景 在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于...
    零点145阅读 1,488评论 0 0
  • 背景在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于D...
    Java架构师Carl阅读 1,885评论 0 1
  • 背景 在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于...
    Java架构_师阅读 3,754评论 0 0
  • (转自:https://blog.csdn.net/ugg/article/details/41894947/ ...
    WY长河阅读 3,411评论 0 0
  • 推荐指数: 6.0 书籍主旨关键词:特权、焦点、注意力、语言联想、情景联想 观点: 1.统计学现在叫数据分析,社会...
    Jenaral阅读 11,019评论 0 5