有时候我们在操作数据的时候会更改多张表的数据,我们往往期望的结果是要么都修改成功,要么都修改失败。这个时候就会使用数据库事务,spring/spring boot框架对事务有较好的支持。随着业务的不断拓展、用户量、数据量不断的扩张,网站总会遇上性能问题,这个时候缓存就上场了。redis是不错的缓存组件,无论你的架构是简单的存储还是需要高可用甚至是数据分片集群,redis都能很好的满足你的需求。
通常使用缓存,是提不到事务的高度的,毕竟缓存有的数据,数据库都有,reload一下就可以了。但是有的业务场景对缓存的要求可能就会更高了,比如促销、活动规则等的一些缓存,这些缓存访问频度非常高,缓存的重要程度在一些系统架构设计中甚至高过了数据库。
在一些缓存和数据库同时使用的业务系统中,有时候我们希望更新数据库的数据同时能够更新缓存的数据。但是这是两个不同的数据源,有时候数据库更新成功了,缓存却更新失败,而对于时间敏感的客户端请求通常是直接访问缓存的,这样一来,缓存和数据库数据不一致性导致的错误可能是比较严重的。
幸运的时,我们现在可以找到不错的解决方案。redis是支持事务的,相关命令可参考:
http://doc.redisfans.com/transaction/index.html
springboot或spring中使用编程式事务和声明式事务都能很好的支持数据库事务。声明式事务使我们推荐的,如果我们要结合redis事务和spring事务,我们该如何处理呢?我们以比较不错的的开源组件redisson为例来说一下。
思路是这样的,我们通常在修改数据库后会更新缓存,我们再这个修改数据库的事务中内嵌一个redis事务,如下:
@Transactional
public void transactionalDemo(){
saveToDb();
saveToCache();
}
上面的代码中,saveToDb使用的是数据库事务,saveToCache使用的是redis事务。注解@Transactional声明了整个方法transactionalDemo是一个事务。由于spring声明式事务包裹了redis的事务,spring声明式事务在我们现有的系统仅针对数据库事务有效,所以只要saveToCache方法执行抛出异常,那么数据库事务也会回滚(rollback)。
redis事务使用很简单,可以参考上文提到的命令,我们使用了redisson,redisson也对redis事务做了一定的支持,虽然功能还不是很多,但足够我们使用。使用redisson创建事务非常简单:
public void saveToCache(){
RTransaction transaction = redissonClient.createTransaction(TransactionOptions.defaults());
RBucket<Integer> bucket = transaction.getBucket(TEST);
bucket.delete();
bucket.set(1);
bucket.set(2);
transaction.commit();
}
如果你在commit之前有抛出异常,或者超时未提交事务,所有的redis指令都不会执行,同时还会抛出异常,外层的事务捕捉到后也不会执行数据库的任何写操作。
为了对redis的操作简单,你甚至可以简单封装一下redisson相关的api,如下是部分代码,主要针对Set(集合)和桶(Redis中的String),其他的可根据需要自行完善:
package cn.lovecto.promotion;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.redisson.api.RBucket;
import org.redisson.api.RSetCache;
import org.redisson.api.RTransaction;
import org.redisson.api.RedissonClient;
import org.redisson.api.TransactionOptions;
/**
* redis事务管理
*
*/
public class Transaction {
/** 默认缓存1小时 */
public static long DEFAULT_CACHE_MILLISECONDS = 1 * 60 * 60 * 1000;
/** redisson的事务管理器 */
private RTransaction transaction;
/** 事务选项 */
private TransactionOptions options = TransactionOptions.defaults();
/** 私有构造方法 */
private Transaction(RedissonClient redisson) {
transaction = redisson.createTransaction(options);
}
/**
* 带参数的事务构造器,所有时间单位均为毫秒
*
* @param redisson
* @param responseTimeout
* 提交事务后的响应超时时间
* @param retryAttempts
* 提交失败尝试次数
* @param retryInterval
* 尝试发送事务的时间间隔
* @param syncTimeout
* 同步数据超时时间
* @param timeout
* 如果事务在这个世界内没有提交,将会自动回滚,-1关闭此功能
*/
private Transaction(RedissonClient redisson, long responseTimeout,
int retryAttempts, long retryInterval, long syncTimeout,
long timeout) {
options.responseTimeout(responseTimeout, TimeUnit.MILLISECONDS)
.retryAttempts(retryAttempts)
.retryInterval(retryInterval, TimeUnit.MILLISECONDS)
.syncSlavesTimeout(syncTimeout, TimeUnit.MILLISECONDS)
.timeout(timeout, TimeUnit.MILLISECONDS);
transaction = redisson.createTransaction(options);
}
/** 静态实例方法 */
public static Transaction of(RedissonClient redisson) {
return new Transaction(redisson);
}
/**
* 带参数的事务构造器,所有时间单位均为毫秒
*
* @param redisson
* @param responseTimeout
* 提交事务后的响应超时时间
* @param retryAttempts
* 提交失败尝试次数
* @param retryInterval
* 尝试发送事务的时间间隔
* @param syncTimeout
* 同步数据超时时间
* @param timeout
* 如果事务在这个世界内没有提交,将会自动回滚,-1关闭此功能
*/
public static Transaction of(RedissonClient redisson, long responseTimeout,
int retryAttempts, long retryInterval, long syncTimeout,
long timeout) {
return new Transaction(redisson, responseTimeout, retryAttempts,
retryInterval, syncTimeout, timeout);
}
/**
* 存入redis
*
* @param key
* @param value
* @return
*/
public <V> Transaction set(String key, V value) {
RBucket<V> bucket = transaction.getBucket(key);
bucket.set(value);
return this;
}
/**
* 删除key,针对redis的String数据结构操作
*
* @param key
* @return
*/
public <V> Transaction del(String key) {
RBucket<V> bucket = transaction.getBucket(key);
bucket.delete();
return this;
}
/**
* 缓存中添加元素,设置过期时间,如expiryTime不满足要求则使用默认时间
*
* @param key
* @param element
* @param expiryTime
*/
public <T> Transaction addToSet(String key, T element, Date expiryTime) {
RSetCache<T> set = transaction.getSetCache(key);
Date now = new Date();
long expiry = expiryTime.getTime() - now.getTime();
expiry = expiry > 0 ? expiry : DEFAULT_CACHE_MILLISECONDS;
set.add(element, expiry, TimeUnit.MILLISECONDS);
return this;
}
/**
* 获取缓存的集合
*
* @param key
* @return
*/
public <T> Set<T> getSet(String key) {
RSetCache<T> set = transaction.getSetCache(key);
return set.readAll();
}
/**
* 从集合中移除元素
*
* @param key
* @param element
*/
public <T> Transaction removeFromSet(String key, T element) {
RSetCache<T> set = transaction.getSetCache(key);
set.remove(element);
return this;
}
/**
* 清除整个集合
*
* @param key
*/
public <T> Transaction clearSet(String key) {
RSetCache<T> set = transaction.getSetCache(key);
set.delete();
return this;
}
/**
* 提交事务
*
* @return
*/
public Transaction commit() {
transaction.commit();
return this;
}
}
要使用上面的Transaction类,非常简单,如下:
Transaction tran = Transaction.of(redissonClient);
tran.set(TEST, 3);
tran.commit();
redis的事务是编程式事务,如果你感兴趣,也可以使用spring AOP的方式实现自定义注解,这样你也可以在你的缓存操作上加上类似@Transactional的注解实现声明式事务啦。通过spring和springboot框架,有了数据库事务和redis事务的结合,我们能够最大限度的保证在修改数据库的同时也修改了缓存数据,极大限度的保证了缓存和数据库的数据一致性。