十、redis分布式锁:基础篇

一、基础知识

1、锁的种类

  • 单机版同一个JVM虚拟机内,synchronized 或者 Lock 接口。
  • 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

2、分布式锁需要具备的条件和刚需

  • 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有
  • 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
  • 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
  • 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放
  • 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁

3、分布式锁

多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。

 

 

二、案例v1.1

1、使用场景

多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。

2、基础代码

复制代码
@RestController
public class DemoController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    public static final String KEY = "MY_REDIS";

    @GetMapping("/test")
    public void test() {
        String result = stringRedisTemplate.opsForValue().get(KEY);
        //  执行业务代码
        //  ...
    }
}
复制代码

 

三、v1.2

1、v1.1问题

没有加锁,不具备独占性。可以使用synchronized或Lock来实现,建议使用Lock,可以自由设定超时时间,sync只能死等。

2、升级

复制代码
@RestController
public class DemoController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final Lock lock = new ReentrantLock();

    @Autowired
    public static final String KEY = "MY_REDIS";

    @GetMapping("/test")
    public void test() {       
        if (lock.tryLock())
        {
            try
            {
                //  执行业务代码
                //  ...
            }finally {
                lock.unlock();
            }
        }
    }
}
复制代码

 

四、v1.3

1、v1.2问题

只能在单机加锁生效,无法使用在分布式系统上。需要一个让所有进程都能访问到的锁来实现。

2、升级

复制代码
public class DemoController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/test")
    public void test() {
        String key = "MY_REDIS";
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

        Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);

        if (!flagLock) {
            System.out.println("抢夺锁失败");
        }

        //  执行业务代码
        //  ...
    }
}
复制代码

 

五、v1.4

1、v1.3问题

只考虑了正常流程,如果出异常,可能无法释放锁,必须要在代码层面finally释放锁。

2、升级

复制代码
@RestController
public class DemoController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/test")
    public void test() {
        String key = "MY_REDIS";
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

        try {
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);

            if (!flagLock) {
                System.out.println("抢夺锁失败");
            }

            //  执行业务代码
            //  ...
        } finally {
            stringRedisTemplate.delete(key);
        }
    }
}
复制代码

 

六、v1.5

1、v1.4问题

如果代码没走带finally这步,比如部署了微服务Jar包的机器挂了,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key。

2、升级


  Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
  stringRedisTemplate.expire(key,10L, TimeUnit.SECONDS);

 

七、v1.6

1、v1.5问题

设置key + 过期时间分开了,必须要合并成一行具备原子性。可能代码只走到设置key的这步就挂了。

2、升级

//  同步设置锁和过期时间
Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS);

 

八、v1.7

1、v1.6问题

可能删除了别人的锁。比如:A线程执行业务代码需要15秒,锁的过期时间只有10秒,这时候B在第11秒的时候进来了。当第15秒时A执行完业务代码后,会直接删除B的锁。

2、升级

复制代码
@RestController
public class DemoController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/test")
    public void test() {
        String key = "MY_REDIS";
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

        try {
            //  同步设置锁和过期时间
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS);

            if (!flagLock) {
                System.out.println("抢夺锁失败");
            }

            //  执行业务代码
            //  ...
        } finally {
            //  判断是否当前线程加的锁
            if (stringRedisTemplate.opsForValue().get(key).equals(value)) {
                stringRedisTemplate.delete(key);
            }
        }
    }
}
复制代码

 

九、v1.8

1、v1.7问题

判断加锁与解锁是不是同一个客户端,finally块的判断+del删除操作不是原子性的,Redis调用Lua脚本通过eval命令保证代码执行的原子性。

2、升级

(1)RedisUtils

复制代码
public class RedisUtils
{
    private static JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPool=new JedisPool(jedisPoolConfig,"192.168.111.147",6379);
    }

    public static Jedis getJedis() throws Exception {
        if(null!=jedisPool){
            return jedisPool.getResource();
        }
        throw new Exception("Jedispool was not init");
    }

}
复制代码

(2)升级代码

复制代码
@RestController
public class DemoController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/test")
    public void test() throws Exception {
        String key = "MY_REDIS";
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();

        try {
            //  同步设置锁和过期时间
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS);

            if (!flagLock) {
                System.out.println("抢夺锁失败");
            }

            //  执行业务代码
            //  ...
        } finally {
            Jedis jedis = RedisUtils.getJedis();

            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(key), Collections.singletonList(value));
                if ("1".equals(result.toString())) {
                    System.out.println("------del REDIS_LOCK_KEY success");
                } else {
                    System.out.println("------del REDIS_LOCK_KEY error");
                }
            } finally {
                if (null != jedis) {
                    jedis.close();
                }
            }
        }
    }
}
复制代码

 

十、v1.9

1、v1.8问题

只能基于单个Redis节点实现分布式锁,无法使用在多个redis节点中实现。官方推荐RedLock之Redisson,Redisson中的 “看门狗” 会自动实现锁的续期

2、升级

(1)RedisConfig

复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //  设置key序列号方式String
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //  设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.1.108:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}
复制代码

(2)升级代码

复制代码
@RestController
public class DemoController {
    @Autowired
    private Redisson redisson;

    @GetMapping("/test")
    public void test() throws Exception {
        String key = "MY_REDIS";
        RLock redissonLock = redisson.getLock(key);
        redissonLock.lock();
        try {
            //  执行业务代码
            //  ...
        } finally {
            redissonLock.unlock();
        }
    }
}
复制代码

3、完善

高并发下会报异常

 

 升级

复制代码
@RestController
public class DemoController {
    @Autowired
    private Redisson redisson;

    @GetMapping("/test")
    public void test() throws Exception {
        String key = "MY_REDIS";
        RLock redissonLock = redisson.getLock(key);
        redissonLock.lock();
        try {
            //  执行业务代码
            //  ...
        } finally {
            if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
                redissonLock.unlock();
            }
        }
    }
}
复制代码

 

posted @   幻月hah  阅读(200)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示