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依赖


    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.4.3
         
    
    com.example
    redislock
    0.0.1-SNAPSHOT
    screw
    Demo project for Spring Boot
    
        1.8
    
    

        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
            org.projectlombok
            lombok
            1.18.12
        

        
            redis.clients
            jedis
            3.3.0
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

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

你可能感兴趣的:(redis实现分布式锁)