最近在學習redis,在網上查了些文章,利用他人已有的知識,總結寫下了這篇文章,大部份內容仍是引用別人的文章內容。通過測試發現spring-data-redis如今有的版本只能支持reids 2.6和2.8版本,更高版本還沒有支持。仍是直接使用jedis比較靈活。
html
redis的安裝過程在之前的博文中已經詳細介紹 linux下安裝redis並自啓動java
jedis下載地址:https://github.com/xetorthio/jedis
jedis社區地址:https://groups.google.com/forum/#!forum/jedis_redisnode
一、鍵值讀取
linux
Jedis jedis = new Jedis("redis服務器ip地址", 6379); jedis.set("foo", "123456"); String value = jedis.get("foo"); System.out.println("____________value="+value);
二、集羣讀取git
Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>(); //Jedis Cluster will attempt to discover cluster nodes automatically jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7379)); JedisCluster jc = new JedisCluster(jedisClusterNodes); jc.set("foo", "bar"); String value = jc.get("foo");
將如下內容放在redis.properties或者文件中,後面有關的.properties文件的內容都跟下面同樣的內容。
github
redis.host=192.168.1.100 redis.port=6379 redis.pass=123456 redis.default.db=0 redis.timeout=100000//客戶端超時時間單位是毫秒 redis.maxActive=300// 最大鏈接數 redis.maxIdle=100//最大空閒數 redis.maxWait=1000//最大創建鏈接等待時間 redis.testOnBorrow=true redis.testOnReturn=true;
java讀取配置文件web
ResourceBundle bundle = ResourceBundle.getBundle("redis"); if (bundle == null) { throw new IllegalArgumentException( "[redis.properties] is not found!"); }
jedisc池須要commons-pool.jar的支持。redis
在沒有使用spring-data-redis的狀況下,須要手工獲取池對象,並在使用完畢後放回對象池中。
spring
在使用redis池,須要經過如下代碼方式從pool中獲取資源。apache
jedisPool.getResource()
資源使用完畢後須要放入pool中
jedisPool.returnResource(jedis);
具體的示例代碼
public class MyJedisPool { // jedis池 private static JedisPool pool; // 靜態代碼初始化池配置 static { // 加載redis配置文件 ResourceBundle bundle = ResourceBundle.getBundle("redis"); if (bundle == null) { throw new IllegalArgumentException("[redis.properties] is not found!"); } // 建立jedis池配置實例 JedisPoolConfig config = new JedisPoolConfig(); // 設置池配置項值 config.setMaxActive(Integer.valueOf(bundle.getString("redis.pool.maxActive"))); config.setMaxIdle(Integer.valueOf(bundle.getString("redis.pool.maxIdle"))); config.setMaxWait(Long.valueOf(bundle.getString("redis.pool.maxWait"))); config.setTestOnBorrow(Boolean.valueOf(bundle.getString("redis.pool.testOnBorrow"))); config.setTestOnReturn(Boolean.valueOf(bundle.getString("redis.pool.testOnReturn"))); //根據配置實例化jedis池 pool = new JedisPool(config, bundle.getString("redis.ip"), Integer.valueOf(bundle.getString("redis.port"))); } /** * 測試jedis池方法 */ public static void test1() { // 從jedis池中獲取一個jedis實例 Jedis jedis = pool.getResource(); // 獲取jedis實例後能夠對redis服務進行一系列的操做 jedis.set("name", "xmong"); System.out.println(jedis.get("name")); jedis.del("name"); System.out.println(jedis.exists("name")); // 釋放對象池,即獲取jedis實例使用後要將對象還回去 pool.returnResource(jedis); } }
#redis1服務器ip # Redis1.ip=172.30.5.113 #redis2服務器ip # Redis2.ip=172.30.5.117 #redis服務器端口號# redis.port=6379
public class MyJedisPool { // jedis池 private static JedisPool pool; // shardedJedis池 private static ShardedJedisPool shardPool; // 靜態代碼初始化池配置 static { // 加載redis配置文件 ResourceBundle bundle = ResourceBundle.getBundle("redis"); if (bundle == null) { throw new IllegalArgumentException("[redis.properties] is not found!"); } // 建立jedis池配置實例 JedisPoolConfig config = new JedisPoolConfig(); // 設置池配置項值 config.setMaxActive(Integer.valueOf(bundle.getString("redis.pool.maxActive"))); config.setMaxIdle(Integer.valueOf(bundle.getString("redis.pool.maxIdle"))); config.setMaxWait(Long.valueOf(bundle.getString("redis.pool.maxWait"))); config.setTestOnBorrow(Boolean.valueOf(bundle.getString("redis.pool.testOnBorrow"))); config.setTestOnReturn(Boolean.valueOf(bundle.getString("redis.pool.testOnReturn"))); // 根據配置實例化jedis池 // pool = new JedisPool(config, bundle.getString("redis.ip"), // Integer.valueOf(bundle.getString("redis.port"))); // 建立多個redis共享服務 JedisShardInfo jedisShardInfo1 = new JedisShardInfo(bundle.getString("redis1.ip"), Integer.valueOf(bundle.getString("redis.port"))); JedisShardInfo jedisShardInfo2 = new JedisShardInfo(bundle.getString("redis2.ip"), Integer.valueOf(bundle.getString("redis.port"))); List<JedisShardInfo> list = new LinkedList<JedisShardInfo>(); list.add(jedisShardInfo1); list.add(jedisShardInfo2); // 根據配置文件,建立shared池實例 shardPool = new ShardedJedisPool(config, list); } /** * 測試jedis池方法 */ public static void test1() { // 從jedis池中獲取一個jedis實例 Jedis jedis = pool.getResource(); // 獲取jedis實例後能夠對redis服務進行一系列的操做 jedis.set("name", "xmong"); System.out.println(jedis.get("name")); jedis.del("name"); System.out.println(jedis.exists("name")); // 釋放對象池,即獲取jedis實例使用後要將對象還回去 pool.returnResource(jedis); } /** * 測試shardedJedis池方法 */ public static void test2() { // 從shard池中獲取shardJedis實例 ShardedJedis shardJedis = shardPool.getResource(); // 向redis服務插入兩個key-value對象 shardJedis.set("aaa", "xmong_aaa"); System.out.println(shardJedis.get("aaa")); shardJedis.set("zzz", "xmong_zzz"); System.out.println(shardJedis.get("zzz")); // 釋放資源 shardPool.returnResource(shardJedis); } public static void main(String[] args) { // test1();//執行test1方法 test2();// 執行test2方法 } }
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxActive" value="32"></property> <property name="maxIdle" value="6"></property> <property name="maxWait" value="15000"></property> <property name="minEvictableIdleTimeMillis" value="300000"></property> <property name="numTestsPerEvictionRun" value="3"></property> <property name="timeBetweenEvictionRunsMillis" value="60000"></property> <property name="whenExhaustedAction" value="1"></property> </bean> <bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy"> <!-- config --> <constructor-arg ref="jedisPoolConfig"></constructor-arg> <!-- host --> <constructor-arg value="127.0.0.1"></constructor-arg> <!-- port --> <constructor-arg value="6379"></constructor-arg> <!-- timeout --> <constructor-arg value="15000"></constructor-arg> <!-- password --> <constructor-arg value="0123456"></constructor-arg> <!-- database index --> <constructor-arg value="12"></constructor-arg> </bean>
測試類
public static void main(String[] args) { //resources/beans.xml ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:beans.xml"); JedisPool jedisPool = (JedisPool)context.getBean("jedisPool"); Jedis client = jedisPool.getResource(); try{ client.select(0); client.set("k1", "v1"); System.out.println(client.get("k1")); }catch(Exception e){ e.printStackTrace(); }finally{ jedisPool.returnResource(client);//must be } }
new ClassPathXmlApplicationContext("classpath:beans.xml");是直接讀取beans.xml文件,所以須要將上面的配置內容放在beans.xml中,來獲取bean對象實例。
<context:property-placeholder location="classpath:redis.properties" /> <context:component-scan base-package="com.d.work.main"> </context:component-scan> <context:component-scan base-package="com.d.work.redis"> </context:component-scan> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxActive" value="50" /> <property name="maxIdle" value="8" /> <property name="maxWait" value="1000" /> <property name="testOnBorrow" value="true"/> <property name="testOnReturn" value="true"/> <!-- <property name="testWhileIdle" value="true"/> --> </bean> <bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool" scope="singleton"> <constructor-arg index="0" ref="jedisPoolConfig" /> <constructor-arg index="1"> <list> <bean class="redis.clients.jedis.JedisShardInfo"> <constructor-arg name="host" value="${redis.host}" /> <constructor-arg name="port" value="${redis.port}" /> <constructor-arg name="timeout" value="${redis.timeout}" /> <constructor-arg name="weight" value="1" /> </bean> </list> </constructor-arg> </bean>
spring 提供jsmTemplement,jdbcTemplement,redisTemplement等相似模板。spring 經過context:property-placeholder實現導入配置文件,context:property-placeholder 標籤用來導入properties文件。從而替換${redis.maxIdle}這樣的變量。要使用spring-data-redis,須要下載spring-data-redis-1.5.1.RELEASE.jar
提供了一個高度封裝的「RedisTemplate」類
ValueOperations:簡單K-V操做
SetOperations:set類型數據操做
ZSetOperations:zset類型數據操做
HashOperations:針對map類型的數據操做
ListOperations:針對list類型的數據操做
BoundValueOperations
BoundSetOperations
BoundListOperations
BoundSetOperations
BoundHashOperations
JdkSerializationRedisSerializer:POJO對象的存取場景,使用JDK自己序列化機制,將pojo類經過ObjectInputStream/ObjectOutputStream進行序列化操做,最終redis-server中將存儲字節序列。是目前最經常使用的序列化策略。
StringRedisSerializer:Key或者value爲字符串的場景,根據指定的charset對數據的字節序列編碼成string,是「new String(bytes, charset)」和「string.getBytes(charset)」的直接封裝。是最輕量級和高效的策略。
JacksonJsonRedisSerializer:jackson-json工具提供了javabean與json之間的轉換能力,能夠將pojo實例序列化成json格式存儲在redis中,也能夠將json格式的數據轉換成pojo實例。由於jackson工具在序列化和反序列化時,須要明確指定Class類型,所以此策略封裝起來稍微複雜。【須要jackson-mapper-asl工具支持】
OxmSerializer:提供了將javabean與xml之間的轉換能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存儲的數據將是xml工具。不過使用此策略,編程將會有些難度,並且效率最低;不建議使用。【須要spring-oxm模塊的支持】
針對「序列化和發序列化」中JdkSerializationRedisSerializer和StringRedisSerializer是最基礎的策略,原則上,咱們能夠將數據存儲爲任何格式以便應用程序存取和解析(其中應用包括app,hadoop等其餘工具),不過在設計時仍然不推薦直接使用「JacksonJsonRedisSerializer」和「OxmSerializer」,由於不管是json仍是xml,他們自己仍然是String。
若是你的數據須要被第三方工具解析,那麼數據應該使用StringRedisSerializer而不是JdkSerializationRedisSerializer。
若是你的數據格式必須爲json或者xml,那麼在編程級別,在redisTemplate配置中仍然使用StringRedisSerializer,在存儲以前或者讀取以後,使用「SerializationUtils」工具轉換轉換成json或者xml,請參見下文實例。
示例一
<bean id="propertyConfigurerRedis" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="order" value="1" /> <property name="ignoreUnresolvablePlaceholders" value="true" /> <property name="locations"> <list> <value>classpath:config/redis-manager-config.properties</value> </list> </property> </bean> <!-- jedis pool配置 --> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxActive" value="${redis.maxActive}" /> <property name="maxIdle" value="${redis.maxIdle}" /> <property name="maxWait" value="${redis.maxWait}" /> <property name="testOnBorrow" value="${redis.testOnBorrow}" /> </bean> <!-- spring data redis --> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="usePool" value="true"></property> <property name="hostName" value="${redis.host}" /> <property name="port" value="${redis.port}" /> <property name="password" value="${redis.pass}" /> <property name="timeout" value="${redis.timeout}" /> <property name="database" value="${redis.default.db}"></property> <constructor-arg index="0" ref="jedisPoolConfig" /> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory" /> </bean>
public class RedisBase { private StringRedisTemplate template; /** * @return the template */ public StringRedisTemplate getTemplate() { return template; } /** * @param template the template to set */ public void setTemplate(StringRedisTemplate template) { this.template = template; } }
示例二
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxActive" value="32"></property> <property name="maxIdle" value="6"></property> <property name="maxWait" value="15000"></property> <property name="minEvictableIdleTimeMillis" value="300000"></property> <property name="numTestsPerEvictionRun" value="3"></property> <property name="timeBetweenEvictionRunsMillis" value="60000"></property> <property name="whenExhaustedAction" value="1"></property> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy"> <property name="poolConfig" ref="jedisPoolConfig"></property> <property name="hostName" value="127.0.0.1"></property> <property name="port" value="6379"></property> <property name="password" value="0123456"></property> <property name="timeout" value="15000"></property> <property name="usePool" value="true"></property> </bean> <bean id="jedisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"></property> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> </property> </bean>
public class SpringDataRedisTestMain { /** * @param args */ public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-redis-beans.xml"); RedisTemplate redisTemplate = (RedisTemplate)context.getBean("jedisTemplate"); //其中key採起了StringRedisSerializer //其中value採起JdkSerializationRedisSerializer ValueOperations<String, User> valueOper = redisTemplate.opsForValue(); User u1 = new User("zhangsan",12); User u2 = new User("lisi",25); valueOper.set("u:u1", u1); valueOper.set("u:u2", u2); System.out.println(valueOper.get("u:u1").getName()); System.out.println(valueOper.get("u:u2").getName()); } /** * 若是使用jdk序列化方式,bean必須實現Serializable,且提供getter/setter方法 * @author qing * */ static class User implements Serializable{ /** * */ private static final long serialVersionUID = -3766780183428993793L; private String name; private Date created; private int age; public User(){} public User(String name,int age){ this.name = name; this.age = age; this.created = new Date(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } }
若是你使用過jedisPool鏈接池,在數據操做以前,你須要pool.getResource()即從鏈接池中獲取「連接資源」(Jedis),在操做以後,你須要(必須)調用pool.returnResource()將資源歸還個鏈接池。可是,spring-data-redis中,咱們彷佛並無直接操做pool,那麼spring是如何作到pool管理的呢??一句話:spring的「看門絕技」--callback。
public <T> T execute(RedisCallback<T> action):這個方法是redisTemplate中執行操做的底層方法,任何基於redisTemplate之上的調用(好比,valueOperations)最終都會被封裝成RedisCallback,redisTemplate在execute方法中將會直接使用jedis客戶端API進行與server通訊,並且在若是使用了鏈接池,則會在操做以後執行returnSource。
java讀取bean方式
方式一
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-redis-beans.xml"); RedisTemplate redisTemplate = (RedisTemplate)context.getBean("jedisTemplate");
方式二
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:setup/applicationContext.xml"); cachedClient = (MemCachedClient)ctx.getBean("memcachedClient");
redis能夠用來作消息訂閱操做。
第一步配置
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxActive" value="32"></property> <property name="maxIdle" value="6"></property> <property name="maxWait" value="15000"></property> <property name="minEvictableIdleTimeMillis" value="300000"></property> <property name="numTestsPerEvictionRun" value="3"></property> <property name="timeBetweenEvictionRunsMillis" value="60000"></property> <property name="whenExhaustedAction" value="1"></property> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy"> <property name="poolConfig" ref="jedisPoolConfig"></property> <property name="hostName" value="127.0.0.1"></property> <property name="port" value="6379"></property> <property name="password" value="0123456"></property> <property name="timeout" value="15000"></property> <property name="usePool" value="true"></property> </bean> <bean id="jedisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"></property> <property name="defaultSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> </bean> <bean id="topcMessageListener" class="com.sample.redis.sdr.TopicMessageListener"> <property name="redisTemplate" ref="jedisTemplate"></property> </bean> <bean id="topicContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer" destroy-method="destroy"> <property name="connectionFactory" ref="jedisConnectionFactory"/> <property name="taskExecutor"><!-- 此處有個奇怪的問題,沒法正確使用其餘類型的Executor --> <bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler"> <property name="poolSize" value="3"></property> </bean> </property> <property name="messageListeners"> <map> <entry key-ref="topcMessageListener"> <bean class="org.springframework.data.redis.listener.ChannelTopic"> <constructor-arg value="user:topic"/> </bean> </entry> </map> </property> </bean>
第二部:發佈消息
String channel = "user:topic"; //其中channel必須爲string,並且「序列化」策略也是StringSerializer //消息內容,將會根據配置文件中指定的valueSerializer進行序列化 //本例中,默認所有采用StringSerializer //那麼在消息的subscribe端也要對「發序列化」保持一致。 redisTemplate.convertAndSend(channel, "from app 1");
第三部:消息接收
public class TopicMessageListener implements MessageListener { private RedisTemplate redisTemplate; public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public void onMessage(Message message, byte[] pattern) { byte[] body = message.getBody();//請使用valueSerializer byte[] channel = message.getChannel(); //請參考配置文件,本例中key,value的序列化方式均爲string。 //其中key必須爲stringSerializer。和redisTemplate.convertAndSend對應 String itemValue = (String)redisTemplate.getValueSerializer().deserialize(body); String topic = (String)redisTemplate.getStringSerializer().deserialize(channel); //... } }
如下示例顯示spring-data-redis實現事物處理,並經過回調方法實現返回數據
//execute a transaction List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() { public List<Object> execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.opsForSet().add("key", "value1"); // This will contain the results of all ops in the transaction return operations.exec(); } }); System.out.println("Number of items added to set: " + txResults.get(0));
sdr提供了4種內置的serializer:
一、JdkSerializationRedisSerializer:使用JDK的序列化手段(serializable接口,ObjectInputStrean,ObjectOutputStream),數據以字節流存儲
二、StringRedisSerializer:字符串編碼,數據以string存儲
三、JacksonJsonRedisSerializer:json格式存儲
四、OxmSerializer:xml格式存儲
其中JdkSerializationRedisSerializer和StringRedisSerializer是最基礎的序列化策略,其中「JacksonJsonRedisSerializer」與「OxmSerializer」都是基於stirng存儲,所以它們是較爲「高級」的序列化(最終仍是使用string解析以及構建java對象)。
RedisTemplate中須要聲明4種serializer,默認爲「JdkSerializationRedisSerializer」:
1) keySerializer :對於普通K-V操做時,key採起的序列化策略
2) valueSerializer:value採起的序列化策略
3) hashKeySerializer: 在hash數據結構中,hash-key的序列化策略
4) hashValueSerializer:hash-value的序列化策略
不管如何,建議key/hashKey採用StringRedisSerializer。
配置JdkSerializationRedisSerializer/StringRedisSerializer
<bean id="jedisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"></property> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="hashKeySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> </property> <property name="hashValueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> </property> </bean>
ValueOperations<String, User> valueOper = redisTemplate.opsForValue(); User user = new User("zhangsan",12); valueOper.set("user:1", user); System.out.println(valueOper.get("user:1").getName());
spring framework4.X.X版本與spring framework3.X.X有些區別:
spring framework 4.X.X須要jackjson2.0,同時 org.springframework.http.converter.json.MappingJacksonHttpMessageConverter改成 MappingJacksonHttpMessageConverter已經改成MappingJackson2HttpMessageConverter
spring framework 4.X.X配置中使用
<mvc:annotation-driven/>
再也不須要手工配置bean
<bean class ="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" > <property name="messageConverters"> <list> <ref bean="mappingJackson2HttpMessageConverter" /> </list> </property> </bean> <bean name="mappingJackson2HttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> </list> </property> </bean>
http://snowolf.iteye.com/blog/1630697
http://my.oschina.net/gccr/blog/307725
http://blog.163.com/asd_wll/blog/static/210310402013654528316/
http://wenku.baidu.com/link?url=fuS8aw93_4_Qvv8WBgazt5eZGiDhv1Np5vCyB8qBUVdWIUxI47IaA5opzI3vwhWth7MrF1KiJn_o1aBvWmFdeNxbmbcSnyCTEd54C0iLLEC
http://my.oschina.net/gccr/blog/307725
http://shift-alt-ctrl.iteye.com/blog/1886831
http://www.cnblogs.com/liuling/p/2014-4-19-04.html
http://www.cnblogs.com/tankaixiong/p/3660075.html
http://blog.csdn.net/neubuffer/article/details/17003909
http://blog.csdn.net/liuzhigang1237/article/details/8283797
http://shift-alt-ctrl.iteye.com/blog/1887370
http://shift-alt-ctrl.iteye.com/blog/1887473
http://shift-alt-ctrl.iteye.com/blog/1887644
http://shift-alt-ctrl.iteye.com/blog/1887700
http://shift-alt-ctrl.iteye.com/blog/1886831
http://shift-alt-ctrl.iteye.com/blog/1885910
http://www.open-open.com/lib/view/open1385173126448.html
http://blog.csdn.net/A_lele123/article/details/43406547
http://javacrazyer.iteye.com/blog/1840161
http://redis.readthedocs.org/en/2.4/index.html