本篇文章是通过watch(监控)+mutil(事务)实现应用于在分布式高并发处理等相关场景。下边先通过redis-cli.exe来测试多个线程修改时,遇到问题及解决问题。
高并发下修改同一个key遇到的问题:
1)定义一个hash类型的key,key为:lock_test,元素locker的值初始化为0。
2)实现高并发下对locker元素的值递增:定义64个多线程,并发的对lock_test元素locker的值进行修改。
package com.dx.es; import java.util.concurrent.CountDownLatch; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class Test_UnLock { public static void main(String[] args) { final JedisPool pool = RedisUtil.getPool(); // 获得jedis对象 Jedis jedis = pool.getResource(); jedis.hset("lock_test", "locker", "0"); String val = jedis.hget("lock_test", "locker"); System.out.println("lock_test.locker的初始值為:" + val); jedis.close(); int threahSize = 64; final CountDownLatch threadsCountDownLatch = new CountDownLatch(threahSize); Runnable handler = new Runnable() { public void run() { Jedis jedis = pool.getResource(); Integer integer = Integer.valueOf(jedis.hget("lock_test", "locker")); jedis.hset("lock_test", "locker", String.valueOf(integer + 1)); jedis.close(); threadsCountDownLatch.countDown(); } }; for (int i = 0; i < threahSize; i++) { new Thread(handler).start(); } // 等待所有并行子线程任务完成。 try { threadsCountDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("complete"); val = jedis.hget("lock_test", "locker"); System.out.println(val); } }
RedisUtil.java
package com.dx.es; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisUtil { public static JedisPool getPool() { // 简单创建 Jedis的方法: // final Jedis jedis = new Jedis("127.0.0.1",6379); // 下边使用线程池的方案:jedis对象是线程不安全的,因此在并发情况下要使用JedisPool,默认情况下jedisPool只支持8个连接,因此在声明JedisPool时要先修改JedisPool的最大连接数 JedisPoolConfig config = new JedisPoolConfig(); // 修改最大连接数 config.setMaxTotal(32); // 声明一个线程池 JedisPool pool = new JedisPool(config, "127.0.0.1", 6379); return pool; } }
此时,会出现以下问题:
- A线程获取key的值为0,而B线程也获取jkey的值0,则A把key值递增为1,B线程也实现把key值递增为1。两个线程都执行了key值修改:0到1。
- 在1)中最终key修改为了1,但是c线程获取key的值为0(因为c线程读取key值时,a、b线程还未触发修改,因此c线程读取到的值为0),此时d线程读取到的值为1(因为d线程读取key值时,a、b线程已触发修改,一次d线程取到的值为1)。
- 此时假设d线程优先触发递增,则在c线程未触发提交之前d线程已经把值修改了2,但是c此时并不知道在它获取到值到修改之前这段时间发生了什么,直接把值修改1。
此时执行打印结果为:
lock_test.locker的初始值為:0 complete 24 #备注:也可能是其他值,可能是正确值64的可能性比较小。
通过watch+mutil解决并发修改的问题:
需要掌握Redis 事务命令:
下表列出了 redis 事务的相关命令:
序号 | 命令 | 描述 | 可用版本 |
1 | DISCARD | Redis Discard 命令用于取消事务,放弃执行事务块内的所有命令。 语法 redis Discard 命令基本语法如下: redis 127.0.0.1:6379> DISCARD |
>= 2.0.0 |
2 | EXEC | Redis Exec 命令用于执行所有事务块内的命令。 语法 redis Exec 命令基本语法如下: redis 127.0.0.1:6379>Exec |
>= 1.2.0 |
3 | MULTI | Redis Multi 命令用于标记一个事务块的开始。 事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。 语法 redis Multi 命令基本语法如下: redis 127.0.0.1:6379>Multi |
>= 1.2.0 |
4 | UNWATCH | Redis Unwatch 命令用于取消 WATCH 命令对所有 key 的监视。 语法 redis Unwatch 命令基本语法如下: redis 127.0.0.1:6379> UNWATCH |
>= 2.2.0 |
5 | WATCH | Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断 语法 redis Watch 命令基本语法如下: WATCH key [key ...] |
>= 2.2.0 |
Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
备注:概念性摘自《http://www.runoob.com/redis/redis-transactions.html》
redis-cli.exe下的事务操作:
# 事务被成功执行
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> INCR user_id QUEUED redis 127.0.0.1:6379> INCR user_id QUEUED redis 127.0.0.1:6379> INCR user_id QUEUED redis 127.0.0.1:6379> PING QUEUED redis 127.0.0.1:6379> EXEC 1) (integer) 1 2) (integer) 2 3) (integer) 3 4) PONG
并发情况下使用watch+mutil操作:
事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
A线程:
# 监视 key ,且事务成功执行
redis 127.0.0.1:6379> WATCH lock lock_times
OK
redis 127.0.0.1:6379> MULTI OK redis 127.0.0.1:6379> SET lock "huangz" QUEUED redis 127.0.0.1:6379> INCR lock_times QUEUED redis 127.0.0.1:6379> EXEC 1) OK 2) (integer) 1
B线程:
# 监视 key ,且事务被打断
redis 127.0.0.1:6379> WATCH lock lock_times
OK
redis 127.0.0.1:6379> MULTI OK redis 127.0.0.1:6379> SET lock "joe" # 就在这时,另一个客户端修改了 lock_times 的值 QUEUED redis 127.0.0.1:6379> INCR lock_times QUEUED redis 127.0.0.1:6379> EXEC # 因为 lock_times 被修改, joe 的事务执行失败 (nil)
上边演示了A、B线程并发下的watch+mutil操作情况。
解决高并发下修改同一个key遇到的问题:
package com.dx.es; import java.util.List; import java.util.concurrent.CountDownLatch; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Transaction; public class Test_Lock3 { public static void main(String[] args) { final JedisPool pool = RedisUtil.getPool(); // 对测试key赋初始值 Jedis jedis = pool.getResource(); jedis.hset("lock_test", "locker", "0"); String val = jedis.hget("lock_test", "locker"); System.out.println("lock_test.locker的初始值為:" + val); jedis.close(); int threahSize = 64; final CountDownLatch threadsCountDownLatch = new CountDownLatch(threahSize); Runnable handler = new Runnable() { public void run() { Jedis jedis = pool.getResource(); while (true) { jedis.watch("lock_test"); String val = jedis.hget("lock_test", "locker"); Integer integer = Integer.valueOf(val); Transaction tx = jedis.multi(); tx.hset("lock_test", "locker", String.valueOf(integer + 1)); List<Object> exec = tx.exec(); if (exec == null || exec.isEmpty()) { System.out.println(Thread.currentThread().getName() + ":" + "Error:(" + val + "=>" + (integer + 1) + ")"); } else { String values = ""; for (int i = 0; i < exec.size(); i++) { values += exec.get(i).toString(); } System.out.println(Thread.currentThread().getName() + ":" + values + ":(" + val + "=>" + (integer + 1) + ")"); break; } try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } jedis.close(); threadsCountDownLatch.countDown(); } }; for (int i = 0; i < threahSize; i++) { new Thread(handler).start(); } // 等待所有并行子线程任务完成。 try { threadsCountDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("complete"); val = jedis.hget("lock_test", "locker"); System.out.println(val); } }
打印结果:
lock_test.locker的初始值為:0 Thread-8:0:(0=>1) Thread-56:Error:(1=>2) Thread-53:0:(1=>2) Thread-25:Error:(0=>1) Thread-18:Error:(0=>1) Thread-3:Error:(0=>1) Thread-30:Error:(2=>3) Thread-11:Error:(2=>3) Thread-9:Error:(0=>1) Thread-63:0:(2=>3) Thread-7:Error:(0=>1) Thread-10:0:(3=>4) Thread-34:0:(4=>5) Thread-65:Error:(4=>5) Thread-24:Error:(0=>1) Thread-17:Error:(0=>1) Thread-62:0:(5=>6) Thread-29:Error:(6=>7) Thread-61:0:(6=>7) Thread-64:0:(7=>8) Thread-16:Error:(0=>1) Thread-19:Error:(8=>9) Thread-6:Error:(0=>1) Thread-28:Error:(0=>1) Thread-21:Error:(0=>1) Thread-14:Error:(0=>1) Thread-20:Error:(0=>1) Thread-5:Error:(0=>1) Thread-13:Error:(0=>1) Thread-15:Error:(0=>1) Thread-22:Error:(0=>1) Thread-4:Error:(0=>1) Thread-12:Error:(0=>1) Thread-23:Error:(0=>1) Thread-54:Error:(0=>1) Thread-57:Error:(0=>1) Thread-26:0:(8=>9) Thread-27:Error:(8=>9) Thread-32:Error:(9=>10) Thread-35:Error:(9=>10) Thread-56:0:(9=>10) Thread-33:Error:(10=>11) Thread-50:0:(10=>11) Thread-31:0:(11=>12) Thread-38:0:(12=>13) Thread-25:Error:(13=>14) Thread-36:0:(13=>14) Thread-39:Error:(14=>15) Thread-14:0:(14=>15