热爱技术,追求卓越
不断求索,精益求精

spring数据库事务和基于redisson的redis缓存事务结合使用保持数据一致性

有时候我们在操作数据的时候会更改多张表的数据,我们往往期望的结果是要么都修改成功,要么都修改失败。这个时候就会使用数据库事务,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事务的结合,我们能够最大限度的保证在修改数据库的同时也修改了缓存数据,极大限度的保证了缓存和数据库的数据一致性。

赞(5)
未经允许不得转载:LoveCTO » spring数据库事务和基于redisson的redis缓存事务结合使用保持数据一致性

热爱技术 追求卓越 精益求精