使用Redis事務的注意事項

1、使用Jedis實現事務

    @Test
    public void testJedis(){
        Jedis jedis = jedisPool.getResource();
        jedis.watch("key1");
        //開啓事務
        Transaction transaction = jedis.multi();
        //命令入隊
        transaction.set("key1", "hello");
        transaction.expire("key1", 20);
        //獲取一個新的Jedis實例修改key1
        Jedis jedis2 = jedisPool.getResource();
        jedis2.set("key1", "world");
        //提交事務
        List<Object> result = transaction.exec();
        System.out.println("transaction exec result : "+result);
        System.out.println("jedis get key1: "+jedis.get("key1"));
    }

在這裏插入圖片描述
在事務exec之前,使用jedis2去修改key1,使得watch起作用導致事務失敗。

2、使用redisTemplate時一定要將enableTransactionSupport設置爲true,否則事務將不會起作用。

@Component
public class RedisTest implements CommandLineRunner {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void run(String... args) throws Exception {
        redisTemplate.setEnableTransactionSupport(true);

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (true) {
                        redisTemplate.opsForValue().set("key1", "hw" + i, 5000, TimeUnit.SECONDS);
                        i++;
                    }
                }
            }).start();
        }
        redisTemplate.opsForValue().set("key1", "0");

        new Thread(new Runnable() {
            @Override
            public void run() {
                redisTemplate.watch("key1");
                Object result = redisTemplate.execute(new SessionCallback<Object>() {
                    @Override
                    public <K, V> Object execute(RedisOperations<K, V> redisOperations) throws DataAccessException {
                        redisOperations.multi();
                        redisOperations.opsForValue().set((K) "key1", (V) ("test1"));
                        redisOperations.expire((K) "key1", 50000, TimeUnit.SECONDS);
                        redisOperations.opsForValue().get("key1");
                        return redisOperations.exec();
                    }
                });
                System.out.println("redis exec result:" + result);

            }
        }).start();


        Thread.sleep(5 * 1000);
        System.out.println("get key:" + redisTemplate.opsForValue().get("key1"));
    }
}

一個線程開始事務watch key1,並開啓事務修改key1爲test1,另一個線程一直在修改key1。
執行後事務的結果爲空數組,說明事務執行失敗了,我們的watch成功了。
在這裏插入圖片描述

3、使用自旋讓修改操作成功

高併發的場景下操作redis並不是watch到有其他線程修改就失敗,而需要的效果一般是線程安全的修改成功。借鑑juc中自旋鎖的方式,修改第二個例子:

@Component
public class RedisTest implements CommandLineRunner {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private volatile boolean isExist = false;

    @Override
    public void run(String... args) throws Exception {
        redisTemplate.setEnableTransactionSupport(true);

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (!isExist) {
                        redisTemplate.opsForValue().set("key1", "hw" + i, 5000, TimeUnit.SECONDS);
                        i++;
                    }
                }
            }).start();
        }
        redisTemplate.opsForValue().set("key1", "0");

        new Thread(new Runnable() {
            @Override
            public void run() {
                //事務是否成功
                boolean isSuccess = false;
                while(!isSuccess) {
                    redisTemplate.watch("key1");
                    Object result = redisTemplate.execute(new SessionCallback<Object>() {
                        @Override
                        public <K, V> Object execute(RedisOperations<K, V> redisOperations) throws DataAccessException {
                            redisOperations.multi();
                            redisOperations.opsForValue().set((K) "key1", (V) ("test1"));
                            redisOperations.expire((K) "key1", 50000, TimeUnit.SECONDS);
                            redisOperations.opsForValue().get("key1");
                            return redisOperations.exec();
                        }
                    });
                    if (ObjectUtils.isEmpty(result)) {
//                        isExist = true;
                        isSuccess = false;
                    }else{
                        isSuccess = true;
                    }
                    System.out.println("redis exec result:" + result);
                }

            }
        }).start();


        Thread.sleep(10 * 1000);
        System.out.println("get key:" + redisTemplate.opsForValue().get("key1"));
    }
}

在這裏插入圖片描述
在嘗試多次後事務終於成功了。

4、事務中不適合做邏輯判斷

例如,我想要如下的效果,當key2爲5的時候,我將其修改爲6:

		redisTemplate.setEnableTransactionSupport(true);

        redisTemplate.opsForValue().set("key2", "5", 5000, TimeUnit.SECONDS);
        Object result = redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                operations.multi();
                V key2 = operations.opsForValue().get("key2");
                System.out.println("tranctioning get key2: "+ key2);
                if("5".equals(key2)){
                    operations.opsForValue().set((K)"key2", (V)"6");
                }
                return operations.exec();
            }
        });
        System.out.println("exec result:" + result);
        System.out.println("get key2: "+ redisTemplate.opsForValue().get("key2"));

在這裏插入圖片描述
可以發現事務執行過程中,我們根本獲取不到任何值。這是因爲exec前的所有操作僅僅是將命令放入了隊列中,根本沒有實際執行,這時候獲取結果都是null。

有人說既然事務中不能獲取,是否可以在事務之前先獲取值,事務中判斷呢?更不行噠。看下面這個例子,模擬INCR,10個線程同時操作key3,每個線程進行100次的自增操作,由於事務中無法獲取當前的key3的值,只能在事務之前獲取。

	redisTemplate.setEnableTransactionSupport(true);
	redisTemplate.opsForValue().set("key3", "0");
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j <100 ; j++) {
                    redisTemplate.watch("key3");
                    boolean isSuccess = false;
                    while(!isSuccess) {
                        final Integer[] num = {Integer.parseInt(redisTemplate.opsForValue().get("key3"))};
                        Object result = redisTemplate.execute(new SessionCallback<Object>() {
                            @Override
                            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                                operations.multi();
                                operations.opsForValue().set((K) "key3", (V) (++num[0] + ""), 5000, TimeUnit.SECONDS);
                                return operations.exec();
                            }
                        });
                        if(!ObjectUtils.isEmpty(result)){
                            isSuccess = true;
                        }
                        System.out.println(Thread.currentThread().getName()+"-"+j+"-exec result:" + result);
                    }
                }
            }, "Thread-"+i).start();
        }
        Thread.sleep(20 * 1000);
        System.out.println("get key3:" + redisTemplate.opsForValue().get("key3"));

即使可以保證每次事務都成功,依然無法保證最後的結果是1000。
在這裏插入圖片描述
這種怎麼辦呢?目前所知估計只有lua腳本了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章