1. 简介

string 是 redis 最基本的类型,可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。

string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。

2.使用场景

存储简单的键值对,比如我们需要统计某个网站的点击量,关注量、粉丝量等。

3.string数据结构示意图

string类型在存储数据时,是以key-value格式存储的,如下图所示:

java redis存图片 redis如何存储图片_分布式缓存

java redis存图片 redis如何存储图片_java redis存图片_02

 

基于本次Redis学习,首先创建一个项目,包含一个类库MyRedis.Redis和一个控制台应用程序MyRedis。项目结构如下:

java redis存图片 redis如何存储图片_分布式缓存_03

需要在类库项目中安装这两个程序集:ServiceStack和ServiceStack.Redis

java redis存图片 redis如何存储图片_string_04

java redis存图片 redis如何存储图片_java redis存图片_05

 MyRedis.Redis类库项目说明:

InIt文件夹中的两个文件分别为RedisConfigInfo.cs和RedisManager.cs,这两个文件的作用分别是定义redis的配置信息和初始化redis链接池管理对象。代码如下:

RedisConfigInfo.cs

namespace MyRedis.Redis.Init
{
    /// <summary>
    /// Redis配置信息
    /// 也可以放到配置文件中
    /// </summary>
    public class RedisConfigInfo
    {

        /// <summary>
        /// 默认端口6379
        /// </summary>
        public string WriteServerList = "127.0.0.1:6379";

        /// <summary>
        /// 可读的Redis链接地址
        /// </summary>
        public string ReadServerList = "127.0.0.1:6379";

        /// <summary>
        /// 最大写链接数
        /// </summary>
        public int MaxWritePoolSize = 60;

        /// <summary>
        /// 最大读链接数
        /// </summary>
        public int MaxReadPoolSize = 60;

        /// <summary>
        /// 本地缓存到期时间,单位:秒
        /// </summary>
        public int LocalCacheTime = 180;

        /// <summary>
        /// 自动重启
        /// </summary>
        public bool AutoStart = true;

        /// <summary>
        /// 是否记录日志,该设置仅用于排查Redis运行时出现的问题
        /// 如果Redis工作正常,请关闭该项
        /// </summary>
        public bool RecordeLog = false;

    }
}

RedisManager.cs

using ServiceStack.Redis;

namespace MyRedis.Redis.Init
{
    /// <summary>
    /// Redis管理中心
    /// </summary>
    public class RedisManager
    {
        /// <summary>
        /// redis配置文件信息
        /// </summary>
        private static RedisConfigInfo RedisConfigInfo = new RedisConfigInfo();

        /// <summary>
        /// redis客户端池化管理
        /// </summary>
        private static PooledRedisClientManager prcManager;

        /// <summary>
        /// 静态构造方法,初始化链接池管理对象
        /// </summary>
        static RedisManager()
        {
            CreateManager();
        }

        /// <summary>
        ///创建链接池管理对象
        /// </summary>
        private static void CreateManager()
        {
            string[] WriteServerConStr = RedisConfigInfo.WriteServerList.Split(',');
            string[] ReadServerConStr = RedisConfigInfo.ReadServerList.Split(',');
            prcManager = new PooledRedisClientManager(ReadServerConStr, WriteServerConStr,
                new RedisClientManagerConfig
                {
                    MaxWritePoolSize = RedisConfigInfo.MaxWritePoolSize,
                    MaxReadPoolSize = RedisConfigInfo.MaxReadPoolSize,
                    AutoStart = RedisConfigInfo.AutoStart
                });
        }


        /// <summary>
        /// 客户端缓存操作对象
        /// </summary>
        /// <returns></returns>
        public static IRedisClient GetClient()
        {
            return prcManager.GetClient();
        }
    }
}

Interface文件夹中的RedisBase.cs,定义了是Redis操作的基类,继承自IDisposable接口,主要用于自动释放内存资源。代码如下:

using MyRedis.Redis.Init;
using ServiceStack.Redis;
using System;

namespace MyRedis.Redis.Interface
{
    /// <summary>
    /// RedisBase类,是Redis操作的基类,继承自IDisposable接口,主要用于释放内存
    /// </summary>
    public abstract class RedisBase : IDisposable
    {

        public IRedisClient iClient { get; private set; }

        /// <summary>
        /// 构造时完成链接的打开
        /// </summary>
        public RedisBase()
        {
            iClient = RedisManager.GetClient();
        }


        private bool _disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                {
                    iClient.Dispose();
                    iClient = null;
                }
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// 自定义事物
        /// </summary>
        public void Transcation()
        {
            using (IRedisTransaction irt = this.iClient.CreateTransaction())
            {
                try
                {
                    irt.QueueCommand(r => r.Set("key", 20));
                    irt.QueueCommand(r => r.Increment("key", 20));
                    irt.Commit();//提交事物

                }
                catch (Exception ex)
                {
                    irt.Rollback();
                    throw ex;
                }
            }
        }

