时效性要求很高的数据,库存,采取的是数据库+缓存双写的技术方案,也解决了双写的一致性的问题

缓存数据生产服务,监听一个消息队列,然后数据源服务(商品信息管理服务)发生了数据变更之后,就将数据变更的消息推送到消息队列中

缓存数据生产服务可以去消费到这个数据变更的消息,然后根据消息的指示提取一些参数,然后调用对应的数据源服务的接口,拉去数据,这个时候一般是从mysql库中拉去的

1、zookeeper集群搭建

zookeeper-3.4.5.tar.gz使用WinSCP拷贝到/usr/local目录下。

对zookeeper-3.4.5.tar.gz进行解压缩:tar -zxvf zookeeper-3.4.5.tar.gz。

对zookeeper目录进行重命名:mv zookeeper-3.4.5 zk

配置zookeeper相关的环境变量

vi ~/.bashrc
export ZOOKEEPER_HOME=/usr/local/zk
export PATH=$ZOOKEEPER_HOME/bin
source ~/.bashrc

cd zk/conf
cp zoo_sample.cfg zoo.cfg

vi zoo.cfg

  修改:

dataDir=/usr/local/zk/data

  新增:

server.0=eshop-cache01:2888:3888  
server.1=eshop-cache02:2888:3888
server.2=eshop-cache03:2888:3888

  

cd zk

mkdir data

cd data


vi myid

0


在另外两个节点上按照上述步骤配置ZooKeeper,使用scp将zk和.bashrc拷贝到eshop-cache02和eshop-cache03上即可。唯一的区别是标识号分别设置为1和2。


分别在三台机器上执行:zkServer.sh start。

检查ZooKeeper状态:zkServer.sh status,应该是一个leader,两个follower

jps:检查三个节点是否都有QuromPeerMain进程

 

2、kafka集群搭建

scala,就是一门编程语言,现在比较火,很多比如大数据领域里面的spark(计算引擎)就是用scala编写的


scala-2.11.4.tgz使用WinSCP拷贝到/usr/local目录下。

对scala-2.11.4.tgz进行解压缩:tar -zxvf scala-2.11.4.tgz。

对scala目录进行重命名:mv scala-2.11.4 scala


配置scala相关的环境变量

vi ~/.bashrc

export SCALA_HOME=/usr/local/scala

export PATH=$SCALA_HOME/bin

source ~/.bashrc


查看scala是否安装成功:scala -version


按照上述步骤在其他机器上都安装好scala。使用scp将scala和.bashrc拷贝到另外两台机器上即可。

kafka_2.9.2-0.8.1.tgz使用WinSCP拷贝到/usr/local目录下。

对kafka_2.9.2-0.8.1.tgz进行解压缩:tar -zxvf kafka_2.9.2-0.8.1.tgz。

对kafka目录进行改名:mv kafka_2.9.2-0.8.1 kafka


配置kafka

vi /usr/local/kafka/config/server.properties

broker.id:依次增长的整数,0、1、2,集群中Broker的唯一id

zookeeper.connect=192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181


安装slf4j

slf4j-1.7.6.zip上传到/usr/local目录下

unzip slf4j-1.7.6.zip

把slf4j中的slf4j-nop-1.7.6.jar复制到kafka的libs目录下面


解决kafka Unrecognized VM option 'UseCompressedOops'问题


vi /usr/local/kafka/bin/kafka-run-class.sh


if [ -z "$KAFKA_JVM_PERFORMANCE_OPTS" ]; then

KAFKA_JVM_PERFORMANCE_OPTS="-server -XX:+UseCompressedOops -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -Djava.awt.headless=true"

fi


去掉-XX:+UseCompressedOops即可


按照上述步骤在另外两台机器分别安装kafka。用scp把kafka拷贝到其他机器即可。

唯一区别的,就是server.properties中的broker.id,要设置为1和2


在三台机器上的kafka目录下,分别执行以下命令:nohup bin/kafka-server-start.sh config/server.properties &


使用jps检查启动是否成功


使用基本命令检查kafka是否搭建成功


