Spring整合Redis做數據緩存(Windows環境)
當我們一個項目的數據量很大的時候,就需要做一些緩存機制來減輕數據庫的壓力,提升應用程序的性能,對於java項目來說,最常用的緩存組件有Redis、Ehcache和Memcached。
Ehcache是用java開發的緩存組件,和java結合良好,直接在jvm虛擬機中運行,不需要額外安裝什麽東西,效率也很高;但是由於和java結合的太緊密了,導致緩存共享麻煩,分布式集群應用不方便,所以比較適合單個部署的應用。
Redis需要額外單獨安裝,是通過socket訪問到緩存服務,效率比Ehcache低,但比數據庫要快很多很多,而且處理集群和分布式緩存方便,有成熟的方案,比較適合分布式集群部署的項目;也有很多的應用將Ehcache和Redis結合使用,做成二級緩存。
至於Memcached嘛和Redis很類似,功能方面嘛理論上來說沒有Redis強大(但對於我們來說也完全足夠了),因此我們這裏先不講,後面如果有時間在寫一篇關於Memcached的文章;由於我們後面會涉及到Tomcat的集群部署,所以這裏就先講講Redis的應用,好為後面的文章打個基礎~~下面正式開始!
代碼URL:http://git.oschina.net/tian5017/UserDemoRedis
一、Redis環境準備
Redis有中文官方網站,地址為http://www.redis.cn/
Redis沒有官方的Windows版本,但是微軟開源技術團隊(Microsoft Open Tech group)開發和維護著一個 Win64 的版本,下載地址為
https://github.com/MicrosoftArchive/redis/releases
截止文章完成之時,Redis的最新win64版本為3.2.100,我們這裏就是使用的此版本;下載安裝好之後,打開安裝目錄,如下
上圖紅框中標出的redis-cli.exe就是我們用來操作Redis的客戶端,在此安裝目錄下打開命令行窗口,輸入redis-cli.exe回車,如下
可以看到已經連接到了Redis,地址是127.0.0.1(本機),默認的端口為6379,至於操作Redis的命令,大家可以自己百度,常用的也就那麽幾個,很簡單,我們來簡單演示下
Redis是采用key-value鍵值對來存儲數據的,使用命令 set "haha" "123456",就表示將值(value)"123456"賦給鍵(key)"haha",再用 get "haha",就可以查看到值,好了關於Redis的別的東西,大家自己去玩,我們繼續我們的正題。
二、Spring集成
1、我們都知道,要在java中使用一個組件或者框架什麽的,第一步都是加載它的jar包,java中操作Redis的jar包叫做jedis,這裏我們還是利用上一篇文章《Spring+Mybatis+SpringMVC整合》所建立的UserDemo(項目連接:https://git.oschina.net/tian5017/UserDemo)
2、利用Maven來加載jedis的jar包,在UserDemo中的pom.xml的dependencies中添加如下代碼
<!-- redis相關 --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.8.1</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.7.10.RELEASE</version> </dependency>
除了jedis之外,還需要另外一個jar包spring-data-redis,這是讓Spring管理Redis用的;我們先來改造我們的代碼,試試往Redis裏面存點數據,再來查詢。
3、在/resources/下新建redis.properties文件
配置項如下
##Redis連接信息配置 #redis地址 cache.redis.host=127.0.0.1 #redis端口號 cache.redis.port=6379 #redis密碼 cache.redis.password= #redis使用的數據庫(Redis內置18個數據庫,編號為0-17,默認使用0) cache.redis.db=0 #redis鏈接超時時間 cache.redis.timeout=2000 #redis鏈接池中最大空閑數 cache.redis.maxIdle=5 #redis鏈接池中最大連接數 cache.redis.maxActive=20 #建立連接最長等待時間 cache.redis.maxWait=1000
4、在/resources/springConfig/下新建applicationContext-redis.xml文件作為Spring集成Redis的配置文件
配置代碼如下
<?xml version="1.0" encoding="UTF-8"?> <!-- 緩存redis相關配置 --> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="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/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxTotal" value="${cache.redis.maxActive}"/> <property name="maxIdle" value="${cache.redis.maxIdle}"/> <property name="maxWaitMillis" value="${cache.redis.maxWait}"/> <property name="testOnBorrow" value="${cache.redis.testOnBorrow}"/> </bean> <bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="usePool" value="true"/> <property name="hostName" value="${cache.redis.host}"/> <property name="port" value="${cache.redis.port}"/> <property name="password" value="${cache.redis.password}"/> <property name="timeout" value="${cache.redis.timeout}"/> <property name="database" value="${cache.redis.db}"/> <constructor-arg index="0" ref="jedisPoolConfig"/> </bean> <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" /> <bean id="redisCache" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="redisConnectionFactory"/> <property name="keySerializer" ref="stringRedisSerializer"/> <property name="valueSerializer" ref="stringRedisSerializer"/> <property name="hashKeySerializer" ref="stringRedisSerializer"/> <property name="hashValueSerializer" ref="stringRedisSerializer"/> </bean> </beans>
5、到這裏,我們的準備工作已經全部完成,接下來就是在代碼中使用redis了,我們修改UserServiceImpl.java
修改思路為,查詢數據的時候,先查詢Redis緩存,如果查到了就直接返回數據,如果沒查到數據,就去數據庫中查詢,查到了數據先緩存進Redis再返回,代碼如下:
package com.user.demo.service.impl; import com.alibaba.fastjson.JSON; import com.user.demo.dao.UserDao; import com.user.demo.entity.User; import com.user.demo.service.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Service接口實現類 */ @Service public class UserServiceImpl implements IUserService { @Resource private UserDao userDao; @Autowired private RedisTemplate<String, String> redisCache; @Override public List<User> findAll() { List<User> users = new ArrayList<User>(); //先從redis緩存中獲取數據,如果緩存中沒有,去數據庫中查詢數據,查到後在寫入緩存 Set<String> sets = redisCache.keys("USER*"); if(sets==null || sets.isEmpty()){ users = userDao.findAll(); if(!CollectionUtils.isEmpty(users)){ for(User user : users){ redisCache.opsForValue().set("USER"+user.getUserId(), JSON.toJSONString(user)); } } }else{ Iterator<String> it = sets.iterator(); while (it.hasNext()){ String item = it.next(); String value = redisCache.opsForValue().get(item); users.add(JSON.parseObject(value, User.class)); } } return users; } @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) //事物 public void saveUser(User user) { userDao.saveUser(user); } }
6、接下來我們來驗證緩存是否生效,編譯,運行項目,如下圖
打開redis-cli,輸入命令KEYS * (查看所有的key),如下
可以看到我們的緩存已經加入進去了,key就是我們在UserServiceImpl.java中設置的(redisCache.opsForValue().set("USER"+user.getUserId(), JSON.toJSONString(user))),以USER開頭加上userId構成,為了進一步驗證有了緩存之後,查詢的數據優先來源於緩存,我們在數據庫中刪除一條數據,就刪除userId為1的那條數據
刪除之後,由於我們代碼中檢測緩存的邏輯只是檢測了能否查詢到緩存,而沒有檢測緩存數量是否和數據庫數據量一致,所以緩存中還是會存在這條數據
可以看到,userId為1的數據依然存在,說明數據是從Redis緩存中查詢出來的。
三、數據同步
上面其實說明了一個問題,就是數據同步,我們很可能會出現,數據庫中數據變了,但是緩存中的數據還沒有變,因此和數據庫中的數據不一致,所以我們需要一些策略來保證數據庫的數據和Redis的數據能夠保持一致,至於選用什麽策略,那要具體項目具體分析,如果對數據的實時性要求很高的項目,那麽就要在查詢的時候,檢測數據庫的數據和Redis的緩存數據是否一致,如果不一致就要刷新Redis的數據,但是這樣必然對性能會有很高的要求;如果項目對數據的實時性要求沒有那麽高,我們完全可以做一個定時任務,比如每隔10分鐘或者半小時去數據庫拉一次數據,再刷新到Redis緩存中,所以下面我們就來做一個定時任務,每隔10分鐘去拉一次數據,然後往Redis中刷新一次。
四、定時刷新緩存
我們用Spring中的InitializingBean和DisposableBean接口(這兩個接口的具體用法請自行百度,大概就是在SpringBean的生命周期中,影響bean的行為,我們這裏就是影響了bean,讓它去刷新緩存)來實現刷新緩存,在com.user.demo下新建包cache,在cache下面新建類UserCache.java,具體代碼如下
package com.user.demo.cache; import com.alibaba.fastjson.JSON; import com.user.demo.dao.UserDao; import com.user.demo.entity.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.util.List; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Redis緩存User數據並定時刷新 * 每間隔一定時間後(如10分鐘)刷新一次緩存,刷新時另起一個線程,不影響執行主任務的線程 */ @Component public class UserCache implements InitializingBean, DisposableBean { private final Logger log = LoggerFactory.getLogger(UserCache.class); //獲取電腦的CPU核心數量,設置線程池大小為核心數的2倍(比如我的電腦為4核心,那麽這裏的值就為8) private static final int CORE_NUM = Runtime.getRuntime().availableProcessors() * 2; //初始化值為redis.properties中配置的cache.redis.cacheExpire的值,表示每隔多長時間後執行任務 @Value("${cache.redis.cacheExpire}") private long cacheExpire; //執行定時任務的類 private ScheduledThreadPoolExecutor executor = null; @Resource private RedisTemplate<String, String> redisCache; @Resource private UserDao userDao; @Override public void destroy() throws Exception { executor.shutdownNow(); } @Override public void afterPropertiesSet() throws Exception { executor = new ScheduledThreadPoolExecutor(CORE_NUM); RefreshCache refreshCache = new RefreshCache(); refreshCache.run(); executor.scheduleWithFixedDelay(refreshCache, cacheExpire, cacheExpire, TimeUnit.SECONDS); } //內部類,開啟新線程執行緩存刷新 private class RefreshCache implements Runnable { @Override public void run() { log.info("---開始刷新用戶信息緩存---"); List<User> userList = userDao.findAll(); if(!CollectionUtils.isEmpty(userList)){ for(User user : userList){ redisCache.opsForValue().set("USER" + user.getUserId(), JSON.toJSONString(user)); } } } } }
在redis.properties中加入cache.redis.cacheExpire配置項,代碼如下
##Redis連接信息配置 #redis地址 cache.redis.host=127.0.0.1 #redis端口號 cache.redis.port=6379 #redis密碼 cache.redis.password= #redis使用的數據庫(Redis內置18個數據庫,編號為0-17,默認使用0) cache.redis.db=0 #redis鏈接超時時間 cache.redis.timeout=2000 #redis鏈接池中最大空閑數 cache.redis.maxIdle=5 #redis鏈接池中最大連接數 cache.redis.maxActive=20 #建立連接最長等待時間 cache.redis.maxWait=1000 #定時任務執行時間間隔,單位為毫秒,這裏相當於10分鐘 cache.redis.cacheExpire=600
同時修改UserServiceImpl.java中的代碼如下
package com.user.demo.service.impl; import com.alibaba.fastjson.JSON; import com.user.demo.dao.UserDao; import com.user.demo.entity.User; import com.user.demo.service.IUserService; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Service接口實現類 */ @Service public class UserServiceImpl implements IUserService { @Resource private UserDao userDao; @Resource private RedisTemplate<String, String> redisCache; @Override public List<User> findAll() { List<User> result = new ArrayList<User>(); Set<String> sets = redisCache.keys("USER*"); //如果緩存有數據則從緩存中取數據,如果沒有則從數據庫中取數據 if(!CollectionUtils.isEmpty(sets)){ Iterator<String> it = sets.iterator(); while(it.hasNext()){ String item = it.next(); String value = redisCache.opsForValue().get(item); result.add(JSON.parseObject(value, User.class)); } }else{ result = userDao.findAll(); } return result; } @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) //事物 public void saveUser(User user) { userDao.saveUser(user); } }
此時,項目已經可以定時(每隔十分鐘)刷新緩存,我們編譯啟動
我們新提交一個用戶“三德子”,剛提交後,刷新是查不出來數據的,但是數據庫是有數據的
然後等待10分鐘之後,再來查看,可以看到緩存已經被自動刷新到了Redis中
OK,到這裏,這篇文章就結束了,關於Spring集成Redis做數據緩存我們也講的差不多了,其實關於Redis的應用,遠遠不止這麽簡單,我們可以很容易的搭建Redis集群,做分布式數據管理,也可以實現分布式session共享(這個我後面會有一篇文章講到),甚至假如寫數據量很大,我們也可以先緩存進Redis中,再利用多線程來將數據寫入數據庫中,減輕數據庫的負擔(因為數據庫寫操作是很耗費資源的)等等~~那如果大家有什麽意見和建議,也歡迎留言交流;下一篇文章我會講講mysql的讀寫分離,歡迎繼續關註!!
代碼URL:http://git.oschina.net/tian5017/UserDemoRedis
Spring整合Redis做數據緩存(Windows環境)