Viết mã thú vị với các API FileNet P8 của IBM
Phần 3: Lấy một số
Mở đầu
Thói quen ở nhiều nơi có các cửa hàng nhỏ là nắm được các khách hàng đã xếp hàng bằng cách
gán cho họ các số thứ tự sát với thứ tự mà họ tới cửa hàng. Các số thứ tự này thường được in trên
các mảnh giấy nhỏ và được phân phối từ một bộ phân phối cơ khí vật lý duy nhất. Nếu có nhiều
khách hàng đến đồng thời, mối quan hệ này dễ dàng bị phá vỡ bởi phép xã giao và phép lịch sự
thông thường.
TỪ CHỐI BẢO HÀNH
Mã đi kèm là mã ví dụ do Tập đoàn IBM tạo ra. Mã ví dụ này không phải là một phần của bất kỳ
tiêu chuẩn hay sản phẩm nào của IBM và được cung cấp cho bạn chỉ với mục đích giúp bạn
trong việc phát triển các ứng dụng của mình. Mã này được quy định "như nó vốn có", không bảo
hành bất kỳ loại nào. IBM không chịu trách nhiệm về mọi thiệt hại phát sinh do việc bạn sử dụng
mã ví dụ này, ngay cả khi bạn đã được biết về khả năng thiệt hại như vậy.
Các vấn đề tương tự thường phát sinh trong các hệ thống phần mềm. Một hệ thống thường cần
gán các số cho các việc nào đó với đảm bảo rằng các số đó là duy nhất và theo một số mô hình.
Có một số giải pháp chung cho vấn đề này, nhưng các hệ thống phân phối làm phức tạp các vấn
đề. Hầu như không chắc rằng bạn sẽ sử dụng một hệ thống ECM để gán các số cho các khách
hàng tại một hiệu bánh ở góc phố. (Nhưng nếu bạn đã quan tâm về điều đó, tôi có thể nối bạn với
một người bán hàng có thiện ý!) Tuy nhiên, bạn có thể cần gán các số theo trường hợp hoặc các
số mã định danh (ID) khách hàng hoặc các số bộ phận hoặc một cái gì đó đơn giản hơn. Các nhà
cung cấp cơ sở dữ liệu triển khai thực hiện các kiểu cột số thứ tự chỉ dùng cho loại vấn đề này.
Tuy nhiên, P8 không cung cấp truy cập trực tiếp vào các kiểu số thứ tự cơ sở dữ liệu, vì vậy bạn
phải sử dụng các cơ chế khác.
Trong bài này, chúng ta sẽ việc xem xét cách giải quyết vấn đề này trong một môi trường P8.
Hãy tóm tắt các yêu cầu:
1. Chúng ta cần gán các số được bảo đảm tuyệt đối là duy nhất. Hoàn toàn không chấp nhận
gán hai lần với cùng một số.
2. Chúng ta muốn các số đi theo một vài mô hình. Chúng ta không muốn có những cách
quãng trong việc gán số. Mô hình có thể có nhiều thứ, nhưng với các mục đích của mình
chúng ta sẽ chỉ sử dụng mô hình tăng dần đơn giản. Số tiếp theo mà chúng ta nhận được
{
return ++counter;
}
}
Việc sử dụng mã đồng bộ là tốt cho các vấn đề cụ thể, nhưng với trường hợp sử dụng của chúng
ta các nhược điểm là khá rõ. Vì giá trị của bộ đếm chỉ tồn tại trong bộ nhớ của chương trình đang
chạy, nên các giá trị này sẽ bắt đầu lại khi chương trình được khởi động lại. Bạn có thể thay đổi
lớp Dispenser (Bộ phân phối) để lưu các giá trị đã cập nhật vào một tệp, nhưng điều đó sẽ dẫn
đến các vấn đề mới do không thể phối hợp đồng bộ qua các ranh giới của quá trình. Các ứng
dụng chạy độc lập khác nhau (hoặc các bản sao của cùng một ứng dụng), ngay cả khi chúng đang
sử dụng lớp Dispenser, có thể đã xen kẽ đọc và viết vào tệp. Tệ hơn nữa, chúng có thể đang
truy cập vào một tệp có cùng tên trên các máy tính khác nhau. Cả hai tình huống đó đều dẫn đến
các điều kiện vi phạm các yêu cầu của trường hợp sử dụng của chúng ta.
Dưới đây là một ví dụ về những rắc rối có thể do các việc đọc và viết xen kẽ. Giả sử giá trị hiện
tại được ghi trong tệp là 7 và sau đó các việc sau đây diễn ra:
A đọc giá trị 7 từ tệp này.
B đọc giá trị 7 từ tệp này.
A viết giá trị 8 vào tệp này.
C đọc giá trị 8 từ tệp này.
C viết giá trị 9 vào tệp này.
B viết giá trị 8 vào tệp này.
Lúc này, cả hai A và B có cùng một số mà họ tin là duy nhất với họ. Người đọc tệp tiếp theo sẽ
kết thúc bằng một giá trị giống như C đã có. Bạn có thể có một vài ý tưởng về việc sử dụng khóa
tệp hoặc các thủ thuật cụ thể của hệ điều hành khác để đồng bộ hóa truy cập vào một tệp trong
một môi trường phân phối. Tuy nhiên, những khó khăn với kiểu giải pháp đó cũng khá dễ hiểu
trong hầu hết ngành khoa học máy tính và quả là rất khó khăn để nhận diện chính xác nó, do đó,
chúng ta không cần chỉ trích kịch bản này quá nhiều. Nếu bạn cần có sức thuyết phục hơn, hãy
sử dụng máy tìm kiếm ưa thích của mình để nghiên cứu "vấn đề khóa NFS".
Về đầu trang
và các đối tượng P8, phải chịu các sự kiểm tra truy cập do P8 bắt buộc. Do đó chúng ta có thể
mô hình hóa bộ phân phối như là một đối tượng P8. Cụ thể, chúng ta có thể tạo ra một cá thể của
một lớp con của CustomObject gọi là WjcDispenser với một đặc tính tùy chỉnh số nguyên được
gọi là WjcCounter. (Gắn cho các tên tiền tố tùy ý "Wjc" này để tránh các xung đột với tên lớp và
tên thuộc tính khác). Hình 1 cho thấy một biểu đồ UML cho lớp con đơn giản này.
Hình 1. Biểu đồ UML cho WjcDispenser
Chúng ta giả định rằng truy cập an toàn đến đối tượng này có thể được bố trí thuận tiện cho tất cả
người dùng của tất cả các ứng dụng cần sử dụng nó. Bây giờ, chúng ta sẽ giả định rằng bất cứ ai
cũng có thể kết nối tới ObjectStore và cập nhật đối tượng bộ phân phối. Xem thanh bên Sử dụng
một servlet J2EE để có một cách tiếp cận thú vị tới tình huống an toàn này.
Hơn nữa, chúng ta sẽ che đậy việc tạo đối tượng bộ phân phối ban đầu. Một kỹ thuật tốt sẽ là có
các lớp Java và .NET kèm theo để phát hiện xem đối tượng bộ phân phối có còn thiếu không và
tạo nó theo yêu cầu hoặc trong bộ khởi tạo tĩnh cho lớp đó. Hai kỹ thuật phổ biến để định vị đối
tượng bộ phân phối là sử dụng một giá trị ID được định sẵn hoặc để lưu trữ đối tượng này trong
một đường dẫn được định sẵn trong ObjectStore. Cũng có thể sử dụng một truy vấn để tìm ra tất
cả các cá thể của lớp WjcDispenser. Với các ví dụ tiếp theo, chúng ta giả định rằng việc nhận
dạng ObjectStore và vị trí cụ thể của đối tượng bộ phân phối dù thế nào đi nữa cũng được cấu
hình cho ứng dụng này.
Về đầu trang
Các khóa hợp tác của Máy nội dung (CE) FileNet
Khi sử dụng một đối tượng bộ phân phối dựa trên P8, ý tưởng dựa trên khái niệm để nhận được
một số thứ tự là giống nhau: đọc giá trị cũ, cập nhật và lưu giá trị mới và trả về giá trị mới cho
người gọi. Rõ ràng, các quá trình thực hiện cụ thể thay đổi. Tất cả các nhược điểm về đồng bộ
hóa Java hoặc .NET vẫn tiếp tục áp dụng.
Máy chủ CE và các API triển khai thực hiện một tính năng gọi là khóa hợp tác. Tính năng này
ban đầu được triển khai thực hiện để cung cấp ngữ nghĩa khóa hợp tác tương thích với RFC-2518
(WebDAV). Các lớp API cho Folder (Thư mục), Document (Tài liệu) và CustomObject (Đối
tượng tùy chỉnh) có các phương thức để khóa và mở khóa các đối tượng đó. Vì đây là một tính
// the object already being locked, rethrow it.
throw ere;
}
// already locked; try again after a little sleep
try
{
Thread.sleep(100); // milliseconds
}
catch (InterruptedException e) { /* don't worry about this rarity
*/ }
continue;
}
}
int oldValue =
dispenserProperties.getInteger32Value(COUNTER_PROPERTY_NAME);
int newValue = oldValue + 1;
dispenserProperties.putValue(COUNTER_PROPERTY_NAME, newValue);
dispenser.unlock(); // UNLOCK the object
dispenser.save(RefreshMode.NO_REFRESH); // R/T
return newValue;
}
Giả sử rằng đối tượng bộ phân phối được khởi tạo theo cách không tìm nạp (có nghĩa là, thông
qua một phương thức Factory.CustomObject.getInstance()), kỹ thuật này trị giá một
chuyến đi khứ hồi đến máy chủ CE để áp dụng khóa và tìm nạp giá trị thuộc tính hiện tại. Nếu
đối tượng đã bị khóa, chúng ta sẽ không nhận được giá trị hiện tại, vì vậy chúng ta lặp lại một vài
lần để nhận được thay đổi của mình lúc khóa bộ phân phối. Nó trị giá một chuyến đi khứ hồi
khác đến máy chủ CE để lưu trữ giá trị bộ đếm mới. Đó là một chi phí hiệu năng hợp lý cho điều
này và cũng hợp lý cho việc sử dụng toàn bộ tính năng khóa/mở khóa.
Vấn đề chính của việc sử dụng khóa hợp tác P8 cho trường hợp sử dụng này là nó chỉ là khóa
hiện một tính toán tương tự để dự báo giá trị mới của WjcCounter.
Trong thử nghiệm tư duy này, bạn tính rằng các cập nhật cho đối tượng bộ phân phối sẽ xảy ra
một lần tại một thời điểm, cho dù có bao nhiêu ứng dụng khách độc lập đang yêu cầu nó đi nữa.
Máy chủ CE thì không "tối ưu hóa ngay" các cập nhật trung gian (dự phòng). Bạn hãy nhớ rằng
cần bảo đảm các trình xử lý sự kiện không đồng bộ được thực hiện. Trên thực tế, bạn thậm chí có
thể nhớ là đã nghe ở đâu đó rằng các trình xử lý sự kiện không đồng bộ được xử lý qua hàng đợi
(đó là đúng). Tất cả điều đó dường như thêm vào một cập nhật duy nhất, tin cậy và dự báo được
tới WjcCounter cho mỗi chuyến đi khứ hồi của máy khách đến máy chủ CE.
Bạn có thể đã đoán được từ giọng điệu mô tả ở trên là có một số vấn đề bí mật ở đây. Trên thực
tế, có hai vấn đề. Thứ nhất, có một khoảng thời gian ngắn giữa việc cập nhật đối tượng bộ phân
phối trong ObjectStore và việc thực hiện trình xử lý sự kiện không đồng bộ. Nếu có nhiều máy
khách độc lập tình cờ thực hiện cấp nhật cùng một lúc, thì tất cả chúng sẽ thấy cùng một giá trị
được làm mới của WjcCounter và tính toán giá trị cập nhật giống nhau. Thậm chí nếu bạn có thể
khắc phục điều đó và bằng cách nào đó buộc các hoạt động save() của máy khách cụ thể vào
khởi động cụ thể trình xử lý sự kiện, thì sẽ có một vấn đề thứ hai. Vấn đề thứ hai là mặc dù các
sự kiện không đồng bộ được xử lý thực sự thông qua một hàng đợi, cần có nhiều thiết bị đọc
hàng đợi. Vì vậy, không có gì bảo đảm rằng các trình xử lý sự kiện không đồng bộ sẽ thực hiện
theo thứ tự giống như các cập nhật thực hiện nhanh.
Về đầu trang
Người viết đầu tiên sẽ thắng
Máy chủ CE có một tính năng tích hợp sẵn để phát hiện tin cậy các cập nhật xen kẽ. CE triển
khai thực hiện một chính sách gọi là người viết đầu tiên sẽ thắng. Điều đó có nghĩa rằng nếu có
hai yêu cầu đang cập nhật cùng một đối tượng, yêu cầu đầu tiên sẽ thành công còn yêu cầu thứ
hai sẽ thất bại. Với việc cập nhật thất bại này, máy chủ sẽ đưa ra một EngineRuntimeException
với một ExceptionCode của E_OBJECT_MODIFIED. Thông báo ngoại lệ đi kèm là "Đối tượng
đã được sửa đổi vì nó đã được lấy ra". Thế điều đó có nghĩa gì?
Mỗi đối tượng có thể tồn tại độc lập trong một ObjectStore được đánh dấu bằng một Số thứ tự
cập nhật (USN - Update Sequence Number). Đây không phải là một đặc tính theo nghĩa thông
thường, nhưng giá trị của nó được trưng ra qua phương thức
IndependentlyPersistableObject.getUpdateSequenceNumber(). Máy chủ CE tự động gia
* This property filter is used to minimize data returned in fetches and
refreshes.
*/
private static final PropertyFilter PF_COUNTER = new PropertyFilter();
static
{
PF_COUNTER.addIncludeProperty(1, null, null, COUNTER_PROPERTY_NAME,
null);
}
/**
* Get the next value efficiently by exploiting First Writer Wins
*/
public int getNextValue(boolean feelingUnlucky)
{
final Properties dispenserProperties = dispenser.getProperties();
// Object might be updated by someone else, so try a few times
for (int attemptNumber=0; attemptNumber<10; ++attemptNumber)
{
// If cached data invalid, fetch the current value
// from the server. This also covers the fetchless
// instantiation case.
if (feelingUnlucky
|| dispenser.getUpdateSequenceNumber() == null
|| !dispenserProperties.isPropertyPresent(COUNTER_PROPERTY_NAME))
{
// fetchProperties will fail if the USN doesn't match, so null it
out
dispenser.setUpdateSequenceNumber(null);
dispenser.fetchProperties(PF_COUNTER); // R/T
}
/**
* Set by constructor or some other means.
* Fetchless instantiation is OK.
*/
private final CustomObject dispenser;
Thoạt nhìn, điều này dường như có hai chuyến đi khứ hồi giống nhau của máy chủ như mã khóa
hợp tác nêu trên của chúng ta. Điều đó đúng với việc cập nhật bộ đếm đầu tiên. Chúng ta phải
tìm nạp giá trị bộ đếm hiện tại từ máy chủ, nhưng nên nhớ trạng thái bộ đếm sau một cập nhật
thành công. Nếu không có ai khác đã cập nhật đối tượng bộ phân phối vào lúc này, thì các cập
nhật cuối của chúng ta sẽ chỉ trả tiền cho một chuyến đi khứ hồi. Mặt khác, nếu một cặp ứng
dụng đang thay đổi việc cập nhật bộ phân phối này, thì có lẽ như một số loại cân bằng tải, trạng
thái đã nhớ đó gây tác dụng ngược với chúng ta. Trong trường hợp đó, sẽ luôn trả tiền cho ba
chuyến đi khứ hồi để thực hiện một cập nhật (cố gắng cập nhật đã thất bại ban đầu, tìm nạp giá
trị bộ đếm hiện tại và cập nhật thành công cuối cùng). Chi phí phát sinh này phải chịu thêm ngay
cả khi các ứng dụng cạnh tranh đang thực hiện các cập nhật với nhiều loại thời gian khác nhau
giữa chúng (ví dụ, A nhận được một giá trị bộ đếm trên cơ sở một giờ còn B nhận được một giá
trị bộ đếm trên cơ sở nửa giờ). Liệu bạn sẽ phải trả chi phí thêm hay không tùy thuộc vào việc
bạn có bao nhiều ứng dụng đọc độc lập, chúng chồng chéo lên nhau như thế nào và v.v. Tham số
Boolean feelingUnlucky điều khiển phương thức trả chi phí cho một trình tự cập nhật hai
chuyển đi khứ hồi hay đặt cược vào trình tự cập nhật một-hoặc-ba chuyển đi khứ hồi.
Về đầu trang
Các lý do khác
Dưới đây là một số điều bổ sung để xem xét về việc thực hiện và triển khai của bạn.
USN thay cho đặc tính
Do mỗi đối tượng tiếp tục tồn tại độc lập trong một kho lưu trữ đã có một Số thứ tự cập nhật
(USN) tăng đều, tại sao không sử dụng số đó thay vì một đặc tính tuỳ chỉnh cho giá trị bộ đếm?
Bạn có thể làm điều này nếu bạn đang định chấp nhận một số thỏa hiệp, nhưng khác với việc