bin/kafka-topics.sh --zookeeper 192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181 --topic test --replication-factor 1 --partitions 1 --create


bin/kafka-console-producer.sh --broker-list 192.168.31.181:9092,192.168.31.19:9092,192.168.31.227:9092 --topic test


bin/kafka-console-consumer.sh --zookeeper 192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181 --topic test --from-beginning

 

kafka+ehcache+redis完成缓存数据生产服务的开发:

import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ServletListenerRegistrationBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import com.roncoo.eshop.cache.listener.InitListener;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

@EnableAutoConfiguration
@SpringBootApplication
@ComponentScan
@MapperScan("com.roncoo.eshop.cache.mapper")
public class Application {

@Bean
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
return new org.apache.tomcat.jdbc.pool.DataSource();
}

@Bean
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/mybatis/*.xml"));
return sqlSessionFactoryBean.getObject();
}

@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}

@Bean
public JedisCluster JedisClusterFactory() {
Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>();
jedisClusterNodes.add(new HostAndPort("192.168.31.19", 7003));
jedisClusterNodes.add(new HostAndPort("192.168.31.19", 7004));
jedisClusterNodes.add(new HostAndPort("192.168.31.227", 7006));
JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes);
return jedisCluster;
}

@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
ServletListenerRegistrationBean servletListenerRegistrationBean =
new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new InitListener());
return servletListenerRegistrationBean;
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

  

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.roncoo.eshop.kafka.KafkaConsumer;
import com.roncoo.eshop.spring.SpringContext;


/**
* 系统初始化的监听器
*/
public class InitListener implements ServletContextListener {

public void contextInitialized(ServletContextEvent sce) {
ServletContext sc = sce.getServletContext();
ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(sc);
SpringContext.setApplicationContext(context);

new Thread(new KafkaConsumer("cache-message")).start();
}

public void contextDestroyed(ServletContextEvent sce) {

}

}

  

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;

//缓存配置管理类
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean(){
EhCacheManagerFactoryBean ehCacheManagerFactoryBean=
new EhCacheManagerFactoryBean();
ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
ehCacheManagerFactoryBean.setShared(true);
return ehCacheManagerFactoryBean;
}
@Bean
public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean){
return new EhCacheCacheManager(ehCacheManagerFactoryBean.getObject());
}
}

  

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;

// kafka消费者
public class KafkaConsumer implements Runnable {
private ConsumerConnector consumerConnector;
private String topic;

public KafkaConsumer(String topic){
this.consumerConnector=Consumer.createJavaConsumerConnector(
createConsumerConfig());
this.topic=topic;
}

@SuppressWarnings("rawtypes")
public void run(){
Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
topicCountMap.put(topic, 1);

Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =
consumerConnector.createMessageStreams(topicCountMap);
List<KafkaStream<byte[], byte[]>> streams = consumerMap.get(topic);

for (KafkaStream stream : streams) {
new Thread(new KafkaMessageProcessor(stream)).start();
}
}

//创建kafka cosumer config
private static ConsumerConfig createConsumerConfig() {
Properties props = new Properties();
props.put("zookeeper.connect", "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181");
props.put("group.id", "eshop-cache-group");
props.put("zookeeper.session.timeout.ms", "40000");
props.put("zookeeper.sync.time.ms", "200");
props.put("auto.commit.interval.ms", "1000");
return new ConsumerConfig(props);
}
}

  

import com.alibaba.fastjson.JSONObject;
import com.roncoo.eshop.model.ProductInfo;
import com.roncoo.eshop.model.ShopInfo;
import com.roncoo.eshop.service.CacheService;
import com.roncoo.eshop.spring.SpringContext;

import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;

