上一张说到了Ehcache的简单使用,但是Ehcache一般作为本地缓存来使用,而在一个系统可能会根据服务的不用会部署在不同的机器上,那么在每一台机器都设置Ehcache,又要把一些公用的信息缓存一遍,这样不利于使用Ehcache。 而这时,我们可以再做一个缓存,这个缓存保存了一些经常使用,而且可以较大的数据。这个就是Redis。Redis已经成为了最常用的几种NoSql之一了,不仅开源,而且简单易用。
准备工作:
安装zookeeper:
安装zookeeper 极为简单,
http://blog.csdn.net/u014104286/article/details/79165916
在安装好zookeeper、Redis之后,我们需要一个springboot的工程,以便于来实现我们的Redis+Ehcache缓存的系统:其中
pom.xml:
4.0.0
com.ys.test.ehcache
SomeTest-ehcache
0.0.1-SNAPSHOT
jar
SomeTest-ehcache
http://maven.apache.org
org.springframework.boot
spring-boot-dependencies
1.4.3.RELEASE
pom
import
junit
junit
4.7
test
org.springframework.boot
spring-boot-starter-web
org.mybatis
mybatis-spring
1.2.2
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.1.1
mysql
mysql-connector-java
com.alibaba
fastjson
1.1.43
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-jdbc
org.springframework
spring-context-support
net.sf.ehcache
ehcache
2.8.3
org.apache.kafka
kafka_2.11
0.10.2.0
org.apache.zookeeper
zookeeper
3.4.6
redis.clients
jedis
org.springframework.boot
spring-boot-maven-plugin
org.apache.maven.plugins
maven-compiler-plugin
1.8
Ehcache.xml:
加载Ehcache,使得spring中有EhCacheCacheManager的Bean:
EhcacheConfiguration.java:
package com.ys.test.ehcache.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
/**
* 配置ehcache
*
*/
@Configuration
@EnableCaching
public class EhcacheConfiguration {
@Bean
public EhCacheManagerFactoryBean cacheManagerFactoryBean(){
EhCacheManagerFactoryBean bean = new EhCacheManagerFactoryBean();
bean.setConfigLocation(new ClassPathResource("ehcache.xml"));
bean.setShared(true);
return bean;
}
@Bean
public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean bean){
return new EhCacheCacheManager(bean.getObject());
}
}
配置Redis:RedisConfig.java: (说来惭愧,ruby 和 gem安装不上 所以做不了Redis cluster的集群 )
package com.ys.test.ehcache.config;
import java.util.HashSet;
import java.util.Set;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
@Configuration
public class RedisConfig {
@Bean
public JedisCluster JedisClusterFactory() {
Set jedisClusterNodes = new HashSet();
jedisClusterNodes.add(new HostAndPort("192.168.5.112", 4564));
JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes);
return jedisCluster;
}
@Bean
public Jedis JedisFactory() {
Jedis jedis = new Jedis("192.168.5.112", 6379);
jedis.auth("root");
return jedis;
}
}
Application.properties:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/ecacheTest?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# Specify the DBMS
spring.jpa.database = MYSQL
# Show or not log for eachsql query
spring.jpa.show-sql = true
#Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto =update
# Naming strategy
#[org.hibernate.cfg.ImprovedNamingStrategy #org.hibernate.cfg.DefaultNamingStrategy|ImprovedNamingStrategy]
#1.3.3.RELEASE 使用下面 根据Column注解生成列名
#spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.DefaultNamingStrategy
#1.4.3.RELEASE 使用下面的,根据Column注解生成列名
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5Dialect
package com.ys.test.ehcache.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class ProductInfo {
@Id @GeneratedValue
@Column(name="productId")
private Long product_id;
@Column(name="productName")
private String product_name;
@Column(name="price")
private Double price;
@Column(name="modifyTime")
private String modifyTime;
public ProductInfo() {
}
public ProductInfo(Long product_id, String product_name, Double price) {
this.product_id = product_id;
this.product_name = product_name;
this.price = price;
}
public ProductInfo(Long product_id, String product_name, Double price,String modifyTime) {
this.product_id = product_id;
this.product_name = product_name;
this.price = price;
this.modifyTime = modifyTime;
}
public String getModifyTime() {
return modifyTime;
}
public void setModifyTime(String modifyTime) {
this.modifyTime = modifyTime;
}
public Long getProduct_id() {
return product_id;
}
public void setProduct_id(Long product_id) {
this.product_id = product_id;
}
public String getProduct_name() {
return product_name;
}
public void setProduct_name(String product_name) {
this.product_name = product_name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "ProductInfo [product_id=" + product_id + ", product_name=" + product_name + ", price=" + price
+ ", modifyTime=" + modifyTime + "]";
}
}
一个使用Redis+Ehcache做缓存的系统,因为Ehcache是本地的,所以使用者就是本机器而已,但是Redis是分布式的,每台服务机器都可以访问和操作,所以我们还得保证每一个放入Redis缓存中应该是最新的,这样才能保证再其他机器读取的时候拿到的数据是最新的。
2和4:当请求到来的时候(假想为查询请求) 服务1、服务2都先从本地的Ehcache中根据约定的key去寻找缓存对象。当Ehcache中有缓存的对象,就返回给请求客服端。
9和11:当 Ehcache中没有缓存对象的时候,要去Redis中找。如果找到了就通过10和12返回给服务1、服务2,之后服务1和服务2在通过15、16返回给请求客服端。
5和6:当Redis中,及9和11没有查询缓存数据时,我们要去数据库区查询数据,并通过7和8返回给服务1和服务2,同时服务1、服务2需要将从数据库查询的数据放到自己本地的Ehcache中。
13和14:在上一步设置了本地的Ehcache之后,我们还要设置Redis缓存。(但是当请求和修改高峰,并刚好查询的数据有变动的情况,两个查询请求先后查询的结果不同,得到一个旧值和一个新值,查询到旧值的服务准备设置到Redis的时候网络或者机器资源不够了,任务被暂停了一会儿,这个时候新值的服务先把新的数据设置到了Redis,这个时候旧值再去设置的话就会把数据还原到了修改之前,这样从缓存里面拿到的是无效的数据。这时我们需要zookeeper作为一个分布式锁,要设置Redis先要拿到这个对象key对应的锁,才能设置,而且当开始设置的时候再去查一下是否此时有将要设置的缓存对象,没有的话直接设置,如果有的话我们要和自己比较这个对象的修改时间,如果自己是最新的时间就设置这个对象为自己的数据,如果已经存在比自己数据新的时间则不做操作。)
15和16:设置缓存对象到Redis中。
17和18:返回查询结果给请求客服端。
实现以上思路:
我们在上面的配置步骤中获取到了Redis、数据库连接、Ehcache管理对象了,我们还需要zookeeper连接,并要有获取和删除分布式锁的方法:
ZookeeperLockSingle.java:
package com.ys.test.ehcache.zk;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
public class ZookeeperLockSingle {
private final static int COUNTS = 5;
private final Log log = LogFactory.getLog(ZookeeperLockSingle.class);
private ZooKeeper keeper;
//zk链接是异步的,我们需要等待链接上zk才进行操作
private CountDownLatch latch = new CountDownLatch(1);
private ZookeeperLockSingle (){
try {
this.keeper = new ZooKeeper("192.168.5.112:2181,192.168.5.113:2181", 50000,new MyWater());
log.error("等待链接zk...");
latch.await();
} catch (Exception e) {
log.error(e.getMessage());
}
}
private class MyWater implements Watcher{
@Override
public void process(WatchedEvent arg0) {
//状态是链接
if (arg0.getState() == KeeperState.SyncConnected) {
log.error("zk 已经链接成功");
latch.countDown();
}
}
}
private static class GetZookeeperLockSingle{
private static ZookeeperLockSingle zklockSingle = null;
static {
zklockSingle = new ZookeeperLockSingle();
}
private static ZookeeperLockSingle getZookeeperLock(){
return zklockSingle;
}
}
public static ZookeeperLockSingle getSingleZKLock(){
return GetZookeeperLockSingle.getZookeeperLock();
}
/**
* 循环获取分布式锁
* @Function: ZookeeperLockSingle.java
* @Description:
*
* @param productid
* @return
* @return boolean
* @version: v1.0.0
*/
public boolean acquireDistrbutedLock(Long productid){
String path = "/product-lock-"+productid;
boolean flag = false;
int counts = 1;
//这里一直等带获取锁是不是不太好哦
while (true) {
if (counts > COUNTS) {
break;
}
try {
Thread.sleep(200);
String create = this.keeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println(create);
log.info("创建:"+path+"成功");
flag = true;
break;
} catch (Exception e) {
counts++;
log.error("锁:"+path+"创建失败!!!"+"正在进行第"+counts+"次尝试!");
continue;
}
}
return flag;
}
/**
* 释放分布式锁
* @Function: ZookeeperLockSingle.java
* @Description:
*
* @param productid
* @return void
* @version: v1.0.0
*/
public void releaseDistrbuteProductLock(long productid) throws Exception{
String path = "/product-lock-"+productid;
try {
this.keeper.delete(path, -1);
} catch (Exception e) {
log.error(e.getMessage());
throw e;
}
}
}
当能创建指定的目录,则说明拿到了锁。创建失败说明目前有服务在操作对应的product_id的对象。
缓存对象ProductInfo.java:
package com.ys.test.ehcache.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class ProductInfo {
@Id @GeneratedValue
@Column(name="productId")
private Long product_id;
@Column(name="productName")
private String product_name;
@Column(name="price")
private Double price;
@Column(name="modifyTime")
private long modifyTime;
public ProductInfo() {
}
public ProductInfo(Long product_id, String product_name, Double price) {
this.product_id = product_id;
this.product_name = product_name;
this.price = price;
}
public ProductInfo(Long product_id, String product_name, Double price,long modifyTime) {
this.product_id = product_id;
this.product_name = product_name;
this.price = price;
this.modifyTime = modifyTime;
}
public long getModifyTime() {
return modifyTime;
}
public void setModifyTime(long modifyTime) {
this.modifyTime = modifyTime;
}
public Long getProduct_id() {
return product_id;
}
public void setProduct_id(Long product_id) {
this.product_id = product_id;
}
public String getProduct_name() {
return product_name;
}
public void setProduct_name(String product_name) {
this.product_name = product_name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "ProductInfo [product_id=" + product_id + ", product_name=" + product_name + ", price=" + price
+ ", modifyTime=" + modifyTime + "]";
}
}
缓存服务接口ICacheService.java:
package com.ys.test.ehcache.service;
import com.ys.test.ehcache.model.ProductInfo;
/**
* 缓存服务
*/
public interface ICacheService {
/**
* 保存商品信息到本地的ehcache
* @Description:
*
* @param info
* @return
* @throws Exception
* @return ProductInfo
* @version: v1.0.0
*/
ProductInfo saveProductInfo(ProductInfo info) throws Exception;
/**
* 根据商品ID查询商品信息
* @Description:
*
* @param id
* @return
* @throws Exception
* @return ProductInfo
* @version: v1.0.0
*/
ProductInfo getProductInfoById(Long id) throws Exception;
/**
* 根据Id清楚本地Ehcache缓存
* @Function: ICacheService.java
* @Description:
*
* @param id
* @throws Exception
* @return void
* @version: v1.0.0
*/
void releaseProductById(Long id) throws Exception;
/**
*
* @Function: ICacheService.java
* @Description: 把信息保存到Redis
*
* @param info
* @throws Exception
* @return void
* @version: v1.0.0
* @date: 2018年1月29日 下午7:34:19
*/
void saveProductInfoToRedis(ProductInfo info) throws Exception;
/**
* 从Redis中获取缓存信息
* @Function: ICacheService.java
* @Description:
*
* @param id
* @return
* @throws Exception
* @return ProductInfo
* @version: v1.0.0
*/
ProductInfo getProductInfoByIdToRedis(Long id) throws Exception;
/**
* 根据Id清楚本地Redis缓存
* @Function: ICacheService.java
* @Description:
*
* @param id
* @throws Exception
* @return void
* @version: v1.0.0
*/
void releaseProductByIdToRedis(Long id) throws Exception;
}
缓存实现类CacheServiceImpl.java:
package com.ys.test.ehcache.service.impl;
import javax.annotation.Resource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSONObject;
import com.ys.test.ehcache.mapper.ProductService;
import com.ys.test.ehcache.model.ProductInfo;
import com.ys.test.ehcache.service.ICacheService;
import com.ys.test.ehcache.zk.ZookeeperLockSingle;
import redis.clients.jedis.Jedis;
@Service
public class CacheServiceImpl implements ICacheService {
private Log log = LogFactory.getLog(CacheServiceImpl.class);
@Resource
private Jedis cluster;
@Resource
private ProductService productService;
private static final String CACHE_STRATEGY = "local";
private static final String KET_PREFIX = "key_";
@CachePut(value=CACHE_STRATEGY,key="'key_'+#info.getProduct_id()")
@Override
public ProductInfo saveProductInfo(ProductInfo info) throws Exception {
log.error("*********************保存数据********************");
info.setModifyTime(System.currentTimeMillis());
productService.saveProductInfo(info);
return info;
}
@Cacheable(value=CACHE_STRATEGY,key="'key_'+#id")
@Override
public ProductInfo getProductInfoById(Long id) throws Exception {
log.error("*********************没有Ehcache缓存********************");
log.error("*********************查询Redis********************");
ProductInfo productInfoById = null;
try {
productInfoById = getProductInfoByIdToRedis(id);
if (null != productInfoById) {
return productInfoById;
}
} catch (Exception e) {
log.error("*********************查询了Redis无缓存********************");
}
log.error("*********************查询了数据库********************");
productInfoById = productService.getProductInfoById(id);
//保存到Redis
commonSaveToRedis(productInfoById);
return productInfoById;
}
/**
* 以安全的方式保存到Redis中
* @Function: CacheServiceImpl.java
* @Description:
*
* @param id
* @param productInfoById
* @throws Exception
* @return void
* @version: v1.0.0
* @date: 2018年1月29日 下午7:53:29
*/
private void commonSaveToRedis(ProductInfo productInfoById) throws Exception {
long product_id = productInfoById.getProduct_id();
//保存到Redis
ZookeeperLockSingle lockSingle = ZookeeperLockSingle.getSingleZKLock();
boolean acquireDistrbutedLock = lockSingle.acquireDistrbutedLock(product_id);
//获取zookeeper锁
if (acquireDistrbutedLock) {
//比较Redis中缓存对象是否存在
ProductInfo productInfoByIdToRedis = getProductInfoByIdToRedis(product_id);
String key = KET_PREFIX + productInfoById.getProduct_id();
if (null != productInfoByIdToRedis) {
long redisTime = productInfoByIdToRedis.getModifyTime();
long nowTime = productInfoById.getModifyTime();
//比较缓存中数据是否为最新
if (redisTime < nowTime) {
String jsonStr = JSONObject.toJSONString(productInfoById);
String set = cluster.set(key,jsonStr);
log.error("*********************保存数据结果********************"+set);
}
} else {
String jsonStr = JSONObject.toJSONString(productInfoById);
String set = cluster.set(key,jsonStr);
log.error("*********************保存数据结果********************"+set);
}
}
//释放锁
lockSingle.releaseDistrbuteProductLock(product_id);
}
@CacheEvict(value=CACHE_STRATEGY, key="'key_'+#id")
@Override
public void releaseProductById(Long id) throws Exception {
}
@Override
public void saveProductInfoToRedis(ProductInfo info) throws Exception {
commonSaveToRedis(info);
}
@Override
public ProductInfo getProductInfoByIdToRedis(Long id) throws Exception {
String key = KET_PREFIX + id;
String string = cluster.get(key);
ProductInfo parseObject = JSONObject.parseObject(string, ProductInfo.class);
return parseObject;
}
@Override
public void releaseProductByIdToRedis(Long id) throws Exception {
String key = KET_PREFIX + id;
Long del = cluster.del(key);
log.error("*********************删除数据结果********************"+del);
}
}
控制类:CacheServiceController.java:
package com.ys.test.ehcache.controller;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ys.test.ehcache.model.ProductInfo;
import com.ys.test.ehcache.service.ICacheService;
@RestController
public class CacheServiceController {
@Resource
private ICacheService cahceService;
@RequestMapping("/saveInfo")
public boolean saveTest(ProductInfo info) throws Exception {
System.out.println(info.getProduct_name() + ":" + info.getProduct_id());
return cahceService.saveProductInfo(info) == null?false:true;
}
@RequestMapping("/getProductByid")
public ProductInfo getTest(Long id) throws Exception {
System.out.println(id);
return cahceService.getProductInfoById(id);
}
@RequestMapping("/getProductByidTORedis")
public ProductInfo getToRedisTest(Long id) throws Exception {
System.out.println(id);
return cahceService.getProductInfoByIdToRedis(id);
}
}
数据库:
运行结果:运行日志:
日志说明:
我们的http://localhost:8080/getProductByid?id=8请求匹配到控制类的:
@RequestMapping("/getProductByid")
public ProductInfo getTest(Long id) throws Exception {
System.out.println(id);
return cahceService.getProductInfoById(id);
}
方法,其中调用了我们缓存服务的cahceService.getProductInfoById(id)方法,但是这个方法使用了注解:
@Cacheable(value=CACHE_STRATEGY,key="'key_'+#id"),所以会先从我们本地的Ehcache缓存中查询是否有key 为 key_8的缓存对象,如果找到了就返回找到的对象,(在上面的运行结果中是没有在本地Ehcache中找到key_8的缓存对象的)所以进入了getProductInfoById的方法体中执行了:(从数据库找到缓存对象之后注解@Cacheable 也会把这个对象保存到Ehcache中)
log.error("*********************没有Ehcache缓存********************");
log.error("*********************查询Redis********************");
但是在redis中也没有找到缓存对象,所以执行了:log.error("*********************查询了数据库********************");
在数据库中找到了我们要查询的对象,之后我们就开始设置到Redis中:
1.先链接zookeeper,获取zookeeper的锁,保证在某个时间段中只有一个服务去操作product_id 为8的对象,要获取锁:
2018-01-31 13:35:27.508 INFO 92024 --- [nio-8080-exec-1] c.y.test.ehcache.zk.ZookeeperLockSingle : 创建:/product-lock-8成功
2.获取锁成功之后就先检查redis中是否有product_id 为8的缓存对象,如果没有直接设置从数据库获取的对象保存到redis中,如果redis中有数据,则取出,比较当前的对象的modifytime字段,哪个最新就保存哪个。
而在
当再次请求:http://localhost:8080/getProductByid?id=8 时候,在本地Ehcache中能找到缓存对象,所以直接返回Ehcache中的对象,不再进入getProductInfoById方法中。