首页 > Redis > 正文

Redis应用之分布式锁(set)

佛若2018-09-210人围观

Redis应用之分布式锁(set)

在单机应用的场景下,我们常使用的锁主要是synchronized与Lock;但是在分布式横行的大环境下,显然仅仅这两种锁已经无法满足我们的需求;

需求:秒杀场景下,有若干服务实例,假设有2个,那么分别会有若干请求分别请求这2个服务实例。要求只能有一个请求秒杀成功,本质是秒杀方法在同一时间内只能被同一个线程执行,这就需要使用到分布式锁。

场景分布式锁

  • 基于数据库实现
    • 基于数据库实现分布式锁,主要使用InnoDB下的for update(如使用行级锁,需加唯一索引)
  • 基于Zookeeper实现
    • 在指定节点的目录下,创建一个唯一的瞬时有序节点。可以使用Curator去实现。
  • 基于缓存实现(redis)
    • 主要使用set(setnx用法有缺陷且过时)

详解redis的set命令

我们已知道set用于设置String类型的key/value值,如下:

  1. 127.0.0.1:6379> set name gaoyuan
  2. OK
  3. 127.0.0.1:6379> get name
  4. "gaoyuan"
setnx + expire = 非原子性

在redis2.6.12版本之前,分布式锁常使用setnx来实现。setnx是set if not exists的意思,也就是当值不存在时,才可以创建成功,这样就能保证在同一时间只能有个设置成功。

但是,setnx无法在插入值的同时设置超时时间,setnx 与 expire 是两条独立的语句,这样加锁操作就是非原子性的,那么就会带来问题。(比如,当setnx成功后,准备执行expire前,程序突然出现错误,则添加的数据就无法清除了,因为没有超时时间,不会自动清除)

set key value [EX seconds] [PX milliseconds] [NX|XX]

在redis2.6.12版本之后,redis支持通过set在设置值得同时设置超时时间,此操作是原子操作。

  1. // 设置lock的值为123,存在6秒
  2. 127.0.0.1:6379> set lock 123 EX 6 NX
  3. OK
  4. // 6秒内,重复设置lock的值为123,返回nil(也就是null)
  5. 127.0.0.1:6379> set lock 123 EX 6 NX
  6. (nil)
  7. // 6秒内,获取值,能够获取到
  8. 127.0.0.1:6379> get lock
  9. "123"
  10. // 6秒后,获取值,获取为nil,又可以重新set值了
  11. 127.0.0.1:6379> get lock
  12. (nil)

下面我们利用set的特性来实现分布式锁。

实现分布式锁

我们先看一个不加锁的例子

我们先构造一个对象 MyThread

  1. class MyThread implements Runnable{
  2. int i = 0;
  3. @Override
  4. public void run() {
  5. try {
  6. for(int j=0;j<10;j++){
  7. i = i + 1;
  8. // 这里延时,为了让其他线程进行干扰
  9. TimeUnit.MILLISECONDS.sleep(10);
  10. i = i - 1;
  11. System.out.println("i=" + i);
  12. }
  13. }catch (Exception e){
  14. e.printStackTrace();
  15. }
  16. }
  17. }

执行

  1. ExecutorService executorService = Executors.newFixedThreadPool(3);
  2. MyThread myThread = new MyThread();
  3. executorService.submit(myThread);
  4. executorService.submit(myThread);
  5. executorService.submit(myThread);
  6. executorService.shutdown();

输出

  1. i=0
  2. i=0
  3. i=0
  4. i=3
  5. i=3
  6. i=3
  7. i=4
  8. i=4
  9. ...

可以看出,i居然会出现不等于0的情况。

Redis加锁(set命令)

