【ActiveJdbc】04
一、乐观锁
作者po的乐观锁思想:
http://en.wikipedia.org/wiki/Optimistic_concurrency_control
维基百科,墙了看不到
作者要求表字段必须存在一个record_version
ActiveJDBC 通过一个简单的约定来支持乐观并发:
一个数据库表需要提供一个以record_version
能够存储非十进制类型的类型命名的列,
例如 MySQL 的 LONG、Oracle 的 NUMBER 等。
向表中新加入的记录,ActiveJdbc会默认向这个记录record_version设置值1
值为1时,表示这个记录没有被更新,或者说,没有被操作过
操作冲突:
有时,您的代码可能会从表中读取相同的记录以进行更新。在这些情况下,第一次更新会成功,但第二次不会。让我们检查一下这种情况:
Profile p1 = Profile.findById(1); Profile p2 = Profile.findById(1); p1.set("profile_type", "hotel"); p1.saveIt(); p2.set("profile_type", "vacation"); p2.saveIt(); //<<<========= This will throw a StaleModelException
在上面的代码片段中,在第 1 行和第 2 行,相同的记录被加载到模型中。然后,在第 5 行,更新第一个。
这会将记录的版本增加到 3,并使模型 p2 过时。
以后,当您尝试保存模型 p2 时,您将收到异常。
ActiveJdbc会抛出异常:
org.javalite.activejdbc.StaleModelException:
Failed to update record for model 'class com.acme.Profile', with id = 1 and record_version = 2.
Either this record does not exist anymore, or has been updated to have another record_version.
项目里面会重写或者封装这个异常,改成友好的提示信息抛给前台,做警告弹窗使用
目的就是阻止冲突操作,然后回到主列表刷新记录
更改乐观锁设定:
如果不使用record_version字段,作者也提供了注解,使用这个注解来更换乐观锁字段
默认情况下,版本列称为version_record
。如果已经存在架构或者您需要与其他系统兼容,您可以使用注释覆盖名称:
@VersionColumn("lock_version") public class Item extends Model { ... }
版本号将存储在lock_version
列中,而其他一切都将按预期工作。
作者的建议:
如何使用乐观锁
规则非常简单,ActiveJDBC 查找record_version
列并动态配置自身以处理乐观锁。
这意味着如果此列存在,则将使用乐观锁定。
如果记录不存在,则不会使用乐观锁定。
如果没有此列,后来又添加了,则需要重新启动系统,因为 ActiveJDBC 在开始时会扫描数据库架构。
相反,如果要关闭它,请删除列record_version
并重新启动系统。
使用乐观锁定的应用程序开发人员应该注意异常StaleModelException(即使它是一个 RuntimeException)
并在他们的代码中构建控件以适当地拦截和处理它。
二、生命周期回调
与 ActiveRecord 一样,ActiveJDBC 也有生命周期回调。
这些方法可以在 Model 子类上实现,以获取在模型上执行的特殊生命周期事件的通知。
这些回调在由Model
类实现的接口中捕获:
public interface CallbackListener { void afterLoad(Model m); void beforeSave(Model m); void afterSave(Model m); void beforeCreate(Model m); void afterCreate(Model m); void beforeDelete(Model m); void afterDelete(Model m); void beforeValidation(Model m); void afterValidation(Model m); }
请参阅:回调监听器
子类可以覆盖总共八个调用以获取特定事件的通知。
外部监听器的注册
您可以在任何模型外部实现CallbackListener接口,然后注册它:
CallbackAdapter adapter = new CallbackAdapter() { @Override public void afterLoad(Model m) { //do what you need to after a model is loaded with data from the database. } }; Person.callbackWith(adapter);
这是假设 Person 是一个模型。
您可以实现CallbackListener接口或扩展CallbackAdapter(其中所有方法都使用空白主体实现)并且仅覆盖您需要的那些。
覆盖模型回调方法
Model 类已经扩展了一个类CallbackAdapter,它提供了这八个方法的空实现。开发人员所需要做的就是覆盖一个或多个方法以在特定时间执行任务。
作者下面提供了一个应用场景:
假设我们有一个模型User
:
public class User extends Model{}
用户还有一个密码,需要以加密形式存储在数据库中。在这种情况下使用回调很有用,因为您所要做的就是覆盖一个beforeSave()
方法并提供一些加密例程来确保密码安全:
public class User extends Model{ public void beforeSave(){ set("password" encryptPassword()); } private String encryptPassword(){ //do what it takes } }
该框架将在适当beforeSave()
的上下文中save()
或saveIt()
在适当的时候调用,并且您的代码将对密码进行加密以进行存储。
三、缓存
缓存是每个主要系统不可或缺的一部分,它提高了性能,减少了 IO,并使整体用户体验更加愉快。ActiveJDBC 中的缓存适用于模型实例的查询和创建级别。例如,调用:
List<Library> illLibs = Library.where("state = ?", "IL");
可能会调用数据库,或者结果可能来自缓存,具体取决于缓存和具体模型的Library
配置方式
开启缓存:
ActiveJDBC 提供注解来指定查询哪些表将被缓存:
@Cached public class Library extends Model {}
与其他情况一样,这是一个将模型标记为可缓存
的声明。如果您启用日志记录(通过提供系统属性activejdbc.log
),您将看到来自 ActiveJDBC 的大量输出,类似于:
3076 [main] INFO org.javalite.activejdbc.DB - Query: "SELECT * FROM libraries WHERE id = ?", with parameters: [1], took: 0 milliseconds 3076 [main] INFO org.javalite.activejdbc.cache.QueryCache - HIT, "SELECT * FROM libraries WHERE id = ?", with parameters: [1] 3077 [main] INFO org.javalite.activejdbc.DB - Query: "INSERT INTO libraries (address, state, city) VALUES (?, ?, ?)", with parameters: [123 Pirate Street, CA, Bloomington], took: 1 milliseconds 3077 [main] INFO org.javalite.activejdbc.cache.QueryCache - table cache purged for: libraries 3077 [main] INFO org.javalite.activejdbc.cache.QueryCache - table cache purged for: books 3077 [main] INFO org.javalite.activejdbc.cache.QueryCache - MISS, "SELECT * FROM libraries WHERE id = ?", with parameters: [1] 3078 [main] INFO org.javalite.activejdbc.DB - Query: "SELECT * FROM libraries WHERE id = ?", with parameters: [1], took: 0 milliseconds
缓存配置
缓存配置包括在文件中提供缓存管理器类名activejdbc.properties
。
该文件必须位于类路径的根目录下。
这是一个例子:
#inside file: activejdbc.properties #or EHCache: cache.manager=org.javalite.activejdbc.cache.EHCacheManager #cache.manager=org.javalite.activejdbc.cache.OSCacheManager
这里发生了两件事:
1. 缓存一般是启用的(即使你在类上有@Cached注释也不会启用),以及
2. ActiveJDBC 将使用 EHCacheManager 作为缓存的实现。
自动缓存清除
如果从上面检查日志,您将看到在向LIBRARIES
表中执行插入语句后,系统正在清除与该表以及BOOKS
表相关的缓存。
ActiveJDBC 这样做是因为内存中的缓存可能与数据库中的数据不同步,因此将被清除。
相关表?缓存也被清除。
由于存在关系:图书馆有很多书籍,书籍缓存也可能是陈旧的,这也是清除表BOOKS
的原因。
手动缓存清除
如果您想手动清除缓存(以防您在 Model API 之外进行可变或破坏性数据操作),您可以这样做:
org.javalite.activejdbc.cache.QueryCache
.instance()
.purgeTableCache("books");
清除所有缓存
如果你想清除所有缓存,这里是一个片段:
QueryCache
.instance()
.getCacheManager()
.flush(CacheEvent.ALL);
侦听/传播缓存事件
在更复杂的应用程序中,您可能希望监听缓存事件,然后对它们采取行动:假设跨集群传播缓存清除。
首先,您需要为缓存事件创建一个侦听器:
public class AppCacheEventListener implements CacheEventListener{ public void void onFlush(CacheEvent event){ // implementation goes here } }
然后,注册监听器:
Registry
.cacheManager()
.addCacheEventListener(new AppCacheEventListener());
完成此操作后,如果特定表的缓存被清除,则侦听器将开始收到通知。
缓存什么
虽然缓存是一个复杂的问题,但我建议缓存主要是查找数据。
查找数据是不经常改变的东西。
如果您开始缓存所有内容,您可能会遇到缓存抖动问题,您用数据填充缓存,然后很快将其清除,而没有缓存的好处。
您将使用额外的 CPU、RAM 和 IO(是否配置了集群)来降低性能,而不是提高性能,而最初拥有缓存的好处很少或没有。
内存爆炸问题
ActiveJDBC 缓存对象级别查询的结果。
例如,让我们考虑以下代码:
@Cached public class Student(){} ... List<Student> students = professor.getAll(Student.class);
本质上,该框架会生成如下查询:
SELECT * FROM students WHERE professor_id = ?;
并professor_id
为准备好的语句设置一个参数。
由于模型Student
是@Cached
,那么整个List<Student> students
列表将被缓存。
作为缓存对象的列表的关键是查询文本以及查询的所有参数的组合。
结果,这两个查询:
SELECT * FROM students WHERE professor_id = 1;
SELECT * FROM students WHERE professor_id = 2;
将在缓存中产生两个独立的列表,因为它们的参数不同。
那么,如果您运行数千或数百万个相同但仅参数不同的查询,会发生什么?
你猜对了,你最终会在缓存中得到数百万个无用的对象,最终会得到一个OutOfMemoryError。
解决方案是检查代码,并确保您正在缓存实际可重用的对象。
可以直接访问和管理缓存而不是@Cached
注解:
import org.javalite.activejdbc.Registry; CacheManager manager = Registry.cacheManager(); manager.addCache(group, key, object); ///then later in code: List<Students> students = (List<Students>)manager.getCache(group, key);
通过这种方式,您可以微调仅在缓存中存储特定对象的能力。
缓存数据直接暴露
检索缓存模型的实例时,请注意,对同一查询的后续调用可能会返回完全相同的实例。
ActiveJDBC 作为一个轻量级的框架,不会试图变得智能
并为您管理缓存数据的克隆。
因此,例如,考虑Person
被注释为@Cached
,随后的两次调用Person.findById(1)
将返回相同的实例:
Person p1 = Person.findById(1); System.out.println(p1.get("name")); // prints: John p1.set("name", "Jane"); // changes the cached data directly // don't save p1, and ... Person p2 = Person.findById(1); // ... find the same person again System.out.println(p2.get("name")); // prints: Jane
缓存或乐观锁
缓存和optimistic_locking不能相处。不要同时使用两者。
缓存保证对同一查询的后续调用返回相同的实例。因此,缓存管理器共享的内存中不能存在相同结果集的不同版本。
假设Profile
,一个带有optimistic_locking的模型,也被注释为@Cached
。将发生以下情况:
Profile p1 = Profile.findById(1); Profile p2 = Profile.findById(1); // p1 and p2 are actually references to the same instance. p1.set("profile_type", "hotel"); p1.saveIt(); // record_version of the instance is incremented, then updated in the database. p2.set("profile_type", "vacation"); p2.saveIt(); // As this is the same instance that had record_version incremented ealier, // its record_version value will match the database // and no StaleModelException will be thrown.
关系
ActiveJDBC 管理模型及其各自关系的缓存(见上文),但在某些情况下,您将使用将不相关模型联系在一起的查询:
List<User> users = User.where("id not in (select user_id from restricted_users)");
如果存在缓存的模型 User 和模型 RestrictedUser,并且这些表/模型没有关系,那么上面的行可能会出现逻辑问题。如果您执行上面的行,然后更改 RESTRICTED_USERS 表的内容,那么上面的查询将看不到更改,并将返回陈旧数据。开发人员需要意识到这一点,并谨慎处理这些问题。每当您更改 RESTRICTED_USERS 表中的数据时,请清除 User 模型:
User.purgeCache();
变异或破坏性操作
每当您对模型(INSERT、UPDATE、DELETE)执行可变或破坏性操作时,该模型的整个缓存都会失效。这意味着缓存最适合用于查找数据(废话!)。
该框架还将使所有相关表的缓存失效并删除。例如:
鉴于表:
create table USERS (INT id, name VARCHAR);
create table ADDRESSES (INT id, street VARCHAR, city VARCHAR, user_id INT);
和模型:
@Cached public class User extends Model{} @Cached public class Address extends Model{}
如果你这样做:
user.delete();
该框架将重置缓存:用户和地址模型,而不仅仅是用户。这样做是为了防止应用程序中出现逻辑错误。
不断变化的缓存数据将导致Cache Stampede。
不相关的模型
在某些情况下,您需要在表中使用适当的外键,但希望在代码中断开基于约定的关系
鉴于表:
create table USERS (INT id, name VARCHAR);
create table ADDRESSES (INT id, street VARCHAR, city VARCHAR, user_id INT);
和模型:
@Cached public class User extends Model{} @Cached UnrelatedTo({User.class}) public class Address extends Model{}
如果你这样做:
user.delete();
框架只会重置 User 模型的缓存,不会触及 Address 模型的缓存。
有关更多信息,请参阅 JavaDoc:UnrelatedTo。
缓存提供者
ActiveJDBC 有一个简单的插件框架来添加缓存提供者。目前支持:
- OSCache 现在已经死了。尽管它在我们的许多项目中都运行良好,但我们建议使用 EHCache
- 高速缓存。EHCache 是高性能流行的开源项目。文档请参考:http : //ehcache.org/documentation
- 基于 Redis 的缓存提供程序(最近添加)
EHCache 配置 (v 2.x)
需要在ehcache.xml
类路径根目录下的一个名为found的文件中提供配置。文件内容示例:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="true" monitoring="autodetect"> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" maxElementsOnDisk="10000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU" /> </ehcache>
请注意,ActiveJDBC 不会在 EHCache 中创建命名缓存,而只会使用defaultCache
此文件中元素指定的默认配置。
EHCache 配置 (v 3.6.3)
缓存管理器类的名称:org.javalite.activejdbc.cache.EHCache3Manager
. 在文件中设置以下内容activejdbc.properties
:
cache.manager=org.javalite.activejdbc.cache.EHCache3Manager
此外,您还需要配置 EHCache 本身。为此,添加一个名为activejdbc-ehcache.xml
. 这是简单的 EHCache v3 配置:
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 ../../../main/resources/ehcache-core.xsd"> <cache-template name="activejdbc"> <key-type>java.lang.String</key-type> <value-type>java.lang.Object</value-type> <heap unit="entries">200</heap> </cache-template> </config>
有关更多涉及的配置选项,请参阅 EHCache v3 文档。
Redis缓存配置
缓存管理器类的名称:org.javalite.activejdbc.cache.RedisCacheManager
.
在文件中设置以下内容activejdbc.properties
:
cache.manager=org.javalite.activejdbc.cache.RedisCacheManager
此外,提供一个activejdbc-redis.properties
具有两个属性的属性文件:redis.cache.manager.host
和redis.cache.manager.port
属性文件需要位于类路径的根目录下。
限制: Redis 缓存管理器不支持
CacheManager#flush(CacheEvent)
值为ALL。