ShardingJdbc讀寫分離的BUG踩坑解決

前言

最近公司準備接入ShardingJdbc做讀寫分離瞭,老大讓我們理一理有沒有寫完數據立馬讀的場景,因為主從同步是有延遲的,如果寫完讀取數據走到從庫,而從庫正好有延遲,沒讀取到數據,豈不是造成瞭生產事故。

今天我們來看看,ShardingJdbc作為一個成熟的框架是怎麼處理寫完數據立即讀取的場景的。

數據庫介紹

我本地使用瞭兩個庫來做實驗,寫庫(ds_0_master)和讀庫(ds_0_salve),兩個庫並沒有配置主從,但也不影響實驗操作。

庫裡有一個city 表。主庫的 city 表沒有數據,而從庫的 city 表就一條數據。數據內容如下:

我們討論 4 種業務場景:

  • 常規寫完讀
  • 在一個 service 裡面調用另一個 service2 進行讀
  • 在一個 service 裡面新開一個線程去調用 service2
  • 在一個 service 裡面調用 service2,但 service2 是新開的事務

先直接上實驗結果:

1. 常規寫完讀

@Service
public class CityService {
    @Autowired
    private CityRepository cityRepository;
    @Autowired
    private CityService2 cityService2;
    @Transactional(rollbackFor = Exception.class)
    public void test(){
        City city=new City();
        city.setName("眉山");
        city.setProvince("四川");
        cityRepository.save(city);
        List<City> all = cityRepository.findAll();
        all.forEach(x->{
            System.out.println("cityService:"+((x.getProvince().equals("四川"))?"主庫":"從庫")+":"+x);
        });
    }
}

打印結果:

實驗分析: 我們對 city 表進行插入後,緊接著對 city 表進行瞭查詢,查出的內容是我們剛剛插入的內容。說明查詢操作沒有走讀庫,而是走瞭主庫。

2. 在一個 service 裡面調用另一個 service

代碼如下:

 @Transactional(rollbackFor = Exception.class)
    public void test(){
        City city=new City();
        city.setName("眉山");
        city.setProvince("四川");
        cityRepository.save(city);
        //調用其他service
        cityService2.test();
        List<City> all = cityRepository.findAll();
        all.forEach(x->{
            System.out.println("cityService:"+((x.getProvince().equals("四川"))?"主庫":"從庫")+":"+x);
        });
    }
}

service2 的代碼:

public void test(){
    List<City> all = cityRepository.findAll();
    all.forEach(x->{
        System.err.println("cityService2:"+((x.getProvince().equals("四川"))?"主庫":"從庫")+":"+x);
    });
}

打印結果:

實驗分析:在 service 方法裡調用瞭其他 service,其他 service 也會受到影響。service2 也是走的主庫。

3. 新開一個線程去調用 service2

代碼如下:

@Service
public class CityService {
    @Autowired
    private CityRepository cityRepository;
    @Autowired
    private CityService2 cityService2;
    @Transactional(rollbackFor = Exception.class)
    public void test(){
        City city=new City();
        city.setName("眉山");
        city.setProvince("四川");
        cityRepository.save(city);
        new Thread(()->{cityService2.test();}).start();
        List<City> all = cityRepository.findAll();
        all.forEach(x->{
            System.out.println("cityService:"+((x.getProvince().equals("四川"))?"主庫":"從庫")+":"+x);
        });
    }
}
@Service
public class CityService2 {
    @Autowired
    private CityRepository cityRepository;
    public void test(){
        List<City> all = cityRepository.findAll();
        all.forEach(x->{
            System.err.println("cityService2:"+((x.getProvince().equals("四川"))?"主庫":"從庫")+":"+x);
        });
    }
}

打印結果:

實驗分析: 我們新開瞭線程對 city 表進行查詢,此次查詢讀的是從庫。新開的線程會走從庫,我猜想是新開的線程它認為是沒有寫入/修改操作,所以走瞭從庫。

我又改動瞭 service2,加瞭一段寫入操作。代碼如下:

    public void test(){
        City city=new City();
        city.setName("成都");
        city.setProvince("四川");
        cityRepository.save(city);
        List<City> all = cityRepository.findAll();
        all.forEach(x->{
            System.err.println("cityService2:"+((x.getProvince().equals("四川"))?"主庫":"從庫")+":"+x);
        });
    }

再次執行,結果如下:

和預想的不一樣,依舊是走的從庫。

4. service2 新開一個事務執行

我們調整 service2 的事務傳播行為級別。代碼如下:

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void test(){
        List<City> all = cityRepository.findAll();
        all.forEach(x->{
            System.err.println("cityService2:"+((x.getProvince().equals("四川"))?"主庫":"從庫")+":"+x);
        });
    }

REQUIRES_NEW 的含義是:

強制自己開啟一個新的事務,如果一個事務已經存在,那麼將這個事務掛起.如 ServiceA.methodA()調用 ServiceB.methodB(),methodB()上的傳播級別是 PROPAGATION_REQUIRES_NEW 的話,那麼如果 methodA 報錯,不影響 methodB 的事務,如果 methodB 報錯,那麼 methodA 是可以選擇是回滾或者提交的,就看你是否將 methodB 報的錯誤拋出還是 try catch 瞭.

打印結果:

實驗分析: 這個結果確實是沒想到,service2 新開瞭個事務走的是主庫,而 service 裡面的同一個事務裡的寫後讀,反而走瞭從庫。

實驗總結:

場景 service service2
同一個 service 裡寫完讀 主庫 主庫
service 裡寫完調用另一個 servcie 進行讀操作 主庫 主庫
service 裡寫完新開線程調用另一個 servcie 進行讀操作 主庫 從庫
service 裡寫完新開一個事務調用另一個 servcie 進行讀操作 從庫 主庫

常規的寫完讀操作和寫完在另一個 service 裡進行讀操作,都能夠走到主庫,保證瞭常規業務的正確性,也滿足瞭我們一般的使用場景瞭。而新開線程進行讀操作的情況其實比較少,如果非要使用,我們可以用強制指定主庫的方式進行處理。

最後一種情況,service中調用另一個service2(新開事務),原本 service 裡同一個事務的寫完讀操作走到瞭從庫,一不註意容易引起實際業務bug,需要使用者謹慎使用。大傢覺得這是不是ShardingJdbc的一個BUG呢?

以上就是ShardingJdbc讀寫分離的BUG踩坑解決的詳細內容,更多關於ShardingJdbc讀寫分離BUG的資料請關註WalkonNet其它相關文章!

推薦閱讀: