https://download.redis.io/releases/ 사용하려는 레디스 버전을 확인 후 다운로드

wget https://download.redis.io/releases/redis-3.0.7.tar.gz
tar xzf redis-3.0.7.tar.gz
make
make install PREFIX=/usr/local/redis

or

yum install redis-3.0.7

 

redis.conf 설정 (https://redis.io/docs/latest/operate/oss_and_stack/management/config/)

# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

################################## INCLUDES ###################################

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf

################################ GENERAL  #####################################

# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize no

# When running daemonized, Redis writes a pid file in /var/run/redis.pid by
# default. You can specify a custom pid file location here.
pidfile /var/run/redis.pid

# Accept connections on the specified port, default is 6379.
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379

```

 

sentinel.conf 설정 ( High availability with Redis Sentinel | Docs)

# Example sentinel.conf

# By default protected mode is disabled in sentinel mode. Sentinel is reachable
# from interfaces different than localhost. Make sure the sentinel instance is
# protected from the outside world via firewalling or other means.
protected-mode no

# port <sentinel-port>
# The port that this sentinel instance will run on
port 26379

# By default Redis Sentinel does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis-sentinel.pid when
# daemonized.
daemonize no

# When running daemonized, Redis Sentinel writes a pid file in
# /var/run/redis-sentinel.pid by default. You can specify a custom pid file
# location here.
pidfile /var/run/redis-sentinel.pid

# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
# nothing (nothing is logged)
loglevel notice

# Specify the log file name. Also the empty string can be used to force
# Sentinel to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile ""

# To enable logging to the system logger, just set 'syslog-enabled' to yes,
# and optionally update the other syslog parameters to suit your needs.
# syslog-enabled no

# Specify the syslog identity.
# syslog-ident sentinel

# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
# syslog-facility local0

# sentinel announce-ip <ip>
# sentinel announce-port <port>
#
# The above two configuration directives are useful in environments where,
# because of NAT, Sentinel is reachable from outside via a non-local address.
#
# When announce-ip is provided, the Sentinel will claim the specified IP address
# in HELLO messages used to gossip its presence, instead of auto-detecting the
# local address as it usually does.
#
# Similarly when announce-port is provided and is valid and non-zero, Sentinel
# will announce the specified TCP port.
#
# The two options don't need to be used together, if only announce-ip is
# provided, the Sentinel will announce the specified IP and the server port
# as specified by the "port" option. If only announce-port is provided, the
# Sentinel will announce the auto-detected local IP and the specified port.
#
# Example:
#
# sentinel announce-ip 1.2.3.4

# dir <working-directory>
# Every long running process should have a well-defined working directory.
# For Redis Sentinel to chdir to /tmp at startup is the simplest thing
# for the process to don't interfere with administrative tasks such as
# unmounting filesystems.
dir /tmp

# sentinel monitor <master-name> <ip> <redis-port> <quorum>
#
# Tells Sentinel to monitor this master, and to consider it in O_DOWN
# (Objectively Down) state only if at least <quorum> sentinels agree.
#
# Note that whatever is the ODOWN quorum, a Sentinel will require to
# be elected by the majority of the known Sentinels in order to
# start a failover, so no failover can be performed in minority.
#
# Replicas are auto-discovered, so you don't need to specify replicas in
# any way. Sentinel itself will rewrite this configuration file adding
# the replicas using additional configuration options.
# Also note that the configuration file is rewritten when a
# replica is promoted to master.
#
# Note: master name should not include special characters or spaces.
# The valid charset is A-z 0-9 and the three characters ".-_".
sentinel monitor mymaster 127.0.0.1 6379 2

# sentinel auth-pass <master-name> <password>
#
# Set the password to use to authenticate with the master and replicas.
# Useful if there is a password set in the Redis instances to monitor.
#
# Note that the master password is also used for replicas, so it is not
# possible to set a different password in masters and replicas instances
# if you want to be able to monitor these instances with Sentinel.
#
# However you can have Redis instances without the authentication enabled
# mixed with Redis instances requiring the authentication (as long as the
# password set is the same for all the instances requiring the password) as
# the AUTH command will have no effect in Redis instances with authentication
# switched off.
#
# Example:
#
# sentinel auth-pass mymaster MySUPER--secret-0123passw0rd

# sentinel auth-user <master-name> <username>
#
# This is useful in order to authenticate to instances having ACL capabilities,
# that is, running Redis 6.0 or greater. When just auth-pass is provided the
# Sentinel instance will authenticate to Redis using the old "AUTH <pass>"
# method. When also an username is provided, it will use "AUTH <user> <pass>".
# In the Redis servers side, the ACL to provide just minimal access to
# Sentinel instances, should be configured along the following lines:
#
#     user sentinel-user >somepassword +client +subscribe +publish \
#                        +ping +info +multi +slaveof +config +client +exec on

# sentinel down-after-milliseconds <master-name> <milliseconds>
#
# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
sentinel down-after-milliseconds mymaster 30000

# IMPORTANT NOTE: starting with Redis 6.2 ACL capability is supported for
# Sentinel mode, please refer to the Redis website https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/
# for more details.

# Sentinel's ACL users are defined in the following format:
#
#   user <username> ... acl rules ...
#
# For example:
#
#   user worker +@admin +@connection ~* on >ffa9203c493aa99
#
# For more information about ACL configuration please refer to the Redis
# website at https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/ and redis server configuration 
# template redis.conf.

# ACL LOG
#
# The ACL Log tracks failed commands and authentication events associated
# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked 
# by ACLs. The ACL Log is stored in memory. You can reclaim memory with 
# ACL LOG RESET. Define the maximum entry length of the ACL Log below.
acllog-max-len 128

# Using an external ACL file
#
# Instead of configuring users here in this file, it is possible to use
# a stand-alone file just listing users. The two methods cannot be mixed:
# if you configure users here and at the same time you activate the external
# ACL file, the server will refuse to start.
#
# The format of the external ACL user file is exactly the same as the
# format that is used inside redis.conf to describe users.
#
# aclfile /etc/redis/sentinel-users.acl

# requirepass <password>
#
# You can configure Sentinel itself to require a password, however when doing
# so Sentinel will try to authenticate with the same password to all the
# other Sentinels. So you need to configure all your Sentinels in a given
# group with the same "requirepass" password. Check the following documentation
# for more info: https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/
#
# IMPORTANT NOTE: starting with Redis 6.2 "requirepass" is a compatibility
# layer on top of the ACL system. The option effect will be just setting
# the password for the default user. Clients will still authenticate using
# AUTH <password> as usually, or more explicitly with AUTH default <password>
# if they follow the new protocol: both will work.
#
# New config files are advised to use separate authentication control for
# incoming connections (via ACL), and for outgoing connections (via
# sentinel-user and sentinel-pass) 
#
# The requirepass is not compatible with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.

# sentinel sentinel-user <username>
#
# You can configure Sentinel to authenticate with other Sentinels with specific
# user name. 

# sentinel sentinel-pass <password>
#
# The password for Sentinel to authenticate with other Sentinels. If sentinel-user
# is not configured, Sentinel will use 'default' user with sentinel-pass to authenticate.

# sentinel parallel-syncs <master-name> <numreplicas>
#
# How many replicas we can reconfigure to point to the new replica simultaneously
# during the failover. Use a low number if you use the replicas to serve query
# to avoid that all the replicas will be unreachable at about the same
# time while performing the synchronization with the master.
sentinel parallel-syncs mymaster 1

# sentinel failover-timeout <master-name> <milliseconds>
#
# Specifies the failover timeout in milliseconds. It is used in many ways:
#
# - The time needed to re-start a failover after a previous failover was
#   already tried against the same master by a given Sentinel, is two
#   times the failover timeout.
#
# - The time needed for a replica replicating to a wrong master according
#   to a Sentinel current configuration, to be forced to replicate
#   with the right master, is exactly the failover timeout (counting since
#   the moment a Sentinel detected the misconfiguration).
#
# - The time needed to cancel a failover that is already in progress but
#   did not produced any configuration change (SLAVEOF NO ONE yet not
#   acknowledged by the promoted replica).
#
# - The maximum time a failover in progress waits for all the replicas to be
#   reconfigured as replicas of the new master. However even after this time
#   the replicas will be reconfigured by the Sentinels anyway, but not with
#   the exact parallel-syncs progression as specified.
#
# Default is 3 minutes.
sentinel failover-timeout mymaster 180000

# SCRIPTS EXECUTION
#
# sentinel notification-script and sentinel reconfig-script are used in order
# to configure scripts that are called to notify the system administrator
# or to reconfigure clients after a failover. The scripts are executed
# with the following rules for error handling:
#
# If script exits with "1" the execution is retried later (up to a maximum
# number of times currently set to 10).
#
# If script exits with "2" (or an higher value) the script execution is
# not retried.
#
# If script terminates because it receives a signal the behavior is the same
# as exit code 1.
#
# A script has a maximum running time of 60 seconds. After this limit is
# reached the script is terminated with a SIGKILL and the execution retried.

# NOTIFICATION SCRIPT
#
# sentinel notification-script <master-name> <script-path>
# 
# Call the specified notification script for any sentinel event that is
# generated in the WARNING level (for instance -sdown, -odown, and so forth).
# This script should notify the system administrator via email, SMS, or any
# other messaging system, that there is something wrong with the monitored
# Redis systems.
#
# The script is called with just two arguments: the first is the event type
# and the second the event description.
#
# The script must exist and be executable in order for sentinel to start if
# this option is provided.
#
# Example:
#
# sentinel notification-script mymaster /var/redis/notify.sh

# CLIENTS RECONFIGURATION SCRIPT
#
# sentinel client-reconfig-script <master-name> <script-path>
#
# When the master changed because of a failover a script can be called in
# order to perform application-specific tasks to notify the clients that the
# configuration has changed and the master is at a different address.
# 
# The following arguments are passed to the script:
#
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#
# <state> is currently always "start"
# <role> is either "leader" or "observer"
# 
# The arguments from-ip, from-port, to-ip, to-port are used to communicate
# the old address of the master and the new address of the elected replica
# (now a master).
#
# This script should be resistant to multiple invocations.
#
# Example:
#
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

# SECURITY
#
# By default SENTINEL SET will not be able to change the notification-script
# and client-reconfig-script at runtime. This avoids a trivial security issue
# where clients can set the script to anything and trigger a failover in order
# to get the program executed.

sentinel deny-scripts-reconfig yes

# REDIS COMMANDS RENAMING (DEPRECATED)
#
# WARNING: avoid using this option if possible, instead use ACLs.
#
# Sometimes the Redis server has certain commands, that are needed for Sentinel
# to work correctly, renamed to unguessable strings. This is often the case
# of CONFIG and SLAVEOF in the context of providers that provide Redis as
# a service, and don't want the customers to reconfigure the instances outside
# of the administration console.
#
# In such case it is possible to tell Sentinel to use different command names
# instead of the normal ones. For example if the master "mymaster", and the
# associated replicas, have "CONFIG" all renamed to "GUESSME", I could use:
#
# SENTINEL rename-command mymaster CONFIG GUESSME
#
# After such configuration is set, every time Sentinel would use CONFIG it will
# use GUESSME instead. Note that there is no actual need to respect the command
# case, so writing "config guessme" is the same in the example above.
#
# SENTINEL SET can also be used in order to perform this configuration at runtime.
#
# In order to set a command back to its original name (undo the renaming), it
# is possible to just rename a command to itself:
#
# SENTINEL rename-command mymaster CONFIG CONFIG

# HOSTNAMES SUPPORT
#
# Normally Sentinel uses only IP addresses and requires SENTINEL MONITOR
# to specify an IP address. Also, it requires the Redis replica-announce-ip
# keyword to specify only IP addresses.
#
# You may enable hostnames support by enabling resolve-hostnames. Note
# that you must make sure your DNS is configured properly and that DNS
# resolution does not introduce very long delays.
#
SENTINEL resolve-hostnames no

# When resolve-hostnames is enabled, Sentinel still uses IP addresses
# when exposing instances to users, configuration files, etc. If you want
# to retain the hostnames when announced, enable announce-hostnames below.
#
SENTINEL announce-hostnames no

# When master_reboot_down_after_period is set to 0, Sentinel does not fail over
# when receiving a -LOADING response from a master. This was the only supported
# behavior before version 7.0.
#
# Otherwise, Sentinel will use this value as the time (in ms) it is willing to
# accept a -LOADING response after a master has been rebooted, before failing
# over.

SENTINEL master-reboot-down-after-period mymaster 0

 

failover 시 slave 노드를 master 노드로 승격시키기 위해 설정된 정족수(quorum) 이상 개수의 동의가 필요한데 그에 맞는 sentinel을 여러 개 구성해야 한다. 최소 quorum은 2개이상, sentinel 3개 이상이어야 안정된 서비스가 보장된다.

처음 모든 sentinel.conf 설정 시 mymaster는 현재 role이 마스터인 노드의 정보만을 입력한다.


sentinel 사용 시 필요한 포트의 방화벽은 열어두어야 한다.

firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port protocol="tcp" port="26379" accept'
firewall-cmd --reload
firewall-cmd --list-rich-rules

redis-cli -a password info를 통해 연결된 정보를 확인

# Replication
role:master
connected_slaves:2
slave0:ip=node1,port=6379,state=online,offset=13633266,lag=0
slave1:ip=node2,port=6379,state=online,offset=13633266,lag=1

 

sentinel.conf sentinel이 자동으로 작성한 정보 확인

# Generated by CONFIG REWRITE
sentinel leader-epoch mymaster 255
sentinel known-slave mymaster node1 6379
sentinel known-slave mymaster node2 6379
sentinel known-sentinel mymaster node1 26379 uid1
sentinel known-sentinel mymaster node2 26379 uid2
sentinel current-epoch 255

failover sentinel 로그

14698:X 21 Oct 12:10:42.132 # +sdown master mymaster node1 6379
14698:X 21 Oct 12:10:42.223 # +odown master mymaster node1 6379 #quorum 2/2
14698:X 21 Oct 12:10:42.223 # +new-epoch 256
14698:X 21 Oct 12:10:42.223 # +try-failover master mymaster node1 6379
14698:X 21 Oct 12:10:42.224 # +vote-for-leader node1Uid 256
14698:X 21 Oct 12:10:42.272 # node2Uid voted for node1Uid 256
14698:X 21 Oct 12:10:42.280 # +elected-leader master mymaster node1 6379
14698:X 21 Oct 12:10:42.280 # +failover-state-select-slave master mymaster node1 6379
14698:X 21 Oct 12:10:42.352 # +selected-slave slave node2:6379 node2 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:42.352 * +failover-state-send-slaveof-noone slave node2:6379 node2 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:42.390 # node3Uid voted for node1Uid 256
14698:X 21 Oct 12:10:42.435 * +failover-state-wait-promotion slave node2:6379 node2 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:43.135 # +promoted-slave slave node2:6379 node2 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:43.135 # +failover-state-reconf-slaves master mymaster node1 6379
14698:X 21 Oct 12:10:43.206 * +slave-reconf-sent slave node3:6379 node3 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:43.273 * +slave-reconf-inprog slave node3:6379 node3 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:43.466 # -odown master mymaster node1 6379
14698:X 21 Oct 12:10:49.253 # +new-epoch 257
14698:X 21 Oct 12:10:53.146 # +failover-end-for-timeout master mymaster node1 6379
14698:X 21 Oct 12:10:53.146 # +failover-end master mymaster node1 6379
14698:X 21 Oct 12:10:53.146 * +slave-reconf-sent-be slave node3:6379 node3 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:53.146 * +slave-reconf-sent-be slave node2:6379 node2 6379 @ mymaster node1 6379
14698:X 21 Oct 12:10:53.146 # +switch-master mymaster node1 6379 node2 6379
14698:X 21 Oct 12:10:53.147 * +slave slave node3:6379 node3 6379 @ mymaster node2 6379
14698:X 21 Oct 12:10:53.147 * +slave slave node1:6379 node1 6379 @ mymaster node2 6379
14698:X 21 Oct 12:10:58.182 # +sdown slave node1:6379 node1 6379 @ mymaster node2 6379

로그 파일이 하나로 append되기 때문에 logrotate로 일자별 분리

sentinel.conf, redis.conf 설정에서 pidfile "/var/run/sentinel.pid" 위치를 지정하는 편이 편함

sudo vi /etc/logrotate.d/redis-sentinel

# redis-sentinel content
/var/log/redis/sentinel.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    dateext
    dateformat -%Y-%m-%d
    sharedscripts
    postrotate
        # Sentinel PID 파일 위치 확인 필요
        if [ -f /var/run/redis/sentinel.pid ]; then
            kill -HUP $(cat /var/run/redis/sentinel.pid)
        else
            kill -HUP $(pidof redis-sentinel)
        fi
    endscript
}

sudo logrotate -f /etc/logrotate.d/redis-sentinel

Spring 4.0.5 기준 CacheErrorHandler(since 4.1)가 없기 때문에 직접 예외처리, circuitBreaker같은 기능도 추가

import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCache;

public class SafeRedisCache implements Cache {

    private final static Logger logger = LoggerFactory.getLogger(SafeRedisCache.class);

    private final RedisCache delegate;

    private final int failureThreshold;
    private final long openStateDurationMillis;
    private final AtomicInteger failureCount = new AtomicInteger(0);
    private volatile long lastFailureTime = 0L;
    private volatile boolean circuitState = false;

    public SafeRedisCache(RedisCache delegate) {
        this(delegate, 5, 60000L);
    }

    public SafeRedisCache(RedisCache delegate, int failureThreshold, long openStateDurationMillis) {
        this.delegate = delegate;
        this.failureThreshold = failureThreshold;
        this.openStateDurationMillis = openStateDurationMillis;
    }

    @Override
    public String getName() {
        return delegate.getName();
    }

    @Override
    public Object getNativeCache() {
        return delegate.getNativeCache();
    }

    @Override
    public ValueWrapper get(Object key) {
        if (isOpenCircuit()) {
            return null;
        }
        try {
            ValueWrapper valueWrapper = delegate.get(key);
            onSuccess();
            return valueWrapper;
        } catch (RuntimeException e) {
            onFailure(e);
            return null;
        }
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        if (isOpenCircuit()) {
            return null;
        }
        try {
            T value = (T) delegate.get(key);
            onSuccess();
            return value;
        } catch (RuntimeException e) {
            onFailure(e);
            return null;
        }
    }

    @Override
    public void put(Object key, Object value) {
        if (isOpenCircuit()) {
            return;
        }
        try {
            delegate.put(key, value);
            onSuccess();
        } catch (RuntimeException e) {
            onFailure(e);
        }
    }

    @Override
    public void evict(Object key) {
        if (isOpenCircuit()) {
            return;
        }
        try {
            delegate.evict(key);
            onSuccess();
        } catch (RuntimeException e) {
            onFailure(e);
        }
    }

    @Override
    public void clear() {
        if (isOpenCircuit()) {
            return;
        }
        try {
            delegate.clear();
            onSuccess();
        } catch (RuntimeException e) {
            onFailure(e);
        }
    }

    private void onFailure(RuntimeException e) {
        int failures = failureCount.incrementAndGet();
        if (!circuitState && failures >= failureThreshold) {
            openCircuit();
        }
    }

    private void onSuccess() {
        circuitState = false;
        failureCount.set(0);
    }

    private void openCircuit() {
        circuitState = true;
        lastFailureTime = System.currentTimeMillis();
    }

    private boolean isOpenCircuit() {
        if (circuitState) {
            long now = System.currentTimeMillis();
            if (now - lastFailureTime >= openStateDurationMillis) {
                circuitState = false;
                return false;
            }
            return true;
        }
        return false;
    }
}

 

cache value별로 expire을 관리할 수 있다. setUsePrefix를 사용해야 @Cacheable(value = "")로 등록한 값이 key값 앞에 자동으로 붙는다.

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.cache.DefaultRedisCachePrefix;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;

public class SafeRedisCacheManager implements CacheManager {

    private final RedisCacheManager delegate;

    public SafeRedisCacheManager(RedisTemplate redisTemplate, DefaultRedisCachePrefix defaultRedisCachePrefix) {
        this.delegate = new RedisCacheManager(redisTemplate);
        delegate.setUsePrefix(true);
        delegate.setCachePrefix(defaultRedisCachePrefix);
        delegate.setDefaultExpiration(3600L);

        Map<String, Long> expires = new HashMap<String, Long>();
        expires.put("code", 3600L);
        expires.put("leftMenu", 86400L);
        expires.put("topMenu", 86400L);
        delegate.setExpires(expires);
    }

    @Override
    public Cache getCache(String name) {
        Cache original = delegate.getCache(name);
        if (original instanceof RedisCache) {
            return new SafeRedisCache((RedisCache) original);
        }
        return original;
    }

    @Override
    public Collection<String> getCacheNames() {
        return delegate.getCacheNames();
    }
}

 

Redis cache 사용하도록 spring 빈 등록

<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:cache="http://www.springframework.org/schema/cache"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">	
    
    ```
    
    <cache:annotation-driven cache-manager="cacheManager"/>

	<beans:bean id="masterNode" class="org.springframework.data.redis.connection.RedisNode">
		<beans:property name="name" value="mymaster"/>
	</beans:bean>

	<beans:bean id="redisSentinelConfig" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
		<beans:property name="master" ref="masterNode"/>
		<beans:property name="sentinels">
			<beans:set>
				<beans:bean class="org.springframework.data.redis.connection.RedisNode">
					<beans:constructor-arg value="node1-ip"/>
					<beans:constructor-arg value="26379"/>
				</beans:bean>
				<beans:bean class="org.springframework.data.redis.connection.RedisNode">
					<beans:constructor-arg value="node2-ip"/>
					<beans:constructor-arg value="26379"/>
				</beans:bean>
				<beans:bean class="org.springframework.data.redis.connection.RedisNode">
					<beans:constructor-arg value="node3-ip"/>
					<beans:constructor-arg value="26379"/>
				</beans:bean>
			</beans:set>
		</beans:property>
	</beans:bean>

	<beans:bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
		<beans:constructor-arg type="org.springframework.data.redis.connection.RedisSentinelConfiguration" ref="redisSentinelConfig"/>
		<beans:property name="password" value="password"/>
		<beans:property name="usePool" value="true"/>
		<beans:property name="timeout" value="100"/>
	</beans:bean>

	<beans:bean id="jacksonSerializer" class="com.mypackage.JacksonJsonRedisSerializer"/>

	<beans:bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
		<beans:property name="connectionFactory" ref="redisConnectionFactory"/>
		<beans:property name="keySerializer">
			<beans:bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
		</beans:property>
		<beans:property name="hashKeySerializer">
			<beans:bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
		</beans:property>
		<beans:property name="valueSerializer" ref="jacksonSerializer"/>
		<beans:property name="hashValueSerializer" ref="jacksonSerializer"/>
	</beans:bean>

	<beans:bean id="redisCachePrefix" class="org.springframework.data.redis.cache.DefaultRedisCachePrefix"/>

	<beans:bean id="cacheManager" class="com.mypackage.SafeRedisCacheManager">
		<beans:constructor-arg ref="redisTemplate"/>
		<beans:constructor-arg ref="redisCachePrefix"/>
	</beans:bean>

 

예외를 try-catch문으로 잡았는데 롤백이 되는 이유가 뭘까..

 

결제 API를 사용한 결제 로직이 끝나면 결제 성공/실패 문자를 발송하도록 구현했다. 이때 문자 발송 시 예외가 발생하더라도 결제는 완료되도록 하고 싶었다. Spring의 기본 트랜잭션 전파 속성이 REQUIRED로 안쪽 트랜잭션이 롤백되면 바깥쪽 트랜잭션도 롤백된다는것을 알고 문자 발송 서비스 호출하는 부분을 try-catch문을 사용해서 예외를 처리했다. 당연히 예외를 처리해서 롤백되지 않을거라 생각했고 문제없이 결제가 완료될 것이라고 생각했지만 전부 롤백되고 있었다.

 

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:720) ~[spring-tx-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:478) ~[spring-tx-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:272) ~[spring-tx-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) ~[spring-tx-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98) ~[spring-tx-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262) ~[spring-tx-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) ~[spring-tx-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.0.5.RELEASE.jar:4.0.5.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644) ~[spring-aop-4.0.5.RELEASE.jar:4.0.5.RELEASE]

 

Transaction rolled back because it has been marked as rollback-only가 뭘까.. 궁금해서 디버깅을 해봤다..

 

1. TransactionAspectSupport.class

protected void completeTransactionAfterThrowing(TransactionAspectSupport.TransactionInfo txInfo, Throwable ex) {
        if (txInfo != null && txInfo.hasTransaction()) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex);
            }

            if (txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); //breakPoint
                } catch (TransactionSystemException var7) {
                    this.logger.error("Application exception overridden by rollback exception", ex);
                    var7.initApplicationException(ex);
                    throw var7;
                } catch (RuntimeException var8) {
                    this.logger.error("Application exception overridden by rollback exception", ex);
                    throw var8;
                } catch (Error var9) {
                    this.logger.error("Application exception overridden by rollback error", ex);
                    throw var9;
                }
            } else {
                try {
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                } catch (TransactionSystemException var4) {
                    this.logger.error("Application exception overridden by commit exception", ex);
                    var4.initApplicationException(ex);
                    throw var4;
                } catch (RuntimeException var5) {
                    this.logger.error("Application exception overridden by commit exception", ex);
                    throw var5;
                } catch (Error var6) {
                    this.logger.error("Application exception overridden by commit error", ex);
                    throw var6;
                }
            }
        }

    }

 

2. AbstractPlatformTransactionManager

private void processRollback(DefaultTransactionStatus status) {
        try {
            try {
                this.triggerBeforeCompletion(status);
                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        this.logger.debug("Rolling back transaction to savepoint");
                    }

                    status.rollbackToHeldSavepoint();
                } else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        this.logger.debug("Initiating transaction rollback");
                    }

                    this.doRollback(status);
                } else if (status.hasTransaction()) {
                    if (!status.isLocalRollbackOnly() && !this.isGlobalRollbackOnParticipationFailure()) {
                        if (status.isDebug()) {
                            this.logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
                        }
                    } else {
                        if (status.isDebug()) {
                            this.logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
                        }

                        this.doSetRollbackOnly(status); //breakPoint
                    }
                } else {
                    this.logger.debug("Should roll back transaction but cannot - no transaction available");
                }
            } catch (RuntimeException var7) {
                this.triggerAfterCompletion(status, 2);
                throw var7;
            } catch (Error var8) {
                this.triggerAfterCompletion(status, 2);
                throw var8;
            }

            this.triggerAfterCompletion(status, 1);
        } finally {
            this.cleanupAfterCompletion(status);
        }

    }

 

3. DataSourceTransactionManager.class

protected void doSetRollbackOnly(DefaultTransactionStatus status) {
        DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)status.getTransaction();
        if (status.isDebug()) {
            this.logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + "] rollback-only");
        }

        txObject.setRollbackOnly(); //breakPoint
    }
    
    private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport {
        private boolean newConnectionHolder;
        private boolean mustRestoreAutoCommit;

        private DataSourceTransactionObject() {
        }

        public void setConnectionHolder(ConnectionHolder connectionHolder, boolean newConnectionHolder) {
            super.setConnectionHolder(connectionHolder);
            this.newConnectionHolder = newConnectionHolder;
        }

        public boolean isNewConnectionHolder() {
            return this.newConnectionHolder;
        }

        public void setMustRestoreAutoCommit(boolean mustRestoreAutoCommit) {
            this.mustRestoreAutoCommit = mustRestoreAutoCommit;
        }

        public boolean isMustRestoreAutoCommit() {
            return this.mustRestoreAutoCommit;
        }

        public void setRollbackOnly() {
            this.getConnectionHolder().setRollbackOnly(); //breakPoint
        }

        public boolean isRollbackOnly() {
            return this.getConnectionHolder().isRollbackOnly();
        }
    }

 

4. ResourceHolderSupport.class

public void setRollbackOnly() {
        this.rollbackOnly = true; //breakPoint
}

 

5. TransactionAspectSupport.class

protected void commitTransactionAfterReturning(TransactionAspectSupport.TransactionInfo txInfo) {
        if (txInfo != null && txInfo.hasTransaction()) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
            }

            txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); //breakPoint
        }

    }

 

 

6. AbstractPlatformTransactionManager.class

public final void commit(TransactionStatus status) throws TransactionException {
        if (status.isCompleted()) {
            throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");
        } else {
            DefaultTransactionStatus defStatus = (DefaultTransactionStatus)status;
            if (defStatus.isLocalRollbackOnly()) {
                if (defStatus.isDebug()) {
                    this.logger.debug("Transactional code has requested rollback");
                }

                this.processRollback(defStatus);
            } else if (!this.shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
                if (defStatus.isDebug()) {
                    this.logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
                }

                this.processRollback(defStatus); //breakPoint
                if (status.isNewTransaction() || this.isFailEarlyOnGlobalRollbackOnly()) {
                    throw new UnexpectedRollbackException("Transaction rolled back because it has been marked as rollback-only");
                }
            } else {
                this.processCommit(defStatus);
            }
        }
    }

 

정리하면.. 트랜잭션 전파 속성이 REQUIRED일 경우 예외가 발생하면 rollbackOnly 값을 true로 마킹해 나중에 커밋할 때마킹된 값을 확인하여 전부 롤백하고 있었다.

