1、什么是redis?
在过去的几年中,NoSQL数据库一度成为高并发、海量数据存储解决方案的代名词,与之相应的产品也呈现出雨后春笋般的生机。然而在众多产品中能够脱颖而出的却屈指可数,如Redis、MongoDB、BerkeleyDB和CouchDB等。由于每种产品所拥有的特征不同,因此它们的应用场景也存在着一定的差异,下面仅给出简单的说明:
1). BerkeleyDB是一种极为流行的开源嵌入式数据库,在更多情况下可用于存储引擎,比如BerkeleyDB在被Oracle收购之前曾作为MySQL的存储引擎,由此可以预见,该产品拥有极好的并发伸缩性,支持事务及嵌套事务,海量数据存储等重要特征,在用于存储实时数据方面具有极高的可用价值。然而需要指出的是,该产品的Licence为GPL,这就意味着它并不是在所有情况下都是免费使用的。
2). 对MongoDB的定义为Oriented-Document数据库服务器,和BerkeleyDB不同的是该数据库可以像其他关系型数据库服务器那样独立的运行并提供相关的数据服务。从该产品的官方文档中我们可以获悉,MongoDB主要适用于高并发的论坛或博客网站,这些网站具有的主要特征是并发访问量高、多读少写、数据量大、逻辑关系简单,以及文档数据作为主要数据源等。和BerkeleyDB一样,该产品的License同为GPL。
3). Redis,典型的NoSQL数据库服务器,和BerkeleyDB相比,它可以作为服务程序独立运行于自己的服务器主机。在很多时候,人们只是将Redis视为Key/Value数据库服务器,然而事实并非如此,在目前的版本中,Redis除了Key/Value之外还支持List、Hash、Set和Ordered Set等数据结构,因此它的用途也更为宽泛。对于此种误解,Redis官网也进行了相应的澄清。和以上两种产品不同的是,Redis的License是Apache License,就目前而言,它是完全免费。
4). memcached,数据缓存服务器。为什么在这里要给出该产品的解释呢?很简单,因为笔者认为它在使用方式上和Redis最为相似。毕竟这是一篇关于Redis的技术系列博客,有鉴于此,我们将简要的对比一下这两个产品。首先说一下它们之间的最大区别,memcached只是提供了数据缓存服务,一旦服务器宕机,之前在内存中缓存的数据也将全部消失,因此可以看出memcached没有提供任何形式的数据持久化功能,而Redis则提供了这样的功能。再有就是Redis提供了更为丰富的数据存储结构,如Hash和Set。至于它们的相同点,主要有两个,一是完全免费,再有就是它们的提供的命令形式极为接近。
Redis的优势:
1). 和其他NoSQL产品相比,Redis的易用性极高,因此对于那些有类似产品使用经验的开发者来说,一两天,甚至是几个小时之后就可以利用Redis来搭建自己的平台了。
2). 在解决了很多通用性问题的同时,也为一些个性化问题提供了相关的解决方案,如索引引擎、统计排名、消息队列服务等。
目前版本中Redis存在的主要问题:
1). 在官方版本中没有提供Windows平台的支持,已发布的正式版本中只是支持类Unix和MacOSX平台。
2). 没有提供集群的支持,然而据官网所述,预计在2.6版本中会加入该特征。
3). Publication/Subscription功能中,如果master宕机,slave无法自动提升为master。
和关系型数据库的比较:
在目前版本(2.4.7)的Redis中,提供了对五种不同数据类型的支持,其中只有一种类型,既string类型可以被视为Key-Value结构,而其他的数据类型均有适用于各自特征的应用场景,至于具体细节我们将会在该系列后面的博客中予以说明。
相比于关系型数据库,由于其存储结构相对简单,因此Redis并不能对复杂的逻辑关系提供很好的支持,然而在适用于Redis的场景中,我们却可以由此而获得效率上的显著提升。即便如此,Redis还是为我们提供了一些数据库应该具有的基础概念,如:在同一连接中可以选择打开不同的数据库,然而不同的是,Redis中的数据库是通过数字来进行命名的,缺省情况下打开的数据库为0。如果程序在运行过程中打算切换数据库,可以使用Redis的select命令来打开其他数据库,如select 1,如果此后还想再切换回缺省数据库,只需执行select 0即可。
在数据存储方面,Redis遵循了现有NoSQL数据库的主流思想,即Key作为数据检索的唯一标识,我们可以将其简单的理解为关系型数据库中索引的键,而Value则作为数据存储的主要对象,其中每一个Value都有一个Key与之关联,这就好比索引中物理数据在数据表中存储的位置。在Redis中,Value将被视为二进制字节流用于存储任何格式的数据,如Json、XML和序列化对象的字节流等,因此我们也可以将其想象为RDB中的BLOB类型字段。由此可见,在进行数据查询时,我们只能基于Key作为我们查询的条件,当然我们也可以应用Redis中提供的一些技巧将Value作为其他数据的Key,这些知识我们都会在后面的博客中予以介绍。
如何持久化内存数据:(主从复制)
缺省情况下,Redis会参照当前数据库中数据被修改的数量,在达到一定的阈值后会将数据库的快照存储到磁盘上,这一点我们可以通过配置文件来设定该阈值。通常情况下,我们也可以将Redis设定为定时保存。如当有1000个以上的键数据被修改时,Redis将每隔60秒进行一次数据持久化操作。缺省设置为,如果有9个或9个以下数据修改是,Redis将每15分钟持久化一次。
从上面提到的方案中可以看出,如果采用该方式,Redis的运行时效率将会是非常高效的,既每当有新的数据修改发生时,仅仅是内存中的缓存数据发生改变,而这样的改变并不会被立即持久化到磁盘上,从而在绝大多数的修改操作中避免了磁盘IO的发生。然而事情往往是存在其两面性的,在该方法中我们确实得到了效率上的提升,但是却失去了数据可靠性。如果在内存快照被持久化到磁盘之前,Redis所在的服务器出现宕机,那么这些未写入到磁盘的已修改数据都将丢失。为了保证数据的高可靠性,Redis还提供了另外一种数据持久化机制--Append模式。如果Redis服务器被配置为该方式,那么每当有数据修改发生时,都会被立即持久化到磁盘。
2、为什么要使用redis?
解决高并发的问题:
3 使用redis做缓存
3.1 安装redis
n 版本说明
本教程使用redis3.0版本。3.0版本主要增加了redis集群功能。
安装的前提条件:
需要安装gcc:yum install gcc-c++
1、下载redis的源码包。
2、把源码包上传到linux服务器
3、解压源码包
tar -zxvf redis-3.0.0.tar.gz
4、Make
5、Make install
[root@bogon redis-3.0.0]# make install PREFIX=/usr/local/redis
3.2 启动redis
1、前端启动模式
/usr/local/redis/bin/redis-server
默认是前端启动模式,端口是6379
2、后端启动
1)从redis的源码目录中复制redis.conf到redis的安装目录。
2)修改配置文件
3)[root@bogon bin]# ./redis-server redis.conf
3.3 Redis常用命令
127.0.0.1:6379> set a 10
OK
127.0.0.1:6379> get a
"10"
3.4 常用数据类型
String
Hash
List
Set
SortedSet
4 Redis集群的搭建
4.1 Redis集群相关概念
4.1.1 redis-cluster架构图
redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大
Key:a
计算a的hash值,例如值为100,100这个槽在server1上,所以a应该放到server1.
Key:hello
Hash值:10032,此槽在server2上。Hell可以应该存在server2.
4.1.2 redis-cluster投票:容错
(1)领着投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超过(cluster-node-timeout),认为当前master节点挂掉.
(2):什么时候整个集群不可用(cluster_state:fail)?
a:如果集群任意master挂掉,且当前master没有slave.集群进入fail状态,也可以理解成集群的slot映射[0-16383]不完成时进入fail状态. ps : redis-3.0.0.rc1加入cluster-require-full-coverage参数,默认关闭,打开集群兼容部分失败.
b:如果集群超过半数以上master挂掉,无论是否有slave集群进入fail状态.
ps:当集群不可用时,所有对集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)错误
4.2 我们的集群结构
集群中有三个节点的集群,每个节点有一主一备。需要6台虚拟机。
搭建一个伪分布式的集群,使用6个redis实例来模拟。
4.3 搭建集群需要的环境
搭建集群需要使用到官方提供的ruby脚本。
需要安装ruby的环境。
安装ruby
yum install ruby
yum install rubygems
redis集群管理工具redis-trib.rb
[root@bogon ~]# cd redis-3.0.0
[root@bogon redis-3.0.0]# cd src
[root@bogon src]# ll *.rb
-rwxrwxr-x. 1 root root 48141 Apr 1 07:01 redis-trib.rb
[root@bogon src]#
脚本需要的ruby包:
需要上传到linux服务。
安装ruby的包:
gem install redis-3.0.0.gem
[root@bogon ~]# gem install redis-3.0.0.gem
Successfully installed redis-3.0.0
1 gem installed
Installing ri documentation for
redis-3.0.0...
Installing RDoc documentation for
redis-3.0.0...
4.4
集群的搭建
第一步:创建6个redis实例,端口号从7001~7006
第二步:修改redis的配置文件
1、修改端口号
2、打开cluster-enable前面的注释。
第三步:把创建集群的ruby脚本复制到redis-cluster目录下。
第四步:启动6个redis实例
第五步:创建集群。
./redis-trib.rb create --replicas 1 |
[root@bogon redis-cluster]# ./redis-trib.rb
create --replicas 1 192.168.25.153:7001 192.168.25.153:7002 192.168.25.153:7003
192.168.25.153:7004 192.168.25.153:7005
192.168.25.153:7006
>>> Creating cluster
Connecting to node 192.168.25.153:7001: OK
Connecting to node 192.168.25.153:7002: OK
Connecting to node 192.168.25.153:7003: OK
Connecting to node 192.168.25.153:7004: OK
Connecting to node 192.168.25.153:7005: OK
Connecting to node 192.168.25.153:7006: OK
>>> Performing hash slots
allocation on 6 nodes...
Using 3 masters:
192.168.25.153:7001
192.168.25.153:7002
192.168.25.153:7003
Adding replica 192.168.25.153:7004 to
192.168.25.153:7001
Adding replica 192.168.25.153:7005 to
192.168.25.153:7002
Adding replica 192.168.25.153:7006 to
192.168.25.153:7003
M: 5a8523db7e12ca600dc82901ced06741b3010076
192.168.25.153:7001
slots:0-5460 (5461 slots) master
M: bf6f0929044db485dea9b565bb51e0c917d20a53
192.168.25.153:7002
slots:5461-10922 (5462 slots) master
M: c5e334dc4a53f655cb98fa3c3bdef8a808a693ca
192.168.25.153:7003
slots:10923-16383 (5461 slots) master
S: 2a61b87b49e5b1c84092918fa2467dd70fec115f
192.168.25.153:7004
replicates 5a8523db7e12ca600dc82901ced06741b3010076
S: 14848b8c813766387cfd77229bd2d1ffd6ac8d65
192.168.25.153:7005
replicates bf6f0929044db485dea9b565bb51e0c917d20a53
S: 3192cbe437fe67bbde9062f59d5a77dabcd0d632
192.168.25.153:7006
replicates c5e334dc4a53f655cb98fa3c3bdef8a808a693ca
Can I set the above configuration? (type
'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config
epoch to each node
>>> Sending CLUSTER MEET messages
to join the cluster
Waiting for the cluster to join.....
>>> Performing Cluster Check
(using node 192.168.25.153:7001)
M: 5a8523db7e12ca600dc82901ced06741b3010076
192.168.25.153:7001
slots:0-5460 (5461 slots) master
M: bf6f0929044db485dea9b565bb51e0c917d20a53
192.168.25.153:7002
slots:5461-10922 (5462 slots) master
M: c5e334dc4a53f655cb98fa3c3bdef8a808a693ca
192.168.25.153:7003
slots:10923-16383 (5461 slots) master
M: 2a61b87b49e5b1c84092918fa2467dd70fec115f
192.168.25.153:7004
slots: (0 slots) master
replicates 5a8523db7e12ca600dc82901ced06741b3010076
M: 14848b8c813766387cfd77229bd2d1ffd6ac8d65
192.168.25.153:7005
slots: (0 slots) master
replicates bf6f0929044db485dea9b565bb51e0c917d20a53
M: 3192cbe437fe67bbde9062f59d5a77dabcd0d632
192.168.25.153:7006
slots: (0 slots) master
replicates c5e334dc4a53f655cb98fa3c3bdef8a808a693ca
[OK] All nodes agree about slots
configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
[root@bogon redis-cluster]#
4.5
测试集群
[root@bogon redis-cluster]#
redis01/redis-cli -h 192.168.25.153 -p 7002 -c
[root@bogon redis-cluster]#
redis01/redis-cli -h 192.168.25.153 -p 7002
192.168.25.153:7002> set a 100
(error) MOVED 15495 192.168.25.153:7003
192.168.25.153:7002>
[root@bogon redis-cluster]#
redis01/redis-cli -h 192.168.25.153 -p 7002 -c
192.168.25.153:7002> set a 100
-> Redirected to slot [15495] located at
192.168.25.153:7003
OK
192.168.25.153:7003>
4.6
关闭redis
redis01/redis-cli -p 7001 shutdown
4.7
Redis客户端
4.7.1 Redis-cli
自带客户端。使用最多的。
4.7.2 图形化界面客户端
只支持单机版,不支持集群。
4.7.3 Jedis客户端
4.7.3.1
单机版
需要把jedis的jar包添加到工程中,如果是maven需要添加jar包的坐标。
public class JedisTest { @Test public void testJedisSingle() { //创建一个jedis的对象。 Jedis jedis = new Jedis("192.168.25.153", 6379); //调用jedis对象的方法,方法名称和redis的命令一致。 jedis.set("key1", "jedis String string = jedis.get("key1"); System.out.println(string); //关闭jedis。 jedis.close(); } /** * 使用连接池 */ @Test public void testJedisPool() { //创建jedis连接池 JedisPool pool = new JedisPool("192.168.25.153", 6379); //从连接池中获得Jedis对象 Jedis jedis = pool.getResource(); String string = jedis.get("key1"); System.out.println(string); //关闭jedis对象 jedis.close(); pool.close(); } } |
4.7.3.2
集群版
@Test public void testJedisCluster() { HashSet<HostAndPort> nodes.add(new HostAndPort("192.168.25.153", 7001)); nodes.add(new HostAndPort("192.168.25.153", 7002)); nodes.add(new HostAndPort("192.168.25.153", 7003)); nodes.add(new HostAndPort("192.168.25.153", 7004)); nodes.add(new HostAndPort("192.168.25.153", 7005)); nodes.add(new HostAndPort("192.168.25.153", 7006)); JedisCluster cluster = new JedisCluster(nodes); cluster.set("key1", "1000"); String string = cluster.get("key1"); System.out.println(string); cluster.close(); } |
5 业务逻辑中添加缓存
需要在taotao-rest工程中添加缓存。
5.1
jedis整合spring
5.1.1 单机版整合
5.1.1.1
配置
<!-- 连接池配置 <bean <!-- 最大连接数 <property name="maxTotal" value="30" /> <!-- 最大空闲连接数 --> <property name="maxIdle" value="10" /> <!-- 每次释放连接的最大数目 --> <property name="numTestsPerEvictionRun" value="1024" <!-- 释放连接的扫描间隔(毫秒) --> <property name="timeBetweenEvictionRunsMillis" value="30000" <!-- 连接最小空闲时间 --> <property name="minEvictableIdleTimeMillis" value="1800000" <!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 --> <property name="softMinEvictableIdleTimeMillis" value="10000" <!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 --> <property name="maxWaitMillis" value="1500" <!-- 在获取连接的时候检查有效性, 默认false --> <property name="testOnBorrow" value="true" <!-- 在空闲时检查有效性, 默认false --> <property name="testWhileIdle" value="true" <!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true --> <property name="blockWhenExhausted" value="false" </bean> <!-- jedis客户端单机版 --> <bean <constructor-arg name="host" value="192.168.25.153"></constructor-arg> <constructor-arg name="port" value="6379"></constructor-arg> <constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg> </bean> |
5.1.1.2
测试
/** * 单机版测试 * <p>Title: * <p>Description: </p> */ @Test public void testSpringJedisSingle() ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-*.xml"); JedisPool pool = (JedisPool) applicationContext.getBean("redisClient"); Jedis jedis = pool.getResource(); String string = jedis.get("key1"); System.out.println(string); jedis.close(); pool.close(); } |
5.1.2 集群版整合
5.1.2.1
配置
<bean id="redisClient" class="redis.clients.jedis.JedisCluster"> <constructor-arg name="nodes"> <set> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153"></constructor-arg> <constructor-arg name="port" value="7001"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153"></constructor-arg> <constructor-arg name="port" value="7002"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153"></constructor-arg> <constructor-arg name="port" value="7003"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153"></constructor-arg> <constructor-arg name="port" value="7004"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153"></constructor-arg> <constructor-arg name="port" value="7005"></constructor-arg> </bean> <bean class="redis.clients.jedis.HostAndPort"> <constructor-arg name="host" value="192.168.25.153"></constructor-arg> <constructor-arg name="port" value="7006"></constructor-arg> </bean> </set> </constructor-arg> <constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg> </bean> |
5.1.2.2
测试
@Test public void testSpringJedisCluster() { ApplicationContext applicationContext = new JedisCluster jedisCluster = (JedisCluster) applicationContext.getBean("redisClient"); String string = jedisCluster.get("key1"); System.out.println(string); jedisCluster.close(); } |
5.2
添加jedis dao
5.2.1 单机版
public class @Autowired private JedisPool jedisPool; @Override public String Jedis jedis = jedisPool.getResource(); String string = jedis.get(key); jedis.close(); return string; } @Override public String Jedis jedis = jedisPool.getResource(); String string = jedis.set(key, value); jedis.close(); return string; } @Override public String Jedis jedis = jedisPool.getResource(); String string = jedis.hget(hkey, key); jedis.close(); return string; } @Override public long hset(String hkey, String key, String value) { Jedis jedis = jedisPool.getResource(); Long result = jedis.hset(hkey, key, value); jedis.close(); return result; } @Override public long incr(String key) { Jedis jedis = jedisPool.getResource(); Long result = jedis.incr(key); jedis.close(); return result; } @Override public long expire(String key, int second) { Jedis jedis = jedisPool.getResource(); Long result = jedis.expire(key, second); jedis.close(); return result; } @Override public long ttl(String key) { Jedis jedis = jedisPool.getResource(); Long result = jedis.ttl(key); jedis.close(); return result; } } |
Spring配置文件
5.2.2 集群版
public class @Autowired private JedisCluster jedisCluster; @Override public String return jedisCluster.get(key); } @Override public String return jedisCluster.set(key, value); } @Override public String return jedisCluster.hget(hkey, key); } @Override public long hset(String hkey, String key, String value) { return jedisCluster.hset(hkey, key, value); } @Override public long incr(String key) { return jedisCluster.incr(key); } @Override public long expire(String key, int second) { return jedisCluster.expire(key, second); } @Override public long ttl(String key) { return jedisCluster.ttl(key); } } |
5.3
把缓存添加到业务逻辑
注意:缓存的添加不能影响正常的业务逻辑。
@Override public //从缓存中取内容 try { String result = jedisClient.hget(INDEX_CONTENT_REDIS_KEY, contentCid + ""); if (!StringUtils.isBlank(result)) { //把字符串转换成list List<TbContent> return resultList; } } e.printStackTrace(); } //根据内容分类id查询内容列表 TbContentExample example = new TbContentExample(); Criteria criteria = example.createCriteria(); criteria.andCategoryIdEqualTo(contentCid); //执行查询 List<TbContent> //向缓存中添加内容 try { //把list转换成字符串 String cacheString = JsonUtils.objectToJson(list); jedisClient.hset(INDEX_CONTENT_REDIS_KEY, contentCid + "", cacheString); } e.printStackTrace(); } return list; } |
课后作业:商品类目展示添加缓存
6 缓存同步
图中我们只需要在服务层写要同步的接口,然后后台或者服务层前来调用既可以同步。在后台的业务逻辑中,直接与数据库打交道,如果后台数据库的内容修改了,我们需要将缓存中的key值删除,然后重新写入缓存。这样就实现了缓存的同步。这个写的接口就在rest服务层,而调用rest都是采用的httpclient的方式。
当后台管理系统,修改内容之后需要通知redis把修改的内容对应的分类id的key删除。
6.1
添加缓存后的系统架构
6.2
解决方案
在taotao-rest工程中发布一个服务。当后台管理系统修改内容后,调用此服务,同步缓存。
6.3
Dao层
使用JedisClient接口对应的实现类。
6.4
Service层
接收内容分类id,调用dao删除redis中对应的hash中key为分类id的项。
参数:内容分类id
返回值:TaotaoResult
@Service public class @Autowired private JedisClient jedisClient; @Value("${INDEX_CONTENT_REDIS_KEY}") private String INDEX_CONTENT_REDIS_KEY; @Override public TaotaoResult try { jedisClient.hdel(INDEX_CONTENT_REDIS_KEY, contentCid + ""); } catch (Exception e) { e.printStackTrace(); return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e)); } return TaotaoResult.ok(); } } |
6.5
Controller层
接收内容分类id,调用Service返回taotaoResult。
@Controller @RequestMapping("/cache/sync") public class RedisController @Autowired private RedisService redisService; @RequestMapping("/content/{contentCid}") public TaotaoResult TaotaoResult result = redisService.syncContent(contentCid); return result; } } |
6.6
同步缓存服务的调用
需要在后台管理系统中添加一个服务调用的逻辑。当修改内容信息后,需要调用此服务同步缓存。
需要在哪个地方添加缓存,就先将数据存入缓存,然后再获取,