        /// <summary>
        /// 清除全部数据
        /// </summary>
        public virtual void FlushAll()
        {
            iClient.FlushAll();
        }

        /// <summary>
        /// 保存数据DB文件到硬盘
        /// </summary>
        public void Save()
        {
            iClient.Save();//阻塞式save
        }

        /// <summary>
        /// 异步保存数据DB文件到硬盘
        /// </summary>
        public void SaveAsync()
        {
            iClient.SaveAsync();//异步save
        }


    }
}

Service文件夹中的5个文件是对Redis中5种数据类型常用API方法的封装。

本次先看下RedisStringService.cs,此文件是对string数据类型中部分常用方法的封装。代码如下:

using MyRedis.Redis.Interface;
using System;
using System.Collections.Generic;

namespace MyRedis.Redis.Service
{
    public class RedisStringService : RedisBase
    {

        #region 赋值

        /// <summary>
        /// 设置key的value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool Set<T>(string key, T value)
        {
            return base.iClient.Set<T>(key, value);
        }

        /// <summary>
        /// 设置key的value并设置过期时间
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="dt"></param>
        /// <returns></returns>
        public bool Set<T>(string key, T value, DateTime dt)
        {
            return base.iClient.Set<T>(key, value, dt);
        }


        /// <summary>
        /// 设置key的value并设置过期时间
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="dt"></param>
        /// <returns></returns>
        public bool Set<T>(string key, T value, TimeSpan sp)
        {
            return base.iClient.Set<T>(key, value, sp);
        }

        /// <summary>
        /// 设置多个key/value
        /// </summary>
        /// <param name="dic"></param>
        public void Set(Dictionary<string, string> dic)
        {
            base.iClient.SetAll(dic);
        }
        #endregion


        #region 追加

        /// <summary>
        /// 在原有的key的value值之后追加value,没有就新增一项
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public long Append(string key, string value)
        {
            return base.iClient.AppendToValue(key, value);
        }

        #endregion


        #region 获取值
        /// <summary>
        /// 获取key的value
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string Get(string key)
        {
            return base.iClient.GetValue(key);
        }

        /// <summary>
        /// 获取多个key的values值
        /// </summary>
        /// <param name="keys"></param>
        /// <returns></returns>
        public List<string> Get(List<string> keys)
        {
            return base.iClient.GetValues(keys);
        }

        /// <summary>
        /// 获取多个key的values值(泛型)
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="keys"></param>
        /// <returns></returns>
        public List<T> Get<T>(List<string> keys)
        {
            return base.iClient.GetValues<T>(keys);
        }

        #endregion


        #region 获取旧值赋上新值

        /// <summary>
        /// 获取旧值赋上新值
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public string GetAndSetValue(string key, string value)
        {
            return base.iClient.GetAndSetValue(key, value);
        }

        #endregion


        #region 辅助方法

        /// <summary>
        /// 获取值的长度
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public long GetLength(string key)
        {
            return base.iClient.GetStringCount(key);
        }

        /// <summary>
        /// 自增1,返回自增后的值
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public long Incr(string key)
        {
            return base.iClient.IncrementValue(key);
        }

        /// <summary>
        /// 自增count,返回自增后的值
        /// </summary>
        /// <param name="key"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public long IncrBy(string key, int count)
        {
            return base.iClient.IncrementValueBy(key, count);
        }


        /// <summary>
        /// 自减1,返回自减后的值
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public long Decr(string key)
        {
            return base.iClient.DecrementValue(key);
        }

        /// <summary>
        /// 自减count,返回自减后的值
        /// </summary>
        /// <param name="key"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public long DecrBy(string key, int count)
        {
            return base.iClient.DecrementValueBy(key, count);
        }
        #endregion

    }
}

上面的代码创建完成后,项目的基础就构建完成了,下面开始学习string数据类型常用api的使用。

在控制台应用程序中创建ServiceStackTest.cs类,用于调用Redis的api方法。代码如下:

using MyRedis.Redis.Service;
using System;
using System.Threading;

namespace MyRedis
{
    /// <summary>
    /// Redis常用api调用
    /// </summary>
    public class ServiceStackTest
    {
        public class Student
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Remark { get; set; }
            public int Age { get; set; }
        }