어노테이션 기반 캐싱

 

캐시를 선언하기 위한 Spring caching abstraction에서 제공하는 어노테이션 셋이 있다.

  • @Cacheable: 캐시 저장 트리거
  • @CacheEvict: 캐시 삭제 트리거
  • @CachePut: 캐시 업데이트
  • @Caching: 여러 캐시 작업을 그룹화
  • @CacheConfig: 클래스 수준에서 공통적인 캐시 설정

 

@Cacheable

 

어노테이션 이름과 같이 캐싱 될(cacheable) 메서드를 지정할 때 사용된다. 캐싱 될 메서드로 지정된다는 의미는 메서드의 결과값이 캐시에 저장되고 이후에 같은 인자 값이 넘어온 경우에 메서드를 호출하지 않고 캐시에 저장된 값을 바로 반환한다.

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

 

위 코드에서 findBook 메서드는 books 이름의 캐시와 연결되어 있다. 메서드가 호출될 떄마다 캐시를 검사하여 반복할 필요가 없는지 확인한다.

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}

 

위 코드와 같이 2개 이상의 캐시가 사용되도록 여러 이름을 지정할 수 있다. 이떄는 적어도 하나의 캐시에 적중되면 그 값이 반환된다.

 

Key Generation

 

캐시는 본질적으로  key-value 구조로 저장하기 떄문에 캐싱 될 메서드를 호출할 떄마다 캐시에 접근하기 위한 키가 필요하다. 이떄 Spring caching abstraction은 간단한 키 생성 알고리즘을 따른다.

  • 파라미터가 없으면 SimpleKey.EMPTY
  • 파라미터가 1개 있으면 그 파라미터의 인스턴스
  • 파라미터가 2개 이상 있으면 모든 파라미터를 포함한 SimpleKey

