后台索引生产/消费模式
这是种模式在现实生活中的例子很多:
邮局寄信
生产者:你,消费者:投递员,任务列表:邮筒
你写信然后扔到邮筒中去,给任务列表中添加了一个任务。投递员取走有邮筒里的信,消费掉任务列表里的一个任务。
邮局这样做的好处在于:
1.解耦 你不必去认识投递员,万一认识的那个投递员不干了,你又要重新认识一个投递员。
2.支持并发 你不必在某个地点傻等着投递员,同时,投递员也不需挨家挨户的问,哪家需要寄信。
对比邮局寄信的事情,类似博客、论坛等发文章的网站,创建文章索引库也有些类似。
生产者:创建任务,添加到任务列表中,例如添加一篇随笔。
消费者:将任务列表中,某一篇随笔添加到索引库中,这样在搜索的时候,才能够搜索出来新发的随笔。
创建索引库是耗时很长的工作,所以启动有一个消费者线程一直保持对IndexWriter写的状态,有新任务进入的时候对IndexWriter写入,写入完成之后关闭。然后下次while循环扫描的时候判断如果队列汇总没有任务,则sleep5秒钟后再判断,防止不断判断给服务器cpu压力。
IndexManager.cs代码:
public class IndexManager { //单例 private IndexManager() { } private static IndexManager instance = new IndexManager(); public static IndexManager Instance() { return instance; } //任务列表 private List<IndexJob> jobs = new List<IndexJob>(); //启动消费者线程 public void Start() { Thread threadIndex = new Thread(Index); threadIndex.IsBackground = true; threadIndex.Start(); } //创建索引 private void Index() { while (true) { //防止空转造成cpu占用率过高 if (jobs.Count <= 0) { //logger.Debug("没有任务,再睡会!"); Thread.Sleep(5 * 1000); continue; } //为什么每次循环都要打开、关闭索引库。因为关闭索引库以后才会把写入的数据提交到索引库中。也可以每次操作都“提交”(参考Lucene.net文档) string indexPath = "c:/cmsindex"; FSDirectory directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NativeFSLockFactory()); bool isUpdate = IndexReader.IndexExists(directory); //logger.Debug("索引库存在状态" + isUpdate); if (isUpdate) { //如果索引目录被锁定(比如索引过程中程序异常退出),则首先解锁 if (IndexWriter.IsLocked(directory)) { //logger.Debug("开始解锁索引库"); IndexWriter.Unlock(directory); //logger.Debug("解锁索引库完成"); } } IndexWriter writer = new IndexWriter(directory, new PanGuAnalyzer(), !isUpdate, Lucene.Net.Index.IndexWriter.MaxFieldLength.UNLIMITED); //后台线程的实际工作 ProcessJobs(writer); writer.Close(); directory.Close();//不要忘了Close,否则索引结果搜不到 //logger.Debug("全部索引完毕"); } } //后台线程工作 private void ProcessJobs(IndexWriter writer) { foreach (var job in jobs.ToArray()) { //todo:异常处理 jobs.Remove(job);// 消费掉 //因为是自己的网站,所以直接读取数据库,不用webclient了 //为避免重复索引,所以先删除number=i的记录,再重新添加 writer.DeleteDocuments(new Term("number", job.Id.ToString())); //如果“添加文章”任务再添加, if (job.JobType == JobType.Add) { BLL.newsrupeng newBll = new BLL.newsrupeng(); var art = newBll.GetModel(job.Id); if (art == null)//有可能刚添加就被删除了 { continue; } string title = art.title; string body = art.content;//去掉标签 Document document = new Document(); //只有对需要全文检索的字段才ANALYZED document.Add(new Field("number", job.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("title", title, Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("body", body, Field.Store.YES, Field.Index.ANALYZED, Lucene.Net.Documents.Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(document); //logger.Debug("索引" + job.Id + "完毕"); } } } //添加任务 public void AddArticle(int artId) { IndexJob job = new IndexJob(); job.Id = artId; job.JobType = JobType.Add; //logger.Debug(artId+"加入任务列表"); jobs.Add(job);//把任务加入商品库 } //删除任务 public void RemoveArticle(int artId) { IndexJob job = new IndexJob(); job.JobType = JobType.Remove; job.Id = artId; //logger.Debug(artId + "加入删除任务列表"); jobs.Add(job);//把任务加入商品库 } } /// <summary> /// 索引任务 /// </summary> class IndexJob { public int Id { get; set; } public JobType JobType { get; set; } } enum JobType { Add, Remove }
一个winform例子更佳能够体现这个模式。
启动后台线程,向文本框中输入值,然后生产,listbox中经过5秒之后,才能显示出来刚刚添加文本。
代码:
public partial class Form2 : Form { public Form2() { InitializeComponent(); } private List<string> jobList = new List<string>(); //添加任务列表 private void button1_Click(object sender, EventArgs e) { if (textBox1.Text!="") { jobList.Add(textBox1.Text); textBox1.Clear(); textBox1.Focus(); } } private void button2_Click(object sender, EventArgs e) { Start(); } private void Start() { Thread thread = new Thread(ProcessDo); thread.IsBackground = true; thread.Start(); } //工作 private void ProcessDo() { while (true) { if (jobList.Count<=0) { Thread.Sleep(5*1000); continue; } foreach (var s in jobList.ToArray()) { MyDelegate d = (txt) => { listBox1.Items.Add(s); }; listBox1.Invoke(d, s); jobList.Remove(s);//消费了商品就把商品从“仓库”中移除掉 } } } } delegate void MyDelegate(string s);