        public static void Show()
        {
            Student student1 = new Student()
            {
                Id = 11,
                Name = "张三",
                Remark = "测试备注张三信息",
                Age = 20
            };

            Student student2 = new Student()
            {
                Id = 22,
                Name = "李四",
                Remark = "测试备注李四信息",
                Age = 30
            };

            #region RedisStringService
            using (RedisStringService service = new RedisStringService())
            {
                service.Set<string>("s", "测试");
                service.Set<Student>("student", student1);
                Console.WriteLine(service.Get("s"));

                service.Append("s", "123456");
                Console.WriteLine(service.Get("s"));

                Console.WriteLine(service.GetAndSetValue("s", "嘿嘿"));
                Console.WriteLine(service.Get("s"));

                service.Set<string>("s1", "张", DateTime.Now.AddSeconds(5));//设置过期时间为5秒钟
                Thread.Sleep(5100);
                Console.WriteLine(service.Get("s1"));//数据已过期,在redis中查询不到key为s1的数据

                service.Set<int>("Age", 22);
                Console.WriteLine(service.Incr("Age"));
                Console.WriteLine(service.IncrBy("Age", 3));
                Console.WriteLine(service.Decr("Age"));
                Console.WriteLine(service.DecrBy("Age", 3));
            }
            #endregion
        }
    }
}

main方法调用代码:

using System;
using MyRedis.Demo;

namespace MyRedis
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ServiceStackTest.Show();

                Console.ReadLine();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }

        }
    }
}

上面代码执行过程中可以看具体方法的执行情况,这里就不一一调试了,执行后可以从可视化工具看到Redis中存储的数据信息:

java redis存图片 redis如何存储图片_java redis存图片_06

 

java redis存图片 redis如何存储图片_java redis存图片_07

 

java redis存图片 redis如何存储图片_Redis_08

在实际应用场景中,会用Redis解决商品超卖的问题,在秒杀商品时,同一时刻有多个用户购买同一件商品。为了防止超卖情况的发生,可以使用Redis来解决。下面一个实例,模拟5000个用户同时抢购10件商品:

OverSellDemo.cs

using MyRedis.Redis.Service;
using System;
using System.Threading.Tasks;

namespace MyRedis.Demo
{
    /// <summary>
    /// 使用Redis防超卖实现
    /// </summary>
    public class OverSellDemo
    {
        private static bool IsGoOn = true;//秒杀活动是否结束
        public static void Show()
        {
            try
            {
                using (RedisStringService service = new RedisStringService())
                {
                    service.Set<int>("Stock", 10);//代表库存
                }

                for (int i = 0; i < 5000; i++)//模拟5000个用户同时访问
                {
                    int k = i;

                    //一个线程就是一个用户请求
                    Task.Run(() =>
                    {
                        using (RedisStringService service = new RedisStringService())
                        {
                            if (IsGoOn)
                            {
                                //Redis是单线程处理 所有不会有同时-1
                                long index = service.Decr("Stock");//-1并返回
                                if (index >= 0)
                                {
                                    Console.WriteLine($"{k.ToString("000")} 秒杀成功,商品为{index}");
                                    //去数据库增加订单等等操作
                                }
                                else
                                {
                                    if (IsGoOn)
                                    {
                                        IsGoOn = false;
                                    }
                                    Console.WriteLine($"{k.ToString("000")} 秒杀失败,商品为{index}");
                                }
                            }
                            else
                            {
                                Console.WriteLine($"{k.ToString("000")} 库存不足 秒杀停止");
                            }
                        }
                    });
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw ex;
            }
        }
    }
}

执行结果: 

java redis存图片 redis如何存储图片_分布式缓存_09

从结果可以看到商品只有10件被卖出去了,后面的请求中有两个请求秒杀失败,其他的都是返回库存不足。这不仅达到了防止超卖的目的,同时也阻挡了大量无效请求,大大降低了数据库的压力。在Redis缓存中没有库存的情况下,程序不会去调用数据库。

上面的实例中,Redis可以做到防超卖的原因是Redis是原子性操作,保证一个数值只会出现一次,防止一个商品卖给多个人。

下面比较下不使用Redis时高并发和使用Redis高并发的秒杀商品的两种情况:

假如秒杀分为3个步骤:1.获取库存 2.程序中对库存减一 3.将减一后的结果保存

不使用Redis:(3个线程并发,初始值为10)

A线程  步骤1→ 步骤2→ 步骤3    减一后结果为9
B线程 步骤1→ 步骤2→ 步骤3     减一后结果可能为9,可能为8
C线程 步骤1→ 步骤2→ 步骤3     减一后结果可能为9,可能为8,可能为7

使用Redis:(3个线程并发,初始值为10)

A线程 步骤1→ 步骤2→ 步骤3     减一后结果为9
B线程 步骤1→ 步骤2→ 步骤3     减一后结果为8
C线程 步骤1→ 步骤2→ 步骤3     减一后结果为7

造成这样结果的原因就是步骤1、2、3在不使用Redis情况下,三个操作是不连续的,三个线程可能同时做步骤一,也可能同时做步骤二、步骤三。

使用Redis后就不一样了,Redis是单线程,一次只可能做一个步骤,只有一个线程的三个步骤执行完毕,再去执行下一个线程的三个步骤。这里不用考虑Redis单线程执行效率问题,虽然Redis是单线程执行,但是效率非常快。