이 접근 방식은 파라미터에 자연키가 있고 유효한 hashCode(), equals() 메서드를 구현한 경우에 잘 동작하지만 아닐 경우 다른 전략을 찾아야 한다.

 

Custom Key Generation Declaration

 

결국 같은 인자 값이라는 말은 Default Key Generation으로 생성된 키 값이 같다는 말과 같다. 그러므로 여러 인자들 중 하나만이라도 값이 달라지면 Default Key Geneartion으로 생성된 키 값이 달라져 메서드를 호출해 결과값을 캐시에 저장한다. 만약 하나의 인자만 중요하고 나머지 인자들은 캐시에 쓸모가 없다면 어떻게 해야 할까?

 

이러한 경우 @Cacheable 속성 중 key 속성 값을 지정하여 Custom key가 생성되는 방식을 지정할 수 있다.

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

 

checkWarehouse, includeUsed 인자 값이 달라져도 isbn인자 값이 변경되지 않으면 캐시에 영향을 주지 않는다.

 

CacheManager

 

다양한 캐시 관리를 위해 여러개의 cacheManager을 사용하고 있는 경우 cacheManager 속성 값을 이용해서 캐시별 관리자를 지정할 수 있다.

@Cacheable(cacheNames = "customers")
public Customer getCustomerDetail(Integer customerId) {
    return customerDetailRepository.getCustomerDetail(customerId); //CaffeineCacheManager
}

@Cacheable(cacheNames = "customerOrders", cacheManager = "alternateCacheManager")
public List<Order> getCustomerOrders(Integer customerId) {
    return customerDetailRepository.getCustomerOrders(customerId); //ConcurrentMapCacheManager
}

 

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig {

    @Bean
    @Primary
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }
}

 

Synchronized caching
@Cacheable(cacheNames="foos", sync=true) 
public Foo executeExpensiveOperation(String id) {...}

 

Conditional caching

 

캐싱될 메서드로 지정하더라도 목적에따라 결과를 캐시에 저장하지 않을 수 있다.

@Cacheable(cacheNames="book", condition="#name.length() < 32") 
public Book findBook(String name)

 

위 코드는 name 인자 값의 길이가 32보다 짧은 경우에만 캐시에 저장하도록 조건을 추가한 경우이다.

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") 
public Book findBook(String name)

 

위 코드는 unless 속성 값을 설정하여 결과 값이 hardback인 경우 캐시에 저장하지 않도록 조건을 추가한 경우이다.

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)

 

반환 타입을 Optional로 지정할 수 있다.

 

@CachePut

 

@Cacheable 어노테이션은 캐시를 사용하여 메서드 호출을 최소화 하지만 @CachePut 어노테이션은 캐시를 업데이트 하기 위해 메서드 호출을 강제한다.

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

 

@CacheEvict

 

캐시에서 오래되거나 사용되지 않는 데이터를 삭제할 때 사용된다. allEntries 속성 값에 따라 key 기반 데이터 삭제가 아닌 캐시에 저장된 모든 데이터를 삭제 할 수  있다.

@CacheEvict(cacheNames="books", allEntries=true) 
public void loadBooks(InputStream batch)

 

@Caching

 

캐시 어노테이션을 여러개 추가하고 싶을때 사용된다.

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

 

@CacheConfig

 

메서드별로 어노테이션을 추가할떄 공통적인 속성 값을 매번 설정하는 것은 비효율적이다. @CacheConfig 어노테이션을 사용하여 클래스 레밸에서 공통적인 캐시 설정을 할 수 있다. 주로 cache names, custom keyGenerator, custom cacheManager, custom cacheResolver을 설정한다.

@CacheConfig("books") 
public class BookRepositoryImpl implements BookRepository {

	@Cacheable
	public Book findBook(ISBN isbn) {...}
}

 

Enabling Caching Annotations

 

Spring cache를 사용하기 위해 필수 설정, @EnableCaching 어노테이션 추가

 

 

출처:

https://docs.spring.io/spring-framework/reference/integration/cache/annotations.html

 

Declarative Annotation-based Caching :: Spring Framework