//kafka消息处理线程
@SuppressWarnings("rawtypes")
public class KafkaMessageProcessor implements Runnable {

private KafkaStream kafkaStream;
private CacheService cacheService;

public KafkaMessageProcessor(KafkaStream kafkaStream) {
this.kafkaStream = kafkaStream;
this.cacheService = (CacheService) SpringContext.getApplicationContext()
.getBean("cacheService");
}

@SuppressWarnings("unchecked")
public void run(){
ConsumerIterator<byte[],byte[]> it= kafkaStream.iterator();
while(it.hasNext()){
String message =new String(it.next().message());
// 首先将message转换成json对象
JSONObject messageJSONObject =JSONObject.parseObject(message);

// 从这里提取出消息对应的服务的标识
String serviceId =messageJSONObject.getString("serviceId");

// 如果是商品信息服务
if("productInfoService".equals(serviceId)){
processProductInfoChangeMessage(messageJSONObject);
}else if("shopInfoService".equals(serviceId)) {
processShopInfoChangeMessage(messageJSONObject);
}
}
}

//处理商品信息变更的消息
private void processProductInfoChangeMessage(JSONObject messageJSONObject){
// 提取出商品id
Long productId = messageJSONObject.getLong("productId");

// 调用商品信息服务的接口
// 直接用注释模拟:getProductInfo?productId=1,传递过去
// 商品信息服务,一般来说就会去查询数据库,去获取productId=1的商品信息,然后返回回来

String productInfoJSON = "{\"id\": 1, \"name\": \"iphone7手机\", \"price\": 5599, \"pictureList\":\"a.jpg,b.jpg\", \"specification\": \"iphone7的规格\", \"service\": \"iphone7的售后服务\", \"color\": \"红色,白色,黑色\", \"size\": \"5.5\", \"shopId\": 1}";
ProductInfo productInfo = JSONObject.parseObject(productInfoJSON, ProductInfo.class);
cacheService.saveProductInfo2LocalCache(productInfo);
System.out.println("===================获取刚保存到本地缓存的商品信息:" + cacheService.getProductInfoFromLocalCache(productId));
cacheService.saveProductInfo2ReidsCache(productInfo);
}

//处理店铺信息变更的消息
private void processShopInfoChangeMessage(JSONObject messageJSONObject){
// 提取出商品id
Long productId = messageJSONObject.getLong("productId");
Long shopId = messageJSONObject.getLong("shopId");

String shopInfoJSON = "{\"id\": 1, \"name\": \"小王的手机店\", \"level\": 5, \"goodCommentRate\":0.99}";
ShopInfo shopInfo = JSONObject.parseObject(shopInfoJSON, ShopInfo.class);
cacheService.saveShopInfo2LocalCache(shopInfo);
System.out.println("===================获取刚保存到本地缓存的店铺信息:" + cacheService.getShopInfoFromLocalCache(shopId));
cacheService.saveShopInfo2ReidsCache(shopInfo);
}
}

  

public class ProductInfo {
private Long id;
private String name;
private Double price;

public ProductInfo() {

}

public ProductInfo(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}

public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}

  

//店铺信息
public class ShopInfo {
private Long id;
private String name;
private Integer level;
private Double goodCommentRate;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getLevel() {
return level;
}

public void setLevel(Integer level) {
this.level = level;
}

public Double getGoodCommentRate() {
return goodCommentRate;
}

public void setGoodCommentRate(Double goodCommentRate) {
this.goodCommentRate = goodCommentRate;
}

@Override
public String toString() {
return "ShopInfo [id=" + id + ", name=" + name + ", level=" + level
+ ", goodCommentRate=" + goodCommentRate + "]";
}
}

  

import com.roncoo.eshop.model.ProductInfo;
import com.roncoo.eshop.model.ShopInfo;

//缓存service接口
public interface CacheService {
//将商品信息保存到本地缓存中
public ProductInfo saveLocalCache(ProductInfo productInfo);

//从本地缓存中获取商品信息
public ProductInfo getLocalCache(Long id);

//将商品信息保存到本地的ehcache缓存中
public ProductInfo saveProductInfo2LocalCache(ProductInfo productInfo);

//从本地ehcache缓存中获取商品信息
public ProductInfo getProductInfoFromLocalCache(Long productId);

// 将店铺信息保存到本地的ehcache缓存中
public ShopInfo saveShopInfo2LocalCache(ShopInfo shopInfo);

//从本地ehcache缓存中获取店铺信息
public ShopInfo getShopInfoFromLocalCache(Long shopId);

//将商品信息保存到redis中
public void saveProductInfo2ReidsCache(ProductInfo productInfo);

//将店铺信息保存到redis中
public void saveShopInfo2ReidsCache(ShopInfo shopInfo);

}

  

