Redis修改数据多线程并发-并发锁

其实说多线程修改数据也不合适,毕竟redis服务端是单线程的,所有命令串行执行,只是在客户端并发发送命令的时候,导致串行的命令一些排列问题和网络时间差等造成数据不一致。

先写一个实际的例子,测试多线程并发修改redis的某个键值,而结果不是我们想要的。

2个线程同时修改键值mykey1,初始值为0,每次加1,线程共循环100次,2个线程下来,结果应该为200,但实际不是,例子代码如下:

TestRedis类的代码如下:

public class TestRedis {
static JedisSingleHandle jedis = new JedisSingleHandle();
public static void main(String[] args)
{
  try {
    jedis.set("mykey1", 0);
  } catch (Exception e) {
    e.printStackTrace();
  }
  TestRedisConcurrency myThread1 = new TestRedisConcurrency("a");
  TestRedisConcurrency myThread2 = new TestRedisConcurrency("b");
  myThread1.start();
  myThread2.start();
  System.out.println("等待两个线程结束");
}
}

TestRedisConcurrency 类的代码如下:

public class TestRedisConcurrency extends Thread {
JedisSingleHandle jedis = new JedisSingleHandle();
private String name;
TestRedisConcurrency(String name){
  this.name = name;
  System.out.println("线程="+this.name);
}

@Override
public void run() {
  for (int i = 0; i < 100; i++)
  {
    try {
      int result = (int) jedis.get("mykey1");
      jedis.set("mykey1", result + 1);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  //
  try {
    System.out.println("线程"+name+"结束,输出" + jedis.get("mykey1"));
  } catch (UnsupportedEncodingException e) {
    e.printStackTrace();
  }
}

}

运行TestRedis ,结果如下:

线程b结束,输出105
线程a结束,输出107

是的,和我们单线程,跑两个100,会输出200。现在是两个并发线程同时跑,由于并发,造成的数据结果往往不是我们想要的。那么如何解决这个问题呢?

1,在我的JedisSingleHandle类中加如下2个方法:

/**
* 获取分布式锁
* @param lockName 竞争获取锁key
* @param acquireTimeoutInMS 获取锁这个动作的超时时间
* @param lockTimeoutInMS 锁的超时时间(毫秒数):需要保证大于1秒,所以lockTimeoutInMS要大于1000
* @return 获取锁标识
*/
public String acquireLockWithTimeout(String lockName, long acquireTimeoutInMS, long lockTimeoutInMS) {
  Jedis jedis = null;
  String retIdentifier = null;
  try {
    if (jedis == null) {
      jedis = JedisUtil.getJedisPool().getResource();
      jedis.select(JedisUtil.dbIndex0);
    }
    String identifier = UUID.randomUUID().toString();
    String lockKey = "lock:" + lockName;
    int lockExpire = (int) (lockTimeoutInMS / 1000); // 锁的超时时间(秒数),需要保证大于1秒,所以lockTimeoutInMS要大于1000
    long end = System.currentTimeMillis() + acquireTimeoutInMS; // 获取锁这个动作的超时时间
    while (System.currentTimeMillis() < end) {
      if (jedis.setnx(lockKey, identifier) == 1) { // 成功获取锁
        jedis.expire(lockKey, lockExpire); // 锁的超时时间(秒数)
        retIdentifier = identifier;
      }
      if (jedis.ttl(lockKey) == -1) {
        jedis.expire(lockKey, lockExpire); // 锁的超时时间(秒数)
      }
      //
      try {
        Thread.sleep(10);
      } catch (InterruptedException ie) {
        Thread.currentThread().interrupt();
      }
    } // end while loop
  } catch (Exception e) {
    e.printStackTrace();;
  } finally {
    if (jedis != null)
      jedis.close();
  }
  //
  return retIdentifier;
}

/**
* 释放锁
* @param lockName 竞争获取锁key
* @param identifier 释放锁标识
* @return
*/

public boolean releaseLock(String lockName, String identifier) {

  Jedis jedis = null;
  String lockKey = "lock:" + lockName;
  boolean retFlag = false;
  try {
    if (jedis == null) {
      jedis = JedisUtil.getJedisPool().getResource();
      jedis.select(JedisUtil.dbIndex0);
    }
    while (true) {
      jedis.watch(lockKey);
      if (identifier.equals(jedis.get(lockKey))) {
        Transaction trans = jedis.multi();
        trans.del(lockKey);
        List<Object> results = trans.exec();
        if (results == null) {
          continue;
        }
        retFlag = true;
      }
      jedis.unwatch();
      break;
    } // end while loop

  } catch (Exception e) {
    e.printStackTrace();;
  } finally {
    if (jedis != null)
      jedis.close();
  }
  return retFlag;
}

2,重写TestRedisConcurrency类如下:

public class TestRedisConcurrency extends Thread {
  private static Logger logger = LoggerFactory.getLogger(TestRedisConcurrency.class);
  JedisSingleHandle jedis = new JedisSingleHandle();
  private String name;
  private String lockKey;
  TestRedisConcurrency(String name, String lockKey){
    this.name = name;
    this.lockKey = lockKey;
  }

  @Override
  public void run() {
    for (int i = 0; i < 100; i++) {
      try {
        boolean ok = true;
        while (ok) {
          String str = jedis.acquireLockWithTimeout(lockKey, 10, 1000); // 锁超时时间需要保证大于1秒,所以lockTimeoutInMS参数要大于1000
          if (str != null) {
            int result = (int) jedis.get("mykey1");
            jedis.set("mykey1", result + 1);
            jedis.releaseLock(lockKey, str); // 释放锁
            ok = false; // 更新成功后,退出while循环。
          }
        } // end while loop
      } catch (Exception e) {
        e.printStackTrace();
      }
    } // end for loop
    //
    try {
      System.out.println("线程"+name+"结束,输出" + jedis.get("mykey1"));
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    }
  }
}

3,重写 TestRedis 类,代码如下:

public class TestRedis {
  static JedisSingleHandle jedis = new JedisSingleHandle();
  public static void main(String[] args)
  {
    try {
      jedis.set("mykey1", 0);
    } catch (Exception e) {
      e.printStackTrace();
    }
    //
    String lockKey = "mykey1_lock"; // 多个线程共用的一个锁
    TestRedisConcurrency myThread1 = new TestRedisConcurrency("a", lockKey);
    TestRedisConcurrency myThread2 = new TestRedisConcurrency("b", lockKey);
    myThread1.start();
    myThread2.start();
    System.out.println("------ 等待两个线程结束");
  }
}

4,测试,运行TestRedis,得到的结果如下:

------ 等待两个线程结束
线程b结束,输出190
线程a结束,输出200

测试多次,后面这个线程的结果总是200,测试OK。

posted @ 2018-03-01 10:40  涓水之子  阅读(1703)  评论(0)    收藏  举报