The caching abstraction lets you use your own annotations to identify what method triggers cache population or eviction. This is quite handy as a template mechanism, as it eliminates the need to duplicate cache annotation declarations, which is especially

docs.spring.io

https://www.baeldung.com/spring-multiple-cache-managers

 

인텔리제이 사용하는 경우
  • 톰캣 설정 Deployment에 추가

 

이클립스 사용하는 경우
  • server.xml 추가
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
	<Context docBase="업로드 경로" path="/upload" reloadable="false"/>
</Host>

img 태그 src 값에 '/upload/하위경로'으로 설정하면 해당 이미지를 불러옴

Spring-JDBC re-throwing SQLException as DataAccessException 

 

  • 추상클래스인 DataAccessException의 구현체 UncategorizedSQLException을 사용해서 예외를 SQLException으로 잡을 수 있다. 
  • Wrapper되지 않은 SQL Server에서 발생한 경고 또는 오류만 잡고 싶을때 사용한다. 

 

출처: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/UncategorizedSQLException.html

Datasource.xml

<bean id="datasource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="" />
    <property name="url" value="" />
    <property name="username" value="" />
    <property name="password" value="" />
    
    <property name="initialSize" value="" />
    <property name="validationQuery" value="" />
    <property name="removeAbandonedTimeout" value="" />
    <property name="removeAbandoned" value="" />
    <property name="logAbandoned" value="" />
    <property name="maxWait" value="" />
    <property name="maxIdle" value="" />
    <property name="maxActive" value="" />
    <property name="minEvictableIdleTimeMillis" value="" />
    <property name="testOnBorrow" value="" />
    <property name="testWhileIdle" value="" />
    <property name="timeBetweenEvictionRunsMillis" value="" />
</bean>

<bean id="datasource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="" />
    <property name="url" value="" />
    <property name="username" value="" />
    <property name="password" value="" />
    
    <property name="initialSize" value="" />
    <property name="validationQuery" value="" />
    <property name="removeAbandonedTimeout" value="" />
    <property name="removeAbandoned" value="" />
    <property name="logAbandoned" value="" />
    <property name="maxWait" value="" />
    <property name="maxIdle" value="" />
    <property name="maxActive" value="" />
    <property name="minEvictableIdleTimeMillis" value="" />
    <property name="testOnBorrow" value="" />
    <property name="testWhileIdle" value="" />
    <property name="timeBetweenEvictionRunsMillis" value="" />
</bean>
  • initialSize: 디비 커넥션 풀의 커넥션 개수 설정
  • validationQuery: 커넥션 풀의 연결을 확인하는데 사용되는 쿼리
  • removeAbandoned: true일 경우 removeAbandonedTimeout에 지정된 시간보다 오래 할당된 커넥션을 유효한지 여부를 검사하고 유효하지 않으면 커넥션 풀에서 제거
  • removeAbandonedTimeout: 커넥션 풀 클리너가 
  • maxWait: 커넥션을 할당 받기까지 대기하는 시간
  • maxIdle: 커넥션 풀에 유지될 수 있는 비활성화 커넥션의 최대 개수
  • maxActive: 커넥션 풀이 제공할 커넥션의 최대 개수
  • minEvictableIdleTimeMillis: 비활성화 상태를 유지하는 커넥션의 추출 대기 시간
  • testOnBorrow: true일 경우 커넥션 풀에서 커넥션을 사용할 때 그 커넥션이 유효한지 여부를 검사
  • testWhileIdle:  true일 경우 비활성화 상태인 커넥션을 추출하여 유효한지 여부를 검사하고 유효하지 않으면 커넥션 풀에서 제거
  • timeBetweenEvictionRunsMillis: 비활성화 상태인 커넥션을 추출하는 쓰레드의 실행 주기를 지정
  • 출처: https://commons.apache.org/proper/commons-dbcp/apidocs/src-html/org/apache/commons/dbcp2/BasicDataSource.html#line.67

mybatis.xml

<bean id="sqlSessionFactory" name="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="datasource1" />
    <property name="configLocation" value="" />
    <property name="mapperLocations" value="" />
</bean>

<bean id="sqlSessionTemplate" name="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.base.datasource1" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate" />
</bean>

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="datasource1" />
</bean>

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    	<tx:method name="*" rollback-for="Exception"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="requiredTx" expression="execution(* com.base.datasource1..service..*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
</aop:config>
	

<bean id="sqlSessionFactory2" name="sqlSessionFactory2" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="datasource2" />
    <property name="configLocation" value="" />
    <property name="mapperLocations" values="" />
</bean>

<bean id="sqlSessionTemplate2" name="sqlSessionTemplate2" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory2" />
</bean>

<bean id="mapperScanner2" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.base.datasource2" />
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory2"/>
    <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate2" />
</bean>

<bean id="txManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="datasource2" />
</bean>

<tx:advice id="txAdvice2" transaction-manager="txManager2">
    <tx:attributes>
    	<tx:method name="*" rollback-for="Exception"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="requiredTx2" expression="execution(* com.base.datasource2..service..*.*(..))"/>
    <aop:advisor advice-ref="txAdvice2" pointcut-ref="requiredTx2" />
</aop:config>

+ Recent posts