import javax.annotation.Resource;

import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSONObject;
import com.roncoo.eshop.model.ProductInfo;
import com.roncoo.eshop.model.ShopInfo;
import com.roncoo.eshop.service.CacheService;

import redis.clients.jedis.JedisCluster;

//缓存Service实现类
@Service("cacheService")
public class CacheServiceImpl implements CacheService {
public static final String CACHE_NAME ="local";

@Resource
private JedisCluster jedisCluster;

//将商品信息保存到本地缓存中
@CachePut(value = CACHE_NAME, key = "'key_'+#productInfo.getId()")
public ProductInfo saveLocalCache(ProductInfo productInfo) {
return productInfo;
}

// 从本地缓存中获取商品信息
@Cacheable(value = CACHE_NAME, key = "'key_'+#id")
public ProductInfo getLocalCache(Long id) {
return null;
}

//将商品信息保存到本地的ehcache缓存中
@CachePut(value=CACHE_NAME,key = "'product_info_'+#productInfo.getId()")
public ProductInfo saveProductInfo2LocalCache(ProductInfo productInfo) {
return productInfo;
}

//从本地ehcache缓存中获取商品信息
@Cacheable(value = CACHE_NAME, key = "'product_info_'+#productId")
public ProductInfo getProductInfoFromLocalCache(Long productId) {
return null;
}

//将店铺信息保存到本地的ehcache缓存中
@CachePut(value = CACHE_NAME, key = "'shop_info_'+#shopInfo.getId()")
public ShopInfo saveShopInfo2LocalCache(ShopInfo shopInfo) {
return shopInfo;
}

//从本地ehcache缓存中获取店铺信息
@Cacheable(value = CACHE_NAME, key = "'shop_info_'+#shopId")
public ShopInfo getShopInfoFromLocalCache(Long shopId) {
return null;
}

//将商品信息保存到redis中
public void saveProductInfo2ReidsCache(ProductInfo productInfo){
String key="product_info_" + productInfo.getId();
jedisCluster.set(key, JSONObject.toJSONString(productInfo));
}

//将店铺信息保存到redis中
public void saveShopInfo2ReidsCache(ShopInfo shopInfo){
String key="shop_info_" + shopInfo.getId();
jedisCluster.set(key, JSONObject.toJSONString(shopInfo));
}
}

  

import org.springframework.context.ApplicationContext;

/**
* spring上下文
*/
public class SpringContext {

private static ApplicationContext applicationContext;

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

public static void setApplicationContext(ApplicationContext applicationContext) {
SpringContext.applicationContext = applicationContext;
}

}

  

import javax.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.roncoo.eshop.model.ProductInfo;
import com.roncoo.eshop.service.CacheService;

@Controller
public class CacheController {
@Resource
private CacheService cacheService;

@RequestMapping("/testPutCache")
@ResponseBody
public String testPutCache(ProductInfo productInfo) {
cacheService.saveLocalCache(productInfo);
return "success";
}

@RequestMapping("/testGetCache")
@ResponseBody
public ProductInfo testGetCache(Long id) {
return cacheService.getLocalCache(id);
}
}

  如何提升缓存命中率:

分发层+应用层,双层nginx

分发层nginx,负责流量分发的逻辑和策略,这个里面它可以根据你自己定义的一些规则,比如根据productId去进行hash,然后对后端的nginx数量取模

将某一个商品的访问的请求,就固定路由到一个nginx后端服务器上去,保证说只会从redis中获取一次缓存数据,后面全都是走nginx本地缓存了

后端的nginx服务器,就称之为应用服务器; 最前端的nginx服务器,被称之为分发服务器

大幅度提升你的nginx本地缓存这一层的命中率,大幅度减少redis后端的压力,提升性能

 缓存命中率低的原因:

zookeeper+kafka集群的安装_spring