获取锁的方法

  1. /**
  2. * 获取锁
  3. * 利用set key value [EX seconds] [PX milliseconds] [NX|XX] 命令实现锁机制
  4. * @author GaoYuan
  5. */
  6. public static String tryLock(Jedis jedis, int timeout) throws Exception{
  7. if(timeout == 0){
  8. timeout = 5000;
  9. }
  10. String returnId = null;
  11. // 生成随机标识
  12. String id = UUID.randomUUID().toString();
  13. // 设置锁超时10秒
  14. int lockExpireMs = 10000;
  15. long startTime = System.currentTimeMillis();
  16. // 超时时间内循环获取
  17. while ((System.currentTimeMillis() - startTime) < timeout){
  18. String result = jedis.set(lockKey, id, "NX", "PX", lockExpireMs);
  19. if(result != null){
  20. returnId = id;
  21. break;
  22. }
  23. TimeUnit.MILLISECONDS.sleep(100);
  24. }
  25. if(returnId == null){
  26. // 获取锁超时,抛出异常
  27. throw new Exception("获取锁超时");
  28. }
  29. // 将set的值返回,用于后续的解锁
  30. return returnId;
  31. }

释放锁的方法(释放锁的方式有两种)

释放方法一:

  1. /**
  2. * 释放锁 - 利用redis的watch + del
  3. * @author GaoYuan
  4. */
  5. public static boolean unLock(Jedis jedis, String id){
  6. boolean result = false;
  7. while(true){
  8. if(jedis.get(lockKey) == null){
  9. return false;
  10. }
  11. // 配置监听
  12. jedis.watch(lockKey);
  13. // 这里确保是加锁者进行解锁
  14. if(id!=null && id.equals(jedis.get(lockKey))){
  15. Transaction transaction = jedis.multi();
  16. transaction.del(lockKey);
  17. List<Object> results = transaction.exec();
  18. if(results == null){
  19. continue;
  20. }
  21. result = true;
  22. }
  23. // 释放监听
  24. jedis.unwatch();
  25. break;
  26. }
  27. return result;
  28. }

释放方法二:

  1. /**
  2. * 释放锁 - 利用lua脚本
  3. * @author GaoYuan
  4. */
  5. public static boolean unLockByLua(Jedis jedis, String id){
  6. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  7. Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(id));
  8. if (Objects.equals(1, result)) {
  9. return true;
  10. }
  11. return false;
  12. }

改造之前的例子

  1. class MyThread implements Runnable{
  2. int i = 0;
  3. @Override
  4. public void run() {
  5. try {
  6. for(int j=0;j<10;j++){
  7. Jedis jedis = new Jedis(JedisConfig.HOST, JedisConfig.PORT);
  8. try {
  9. // 尝试获取锁,有超时时间
  10. String id = RedisLock.tryLock(jedis,5000);
  11. i = i + 1;
  12. // 这里延时,为了让其他线程进行干扰(当然,加锁就不会有干扰)
  13. TimeUnit.MILLISECONDS.sleep(10);
  14. i = i - 1;
  15. // 加锁后,期望值 i=0
  16. System.out.println("i=" + i);
  17. // 释放锁
  18. RedisLock.unLock(jedis, id);
  19. }catch (Exception e){
  20. // e.printStackTrace();
  21. System.out.println("获取锁超时");
  22. }
  23. }
  24. }catch (Exception e){
  25. e.printStackTrace();
  26. }
  27. }
  28. }

运行输出

  1. i=0
  2. i=0
  3. i=0
  4. i=0
  5. i=0
  6. i=0
  7. ...

将run方法中的延时时间设置成1秒(1000)后,会打印超时的情况

  1. i=0
  2. i=0
  3. i=0
  4. 获取锁超时
  5. 获取锁超时
  6. i=0
  7. ...

至此利用jedis实现了分布式锁。

码云

完整代码见:
https://gitee.com/gmarshal/foruo-demo/tree/master/foruo-demo-redis/foruo-demo-redis-lock

博客

开源中国博客地址

https://my.oschina.net/gmarshal/blog/2120428

个人博客地址

http://blog.foruo.top

欢迎关注我的个人微信订阅号:(据说这个头像程序猿专用)

输入图片说明